Module: Rigor::Inference::ScopeIndexer
- Defined in:
- lib/rigor/inference/scope_indexer.rb
Overview
Builds a per-node scope index for a Prism program by running ‘Rigor::Inference::StatementEvaluator` over the root and recording the entry scope visible at every node. Expression-interior nodes the evaluator does not specialise (call receivers, arguments, array/hash elements, …) inherit their nearest statement-y ancestor’s recorded scope, so a downstream caller that looks up the scope for any Prism node in the tree always gets the scope that was effectively visible at that point.
The CLI commands ‘rigor type-of` and `rigor type-scan` consume the index so that local-variable bindings established earlier in the program are visible to the typer when probing later nodes. Without the index, both commands would type every node under an empty scope and miss the constant-folding / dispatch precision that Slice 3 phase 2’s StatementEvaluator unlocks.
The returned object is an identity-comparing Hash:
“‘ruby index = Rigor::Inference::ScopeIndexer.index(program, default_scope: Scope.empty) index #=> the Rigor::Scope visible at that node “`
Nodes that are not part of the program subtree (e.g. synthesised virtual nodes that the caller looks up after the fact) yield the ‘default_scope`. The returned Hash is mutable in principle but callers MUST treat it as read-only; the indexer itself never exposes a way to update it past construction. rubocop:disable Metrics/ModuleLength
Class Method Summary collapse
-
.build_class_cvar_index(root, default_scope) ⇒ Object
Slice 7 phase 6 — class-cvar pre-pass.
-
.build_class_ivar_index(root, default_scope) ⇒ Object
Slice 7 phase 2.
-
.build_declaration_artifacts(root) ⇒ Object
Walks the program once for ‘Prism::ModuleNode` and `Prism::ClassNode`, recording the `Singleton` type for the outermost `constant_path` node of each declaration.
-
.build_discovered_methods(root) ⇒ Object
Slice 7 phase 12 — in-source method discovery pre-pass.
-
.build_in_source_constants(root, default_scope) ⇒ Object
Slice 7 phase 9 — in-source constant value pre-pass.
-
.build_program_global_index(root, default_scope) ⇒ Object
Slice 7 phase 6 — program-global pre-pass.
- .collect_def_cvar_writes(def_node, qualified_prefix, default_scope, accumulator) ⇒ Object
- .collect_def_ivar_writes(def_node, qualified_prefix, default_scope, accumulator) ⇒ Object
- .gather_cvar_writes(node, scope, class_name, accumulator) ⇒ Object
- .gather_global_writes(node, scope, accumulator) ⇒ Object
- .gather_ivar_writes(node, scope, class_name, accumulator) ⇒ Object
-
.index(root, default_scope:) ⇒ Hash{Prism::Node => Rigor::Scope}
Build the scope index for a Prism program subtree.
- .literal_method_name(node) ⇒ Object
-
.propagate(node, table, parent_scope) ⇒ Object
Walks ‘node`’s subtree DFS and fills in scope entries for every Prism node the StatementEvaluator did not visit (i.e. expression- interior nodes like the receiver/args of a CallNode).
- .qualified_name_for(constant_path_node) ⇒ Object
- .record_constant_write(node, qualified_prefix, default_scope, accumulator, base_name) ⇒ Object
- .record_cvar_write(node, scope, class_name, accumulator) ⇒ Object
- .record_declarations(node, qualified_prefix, identity_table, discovered) ⇒ Object
- .record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object
- .record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object
- .record_global_write(node, scope, accumulator) ⇒ Object
- .record_ivar_write(node, scope, class_name, accumulator) ⇒ Object
- .render_constant_path(node) ⇒ Object
- .walk_class_cvars(node, qualified_prefix, default_scope, accumulator) ⇒ Object
- .walk_class_ivars(node, qualified_prefix, default_scope, accumulator) ⇒ Object
-
.walk_constant_writes(node, qualified_prefix, default_scope, accumulator) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity.
-
.walk_methods(node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity.
Class Method Details
.build_class_cvar_index(root, default_scope) ⇒ Object
Slice 7 phase 6 — class-cvar pre-pass. Same shape as the ivar pre-pass but collects ‘Prism::ClassVariableWriteNode` writes inside ANY def body (instance or singleton) of the enclosing class, because Ruby cvars are shared across both facets. The resulting table is seeded into both instance and singleton method bodies through `Scope#class_cvars_for`.
195 196 197 198 199 |
# File 'lib/rigor/inference/scope_indexer.rb', line 195 def build_class_cvar_index(root, default_scope) accumulator = {} walk_class_cvars(root, [], default_scope, accumulator) accumulator.transform_values(&:freeze).freeze end |
.build_class_ivar_index(root, default_scope) ⇒ Object
Slice 7 phase 2. Builds the class-level ivar accumulator by walking every ‘Prism::ClassNode` / `Prism::ModuleNode` body, descending into each nested `Prism::DefNode`, and typing every `Prism::InstanceVariableWriteNode` rvalue under a scope that carries the appropriate `self_type` for that def (singleton vs instance). The rvalue is typed with NO local bindings — the pre-pass lacks statement-level threading — so `@x = 1` records `Constant` but `@x = some_local + 1` records `Dynamic` (since `some_local` is unbound at pre-pass time). Multiple writes to the same ivar union via `Type::Combinator.union`.
123 124 125 126 127 |
# File 'lib/rigor/inference/scope_indexer.rb', line 123 def build_class_ivar_index(root, default_scope) accumulator = {} walk_class_ivars(root, [], default_scope, accumulator) accumulator.transform_values(&:freeze).freeze end |
.build_declaration_artifacts(root) ⇒ Object
Walks the program once for ‘Prism::ModuleNode` and `Prism::ClassNode`, recording the `Singleton` type for the outermost `constant_path` node of each declaration. Inner segments of a `class Foo::Bar::Baz` path remain real references (resolved through the ordinary lexical walk), so we annotate ONLY the topmost path node. Nested declarations contribute their fully qualified path: `class A::B; class C; …` produces `A::B` for the outer and `A::B::C` for the inner.
400 401 402 403 404 405 |
# File 'lib/rigor/inference/scope_indexer.rb', line 400 def build_declaration_artifacts(root) identity_table = {}.compare_by_identity discovered = {} record_declarations(root, [], identity_table, discovered) [identity_table.freeze, discovered.freeze] end |
.build_discovered_methods(root) ⇒ Object
Slice 7 phase 12 — in-source method discovery pre-pass. Walks every class/module body and records the methods introduced via ‘Prism::DefNode` (instance + singleton) and via recognised `define_method(:name) { … }` calls. The returned table maps qualified class name to a `Hash[Symbol, :instance | :singleton]`.
329 330 331 332 333 |
# File 'lib/rigor/inference/scope_indexer.rb', line 329 def build_discovered_methods(root) accumulator = {} walk_methods(root, [], false, accumulator) accumulator.transform_values(&:freeze).freeze end |
.build_in_source_constants(root, default_scope) ⇒ Object
Slice 7 phase 9 — in-source constant value pre-pass. Walks the entire program (top-level AND inside class / module / def bodies) for ‘Prism::ConstantWriteNode` and `Prism::ConstantPathWriteNode`, types each rvalue, and accumulates by qualified name. Constants defined inside a class body are qualified with the surrounding class path; constants written via a path (`Foo::BAR = …`) use the rendered path as-is.
281 282 283 284 285 |
# File 'lib/rigor/inference/scope_indexer.rb', line 281 def build_in_source_constants(root, default_scope) accumulator = {} walk_constant_writes(root, [], default_scope, accumulator) accumulator.freeze end |
.build_program_global_index(root, default_scope) ⇒ Object
Slice 7 phase 6 — program-global pre-pass. Globals are process-wide so the accumulator is a flat ‘Hash[Symbol, Type::t]` populated from every `Prism::GlobalVariableWriteNode` in the program (top-level AND inside method bodies). The same accumulator is seeded into every method body and the top-level scope.
253 254 255 256 257 |
# File 'lib/rigor/inference/scope_indexer.rb', line 253 def build_program_global_index(root, default_scope) accumulator = {} gather_global_writes(root, default_scope, accumulator) accumulator.freeze end |
.collect_def_cvar_writes(def_node, qualified_prefix, default_scope, accumulator) ⇒ Object
222 223 224 225 226 227 228 |
# File 'lib/rigor/inference/scope_indexer.rb', line 222 def collect_def_cvar_writes(def_node, qualified_prefix, default_scope, accumulator) return if def_node.body.nil? || qualified_prefix.empty? class_name = qualified_prefix.join("::") body_scope = default_scope.with_self_type(Type::Combinator.nominal_of(class_name)) gather_cvar_writes(def_node.body, body_scope, class_name, accumulator) end |
.collect_def_ivar_writes(def_node, qualified_prefix, default_scope, accumulator) ⇒ Object
150 151 152 153 154 155 156 157 158 159 160 161 162 163 |
# File 'lib/rigor/inference/scope_indexer.rb', line 150 def collect_def_ivar_writes(def_node, qualified_prefix, default_scope, accumulator) return if def_node.body.nil? || qualified_prefix.empty? class_name = qualified_prefix.join("::") self_type = if def_node.receiver.is_a?(Prism::SelfNode) Type::Combinator.singleton_of(class_name) else Type::Combinator.nominal_of(class_name) end body_scope = default_scope.with_self_type(self_type) gather_ivar_writes(def_node.body, body_scope, class_name, accumulator) end |
.gather_cvar_writes(node, scope, class_name, accumulator) ⇒ Object
230 231 232 233 234 235 236 237 |
# File 'lib/rigor/inference/scope_indexer.rb', line 230 def gather_cvar_writes(node, scope, class_name, accumulator) return unless node.is_a?(Prism::Node) record_cvar_write(node, scope, class_name, accumulator) if node.is_a?(Prism::ClassVariableWriteNode) return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) } node.compact_child_nodes.each { |c| gather_cvar_writes(c, scope, class_name, accumulator) } end |
.gather_global_writes(node, scope, accumulator) ⇒ Object
259 260 261 262 263 264 |
# File 'lib/rigor/inference/scope_indexer.rb', line 259 def gather_global_writes(node, scope, accumulator) return unless node.is_a?(Prism::Node) record_global_write(node, scope, accumulator) if node.is_a?(Prism::GlobalVariableWriteNode) node.compact_child_nodes.each { |c| gather_global_writes(c, scope, accumulator) } end |
.gather_ivar_writes(node, scope, class_name, accumulator) ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/rigor/inference/scope_indexer.rb', line 168 def gather_ivar_writes(node, scope, class_name, accumulator) return unless node.is_a?(Prism::Node) record_ivar_write(node, scope, class_name, accumulator) if node.is_a?(Prism::InstanceVariableWriteNode) # Don't recurse into nested defs, classes, or modules; their # ivars belong to their own enclosing class. return if IVAR_BARRIER_NODES.any? { |klass| node.is_a?(klass) } node.compact_child_nodes.each { |c| gather_ivar_writes(c, scope, class_name, accumulator) } end |
.index(root, default_scope:) ⇒ Hash{Prism::Node => Rigor::Scope}
Build the scope index for a Prism program subtree.
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 |
# File 'lib/rigor/inference/scope_indexer.rb', line 52 def index(root, default_scope:) # rubocop:disable Metrics/AbcSize # Slice A-declarations. Build the declaration overrides # first so every scope handed to the StatementEvaluator # already carries the table; structural sharing through # `Scope#with_local` / `#with_fact` / `#with_self_type` # propagates it across every derived scope. declared_types, discovered_classes = build_declaration_artifacts(root) seeded_scope = default_scope .with_declared_types(declared_types) .with_discovered_classes(discovered_classes) # Slice 7 phase 2. Pre-pass over every class/module body # to collect the per-class ivar accumulator. Seeded after # declared_types so the rvalue typer in the pre-pass can # see declaration overrides. class_ivars = build_class_ivar_index(root, seeded_scope) seeded_scope = seeded_scope.with_class_ivars(class_ivars) # Slice 7 phase 6. Same pre-pass shape for cvars (per # class) and globals (program-wide). Globals are also # materialised into the top-level scope's `globals` map # so reads at the top level (and in CLI probes that do # not enter a method body) observe the precise type # without consulting the accumulator on every lookup. class_cvars = build_class_cvar_index(root, seeded_scope) seeded_scope = seeded_scope.with_class_cvars(class_cvars) program_globals = build_program_global_index(root, seeded_scope) seeded_scope = seeded_scope.with_program_globals(program_globals) program_globals.each { |name, type| seeded_scope = seeded_scope.with_global(name, type) } # Slice 7 phase 9. In-source constant value tracking. # Walks every ConstantWriteNode/ConstantPathWriteNode in # the program and types its rvalue under a scope that # carries the surrounding qualified prefix as # `self_type`, so the rvalue typer sees in-class # references resolve correctly. Multiple writes to the # same qualified name union via `Type::Combinator.union`. in_source_constants = build_in_source_constants(root, seeded_scope) seeded_scope = seeded_scope.with_in_source_constants(in_source_constants) # Slice 7 phase 12. In-source method discovery. Walks # every class/module body for `Prism::DefNode` and # recognised `define_method` calls and records the # introduced method names. `rigor check` consults the # table to suppress false positives for methods the # user has defined but no RBS sig describes. discovered_methods = build_discovered_methods(root) seeded_scope = seeded_scope.with_discovered_methods(discovered_methods) table = {}.compare_by_identity table.default = seeded_scope on_enter = ->(node, scope) { table[node] = scope unless table.key?(node) } StatementEvaluator.new(scope: seeded_scope, on_enter: on_enter).evaluate(root) propagate(root, table, seeded_scope) table end |
.literal_method_name(node) ⇒ Object
385 386 387 388 389 |
# File 'lib/rigor/inference/scope_indexer.rb', line 385 def literal_method_name(node) return nil unless node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode) node.unescaped&.to_sym end |
.propagate(node, table, parent_scope) ⇒ Object
Walks ‘node`’s subtree DFS and fills in scope entries for every Prism node the StatementEvaluator did not visit (i.e. expression- interior nodes like the receiver/args of a CallNode). Those nodes inherit their nearest recorded ancestor’s scope.
452 453 454 455 456 457 458 459 460 461 462 463 464 |
# File 'lib/rigor/inference/scope_indexer.rb', line 452 def propagate(node, table, parent_scope) return unless node.is_a?(Prism::Node) current_scope = if table.key?(node) table[node] else table[node] = parent_scope parent_scope end node.compact_child_nodes.each { |child| propagate(child, table, current_scope) } end |
.qualified_name_for(constant_path_node) ⇒ Object
429 430 431 432 433 434 435 436 |
# File 'lib/rigor/inference/scope_indexer.rb', line 429 def qualified_name_for(constant_path_node) case constant_path_node when Prism::ConstantReadNode constant_path_node.name.to_s when Prism::ConstantPathNode render_constant_path(constant_path_node) end end |
.record_constant_write(node, qualified_prefix, default_scope, accumulator, base_name) ⇒ Object
312 313 314 315 316 317 318 319 320 321 |
# File 'lib/rigor/inference/scope_indexer.rb', line 312 def record_constant_write(node, qualified_prefix, default_scope, accumulator, base_name) full = qualified_prefix.empty? ? base_name : "#{qualified_prefix.join('::')}::#{base_name}" body_scope = default_scope unless qualified_prefix.empty? body_scope = body_scope.with_self_type(Type::Combinator.singleton_of(qualified_prefix.join("::"))) end rvalue_type = body_scope.type_of(node.value) existing = accumulator[full] accumulator[full] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type end |
.record_cvar_write(node, scope, class_name, accumulator) ⇒ Object
239 240 241 242 243 244 245 |
# File 'lib/rigor/inference/scope_indexer.rb', line 239 def record_cvar_write(node, scope, class_name, accumulator) rvalue_type = scope.type_of(node.value) accumulator[class_name] ||= {} existing = accumulator[class_name][node.name] accumulator[class_name][node.name] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type end |
.record_declarations(node, qualified_prefix, identity_table, discovered) ⇒ Object
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 |
# File 'lib/rigor/inference/scope_indexer.rb', line 407 def record_declarations(node, qualified_prefix, identity_table, discovered) return unless node.is_a?(Prism::Node) case node when Prism::ModuleNode, Prism::ClassNode name = qualified_name_for(node.constant_path) if name full = (qualified_prefix + [name]).join("::") singleton = Type::Combinator.singleton_of(full) identity_table[node.constant_path] = singleton discovered[full] = singleton child_prefix = qualified_prefix + [name] record_declarations(node.body, child_prefix, identity_table, discovered) if node.body return end end node.compact_child_nodes.each do |child| record_declarations(child, qualified_prefix, identity_table, discovered) end end |
.record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object
363 364 365 366 367 368 369 370 |
# File 'lib/rigor/inference/scope_indexer.rb', line 363 def record_def_method(def_node, qualified_prefix, in_singleton_class, accumulator) return if qualified_prefix.empty? class_name = qualified_prefix.join("::") kind = def_node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance accumulator[class_name] ||= {} accumulator[class_name][def_node.name] = kind end |
.record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object
372 373 374 375 376 377 378 379 380 381 382 383 |
# File 'lib/rigor/inference/scope_indexer.rb', line 372 def record_define_method(call_node, qualified_prefix, in_singleton_class, accumulator) return if qualified_prefix.empty? return if call_node.arguments.nil? || call_node.arguments.arguments.empty? first_arg = call_node.arguments.arguments.first method_name = literal_method_name(first_arg) return if method_name.nil? class_name = qualified_prefix.join("::") accumulator[class_name] ||= {} accumulator[class_name][method_name] = in_singleton_class ? :singleton : :instance end |
.record_global_write(node, scope, accumulator) ⇒ Object
266 267 268 269 270 271 |
# File 'lib/rigor/inference/scope_indexer.rb', line 266 def record_global_write(node, scope, accumulator) rvalue_type = scope.type_of(node.value) existing = accumulator[node.name] accumulator[node.name] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type end |
.record_ivar_write(node, scope, class_name, accumulator) ⇒ Object
180 181 182 183 184 185 186 |
# File 'lib/rigor/inference/scope_indexer.rb', line 180 def record_ivar_write(node, scope, class_name, accumulator) rvalue_type = scope.type_of(node.value) accumulator[class_name] ||= {} existing = accumulator[class_name][node.name] accumulator[class_name][node.name] = existing ? Type::Combinator.union(existing, rvalue_type) : rvalue_type end |
.render_constant_path(node) ⇒ Object
438 439 440 441 442 443 444 445 446 |
# File 'lib/rigor/inference/scope_indexer.rb', line 438 def render_constant_path(node) prefix = case node.parent when Prism::ConstantReadNode then "#{node.parent.name}::" when Prism::ConstantPathNode then "#{render_constant_path(node.parent)}::" else "" end "#{prefix}#{node.name}" end |
.walk_class_cvars(node, qualified_prefix, default_scope, accumulator) ⇒ Object
201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/rigor/inference/scope_indexer.rb', line 201 def walk_class_cvars(node, qualified_prefix, default_scope, accumulator) return unless node.is_a?(Prism::Node) case node when Prism::ClassNode, Prism::ModuleNode name = qualified_name_for(node.constant_path) if name child_prefix = qualified_prefix + [name] walk_class_cvars(node.body, child_prefix, default_scope, accumulator) if node.body return end when Prism::DefNode collect_def_cvar_writes(node, qualified_prefix, default_scope, accumulator) return end node.compact_child_nodes.each do |child| walk_class_cvars(child, qualified_prefix, default_scope, accumulator) end end |
.walk_class_ivars(node, qualified_prefix, default_scope, accumulator) ⇒ Object
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 |
# File 'lib/rigor/inference/scope_indexer.rb', line 129 def walk_class_ivars(node, qualified_prefix, default_scope, accumulator) return unless node.is_a?(Prism::Node) case node when Prism::ClassNode, Prism::ModuleNode name = qualified_name_for(node.constant_path) if name child_prefix = qualified_prefix + [name] walk_class_ivars(node.body, child_prefix, default_scope, accumulator) if node.body return end when Prism::DefNode collect_def_ivar_writes(node, qualified_prefix, default_scope, accumulator) return end node.compact_child_nodes.each do |child| walk_class_ivars(child, qualified_prefix, default_scope, accumulator) end end |
.walk_constant_writes(node, qualified_prefix, default_scope, accumulator) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'lib/rigor/inference/scope_indexer.rb', line 287 def walk_constant_writes(node, qualified_prefix, default_scope, accumulator) # rubocop:disable Metrics/CyclomaticComplexity return unless node.is_a?(Prism::Node) case node when Prism::ClassNode, Prism::ModuleNode name = qualified_name_for(node.constant_path) if name child_prefix = qualified_prefix + [name] walk_constant_writes(node.body, child_prefix, default_scope, accumulator) if node.body return end when Prism::ConstantWriteNode record_constant_write(node, qualified_prefix, default_scope, accumulator, node.name.to_s) return when Prism::ConstantPathWriteNode full = qualified_name_for(node.target) record_constant_write(node, [], default_scope, accumulator, full) if full return end node.compact_child_nodes.each do |child| walk_constant_writes(child, qualified_prefix, default_scope, accumulator) end end |
.walk_methods(node, qualified_prefix, in_singleton_class, accumulator) ⇒ Object
rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 |
# File 'lib/rigor/inference/scope_indexer.rb', line 335 def walk_methods(node, qualified_prefix, in_singleton_class, accumulator) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity return unless node.is_a?(Prism::Node) case node when Prism::ClassNode, Prism::ModuleNode name = qualified_name_for(node.constant_path) if name child_prefix = qualified_prefix + [name] walk_methods(node.body, child_prefix, false, accumulator) if node.body return end when Prism::SingletonClassNode if node.expression.is_a?(Prism::SelfNode) && node.body walk_methods(node.body, qualified_prefix, true, accumulator) return end when Prism::DefNode record_def_method(node, qualified_prefix, in_singleton_class, accumulator) return when Prism::CallNode record_define_method(node, qualified_prefix, in_singleton_class, accumulator) if node.name == :define_method end node.compact_child_nodes.each do |child| walk_methods(child, qualified_prefix, in_singleton_class, accumulator) end end |