Class: Kumi::Core::Analyzer::PassManager

Inherits:
Object
  • Object
show all
Defined in:
lib/kumi/core/analyzer/pass_manager.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(passes) ⇒ PassManager

Returns a new instance of PassManager.



9
10
11
12
# File 'lib/kumi/core/analyzer/pass_manager.rb', line 9

def initialize(passes)
  @passes = passes
  @errors = []
end

Instance Attribute Details

#errorsObject (readonly)

Returns the value of attribute errors.



7
8
9
# File 'lib/kumi/core/analyzer/pass_manager.rb', line 7

def errors
  @errors
end

#passesObject (readonly)

Returns the value of attribute passes.



7
8
9
# File 'lib/kumi/core/analyzer/pass_manager.rb', line 7

def passes
  @passes
end

Instance Method Details

#run(syntax_tree, initial_state = nil, errors = [], options = {}) ⇒ Object



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/kumi/core/analyzer/pass_manager.rb', line 14

def run(syntax_tree, initial_state = nil, errors = [], options = {})
  state = initial_state || AnalysisState.new

  passes.each_with_index do |pass_class, phase_index|
    pass_name = pass_class.name.split("::").last

    # Checkpoint support
    Checkpoint.entering(pass_name:, idx: phase_index, state:) if options[:checkpoint_enabled]

    # Debug support
    debug_on = options[:debug_enabled]
    before = state.to_h if debug_on
    Debug.reset_log(pass: pass_name) if debug_on

    # Performance profiling support
    t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC) if options[:profiling_enabled]

    begin
      pass_instance = pass_class.new(syntax_tree, state)

      if options[:profiling_enabled]
        state = Dev::Profiler.phase("analyzer.pass", pass: pass_name) do
          pass_instance.run(errors)
        end
      else
        state = pass_instance.run(errors)
      end
    rescue StandardError => e
      # Capture exception context
      message = "Error in Analysis Pass(#{pass_name}): #{e.message}"
      error_obj = ErrorReporter.create_error(message, location: nil, type: :semantic, backtrace: e.backtrace)
      errors << error_obj

      if debug_on
        logs = Debug.drain_log
        elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(2) if options[:profiling_enabled]
        Debug.emit(
          pass: pass_name,
          diff: {},
          elapsed_ms: elapsed_ms || 0,
          logs: logs + [{ level: :error, id: :exception, message: e.message, error_class: e.class.name }]
        )
      end

      # Return failure result instead of raising - let caller decide what to do
      phase = ExecutionPhase.new(pass_class: pass_class, index: phase_index)
      converted_error = PassFailure.new(
        message: error_obj.message,
        phase: phase_index,
        pass_name: phase.pass_name,
        location: error_obj.location
      )
      return ExecutionResult.failure(
        final_state: state,
        errors: [converted_error],
        failed_at_phase: phase_index
      )
    end

    # Type checking (PassManager enforces AnalysisState)
    unless state.is_a?(AnalysisState)
      raise "Pass #{pass_name} returned #{state.class}, expected AnalysisState"
    end

    # Debug logging with state diff
    if debug_on
      after = state.to_h

      # Optional immutability guard
      if ENV["KUMI_DEBUG_REQUIRE_FROZEN"] == "1"
        (after || {}).each do |k, v|
          if v.nil? || v.is_a?(Numeric) || v.is_a?(Symbol) || v.is_a?(TrueClass) || v.is_a?(FalseClass) || (v.is_a?(String) && v.frozen?)
            next
          end
          raise "State[#{k}] not frozen: #{v.class}" unless v.frozen?
        end
      end

      diff = Debug.diff_state(before, after)
      logs = Debug.drain_log
      elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(2) if options[:profiling_enabled]

      Debug.emit(
        pass: pass_name,
        diff: diff,
        elapsed_ms: elapsed_ms || 0,
        logs: logs
      )
    end

    # Checkpoint support
    Checkpoint.leaving(pass_name:, idx: phase_index, state:) if options[:checkpoint_enabled]

    # Handle errors
    if !errors.empty?
      phase = ExecutionPhase.new(pass_class: pass_class, index: phase_index)
      converted_errors = errors.map do |error|
        PassFailure.new(
          message: error.message,
          phase: phase_index,
          pass_name: phase.pass_name,
          location: error.respond_to?(:location) ? error.location : nil
        )
      end
      return ExecutionResult.failure(
        final_state: state,
        errors: converted_errors,
        failed_at_phase: phase_index
      )
    end

    # Check stop_after
    if options[:stop_after] && pass_name == options[:stop_after]
      return ExecutionResult.success(final_state: state, stopped: true)
    end
  end

  ExecutionResult.success(final_state: state)
end