Class: Rigor::Inference::CoverageScanner

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/inference/coverage_scanner.rb,
sig/rigor/inference.rbs

Overview

Walks an AST and reports per-node-class coverage of Rigor::Scope#type_of.

For every visited node the scanner runs type_of with a fresh FallbackTracer and inspects the first recorded event:

  • If the first event's node_class matches the visited node's class, the engine entered the fallback (else) branch for this very node — the node is counted as directly unrecognized.
  • Otherwise the typer either succeeded outright or recursed into a child that itself was unrecognized; the visited node is counted as recognized so pass-through wrappers (ProgramNode, StatementsNode, ParenthesesNode, ...) are not double-counted along with their leaves.

This class is intended for tooling probes and CI gates rather than the hot inference path: it allocates a tracer per visited node and discards the inferred type values.

Defined Under Namespace

Classes: Result

Instance Method Summary collapse

Constructor Details

#initialize(scope: nil) ⇒ CoverageScanner

Returns a new instance of CoverageScanner.

Parameters:

  • scope (Rigor::Scope) (defaults to: nil)

    base scope used for every type_of call. Defaults to Scope.empty.

  • scope: (Scope, nil) (defaults to: nil)


45
46
47
# File 'lib/rigor/inference/coverage_scanner.rb', line 45

def initialize(scope: nil)
  @scope = scope || Scope.empty
end

Instance Method Details

#scan(root) ⇒ Result

Parameters:

  • root (Prism::Node)

Returns:



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
# File 'lib/rigor/inference/coverage_scanner.rb', line 51

def scan(root)
  visits = Hash.new(0)
  unrecognized = Hash.new(0)
  events = []

  # Build the per-node scope index once per scan so locals bound earlier in the program flow into the
  # scope used to type every later node. The indexer walks the program with a tracer-less
  # StatementEvaluator and propagates the recorded scope down to expression-interior nodes the evaluator
  # does not visit. The second pass below re-types each node with its own tracer so the per-class
  # fallback statistics stay attributable.
  scope_index = ScopeIndexer.index(root, default_scope: @scope)

  Source::NodeWalker.each(root) do |node|
    visits[node.class] += 1

    tracer = FallbackTracer.new
    scope_index[node].type_of(node, tracer: tracer)

    first_event = tracer.events.first
    next unless first_event && first_event.node_class == node.class

    unrecognized[node.class] += 1
    events << first_event
  end

  Result.new(visits: visits, unrecognized: unrecognized, events: events)
end