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)


110
111
112
# File 'lib/rigor/inference/flow_tracer.rb', line 110

def active?
  @active_count.positive?
end

.bind(name, type) ⇒ Object

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



124
125
126
# File 'lib/rigor/inference/flow_tracer.rb', line 124

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

.describe(type) ⇒ Object



153
154
155
156
157
158
# File 'lib/rigor/inference/flow_tracer.rb', line 153

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[Top]).



138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/rigor/inference/flow_tracer.rb', line 138

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



160
161
162
163
164
165
166
# File 'lib/rigor/inference/flow_tracer.rb', line 160

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.



97
98
99
100
101
102
103
104
105
106
107
# File 'lib/rigor/inference/flow_tracer.rb', line 97

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?).



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

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).



129
130
131
132
133
134
# File 'lib/rigor/inference/flow_tracer.rb', line 129

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