Class: Rigor::Scope
- Inherits:
-
Object
- Object
- Rigor::Scope
- Defined in:
- lib/rigor/scope.rb,
lib/rigor/scope/discovery_index.rb
Overview
Immutable analyzer scope: holds local-variable bindings and a reference to the surrounding Environment. State changes return new scopes through explicit transition methods (#with_local). The central query is #type_of(node), the Rigor counterpart of PHPStan’s $scope->getType($node).
See docs/internal-spec/inference-engine.md for the binding contract. rubocop:disable Metrics/ClassLength,Metrics/ParameterLists
Defined Under Namespace
Classes: ChainKey, DiscoveryIndex, IndexedKey
Instance Attribute Summary collapse
-
#cvars ⇒ Object
readonly
Returns the value of attribute cvars.
-
#declaration_sourced ⇒ Object
readonly
Returns the value of attribute declaration_sourced.
-
#discovery ⇒ Object
readonly
Returns the value of attribute discovery.
-
#dynamic_origins ⇒ Object
readonly
Returns the value of attribute dynamic_origins.
-
#environment ⇒ Object
readonly
Returns the value of attribute environment.
-
#fact_store ⇒ Object
readonly
Returns the value of attribute fact_store.
-
#globals ⇒ Object
readonly
Returns the value of attribute globals.
-
#indexed_narrowings ⇒ Object
readonly
Returns the value of attribute indexed_narrowings.
-
#ivars ⇒ Object
readonly
Returns the value of attribute ivars.
-
#locals ⇒ Object
readonly
Returns the value of attribute locals.
-
#method_chain_narrowings ⇒ Object
readonly
Returns the value of attribute method_chain_narrowings.
-
#self_type ⇒ Object
readonly
Returns the value of attribute self_type.
-
#source_path ⇒ Object
readonly
Returns the value of attribute source_path.
-
#struct_fold_safe_locals ⇒ Object
readonly
Returns the value of attribute struct_fold_safe_locals.
Class Method Summary collapse
Instance Method Summary collapse
- #==(other) ⇒ Object (also: #eql?)
- #class_cvars ⇒ Object
-
#class_cvars_for(class_name) ⇒ Object
Slice 7 phase 6 — class-level cvar accumulator (same shape as ‘class_ivars` but populated from `Prism::ClassVariableWriteNode` writes, and seeded on BOTH instance and singleton method bodies because Ruby cvars are visible from each).
- #class_ivars ⇒ Object
-
#class_ivars_for(class_name) ⇒ Object
Slice 7 phase 2 — class-level ivar accumulator.
- #cvar(name) ⇒ Object
-
#data_member_layout(class_name) ⇒ Object
ADR-48 — per-class table mapping a fully qualified class name to its ordered ‘Data.define` / `Struct.new` member-name list.
- #data_member_layouts ⇒ Object
-
#declaration_sourced?(kind, name) ⇒ Boolean
ADR-58 WD1 — true when ‘(kind, name)`’s binding optionality is purely declaration-sourced (no flow-live write/narrowing has touched it).
-
#declared_types ⇒ Object
ADR-53 Track A — the seed-time discovery tables live on the DiscoveryIndex the scope carries by a single reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the extraction.
- #discovered_class_sources ⇒ Object
- #discovered_classes ⇒ Object
- #discovered_def_nodes ⇒ Object
- #discovered_def_sources ⇒ Object
- #discovered_includes ⇒ Object
-
#discovered_method?(class_name, method_name, kind) ⇒ Boolean
Slice 7 phase 12 — in-source method discovery.
- #discovered_method_visibilities ⇒ Object
-
#discovered_method_visibility(class_name, method_name) ⇒ Object
v0.1.2 — per-class table mapping ‘method_name (Symbol) → :public | :private | :protected`.
- #discovered_methods ⇒ Object
- #discovered_singleton_def_nodes ⇒ Object
- #discovered_superclasses ⇒ Object
-
#evaluate(node, tracer: nil) ⇒ Object
Statement-level evaluation: returns the pair ‘[type, scope’]‘ where `type` is what the node produces and `scope’‘ is the scope observable after the node has run.
- #facts_for(target: nil, bucket: nil) ⇒ Object
- #forget_match_globals ⇒ Object
- #global(name) ⇒ Object
- #hash ⇒ Object
- #in_source_constants ⇒ Object
-
#includes_of(class_name) ⇒ Object
ADR-24 slice 2 — per-class/module table mapping a fully qualified user class or module to the list of module names it ‘include`s / `prepend`s, AS WRITTEN at the mixin call.
- #indexed_narrowing(receiver_kind, receiver_name, key) ⇒ Object
-
#initialize(environment:, locals:, fact_store: Analysis::FactStore.empty, self_type: nil, ivars: EMPTY_VAR_BINDINGS, cvars: EMPTY_VAR_BINDINGS, globals: EMPTY_VAR_BINDINGS, discovery: DiscoveryIndex::EMPTY, indexed_narrowings: EMPTY_INDEXED_NARROWINGS, method_chain_narrowings: EMPTY_CHAIN_NARROWINGS, declaration_sourced: EMPTY_DECLARATION_SOURCED, source_path: nil, struct_fold_safe_locals: EMPTY_FOLD_SAFE, dynamic_origins: {}.compare_by_identity) ⇒ Scope
constructor
A new instance of Scope.
-
#ivar(name) ⇒ Object
Slice 7 phase 1 — instance/class/global variable bindings.
-
#join(other) ⇒ Object
Joins this scope with another at a control-flow merge point.
- #local(name) ⇒ Object
- #local_facts(name, bucket: nil) ⇒ Object
-
#method_chain_narrowing(receiver_kind, receiver_name, method_name) ⇒ Object
Closes the “stable receiver method-chain narrowing” gap (ROADMAP § Future cycles / Type-language / engine — “Method-call receiver narrowing across stable receivers”; 2026-05-28 Redmine survey).
-
#param_inferred_types ⇒ Object
ADR-67 WD3 — call-site-inferred parameter types, keyed by ‘[class_name, method_name, kind]`.
- #program_globals ⇒ Object
- #record_dynamic_origin(node, cause) ⇒ Object
-
#seed_declaration_sourced_ivar(name, type) ⇒ Object
ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index.
-
#singleton_def_for(class_name, method_name) ⇒ Object
Module-singleton call resolution (ADR-57 follow-up) — companion of #user_def_for for SINGLETON-side defs (‘def self.x`, `def Foo.x`, `class << self` bodies, and `module_function` defs).
-
#struct_fold_safe?(name) ⇒ Boolean
True when ‘name`’s ‘Struct` member reads are fold-safe in this body (the local is provably never mutated / aliased / escaped).
-
#struct_member_layout(class_name) ⇒ Object
ADR-48 Struct follow-up — the ‘{ members:, keyword_init: }` layout recorded for a `Struct.new(…)`-defined class, in the constant form (`Point = Struct.new(:x, :y)`) and the named-subclass form (`class Point < Struct.new(:x, :y)`).
- #struct_member_layouts ⇒ Object
-
#superclass_of(class_name) ⇒ Object
ADR-24 slice 2 — per-class table mapping a fully qualified user-class name to its superclass name AS WRITTEN at the ‘class Foo < Bar` declaration (`“Bar”`, possibly a qualified `“A::B”`).
-
#top_level_def_for(method_name) ⇒ Object
v0.0.3 A — top-level def lookup for implicit-self calls.
-
#toplevel? ⇒ Boolean
ADR-34 § “Decision” — predicate identifying a toplevel-shaped scope (no enclosing ‘class` / `module` body).
- #type_of(node, tracer: nil) ⇒ Object
-
#user_def_for(class_name, method_name) ⇒ Object
v0.0.2 #5 — per-class table mapping ‘method_name (Symbol) → Prism::DefNode`.
-
#user_def_site_for(class_name, method_name) ⇒ Object
Companion to #user_def_for: returns the ‘“path:line”` where the project defines `class_name#method_name` (instance-side), or nil.
- #with_cvar(name, type) ⇒ Object
-
#with_declaration_sourced_local(name, type) ⇒ Object
ADR-58 WD1 — a local assignment ‘r = @right` whose RHS is a pure read of a declaration-sourced ivar inherits the mark, so the survey’s exact rotation/traversal shape (‘r = @right; r.key`) does not fire.
-
#with_discovery(index) ⇒ Object
ADR-53 Track A — swaps the whole discovery index in one transition.
- #with_fact(fact) ⇒ Object
- #with_global(name, type) ⇒ Object
- #with_indexed_narrowing(receiver_kind, receiver_name, key, type) ⇒ Object
- #with_ivar(name, type) ⇒ Object
- #with_local(name, type) ⇒ Object
-
#with_local_declaration_mark(name) ⇒ Object
ADR-58 WD1 — re-stamp the local mark on a scope produced by ‘with_local` (which always drops it).
- #with_method_chain_narrowing(receiver_kind, receiver_name, method_name, type) ⇒ Object
-
#with_self_type(type) ⇒ Object
Slice A-engine.
-
#with_source_path(path) ⇒ Object
ADR-28 / ADR-52 slice 5a — per-file source path carried on the scope.
-
#with_struct_fold_safe(locals) ⇒ Object
ADR-48 Struct slice 3 — installs the per-body fold-safe-local set (Inference::StructFoldSafety).
- #without_indexed_narrowing(receiver_kind, receiver_name, key) ⇒ Object
- #without_indexed_narrowings_for(receiver_kind, receiver_name) ⇒ Object
- #without_method_chain_narrowing(receiver_kind, receiver_name, method_name) ⇒ Object
- #without_method_chain_narrowings_for(receiver_kind, receiver_name) ⇒ Object
Constructor Details
#initialize(environment:, locals:, fact_store: Analysis::FactStore.empty, self_type: nil, ivars: EMPTY_VAR_BINDINGS, cvars: EMPTY_VAR_BINDINGS, globals: EMPTY_VAR_BINDINGS, discovery: DiscoveryIndex::EMPTY, indexed_narrowings: EMPTY_INDEXED_NARROWINGS, method_chain_narrowings: EMPTY_CHAIN_NARROWINGS, declaration_sourced: EMPTY_DECLARATION_SOURCED, source_path: nil, struct_fold_safe_locals: EMPTY_FOLD_SAFE, dynamic_origins: {}.compare_by_identity) ⇒ Scope
Returns a new instance of Scope.
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
# File 'lib/rigor/scope.rb', line 131 def initialize( environment:, locals:, fact_store: Analysis::FactStore.empty, self_type: nil, ivars: EMPTY_VAR_BINDINGS, cvars: EMPTY_VAR_BINDINGS, globals: EMPTY_VAR_BINDINGS, discovery: DiscoveryIndex::EMPTY, indexed_narrowings: EMPTY_INDEXED_NARROWINGS, method_chain_narrowings: EMPTY_CHAIN_NARROWINGS, declaration_sourced: EMPTY_DECLARATION_SOURCED, source_path: nil, struct_fold_safe_locals: EMPTY_FOLD_SAFE, dynamic_origins: {}.compare_by_identity ) @environment = environment @locals = locals @fact_store = fact_store @self_type = self_type @ivars = ivars @cvars = cvars @globals = globals @discovery = discovery @indexed_narrowings = indexed_narrowings @method_chain_narrowings = method_chain_narrowings @declaration_sourced = declaration_sourced @source_path = source_path @struct_fold_safe_locals = struct_fold_safe_locals @dynamic_origins = dynamic_origins freeze end |
Instance Attribute Details
#cvars ⇒ Object (readonly)
Returns the value of attribute cvars.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def cvars @cvars end |
#declaration_sourced ⇒ Object (readonly)
Returns the value of attribute declaration_sourced.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def declaration_sourced @declaration_sourced end |
#discovery ⇒ Object (readonly)
Returns the value of attribute discovery.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def discovery @discovery end |
#dynamic_origins ⇒ Object (readonly)
Returns the value of attribute dynamic_origins.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def dynamic_origins @dynamic_origins end |
#environment ⇒ Object (readonly)
Returns the value of attribute environment.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def environment @environment end |
#fact_store ⇒ Object (readonly)
Returns the value of attribute fact_store.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def fact_store @fact_store end |
#globals ⇒ Object (readonly)
Returns the value of attribute globals.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def globals @globals end |
#indexed_narrowings ⇒ Object (readonly)
Returns the value of attribute indexed_narrowings.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def indexed_narrowings @indexed_narrowings end |
#ivars ⇒ Object (readonly)
Returns the value of attribute ivars.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def ivars @ivars end |
#locals ⇒ Object (readonly)
Returns the value of attribute locals.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def locals @locals end |
#method_chain_narrowings ⇒ Object (readonly)
Returns the value of attribute method_chain_narrowings.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def method_chain_narrowings @method_chain_narrowings end |
#self_type ⇒ Object (readonly)
Returns the value of attribute self_type.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def self_type @self_type end |
#source_path ⇒ Object (readonly)
Returns the value of attribute source_path.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def source_path @source_path end |
#struct_fold_safe_locals ⇒ Object (readonly)
Returns the value of attribute struct_fold_safe_locals.
22 23 24 |
# File 'lib/rigor/scope.rb', line 22 def struct_fold_safe_locals @struct_fold_safe_locals end |
Class Method Details
.empty(environment: Environment.default, source_path: nil) ⇒ Object
120 121 122 123 |
# File 'lib/rigor/scope.rb', line 120 def empty(environment: Environment.default, source_path: nil) new(environment: environment, locals: {}.freeze, fact_store: Analysis::FactStore.empty, source_path: source_path) end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
698 699 700 701 702 703 704 705 706 707 708 709 710 |
# File 'lib/rigor/scope.rb', line 698 def ==(other) other.is_a?(Scope) && environment.equal?(other.environment) && @locals == other.locals && fact_store == other.fact_store && self_type == other.self_type && @ivars == other.ivars && @cvars == other.cvars && @globals == other.globals && @indexed_narrowings == other.indexed_narrowings && @method_chain_narrowings == other.method_chain_narrowings && @declaration_sourced == other.declaration_sourced end |
#class_cvars ⇒ Object
42 |
# File 'lib/rigor/scope.rb', line 42 def class_cvars = @discovery.class_cvars |
#class_cvars_for(class_name) ⇒ Object
Slice 7 phase 6 — class-level cvar accumulator (same shape as ‘class_ivars` but populated from `Prism::ClassVariableWriteNode` writes, and seeded on BOTH instance and singleton method bodies because Ruby cvars are visible from each).
350 351 352 353 354 |
# File 'lib/rigor/scope.rb', line 350 def class_cvars_for(class_name) return EMPTY_VAR_BINDINGS if class_name.nil? @discovery.class_cvars[class_name.to_s] || EMPTY_VAR_BINDINGS end |
#class_ivars ⇒ Object
41 |
# File 'lib/rigor/scope.rb', line 41 def class_ivars = @discovery.class_ivars |
#class_ivars_for(class_name) ⇒ Object
Slice 7 phase 2 — class-level ivar accumulator. Keyed by the qualified class name (e.g. ‘“Rigor::Scope”`); the value is a `Hash[Symbol, Type::t]` of every ivar that appears as a write target inside any def body of that class. `StatementEvaluator#build_method_entry_scope` seeds the method body’s ‘ivars` map from this table so a `def get; @x; end` reads the type written in a sibling `def init; @x = 1; end`.
‘ScopeIndexer` populates the table once at index time through a separate pre-pass over the program. The map is frozen and shared by structural reference across every derived scope.
340 341 342 343 344 |
# File 'lib/rigor/scope.rb', line 340 def class_ivars_for(class_name) return EMPTY_VAR_BINDINGS if class_name.nil? @discovery.class_ivars[class_name.to_s] || EMPTY_VAR_BINDINGS end |
#cvar(name) ⇒ Object
249 250 251 |
# File 'lib/rigor/scope.rb', line 249 def cvar(name) @cvars[name.to_sym] end |
#data_member_layout(class_name) ⇒ Object
ADR-48 — per-class table mapping a fully qualified class name to its ordered ‘Data.define` / `Struct.new` member-name list. Populated by `ScopeIndexer` for both the constant-assigned form (`Point = Data.define(:x, :y)`) and the named-subclass form (`class Point < Data.define(:x, :y)`). Consumed by Inference::MethodDispatcher::DataFolding so `Point.new(…)` on a `Singleton` receiver materialises a precise member instance. Returns nil when the class has no recorded layout.
513 514 515 516 517 518 519 520 |
# File 'lib/rigor/scope.rb', line 513 def data_member_layout(class_name) layout = @discovery.data_member_layouts[class_name.to_s] # Record the ancestry dependency only on a hit — DataFolding consults # this for every `Singleton[*].new`, and a miss (the common case: an # ordinary class) must not manufacture a spurious cross-file edge. record_class_dependency(class_name) if layout && Analysis::DependencyRecorder.active? layout end |
#data_member_layouts ⇒ Object
54 |
# File 'lib/rigor/scope.rb', line 54 def data_member_layouts = @discovery.data_member_layouts |
#declaration_sourced?(kind, name) ⇒ Boolean
ADR-58 WD1 — true when ‘(kind, name)`’s binding optionality is purely declaration-sourced (no flow-live write/narrowing has touched it).
298 299 300 |
# File 'lib/rigor/scope.rb', line 298 def declaration_sourced?(kind, name) @declaration_sourced.include?([kind.to_sym, name.to_sym]) end |
#declared_types ⇒ Object
ADR-53 Track A — the seed-time discovery tables live on the DiscoveryIndex the scope carries by a single reference; the per-table readers stay on Scope so engine call sites and plugins are unaffected by the extraction. The whole index is swapped in one transition through #with_discovery.
‘declared_types` carries the identity-comparing `Prism::Node => Rigor::Type` declaration overrides `ExpressionTyper#type_of(node)` MUST consult before any other dispatch (a `module Foo` / `class Bar` header types as `Singleton[<qualified path>]` rather than `Dynamic`).
40 |
# File 'lib/rigor/scope.rb', line 40 def declared_types = @discovery.declared_types |
#discovered_class_sources ⇒ Object
53 |
# File 'lib/rigor/scope.rb', line 53 def discovered_class_sources = @discovery.discovered_class_sources |
#discovered_classes ⇒ Object
44 |
# File 'lib/rigor/scope.rb', line 44 def discovered_classes = @discovery.discovered_classes |
#discovered_def_nodes ⇒ Object
47 |
# File 'lib/rigor/scope.rb', line 47 def discovered_def_nodes = @discovery.discovered_def_nodes |
#discovered_def_sources ⇒ Object
49 |
# File 'lib/rigor/scope.rb', line 49 def discovered_def_sources = @discovery.discovered_def_sources |
#discovered_includes ⇒ Object
52 |
# File 'lib/rigor/scope.rb', line 52 def discovered_includes = @discovery.discovered_includes |
#discovered_method?(class_name, method_name, kind) ⇒ Boolean
Slice 7 phase 12 — in-source method discovery. Maps a qualified class name to a ‘Hash[Symbol, Symbol]` of `method_name => :instance | :singleton`. Populated by `ScopeIndexer` from every `Prism::DefNode` and recognised `define_method` invocation inside class/module bodies. The `rigor check` undefined-method and wrong-arity rules consult this map to suppress diagnostics for methods the user has defined dynamically, even when no RBS sig describes them.
365 366 367 368 369 370 |
# File 'lib/rigor/scope.rb', line 365 def discovered_method?(class_name, method_name, kind) table = @discovery.discovered_methods[class_name.to_s] return false unless table table[method_name.to_sym] == kind end |
#discovered_method_visibilities ⇒ Object
50 |
# File 'lib/rigor/scope.rb', line 50 def discovered_method_visibilities = @discovery.discovered_method_visibilities |
#discovered_method_visibility(class_name, method_name) ⇒ Object
v0.1.2 — per-class table mapping ‘method_name (Symbol) →:public | :private | :protected`. Populated by `ScopeIndexer` for every `def` it sees inside a class body, with the visibility taken from the surrounding `private` / `protected` / `public` modifier state plus any post-hoc `private :name, …` named-argument calls. Consumed by the `def.method-visibility-mismatch` rule so explicit-non-self calls to a private method surface a diagnostic.
575 576 577 578 579 580 |
# File 'lib/rigor/scope.rb', line 575 def discovered_method_visibility(class_name, method_name) table = @discovery.discovered_method_visibilities[class_name.to_s] return nil unless table table[method_name.to_sym] end |
#discovered_methods ⇒ Object
46 |
# File 'lib/rigor/scope.rb', line 46 def discovered_methods = @discovery.discovered_methods |
#discovered_singleton_def_nodes ⇒ Object
48 |
# File 'lib/rigor/scope.rb', line 48 def discovered_singleton_def_nodes = @discovery.discovered_singleton_def_nodes |
#discovered_superclasses ⇒ Object
51 |
# File 'lib/rigor/scope.rb', line 51 def discovered_superclasses = @discovery.discovered_superclasses |
#evaluate(node, tracer: nil) ⇒ Object
Statement-level evaluation: returns the pair ‘[type, scope’]‘ where `type` is what the node produces and `scope’‘ is the scope observable after the node has run. The receiver scope is never mutated. See Inference::StatementEvaluator for the catalogue of nodes that thread scope; everything else defers to #type_of and returns the receiver scope unchanged.
673 674 675 |
# File 'lib/rigor/scope.rb', line 673 def evaluate(node, tracer: nil) Inference::StatementEvaluator.new(scope: self, tracer: tracer).evaluate(node) end |
#facts_for(target: nil, bucket: nil) ⇒ Object
655 656 657 |
# File 'lib/rigor/scope.rb', line 655 def facts_for(target: nil, bucket: nil) fact_store.facts_for(target: target, bucket: bucket) end |
#forget_match_globals ⇒ Object
321 322 323 324 325 |
# File 'lib/rigor/scope.rb', line 321 def forget_match_globals return self unless @globals.keys.any? { |k| MATCH_DATA_GLOBALS.include?(k) } rebuild(globals: @globals.except(*MATCH_DATA_GLOBALS).freeze) end |
#global(name) ⇒ Object
253 254 255 |
# File 'lib/rigor/scope.rb', line 253 def global(name) @globals[name.to_sym] end |
#hash ⇒ Object
713 714 715 |
# File 'lib/rigor/scope.rb', line 713 def hash [Scope, environment.object_id, @locals, fact_store, self_type, @ivars, @cvars, @globals].hash end |
#in_source_constants ⇒ Object
45 |
# File 'lib/rigor/scope.rb', line 45 def in_source_constants = @discovery.in_source_constants |
#includes_of(class_name) ⇒ Object
ADR-24 slice 2 — per-class/module table mapping a fully qualified user class or module to the list of module names it ‘include`s / `prepend`s, AS WRITTEN at the mixin call. Populated by `ScopeIndexer` (per-file plus the cross-file pre-pass) and consumed by `ExpressionTyper#resolve_user_def_through_ancestors` so an implicit-self call resolves against an included module’s ‘def`s, not just the superclass chain. As-written names are resolved to qualified classes at walk time.
545 546 547 548 |
# File 'lib/rigor/scope.rb', line 545 def includes_of(class_name) record_class_dependency(class_name) if Analysis::DependencyRecorder.active? @discovery.discovered_includes[class_name.to_s] || [] end |
#indexed_narrowing(receiver_kind, receiver_name, key) ⇒ Object
Closes the “‘params ||= []; params << x`” precision gap (ROADMAP § Type-language / engine — indexed-collection narrowing through `Hash ||= default`). After `receiver ||= default`, the next read at `receiver` is known non-nil; recording the post-`||=` type keyed on `(receiver_kind, receiver_name, literal_key)` lets the ExpressionTyper’s ‘[]` dispatch hand back the narrowed type. Receiver-rebind and `[]=`/mutator invalidation rules are documented at the call sites in `Inference::StatementEvaluator`.
592 593 594 |
# File 'lib/rigor/scope.rb', line 592 def indexed_narrowing(receiver_kind, receiver_name, key) @indexed_narrowings[indexed_key(receiver_kind, receiver_name, key)] end |
#ivar(name) ⇒ Object
Slice 7 phase 1 — instance/class/global variable bindings. ‘ivar(name)` / `cvar(name)` / `global(name)` return the type currently bound for the named variable, or `nil` when the variable has not been written in the analyzed slice of the program. The first cut tracks bindings only within a single method body (each `def` enters with a fresh binding map), so reads in other methods of the same class fall through to `Dynamic`. Cross-method ivar/cvar inference is a follow-up slice.
245 246 247 |
# File 'lib/rigor/scope.rb', line 245 def ivar(name) @ivars[name.to_sym] end |
#join(other) ⇒ Object
Joins this scope with another at a control-flow merge point. The joined scope is bound to every local that BOTH branches bind, with the type widened to the union of both sides. Names bound in only one branch are dropped from the joined scope; the eventual statement-level evaluator (Slice 3 phase 2) is responsible for nil-injecting half-bound names where the language semantics demand it. The two scopes MUST share the same Environment.
684 685 686 687 688 689 690 691 692 693 694 695 696 |
# File 'lib/rigor/scope.rb', line 684 def join(other) raise ArgumentError, "join requires a Rigor::Scope, got #{other.class}" unless other.is_a?(Scope) unless environment.equal?(other.environment) raise ArgumentError, "join requires both scopes to share the same Environment" end joined_locals = join_bindings(locals, other.locals) joined_ivars = join_bindings(ivars, other.ivars) joined_cvars = join_bindings(cvars, other.cvars) joined_globals = join_bindings(globals, other.globals) build_joined_scope(joined_locals, joined_ivars, joined_cvars, joined_globals, other) end |
#local(name) ⇒ Object
163 164 165 |
# File 'lib/rigor/scope.rb', line 163 def local(name) @locals[name.to_sym] end |
#local_facts(name, bucket: nil) ⇒ Object
659 660 661 |
# File 'lib/rigor/scope.rb', line 659 def local_facts(name, bucket: nil) facts_for(target: Analysis::FactStore::Target.local(name), bucket: bucket) end |
#method_chain_narrowing(receiver_kind, receiver_name, method_name) ⇒ Object
Closes the “stable receiver method-chain narrowing” gap (ROADMAP § Future cycles / Type-language / engine —“Method-call receiver narrowing across stable receivers”; 2026-05-28 Redmine survey). After ‘if x.last.is_a?(Array)` the dominated body’s ‘x.last` reads MUST observe the truthy-narrowed type; the same chain reaching the falsey edge observes the negative narrowing.
Address shape mirrors #indexed_narrowing: stable root variable + no-arg single-hop method name. See ChainKey for the precise contract.
629 630 631 |
# File 'lib/rigor/scope.rb', line 629 def method_chain_narrowing(receiver_kind, receiver_name, method_name) @method_chain_narrowings[chain_key(receiver_kind, receiver_name, method_name)] end |
#param_inferred_types ⇒ Object
ADR-67 WD3 — call-site-inferred parameter types, keyed by ‘[class_name, method_name, kind]`. `build_method_entry_scope` consults this to seed an undeclared `def` parameter with the union of its resolved call-site argument types (precision-additive; an RBS-declared parameter always wins). Empty unless a collection pass seeded it.
61 |
# File 'lib/rigor/scope.rb', line 61 def param_inferred_types = @discovery.param_inferred_types |
#program_globals ⇒ Object
43 |
# File 'lib/rigor/scope.rb', line 43 def program_globals = @discovery.program_globals |
#record_dynamic_origin(node, cause) ⇒ Object
126 127 128 129 |
# File 'lib/rigor/scope.rb', line 126 def record_dynamic_origin(node, cause) @dynamic_origins[node] = cause self end |
#seed_declaration_sourced_ivar(name, type) ⇒ Object
ADR-58 WD1 — used by the method-entry seed to mark an ivar whose only provenance is the class-ivar index. Unlike ‘with_ivar` this binds the type AND records the declaration-sourced mark in one transition.
273 274 275 276 |
# File 'lib/rigor/scope.rb', line 273 def seed_declaration_sourced_ivar(name, type) rebuild(ivars: @ivars.merge(name.to_sym => type).freeze, declaration_sourced: add_declaration_sourced(:ivar, name)) end |
#singleton_def_for(class_name, method_name) ⇒ Object
Module-singleton call resolution (ADR-57 follow-up) — companion of #user_def_for for SINGLETON-side defs (‘def self.x`, `def Foo.x`, `class << self` bodies, and `module_function` defs). Returns the `Prism::DefNode` for `class_name.method_name` invoked on the module/class constant itself, or nil. The `discovered_def_nodes` table is deliberately instance-side only (its ancestor walk binds `self` as `Nominal`), so singleton bodies live in a parallel table the `ScopeIndexer` populates alongside it. Records the same cross-file dependency edge as the instance path (ADR-46).
412 413 414 415 416 417 |
# File 'lib/rigor/scope.rb', line 412 def singleton_def_for(class_name, method_name) table = @discovery.discovered_singleton_def_nodes[class_name.to_s] node = table && table[method_name.to_sym] record_cross_file_method(class_name, method_name, node) if Analysis::DependencyRecorder.active? node end |
#struct_fold_safe?(name) ⇒ Boolean
True when ‘name`’s ‘Struct` member reads are fold-safe in this body (the local is provably never mutated / aliased / escaped).
225 226 227 |
# File 'lib/rigor/scope.rb', line 225 def struct_fold_safe?(name) @struct_fold_safe_locals.include?(name.to_sym) end |
#struct_member_layout(class_name) ⇒ Object
ADR-48 Struct follow-up — the ‘{ members:, keyword_init: }` layout recorded for a `Struct.new(…)`-defined class, in the constant form (`Point = Struct.new(:x, :y)`) and the named-subclass form (`class Point < Struct.new(:x, :y)`). Consumed by Inference::MethodDispatcher::StructFolding so `Point.new(…)` on a `Singleton` receiver materialises a member instance. Returns nil when the class has no recorded struct layout. Mirrors #data_member_layout’s dependency-recording contract.
530 531 532 533 534 |
# File 'lib/rigor/scope.rb', line 530 def struct_member_layout(class_name) layout = @discovery.struct_member_layouts[class_name.to_s] record_class_dependency(class_name) if layout && Analysis::DependencyRecorder.active? layout end |
#struct_member_layouts ⇒ Object
55 56 57 58 59 60 |
# File 'lib/rigor/scope.rb', line 55 def struct_member_layouts = @discovery.struct_member_layouts # ADR-67 WD3 — call-site-inferred parameter types, keyed by # `[class_name, method_name, kind]`. `build_method_entry_scope` consults # this to seed an undeclared `def` parameter with the union of its # resolved call-site argument types (precision-additive; an RBS-declared # parameter always wins). Empty unless a collection pass seeded it. |
#superclass_of(class_name) ⇒ Object
ADR-24 slice 2 — per-class table mapping a fully qualified user-class name to its superclass name AS WRITTEN at the ‘class Foo < Bar` declaration (`“Bar”`, possibly a qualified `“A::B”`). Populated by `ScopeIndexer` — per-file plus the cross-file project pre-pass — and consumed by `ExpressionTyper#try_user_method_inference` to walk the superclass chain when an implicit-self call does not resolve against the enclosing class’s own defs. The as-written name is resolved to a qualified class at walk time against the call’s lexical nesting.
500 501 502 503 |
# File 'lib/rigor/scope.rb', line 500 def superclass_of(class_name) record_class_dependency(class_name) if Analysis::DependencyRecorder.active? @discovery.discovered_superclasses[class_name.to_s] end |
#top_level_def_for(method_name) ⇒ Object
v0.0.3 A — top-level def lookup for implicit-self calls. Returns the ‘Prism::DefNode` for a top-level (or DSL-block-nested, outside any class body) `def <method_name>` in the file, or nil. The sentinel key is owned by `Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY`; consumers should treat its presence as an opaque implementation detail and go through this accessor.
445 446 447 448 449 450 |
# File 'lib/rigor/scope.rb', line 445 def top_level_def_for(method_name) table = @discovery.discovered_def_nodes[Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY] node = table && table[method_name.to_sym] record_cross_file_toplevel(method_name, node) if Analysis::DependencyRecorder.active? node end |
#toplevel? ⇒ Boolean
ADR-34 § “Decision” — predicate identifying a toplevel-shaped scope (no enclosing ‘class` / `module` body). True at the top of a file AND inside a top-level `def` body (since toplevel defs leave `self_type` nil per the existing scope-construction contract — the same nil-`self_type` signal ADR-24’s self-call return adoption historically keyed on before ADR-57 opened the gate unconditionally). Used by ‘CheckRules#unresolved_toplevel_diagnostic` to gate the `call.unresolved-toplevel` rule so it fires only outside class / module bodies, where Rails-DSL metaprogramming leniency (ADR-24 WD3 → WD4) does not apply.
383 384 385 |
# File 'lib/rigor/scope.rb', line 383 def toplevel? @self_type.nil? end |
#type_of(node, tracer: nil) ⇒ Object
663 664 665 |
# File 'lib/rigor/scope.rb', line 663 def type_of(node, tracer: nil) Inference::ExpressionTyper.new(scope: self, tracer: tracer).type_of(node) end |
#user_def_for(class_name, method_name) ⇒ Object
v0.0.2 #5 — per-class table mapping ‘method_name (Symbol) → Prism::DefNode`. Populated by `ScopeIndexer` alongside `discovered_methods` for instance-side defs only (singleton-side and `define_method`-introduced methods do not contribute a static body the engine can re-type). Consumed by `ExpressionTyper` to do inter-procedural return-type inference when the receiver class is user-defined and has no RBS sig.
396 397 398 399 400 401 |
# File 'lib/rigor/scope.rb', line 396 def user_def_for(class_name, method_name) table = @discovery.discovered_def_nodes[class_name.to_s] node = table && table[method_name.to_sym] record_cross_file_method(class_name, method_name, node) if Analysis::DependencyRecorder.active? node end |
#user_def_site_for(class_name, method_name) ⇒ Object
Companion to #user_def_for: returns the ‘“path:line”` where the project defines `class_name#method_name` (instance-side), or nil. Populated only by the cross-file project pre-pass (Inference::ScopeIndexer.discovered_def_index_for_paths) — a `Prism::Location` hides its source file, so the site is recorded at scan time. `CheckRules#undefined_method_diagnostic` consults this to name the defining file when a project monkey-patch on a core/stdlib/gem class is called cross-file, so the diagnostic can point at `pre_eval:` (ADR-17) instead of reading as a bare unresolved call.
483 484 485 486 487 488 |
# File 'lib/rigor/scope.rb', line 483 def user_def_site_for(class_name, method_name) table = @discovery.discovered_def_sources[class_name.to_s] return nil unless table table[method_name.to_sym] end |
#with_cvar(name, type) ⇒ Object
302 303 304 |
# File 'lib/rigor/scope.rb', line 302 def with_cvar(name, type) rebuild(cvars: @cvars.merge(name.to_sym => type).freeze) end |
#with_declaration_sourced_local(name, type) ⇒ Object
ADR-58 WD1 — a local assignment ‘r = @right` whose RHS is a pure read of a declaration-sourced ivar inherits the mark, so the survey’s exact rotation/traversal shape (‘r = @right; r.key`) does not fire. Binds the type and stamps the local’s mark in one transition (the plain ‘with_local` would have dropped it).
283 284 285 286 |
# File 'lib/rigor/scope.rb', line 283 def with_declaration_sourced_local(name, type) written = with_local(name, type) written.with_local_declaration_mark(name) end |
#with_discovery(index) ⇒ Object
ADR-53 Track A — swaps the whole discovery index in one transition. The sole seeding path; the per-table writers it replaced are derived off-‘Scope` through `scope.discovery.with(table_name: table)`.
232 233 234 |
# File 'lib/rigor/scope.rb', line 232 def with_discovery(index) rebuild(discovery: index) end |
#with_fact(fact) ⇒ Object
193 194 195 |
# File 'lib/rigor/scope.rb', line 193 def with_fact(fact) rebuild(fact_store: fact_store.with_fact(fact)) end |
#with_global(name, type) ⇒ Object
306 307 308 |
# File 'lib/rigor/scope.rb', line 306 def with_global(name, type) rebuild(globals: @globals.merge(name.to_sym => type).freeze) end |
#with_indexed_narrowing(receiver_kind, receiver_name, key, type) ⇒ Object
596 597 598 599 600 601 |
# File 'lib/rigor/scope.rb', line 596 def with_indexed_narrowing(receiver_kind, receiver_name, key, type) new_table = @indexed_narrowings.merge( indexed_key(receiver_kind, receiver_name, key) => type ).freeze rebuild(indexed_narrowings: new_table) end |
#with_ivar(name, type) ⇒ Object
257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/rigor/scope.rb', line 257 def with_ivar(name, type) new_indexed_narrowings = drop_indexed_narrowings_for(:ivar, name) new_chain_narrowings = drop_chain_narrowings_for(:ivar, name) # ADR-58 WD1 — a method-local ivar write or narrowing is flow-live: # drop any declaration-sourced mark so subsequent reads of `@name` # observe flow-live provenance and fire as before. The seed path uses # `seed_declaration_sourced_ivar` to (re-)establish the mark. rebuild(ivars: @ivars.merge(name.to_sym => type).freeze, indexed_narrowings: new_indexed_narrowings, method_chain_narrowings: new_chain_narrowings, declaration_sourced: drop_declaration_sourced_for(:ivar, name)) end |
#with_local(name, type) ⇒ Object
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/rigor/scope.rb', line 167 def with_local(name, type) # `rigor trace` — the moment a local enters the scope. Inference::FlowTracer.bind(name, type) if Inference::FlowTracer.active? new_locals = @locals.merge(name.to_sym => type).freeze new_fact_store = fact_store.invalidate_target(Analysis::FactStore::Target.local(name)) # Rebinding `name` invalidates every "after `receiver[key] # ||= default`" narrowing keyed on it — the slot at `name[*]` # is reachable through the old binding only, so the # next read against the new binding does not inherit the # earlier non-nil guarantee. The same logic applies to # method-chain narrowings: `x.last` after `x = something_new` # is a call on the new binding and any prior `is_a?`-driven # narrowing keyed on `(local, :x, :last)` no longer holds. new_indexed_narrowings = drop_indexed_narrowings_for(:local, name) new_chain_narrowings = drop_chain_narrowings_for(:local, name) # ADR-58 WD1 — rebinding a local is a flow-live touch: any prior # declaration-sourced mark on `name` no longer holds (the new value # may carry a method-local nil). `with_declaration_sourced_local` # re-establishes the mark afterward when the RHS is a pure copy of a # declaration-sourced ivar read; the default is to drop it. rebuild(locals: new_locals, fact_store: new_fact_store, indexed_narrowings: new_indexed_narrowings, method_chain_narrowings: new_chain_narrowings, declaration_sourced: drop_declaration_sourced_for(:local, name)) end |
#with_local_declaration_mark(name) ⇒ Object
ADR-58 WD1 — re-stamp the local mark on a scope produced by ‘with_local` (which always drops it). Public so the sibling `with_declaration_sourced_local` can call it across the new post-write receiver without reaching into a private method.
292 293 294 |
# File 'lib/rigor/scope.rb', line 292 def with_local_declaration_mark(name) rebuild(declaration_sourced: add_declaration_sourced(:local, name)) end |
#with_method_chain_narrowing(receiver_kind, receiver_name, method_name, type) ⇒ Object
633 634 635 636 637 638 |
# File 'lib/rigor/scope.rb', line 633 def with_method_chain_narrowing(receiver_kind, receiver_name, method_name, type) new_table = @method_chain_narrowings.merge( chain_key(receiver_kind, receiver_name, method_name) => type ).freeze rebuild(method_chain_narrowings: new_table) end |
#with_self_type(type) ⇒ Object
Slice A-engine. Returns a scope with ‘self_type` set to `type`, preserving locals and facts. `StatementEvaluator` injects this at class-body and method-body boundaries; `ExpressionTyper` consults it when typing `Prism::SelfNode` and implicit-self `Prism::CallNode` receivers.
202 203 204 |
# File 'lib/rigor/scope.rb', line 202 def with_self_type(type) rebuild(self_type: type) end |
#with_source_path(path) ⇒ Object
ADR-28 / ADR-52 slice 5a — per-file source path carried on the scope. The analyzer stamps the current file’s path onto the seed scope; nested rebuilds propagate it so plugin rules (‘dynamic_return`’s ‘file_methods:` gate, sigil checks) can resolve “which file does this call site belong to?” without thread-locals.
212 213 214 |
# File 'lib/rigor/scope.rb', line 212 def with_source_path(path) rebuild(source_path: path) end |
#with_struct_fold_safe(locals) ⇒ Object
ADR-48 Struct slice 3 — installs the per-body fold-safe-local set (Inference::StructFoldSafety). Set once at body entry; inherited unchanged through subsequent flow transitions.
219 220 221 |
# File 'lib/rigor/scope.rb', line 219 def with_struct_fold_safe(locals) rebuild(struct_fold_safe_locals: locals) end |
#without_indexed_narrowing(receiver_kind, receiver_name, key) ⇒ Object
603 604 605 606 607 608 609 |
# File 'lib/rigor/scope.rb', line 603 def without_indexed_narrowing(receiver_kind, receiver_name, key) lookup = indexed_key(receiver_kind, receiver_name, key) return self unless @indexed_narrowings.key?(lookup) new_table = @indexed_narrowings.reject { |k, _| k == lookup }.freeze rebuild(indexed_narrowings: new_table) end |
#without_indexed_narrowings_for(receiver_kind, receiver_name) ⇒ Object
611 612 613 614 615 616 |
# File 'lib/rigor/scope.rb', line 611 def without_indexed_narrowings_for(receiver_kind, receiver_name) new_table = drop_indexed_narrowings_for(receiver_kind, receiver_name) return self if new_table.equal?(@indexed_narrowings) rebuild(indexed_narrowings: new_table) end |
#without_method_chain_narrowing(receiver_kind, receiver_name, method_name) ⇒ Object
640 641 642 643 644 645 646 |
# File 'lib/rigor/scope.rb', line 640 def without_method_chain_narrowing(receiver_kind, receiver_name, method_name) lookup = chain_key(receiver_kind, receiver_name, method_name) return self unless @method_chain_narrowings.key?(lookup) new_table = @method_chain_narrowings.reject { |k, _| k == lookup }.freeze rebuild(method_chain_narrowings: new_table) end |
#without_method_chain_narrowings_for(receiver_kind, receiver_name) ⇒ Object
648 649 650 651 652 653 |
# File 'lib/rigor/scope.rb', line 648 def without_method_chain_narrowings_for(receiver_kind, receiver_name) new_table = drop_chain_narrowings_for(receiver_kind, receiver_name) return self if new_table.equal?(@method_chain_narrowings) rebuild(method_chain_narrowings: new_table) end |