Module: Rigor::Inference::FlowTracer

Defined in:
lib/rigor/inference/flow_tracer.rb

Overview

Thread-local event recorder behind ‘rigor trace`: while a block runs under FlowTracer.record, the inference engine emits a flat, ordered event stream describing HOW it typed the program — expression enter/result pairs, scope binds, union formation, and method-dispatch outcomes. The CLI replays that stream as a terminal animation (or dumps it as JSON); the engine itself never reads the events back, so recording is purely observational and MUST NOT change any inferred type.

Modelled on Analysis::DependencyRecorder: thread-local state, a module-level activation count so the disabled fast path (FlowTracer.active?) is a plain integer read, and a frozen snapshot for consumers. The instrumented hot paths (‘ExpressionTyper#type_of`, `Scope#with_local`, `Type::Combinator.union`, `MethodDispatcher.dispatch`) each guard their emit behind FlowTracer.active?, so a normal (non-tracing) run pays one integer comparison.

Defined Under Namespace

Classes: Event, Recorder

Class Method Summary collapse

Class Method Details

.active?Boolean

Plain integer read (GVL-atomic) — the disabled fast path.

Returns:

  • (Boolean)


119
120
121
# File 'lib/rigor/inference/flow_tracer.rb', line 119

def active?
  @active_count.positive?
end

.bind(name, type) ⇒ Object

‘Scope#with_local` — the moment a local enters the scope.



134
135
136
# File 'lib/rigor/inference/flow_tracer.rb', line 134

def bind(name, type)
  Thread.current[KEY]&.emit(:bind, data: { name: name.to_s, type: describe(type) })
end

.describe(type) ⇒ Object



164
165
166
167
168
169
# File 'lib/rigor/inference/flow_tracer.rb', line 164

def describe(type)
  return "nil" if type.nil?
  return type.describe(:short) if type.respond_to?(:describe)

  type.inspect
end

.dispatch(receiver:, method_name:, args:, result:, location: nil) ⇒ Object

‘MethodDispatcher.dispatch` — resolution or the fail-soft `nil` (“no rule matched”; the caller will widen to `Dynamic`).



149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/rigor/inference/flow_tracer.rb', line 149

def dispatch(receiver:, method_name:, args:, result:, location: nil)
  recorder = Thread.current[KEY]
  return unless recorder

  recorder.emit(
    :dispatch,
    location: location && location_hash(location),
    data: {
      receiver: describe(receiver), method: method_name.to_s,
      args: args.map { |a| describe(a) }.freeze,
      type: result && describe(result), resolved: !result.nil?
    }
  )
end

.location_hash(loc) ⇒ Object



171
172
173
174
175
176
177
# File 'lib/rigor/inference/flow_tracer.rb', line 171

def location_hash(loc)
  {
    start_line: loc.start_line, start_column: loc.start_column,
    end_line: loc.end_line, end_column: loc.end_column,
    start_offset: loc.start_offset, end_offset: loc.end_offset
  }
end

.recordObject

Activates recording on the current thread for the duration of the block and returns the frozen event list. Nests safely; restores the previous recorder on exit.



106
107
108
109
110
111
112
113
114
115
116
# File 'lib/rigor/inference/flow_tracer.rb', line 106

def record
  previous = Thread.current[KEY]
  recorder = Recorder.new
  Thread.current[KEY] = recorder
  @mutex.synchronize { @active_count += 1 }
  yield
  recorder.events.freeze
ensure
  Thread.current[KEY] = previous
  @mutex.synchronize { @active_count -= 1 }
end

.trace_node(node) ⇒ Object

Brackets one expression-typing recursion. Falls through to the bare block when the current thread is not recording (another thread may have flipped active?).



126
127
128
129
130
131
# File 'lib/rigor/inference/flow_tracer.rb', line 126

def trace_node(node, &)
  recorder = Thread.current[KEY]
  return yield unless recorder

  recorder.node(node, &)
end

.union(members, result) ⇒ Object

‘Type::Combinator.union` — the moment branch types merge (including degenerate collapses like `1 | 1 → 1`).



140
141
142
143
144
145
# File 'lib/rigor/inference/flow_tracer.rb', line 140

def union(members, result)
  Thread.current[KEY]&.emit(
    :union,
    data: { members: members.map { |m| describe(m) }.freeze, type: describe(result) }
  )
end