Class: Rigor::Inference::PrecisionScanner

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/inference/precision_scanner.rb

Overview

Measures the *type quality* of inferred expressions — not whether the engine recognises an AST node class (that is ‘CoverageScanner`’s job), but whether the type it produces carries useful static information.

Each visited expression node is classified into one of eight precision tiers (non-expression syntax nodes — argument / parameter lists, parameter declarations, hash pairs, statement wrappers, clause headers — are skipped; see NON_EXPRESSION_NODE_TYPES):

:constant         — Constant[T]: literal value known exactly
:nominal          — Nominal/Singleton: class identity known
:shaped           — Tuple/HashShape/IntegerRange/App: structure known
:refined          — Refined: narrowed by a predicate/assertion
:bot              — Bot: unreachable branch (definitively precise)
:dynamic_specific — Dynamic[X] where X is not Top: origin partial
:dynamic_top      — Dynamic[Top]: completely opaque (the "untyped" hole)
:top              — Top: universal supertype (no information)

The summary exposes ‘precision_ratio` (constant+nominal+shaped+refined+bot over total) and `opaque_ratio` (dynamic_top+top over total).

For Union types the worst member tier is used, since the union is only as precise as its least-precise constituent. Intersection uses the best member (the most specific side wins). Difference follows its base type.

Defined Under Namespace

Classes: FileResult

Constant Summary collapse

TIERS =
%i[
  constant nominal shaped refined bot
  dynamic_specific dynamic_top top
].freeze
NON_EXPRESSION_NODE_TYPES =

Prism node classes that do not denote a value-producing expression, so typing them is meaningless — they have no runtime value to carry a type. Counting them (they always fall to the ‘dynamic_top` fallback) silently diluted the precision ratio: on a real survey target (shugo/textbringer) they were ~49% of every “opaque” node, dragging the headline number ~13 points below the true expression-level precision. We exclude them from BOTH numerator and denominator so the ratio measures what it claims to — the type quality of actual expressions.

The set is deliberately CONSERVATIVE: only nodes that are unambiguously non-expressions in Ruby’s grammar are listed —argument / parameter list containers and the parameter declarations inside them; the ‘key => value` pair node (its key and value are themselves walked and counted); the program / statements sequence wrappers (their value is the last child, already counted — listing them avoids double-counting); and the clause-header nodes whose body, not the header, carries the value. Anything that could be a value expression (`BlockNode`, `BeginNode`, `ImplicitNode`, `ParenthesesNode`, splats, …) is left in so a genuine inference gap stays visible.

Compared by class NAME so a Prism version that lacks one of the newer node classes does not break loading.

%w[
  Prism::ProgramNode
  Prism::StatementsNode
  Prism::ArgumentsNode
  Prism::BlockArgumentNode
  Prism::ParametersNode
  Prism::BlockParametersNode
  Prism::NumberedParametersNode
  Prism::ItParametersNode
  Prism::KeywordHashNode
  Prism::RequiredParameterNode
  Prism::OptionalParameterNode
  Prism::RestParameterNode
  Prism::KeywordRestParameterNode
  Prism::BlockParameterNode
  Prism::RequiredKeywordParameterNode
  Prism::OptionalKeywordParameterNode
  Prism::ForwardingParameterNode
  Prism::NoKeywordsParameterNode
  Prism::ImplicitRestNode
  Prism::AssocNode
  Prism::AssocSplatNode
  Prism::WhenNode
  Prism::InNode
  Prism::ElseNode
  Prism::EnsureNode
  Prism::RescueNode
].to_set.freeze

Instance Method Summary collapse

Constructor Details

#initialize(scope: nil) ⇒ PrecisionScanner

Returns a new instance of PrecisionScanner.

Parameters:

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

    base scope for type inference.



135
136
137
# File 'lib/rigor/inference/precision_scanner.rb', line 135

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

Instance Method Details

#scan(root) ⇒ FileResult

Parameters:

  • root (Prism::Node)

    the parsed AST

Returns:



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'lib/rigor/inference/precision_scanner.rb', line 141

def scan(root)
  scope_index = ScopeIndexer.index(root, default_scope: @scope)
  tier_counts = TIERS.to_h { |t| [t, 0] }
  total = 0

  Source::NodeWalker.each(root) do |node|
    next if NON_EXPRESSION_NODE_TYPES.include?(node.class.name)

    type = scope_index[node].type_of(node)
    tier = classify(type)
    tier_counts[tier] += 1
    total += 1
  end

  FileResult.new(total: total, tier_counts: tier_counts)
end