Class: Rigor::Scope
- Inherits:
-
Object
- Object
- Rigor::Scope
- Defined in:
- lib/rigor/scope.rb,
lib/rigor/scope/discovery_index.rb,
sig/rigor/scope.rbs
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 ⇒ Hash[Symbol, Type::t]
readonly
Returns the value of attribute cvars.
-
#declaration_sourced ⇒ Set[[Symbol, Symbol]]
readonly
Returns the value of attribute declaration_sourced.
-
#discovery ⇒ DiscoveryIndex
readonly
Returns the value of attribute discovery.
-
#dynamic_origins ⇒ Hash[untyped, Symbol]
readonly
Returns the value of attribute dynamic_origins.
-
#environment ⇒ Environment
readonly
Returns the value of attribute environment.
-
#fact_store ⇒ Analysis::FactStore
readonly
Returns the value of attribute fact_store.
-
#globals ⇒ Hash[Symbol, Type::t]
readonly
Returns the value of attribute globals.
-
#indexed_narrowings ⇒ Hash[IndexedKey, Type::t]
readonly
Returns the value of attribute indexed_narrowings.
-
#ivar_origins ⇒ Hash[Symbol, Symbol]
readonly
Returns the value of attribute ivar_origins.
-
#ivars ⇒ Hash[Symbol, Type::t]
readonly
Returns the value of attribute ivars.
-
#local_origins ⇒ Hash[Symbol, Symbol]
readonly
Returns the value of attribute local_origins.
-
#locals ⇒ Hash[Symbol, Type::t]
readonly
Returns the value of attribute locals.
-
#method_chain_narrowings ⇒ Hash[ChainKey, Type::t]
readonly
Returns the value of attribute method_chain_narrowings.
-
#opaque_block_self ⇒ Boolean
readonly
Returns the value of attribute opaque_block_self.
-
#optimistic_ivars ⇒ Hash[Symbol, Symbol]
readonly
Returns the value of attribute optimistic_ivars.
-
#optimistic_locals ⇒ Hash[Symbol, Symbol]
readonly
Returns the value of attribute optimistic_locals.
-
#optimistic_origins ⇒ Hash[untyped, Symbol]
readonly
Returns the value of attribute optimistic_origins.
-
#self_type ⇒ Type::t?
readonly
Returns the value of attribute self_type.
-
#source_path ⇒ String?
readonly
Returns the value of attribute source_path.
-
#struct_fold_safe_locals ⇒ Set[Symbol]
readonly
Returns the value of attribute struct_fold_safe_locals.
-
#void_origins ⇒ Hash[untyped, Inference::VoidOrigin]
readonly
Returns the value of attribute void_origins.
Class Method Summary collapse
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
-
#bindable_top_level_def_for(method_name) ⇒ Object?
Issue #316 — the CONFIDENCE-GATED companion of #top_level_def_for, and the only accessor the type inference may bind through.
- #class_cvars ⇒ Hash[String, Hash[Symbol, Type::t]]
-
#class_cvars_for(class_name) ⇒ Hash[Symbol, Type::t]
Slice 7 phase 6 — class-level cvar accumulator (same shape as
class_ivarsbut populated fromPrism::ClassVariableWriteNodewrites, and seeded on BOTH instance and singleton method bodies because Ruby cvars are visible from each). - #class_ivars ⇒ Hash[String, Hash[Symbol, Type::t]]
-
#class_ivars_for(class_name) ⇒ Hash[Symbol, Type::t]
Slice 7 phase 2 — class-level ivar accumulator.
- #cvar(name) ⇒ Type::t?
-
#data_member_layout(class_name) ⇒ Array[Symbol]?
ADR-48 — per-class table mapping a fully qualified class name to its ordered
Data.define/Struct.newmember-name list. - #data_member_layouts ⇒ Hash[String, Array[Symbol]]
-
#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 ⇒ Hash[untyped, Type::t]
ADR-53 Track A — the seed-time discovery tables live on the DiscoveryIndex; Scope keeps per-table readers as delegates.
- #discovered_class_sources ⇒ Hash[String, Set[String]]
- #discovered_classes ⇒ Hash[String, Type::Singleton]
- #discovered_def_nodes ⇒ Hash[String, Hash[Symbol, untyped]]
- #discovered_def_sources ⇒ Hash[String, Hash[Symbol, String]]
- #discovered_includes ⇒ Hash[String, Array[String]]
-
#discovered_method?(class_name, method_name, kind) ⇒ Boolean
Slice 7 phase 12 — in-source method discovery.
- #discovered_method_visibilities ⇒ Hash[String, Hash[Symbol, Symbol]]
-
#discovered_method_visibility(class_name, method_name) ⇒ Symbol?
v0.1.2 — per-class table mapping
method_name (Symbol) → :public | :private | :protected. - #discovered_methods ⇒ Hash[String, Hash[Symbol, Symbol]]
- #discovered_singleton_def_nodes ⇒ Hash[String, Hash[Symbol, untyped]]
- #discovered_singleton_def_sources ⇒ Hash[String, Hash[Symbol, String]]
- #discovered_superclasses ⇒ Hash[String, String]
-
#entering_opaque_block ⇒ Scope
Issue #316 — marks a block body whose
selfRigor does not model. -
#evaluate(node, tracer: nil) ⇒ [Type::t, Scope]
Statement-level evaluation: returns the pair
[type, scope']wheretypeis what the node produces andscope'is the scope observable after the node has run. - #facts_for(target: nil, bucket: nil) ⇒ Array[Analysis::FactStore::Fact]
- #forget_match_globals ⇒ Scope
- #global(name) ⇒ Type::t?
- #hash ⇒ Integer
- #in_source_constants ⇒ Hash[String, Type::t]
-
#includes_of(class_name) ⇒ Array[String]
ADR-24 slice 2 — per-class/module table mapping a fully qualified user class or module to the list of module names it
includes /prepends, AS WRITTEN at the mixin call. -
#indexed_narrowing(receiver_kind, receiver_name, key) ⇒ Type::t?
Closes the "
params[:f] ||= []; params[:f] << x" precision gap (ROADMAP § Type-language / engine — indexed-collection narrowing throughHash[k] ||= default). -
#inferred_param?(name) ⇒ Boolean
ADR-67 WD6b — true when
name's local binding is a pristine inferred parameter (the call-site union seeded at method entry, untouched by a flow-live write). -
#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, opaque_block_self: false, dynamic_origins: {}.compare_by_identity, local_origins: EMPTY_ORIGINS, ivar_origins: EMPTY_ORIGINS, void_origins: {}.compare_by_identity, optimistic_origins: {}.compare_by_identity, optimistic_locals: EMPTY_ORIGINS, optimistic_ivars: EMPTY_ORIGINS) ⇒ Scope
constructor
A new instance of Scope.
-
#ivar(name) ⇒ Type::t?
Slice 7 phase 1 — instance/class/global variable bindings.
- #ivar_origin(name) ⇒ Symbol?
-
#join(other) ⇒ Scope
Joins this scope with another at a control-flow merge point.
- #local(name) ⇒ Type::t?
- #local_facts(name, bucket: nil) ⇒ Array[Analysis::FactStore::Fact]
-
#local_origin(name) ⇒ Symbol?
ADR-82 WD1 — the propagated origin of the
Dynamicvalue currently bound to a local / instance variable, ornilwhen none is tracked. -
#method_chain_narrowing(receiver_kind, receiver_name, method_name) ⇒ Type::t?
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).
-
#opaque_block_self? ⇒ Boolean
True when this scope sits inside a block whose
selfis unmodelled (#entering_opaque_block). - #optimistic_ivar(name) ⇒ Symbol?
- #optimistic_local(name) ⇒ Symbol?
-
#param_inferred_types ⇒ Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]
ADR-67 WD3 — call-site-inferred parameter types, keyed by
[class_name, method_name, kind]. - #program_globals ⇒ Hash[Symbol, Type::t]
- #record_dynamic_origin(node, cause) ⇒ Scope
-
#record_optimistic_origin(node, cause) ⇒ Scope
Issue #286 — the Inference::OptimisticOrigin cause attached to a call node whose result is nil-free only because
RbsDispatchreads past%a{implicitly-returns-nil}, ornilwhen the value's nil-freeness is a property of its class. -
#record_void_origin(node, origin) ⇒ Scope
ADR-100 WD3 — records that the value introduced at
node(a call node) is atoprecovered from an author-declared-> voidreturn, keyed by the origin site (origin, an Inference::VoidOrigin). -
#run_generation ⇒ Object?
ADR-84 WD2 — the per-run identity token the user-method return memo buckets on (nil outside runner-seeded scopes; the memo then falls back to the per-file
discovered_def_nodesidentity). -
#seed_declaration_sourced_ivar(name, type) ⇒ Scope
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 << selfbodies, andmodule_functiondefs). -
#struct_fold_safe?(name) ⇒ Boolean
True when
name'sStructmember reads are fold-safe in this body (the local is provably never mutated / aliased / escaped). -
#struct_member_layout(class_name) ⇒ { members: Array[Symbol], keyword_init: bool }?
ADR-48 Struct follow-up — the
{ members:, keyword_init: }layout recorded for aStruct.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 ⇒ Hash[String, { members: Array[Symbol], keyword_init: bool }]
-
#superclass_of(class_name) ⇒ String?
ADR-24 slice 2 — per-class table mapping a fully qualified user-class name to its superclass name AS WRITTEN at the
class Foo < Bardeclaration ("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/modulebody). - #type_of(node, tracer: nil) ⇒ Type::t
-
#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) ⇒ String?
Companion to #user_def_for: returns the
"path:line"where the project definesclass_name#method_name(instance-side), or nil. -
#user_method_return(def_node, receiver, arg_types) ⇒ Type::t?
ADR-89 WD2 — the inferred return type of
def_nodecalled withreceiver/arg_types, computed against THIS scope's discovery index (so cross-file dispatches in the body resolve). - #with_cvar(name, type) ⇒ Scope
-
#with_declaration_sourced_local(name, type) ⇒ Scope
ADR-58 WD1 — a local assignment
r = @rightwhose 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) ⇒ Scope
ADR-53 Track A — swaps the whole discovery index in one transition.
- #with_fact(fact) ⇒ Scope
- #with_global(name, type) ⇒ Scope
- #with_indexed_narrowing(receiver_kind, receiver_name, key, type) ⇒ Scope
-
#with_inferred_param_mark(name) ⇒ Scope
ADR-67 WD6b — stamp the "inferred, not declared" provenance mark on a parameter local seeded from the call-site parameter-inference table (Inference::ParameterInferenceCollector).
- #with_ivar(name, type) ⇒ Scope
- #with_ivar_origin(name, cause) ⇒ Scope
- #with_local(name, type) ⇒ Scope
-
#with_local_declaration_mark(name) ⇒ Scope
ADR-58 WD1 — re-stamp the local mark on a scope produced by
with_local(which always drops it). -
#with_local_origin(name, cause) ⇒ Scope
Records the cause of the
Dynamicvalue being bound toname. - #with_method_chain_narrowing(receiver_kind, receiver_name, method_name, type) ⇒ Scope
- #with_optimistic_ivar(name, cause) ⇒ Scope
- #with_optimistic_local(name, cause) ⇒ Scope
-
#with_self_type(type) ⇒ Scope
Slice A-engine.
-
#with_source_path(path) ⇒ Scope
ADR-28 / ADR-52 slice 5a — per-file source path carried on the scope.
-
#with_struct_fold_safe(locals) ⇒ Scope
ADR-48 Struct slice 3 — installs the per-body fold-safe-local set (Inference::StructFoldSafety).
- #without_indexed_narrowing(receiver_kind, receiver_name, key) ⇒ Scope
- #without_indexed_narrowings_for(receiver_kind, receiver_name) ⇒ Scope
-
#without_inferred_param_mark(name) ⇒ Scope
ADR-67 WD6b — explicitly clear the inferred-parameter taint on
name. - #without_method_chain_narrowing(receiver_kind, receiver_name, method_name) ⇒ Scope
- #without_method_chain_narrowings_for(receiver_kind, receiver_name) ⇒ Scope
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, opaque_block_self: false, dynamic_origins: {}.compare_by_identity, local_origins: EMPTY_ORIGINS, ivar_origins: EMPTY_ORIGINS, void_origins: {}.compare_by_identity, optimistic_origins: {}.compare_by_identity, optimistic_locals: EMPTY_ORIGINS, optimistic_ivars: EMPTY_ORIGINS) ⇒ Scope
Returns a new instance of Scope.
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/rigor/scope.rb', line 138 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, opaque_block_self: false, dynamic_origins: {}.compare_by_identity, local_origins: EMPTY_ORIGINS, ivar_origins: EMPTY_ORIGINS, void_origins: {}.compare_by_identity, optimistic_origins: {}.compare_by_identity, optimistic_locals: EMPTY_ORIGINS, optimistic_ivars: EMPTY_ORIGINS ) @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 @opaque_block_self = opaque_block_self @dynamic_origins = dynamic_origins @local_origins = local_origins @ivar_origins = ivar_origins @void_origins = void_origins @optimistic_origins = optimistic_origins @optimistic_locals = optimistic_locals @optimistic_ivars = optimistic_ivars freeze end |
Instance Attribute Details
#cvars ⇒ Hash[Symbol, Type::t] (readonly)
Returns the value of attribute cvars.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def cvars @cvars end |
#declaration_sourced ⇒ Set[[Symbol, Symbol]] (readonly)
Returns the value of attribute declaration_sourced.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def declaration_sourced @declaration_sourced end |
#discovery ⇒ DiscoveryIndex (readonly)
Returns the value of attribute discovery.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def discovery @discovery end |
#dynamic_origins ⇒ Hash[untyped, Symbol] (readonly)
Returns the value of attribute dynamic_origins.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def dynamic_origins @dynamic_origins end |
#environment ⇒ Environment (readonly)
Returns the value of attribute environment.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def environment @environment end |
#fact_store ⇒ Analysis::FactStore (readonly)
Returns the value of attribute fact_store.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def fact_store @fact_store end |
#globals ⇒ Hash[Symbol, Type::t] (readonly)
Returns the value of attribute globals.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def globals @globals end |
#indexed_narrowings ⇒ Hash[IndexedKey, Type::t] (readonly)
Returns the value of attribute indexed_narrowings.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def indexed_narrowings @indexed_narrowings end |
#ivar_origins ⇒ Hash[Symbol, Symbol] (readonly)
Returns the value of attribute ivar_origins.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def ivar_origins @ivar_origins end |
#ivars ⇒ Hash[Symbol, Type::t] (readonly)
Returns the value of attribute ivars.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def ivars @ivars end |
#local_origins ⇒ Hash[Symbol, Symbol] (readonly)
Returns the value of attribute local_origins.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def local_origins @local_origins end |
#locals ⇒ Hash[Symbol, Type::t] (readonly)
Returns the value of attribute locals.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def locals @locals end |
#method_chain_narrowings ⇒ Hash[ChainKey, Type::t] (readonly)
Returns the value of attribute method_chain_narrowings.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def method_chain_narrowings @method_chain_narrowings end |
#opaque_block_self ⇒ Boolean (readonly)
Returns the value of attribute opaque_block_self.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def opaque_block_self @opaque_block_self end |
#optimistic_ivars ⇒ Hash[Symbol, Symbol] (readonly)
Returns the value of attribute optimistic_ivars.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def optimistic_ivars @optimistic_ivars end |
#optimistic_locals ⇒ Hash[Symbol, Symbol] (readonly)
Returns the value of attribute optimistic_locals.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def optimistic_locals @optimistic_locals end |
#optimistic_origins ⇒ Hash[untyped, Symbol] (readonly)
Returns the value of attribute optimistic_origins.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def optimistic_origins @optimistic_origins end |
#self_type ⇒ Type::t? (readonly)
Returns the value of attribute self_type.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def self_type @self_type end |
#source_path ⇒ String? (readonly)
Returns the value of attribute source_path.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def source_path @source_path end |
#struct_fold_safe_locals ⇒ Set[Symbol] (readonly)
Returns the value of attribute struct_fold_safe_locals.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def struct_fold_safe_locals @struct_fold_safe_locals end |
#void_origins ⇒ Hash[untyped, Inference::VoidOrigin] (readonly)
Returns the value of attribute void_origins.
21 22 23 |
# File 'lib/rigor/scope.rb', line 21 def void_origins @void_origins end |
Class Method Details
.empty(environment: Environment.default, source_path: nil) ⇒ Scope
117 118 119 120 |
# File 'lib/rigor/scope.rb', line 117 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) ⇒ Boolean Also known as: eql?
787 788 789 790 791 792 793 794 795 796 797 798 799 |
# File 'lib/rigor/scope.rb', line 787 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 |
#bindable_top_level_def_for(method_name) ⇒ Object?
Issue #316 — the CONFIDENCE-GATED companion of #top_level_def_for, and the only accessor the type
inference may bind through. #top_level_def_for stays unrestricted because it also serves the
suppression side (call.unresolved-toplevel, call.undefined-method): a name the project defines at
the top level must never be reported as unresolved, whatever this gate decides.
Returns nil — decline to bind, stay silent — when BOTH hold:
- The call site sits inside a block whose
selfis unmodelled (#opaque_block_self?) and no narrowedself_typesays otherwise. A top-leveldefis a private method onObject, so it is callable from anyself; what the analyzer cannot see is whether the block's realselfgained a PUBLIC same-named method byinclude/extend, which wins the MRO over the privateObjectdef. RSpec'soutput/include/matchmatchers against a project's owndef outputare exactly this. - The
deflives in a DIFFERENT file from the call site. Collocation is the evidence that the two belong to one lexical structure — theRSpec.describe do; def helper; end; it { helper } endcase v0.0.3 A and #319 deliberately serve. Cross-file, the two share only a name.
Both conditions are required, so a top-level helper called from genuine top-level code keeps resolving (cross-file included), and a helper defined beside its DSL-block call site keeps resolving too. When the project pre-pass recorded no source for the name, the file test cannot be answered and the historical bind is kept.
549 550 551 552 553 554 555 |
# File 'lib/rigor/scope.rb', line 549 def bindable_top_level_def_for(method_name) node = top_level_def_for(method_name) return node if node.nil? return node unless @opaque_block_self && @self_type.nil? same_file_top_level_def?(method_name) ? node : nil end |
#class_cvars ⇒ Hash[String, Hash[Symbol, Type::t]]
40 |
# File 'lib/rigor/scope.rb', line 40 def class_cvars = @discovery.class_cvars |
#class_cvars_for(class_name) ⇒ Hash[Symbol, Type::t]
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).
440 441 442 443 444 |
# File 'lib/rigor/scope.rb', line 440 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 ⇒ Hash[String, Hash[Symbol, Type::t]]
39 |
# File 'lib/rigor/scope.rb', line 39 def class_ivars = @discovery.class_ivars |
#class_ivars_for(class_name) ⇒ Hash[Symbol, Type::t]
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.
431 432 433 434 435 |
# File 'lib/rigor/scope.rb', line 431 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) ⇒ Type::t?
316 317 318 |
# File 'lib/rigor/scope.rb', line 316 def cvar(name) @cvars[name.to_sym] end |
#data_member_layout(class_name) ⇒ Array[Symbol]?
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[Point] receiver
materialises a precise member instance. Returns nil when the class has no recorded layout.
621 622 623 624 625 626 627 |
# File 'lib/rigor/scope.rb', line 621 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 ⇒ Hash[String, Array[Symbol]]
53 |
# File 'lib/rigor/scope.rb', line 53 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).
362 363 364 |
# File 'lib/rigor/scope.rb', line 362 def declaration_sourced?(kind, name) @declaration_sourced.include?([kind.to_sym, name.to_sym]) end |
#declared_types ⇒ Hash[untyped, Type::t]
ADR-53 Track A — the seed-time discovery tables live on the DiscoveryIndex; Scope keeps per-table readers as delegates.
38 |
# File 'lib/rigor/scope.rb', line 38 def declared_types = @discovery.declared_types |
#discovered_class_sources ⇒ Hash[String, Set[String]]
52 |
# File 'lib/rigor/scope.rb', line 52 def discovered_class_sources = @discovery.discovered_class_sources |
#discovered_classes ⇒ Hash[String, Type::Singleton]
42 |
# File 'lib/rigor/scope.rb', line 42 def discovered_classes = @discovery.discovered_classes |
#discovered_def_nodes ⇒ Hash[String, Hash[Symbol, untyped]]
45 |
# File 'lib/rigor/scope.rb', line 45 def discovered_def_nodes = @discovery.discovered_def_nodes |
#discovered_def_sources ⇒ Hash[String, Hash[Symbol, String]]
47 |
# File 'lib/rigor/scope.rb', line 47 def discovered_def_sources = @discovery.discovered_def_sources |
#discovered_includes ⇒ Hash[String, Array[String]]
51 |
# File 'lib/rigor/scope.rb', line 51 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.
A name defined on both sides of one class records Rigor::Scope::DiscoveryIndex::METHOD_KIND_BOTH and matches either kind.
452 453 454 455 456 457 458 |
# File 'lib/rigor/scope.rb', line 452 def discovered_method?(class_name, method_name, kind) table = @discovery.discovered_methods[class_name.to_s] return false unless table recorded = table[method_name.to_sym] recorded == kind || recorded == DiscoveryIndex::METHOD_KIND_BOTH end |
#discovered_method_visibilities ⇒ Hash[String, Hash[Symbol, Symbol]]
49 |
# File 'lib/rigor/scope.rb', line 49 def discovered_method_visibilities = @discovery.discovered_method_visibilities |
#discovered_method_visibility(class_name, method_name) ⇒ Symbol?
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.
668 669 670 671 672 673 |
# File 'lib/rigor/scope.rb', line 668 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 ⇒ Hash[String, Hash[Symbol, Symbol]]
44 |
# File 'lib/rigor/scope.rb', line 44 def discovered_methods = @discovery.discovered_methods |
#discovered_singleton_def_nodes ⇒ Hash[String, Hash[Symbol, untyped]]
46 |
# File 'lib/rigor/scope.rb', line 46 def discovered_singleton_def_nodes = @discovery.discovered_singleton_def_nodes |
#discovered_singleton_def_sources ⇒ Hash[String, Hash[Symbol, String]]
48 |
# File 'lib/rigor/scope.rb', line 48 def discovered_singleton_def_sources = @discovery.discovered_singleton_def_sources |
#discovered_superclasses ⇒ Hash[String, String]
50 |
# File 'lib/rigor/scope.rb', line 50 def discovered_superclasses = @discovery.discovered_superclasses |
#entering_opaque_block ⇒ Scope
Issue #316 — marks a block body whose self Rigor does not model. Ruby gives a block no self of its
own: the yielding method decides, and instance_eval / instance_exec (the mechanism behind every
self-rebinding DSL — RSpec example groups, Class.new { … }, Rake, Sinatra) is indistinguishable from
Array#each without knowing the callee. The flag is set at every block entry that leaves self_type
unnarrowed and is inherited by every scope derived inside the block; it never leaks past the block,
because eval_call returns the caller's scope unchanged.
286 287 288 289 290 |
# File 'lib/rigor/scope.rb', line 286 def entering_opaque_block return self if @opaque_block_self rebuild(opaque_block_self: true) end |
#evaluate(node, tracer: nil) ⇒ [Type::t, Scope]
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.
764 765 766 |
# File 'lib/rigor/scope.rb', line 764 def evaluate(node, tracer: nil) Inference::StatementEvaluator.new(scope: self, tracer: tracer).evaluate(node) end |
#facts_for(target: nil, bucket: nil) ⇒ Array[Analysis::FactStore::Fact]
740 741 742 |
# File 'lib/rigor/scope.rb', line 740 def facts_for(target: nil, bucket: nil) fact_store.facts_for(target: target, bucket: bucket) end |
#forget_match_globals ⇒ Scope
418 419 420 421 422 |
# File 'lib/rigor/scope.rb', line 418 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) ⇒ Type::t?
320 321 322 |
# File 'lib/rigor/scope.rb', line 320 def global(name) @globals[name.to_sym] end |
#hash ⇒ Integer
802 803 804 |
# File 'lib/rigor/scope.rb', line 802 def hash [Scope, environment.object_id, @locals, fact_store, self_type, @ivars, @cvars, @globals].hash end |
#in_source_constants ⇒ Hash[String, Type::t]
43 |
# File 'lib/rigor/scope.rb', line 43 def in_source_constants = @discovery.in_source_constants |
#includes_of(class_name) ⇒ Array[String]
ADR-24 slice 2 — per-class/module table mapping a fully qualified user class or module to the list of
module names it includes / prepends, 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 defs, not just the superclass chain. As-written
names are resolved to qualified classes at walk time.
645 646 647 648 |
# File 'lib/rigor/scope.rb', line 645 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) ⇒ Type::t?
Closes the "params[:f] ||= []; params[:f] << x" precision gap (ROADMAP § Type-language / engine —
indexed-collection narrowing through Hash[k] ||= default). After receiver[key] ||= default, the next
read at receiver[key] 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.
681 682 683 |
# File 'lib/rigor/scope.rb', line 681 def indexed_narrowing(receiver_kind, receiver_name, key) @indexed_narrowings[indexed_key(receiver_kind, receiver_name, key)] end |
#inferred_param?(name) ⇒ Boolean
ADR-67 WD6b — true when name's local binding is a pristine inferred parameter (the call-site union
seeded at method entry, untouched by a flow-live write). The guard predicate the negative in-body rules
consult.
398 399 400 |
# File 'lib/rigor/scope.rb', line 398 def inferred_param?(name) @declaration_sourced.include?([:inferred_param, name.to_sym]) end |
#ivar(name) ⇒ Type::t?
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[Top]. Cross-method ivar/cvar inference is a follow-up slice.
312 313 314 |
# File 'lib/rigor/scope.rb', line 312 def ivar(name) @ivars[name.to_sym] end |
#ivar_origin(name) ⇒ Symbol?
212 |
# File 'lib/rigor/scope.rb', line 212 def ivar_origin(name) = @ivar_origins[name.to_sym] |
#join(other) ⇒ Scope
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.
773 774 775 776 777 778 779 780 781 782 783 784 785 |
# File 'lib/rigor/scope.rb', line 773 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) ⇒ Type::t?
228 229 230 |
# File 'lib/rigor/scope.rb', line 228 def local(name) @locals[name.to_sym] end |
#local_facts(name, bucket: nil) ⇒ Array[Analysis::FactStore::Fact]
744 745 746 |
# File 'lib/rigor/scope.rb', line 744 def local_facts(name, bucket: nil) facts_for(target: Analysis::FactStore::Target.local(name), bucket: bucket) end |
#local_origin(name) ⇒ Symbol?
ADR-82 WD1 — the propagated origin of the Dynamic value currently bound to a local / instance
variable, or nil when none is tracked. Consulted by Inference::ProtectionScanner when a dispatch's
receiver is a bare x / @x read whose own node carries no origin.
211 |
# File 'lib/rigor/scope.rb', line 211 def local_origin(name) = @local_origins[name.to_sym] |
#method_chain_narrowing(receiver_kind, receiver_name, method_name) ⇒ Type::t?
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.
714 715 716 |
# File 'lib/rigor/scope.rb', line 714 def method_chain_narrowing(receiver_kind, receiver_name, method_name) @method_chain_narrowings[chain_key(receiver_kind, receiver_name, method_name)] end |
#opaque_block_self? ⇒ Boolean
True when this scope sits inside a block whose self is unmodelled (#entering_opaque_block).
293 |
# File 'lib/rigor/scope.rb', line 293 def opaque_block_self? = @opaque_block_self |
#optimistic_ivar(name) ⇒ Symbol?
194 |
# File 'lib/rigor/scope.rb', line 194 def optimistic_ivar(name) = @optimistic_ivars[name.to_sym] |
#optimistic_local(name) ⇒ Symbol?
193 |
# File 'lib/rigor/scope.rb', line 193 def optimistic_local(name) = @optimistic_locals[name.to_sym] |
#param_inferred_types ⇒ Hash[[String, Symbol, Symbol], Hash[Symbol, Type::t]]
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.
59 60 61 |
# File 'lib/rigor/scope.rb', line 59 def param_inferred_types = @discovery.param_inferred_types # ADR-84 WD2 — the per-run identity token the user-method return memo buckets on (nil outside runner-seeded # scopes; the memo then falls back to the per-file `discovered_def_nodes` identity). |
#program_globals ⇒ Hash[Symbol, Type::t]
41 |
# File 'lib/rigor/scope.rb', line 41 def program_globals = @discovery.program_globals |
#record_dynamic_origin(node, cause) ⇒ Scope
123 124 125 126 |
# File 'lib/rigor/scope.rb', line 123 def record_dynamic_origin(node, cause) @dynamic_origins[node] = cause self end |
#record_optimistic_origin(node, cause) ⇒ Scope
Issue #286 — the Inference::OptimisticOrigin cause attached to a call node whose result is nil-free
only because RbsDispatch reads past %a{implicitly-returns-nil}, or nil when the value's
nil-freeness is a property of its class. Mirrors #dynamic_origins / #void_origins: advisory
metadata, ignored by == / hash, and never varying a flow decision on its own.
188 189 190 191 |
# File 'lib/rigor/scope.rb', line 188 def record_optimistic_origin(node, cause) @optimistic_origins[node] = cause self end |
#record_void_origin(node, origin) ⇒ Scope
ADR-100 WD3 — records that the value introduced at node (a call node) is a top recovered from an
author-declared -> void return, keyed by the origin site (origin, an Inference::VoidOrigin). The
value-context check rule static.value-use.void consumes this table. Mirrors #record_dynamic_origin
exactly: identity-keyed advisory metadata, mutated in place on the shared table (threaded by reference
through #join / #rebuild), excluded from == / hash, so it never forks a flow-dedup or cache key.
133 134 135 136 |
# File 'lib/rigor/scope.rb', line 133 def record_void_origin(node, origin) @void_origins[node] = origin self end |
#run_generation ⇒ Object?
ADR-84 WD2 — the per-run identity token the user-method return memo buckets on (nil outside runner-seeded
scopes; the memo then falls back to the per-file discovered_def_nodes identity).
62 |
# File 'lib/rigor/scope.rb', line 62 def run_generation = @discovery.run_generation |
#seed_declaration_sourced_ivar(name, type) ⇒ Scope
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.
340 341 342 343 |
# File 'lib/rigor/scope.rb', line 340 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).
490 491 492 493 494 495 |
# File 'lib/rigor/scope.rb', line 490 def singleton_def_for(class_name, method_name) table = @discovery.discovered_singleton_def_nodes[class_name.to_s] entry = table && table[method_name.to_sym] # live node or DefHandle (ADR-85 WD3) record_cross_file_method(class_name, method_name, entry, singleton: true) if Analysis::DependencyRecorder.active? Inference::DefNodeResolver.resolve(entry) 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).
297 298 299 |
# File 'lib/rigor/scope.rb', line 297 def struct_fold_safe?(name) @struct_fold_safe_locals.include?(name.to_sym) end |
#struct_member_layout(class_name) ⇒ { members: Array[Symbol], keyword_init: bool }?
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[Point] receiver materialises a member instance. Returns nil when the class
has no recorded struct layout. Mirrors #data_member_layout's dependency-recording contract.
634 635 636 637 638 |
# File 'lib/rigor/scope.rb', line 634 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 ⇒ Hash[String, { members: Array[Symbol], keyword_init: bool }]
54 55 56 57 58 |
# File 'lib/rigor/scope.rb', line 54 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) ⇒ String?
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.
611 612 613 614 |
# File 'lib/rigor/scope.rb', line 611 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.
522 523 524 525 526 527 |
# File 'lib/rigor/scope.rb', line 522 def top_level_def_for(method_name) table = @discovery.discovered_def_nodes[Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY] entry = table && table[method_name.to_sym] # live node or DefHandle (ADR-85 WD3) record_cross_file_toplevel(method_name, entry) if Analysis::DependencyRecorder.active? Inference::DefNodeResolver.resolve(entry) 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.
466 467 468 |
# File 'lib/rigor/scope.rb', line 466 def toplevel? @self_type.nil? end |
#type_of(node, tracer: nil) ⇒ Type::t
748 749 750 |
# File 'lib/rigor/scope.rb', line 748 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.
474 475 476 477 478 479 480 481 482 |
# File 'lib/rigor/scope.rb', line 474 def user_def_for(class_name, method_name) table = @discovery.discovered_def_nodes[class_name.to_s] # ADR-85 WD3 — the value is either a live `Prism::DefNode` (cold / re-walked file) or a `DefHandle` # (unchanged file, bundle-rebuilt index). Dependency recording keys on the table's PRESENCE (both are # truthy), so it is sound regardless of resolution; only the returned node is resolved lazily. entry = table && table[method_name.to_sym] record_cross_file_method(class_name, method_name, entry) if Analysis::DependencyRecorder.active? Inference::DefNodeResolver.resolve(entry) end |
#user_def_site_for(class_name, method_name) ⇒ String?
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.
591 592 593 594 595 596 597 598 599 600 601 602 603 |
# File 'lib/rigor/scope.rb', line 591 def user_def_site_for(class_name, method_name) table = @discovery.discovered_def_sources[class_name.to_s] site = table && table[method_name.to_sym] # ADR-88 WD3 — record the SAME instance-side cross-file method edge {#user_def_for} records at :378, so a # move / body-edit of `class_name#method_name`'s definition re-checks the consumer that named the # defining file. `CheckRules#undefined_method_diagnostic` reads this to set `project_definition_site` # (`"path:line"`) on a `call.undefined-method` for a project monkey-patch; without the edge, a line-shift # in the defining file left the cached diagnostic pointing at a stale line (the ADR-46 symbol-granularity # closure never re-checked the caller). Recording keys on the source-entry PRESENCE (truthy `site`), # sound whether or not the caller also went through {#user_def_for}. record_cross_file_method(class_name, method_name, site) if Analysis::DependencyRecorder.active? site end |
#user_method_return(def_node, receiver, arg_types) ⇒ Type::t?
ADR-89 WD2 — the inferred return type of def_node called with receiver / arg_types, computed
against THIS scope's discovery index (so cross-file dispatches in the body resolve). The incremental
session re-drives a declaration-stable changed callee at each previously-observed call key to prove its
return is unchanged before skipping its symbol dependents.
756 757 758 |
# File 'lib/rigor/scope.rb', line 756 def user_method_return(def_node, receiver, arg_types) Inference::ExpressionTyper.new(scope: self).return_type_for(def_node, receiver, arg_types) end |
#with_cvar(name, type) ⇒ Scope
402 403 404 |
# File 'lib/rigor/scope.rb', line 402 def with_cvar(name, type) rebuild(cvars: @cvars.merge(name.to_sym => type).freeze) end |
#with_declaration_sourced_local(name, type) ⇒ Scope
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).
348 349 350 351 |
# File 'lib/rigor/scope.rb', line 348 def with_declaration_sourced_local(name, type) written = with_local(name, type) written.with_local_declaration_mark(name) end |
#with_discovery(index) ⇒ Scope
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).
303 304 305 |
# File 'lib/rigor/scope.rb', line 303 def with_discovery(index) rebuild(discovery: index) end |
#with_fact(fact) ⇒ Scope
256 257 258 |
# File 'lib/rigor/scope.rb', line 256 def with_fact(fact) rebuild(fact_store: fact_store.with_fact(fact)) end |
#with_global(name, type) ⇒ Scope
406 407 408 |
# File 'lib/rigor/scope.rb', line 406 def with_global(name, type) rebuild(globals: @globals.merge(name.to_sym => type).freeze) end |
#with_indexed_narrowing(receiver_kind, receiver_name, key, type) ⇒ Scope
685 686 687 688 689 690 |
# File 'lib/rigor/scope.rb', line 685 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_inferred_param_mark(name) ⇒ Scope
ADR-67 WD6b — stamp the "inferred, not declared" provenance mark on a parameter local seeded from the
call-site parameter-inference table (Inference::ParameterInferenceCollector). Rides the ADR-58 WD1
declaration-sourced side-mark machinery under a distinct :inferred_param kind (never a carrier field,
so the displayed type is unchanged) so the negative in-body rules can decline on a receiver / argument
whose type is an open-call-site lower bound — firing against a lower bound is a false positive by
construction (the ADR-67 WD1 reasoning at the parameter boundary, carried one hop into the body). The
distinct kind keeps the inferred-param sites separable from ADR-58's ivar-copy :local mark, which a
later un-guarding slice (WD6b) needs — and the two kinds behave OPPOSITELY on both axes: :local is
dropped by with_local and intersected by join, while :inferred_param is sticky across with_local
and unioned by join. See #without_inferred_param_mark below for the clearing contract, and
docs/internal-spec/inference-engine.md § "Declaration-sourced provenance mark (ADR-58)" for the
normative statement of both.
378 379 380 |
# File 'lib/rigor/scope.rb', line 378 def with_inferred_param_mark(name) rebuild(declaration_sourced: add_declaration_sourced(:inferred_param, name)) end |
#with_ivar(name, type) ⇒ Scope
324 325 326 327 328 329 330 331 332 333 334 335 336 |
# File 'lib/rigor/scope.rb', line 324 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), ivar_origins: drop_origin(@ivar_origins, name), optimistic_ivars: drop_origin(@optimistic_ivars, name)) end |
#with_ivar_origin(name, cause) ⇒ Scope
222 223 224 225 226 |
# File 'lib/rigor/scope.rb', line 222 def with_ivar_origin(name, cause) return self if cause.nil? rebuild(ivar_origins: @ivar_origins.merge(name.to_sym => cause).freeze) end |
#with_local(name, type) ⇒ Scope
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/rigor/scope.rb', line 232 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), local_origins: drop_origin(@local_origins, name), optimistic_locals: drop_origin(@optimistic_locals, name)) end |
#with_local_declaration_mark(name) ⇒ Scope
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.
356 357 358 |
# File 'lib/rigor/scope.rb', line 356 def with_local_declaration_mark(name) rebuild(declaration_sourced: add_declaration_sourced(:local, name)) end |
#with_local_origin(name, cause) ⇒ Scope
Records the cause of the Dynamic value being bound to name. A nil cause is a no-op (the common
case — most bindings are concrete or have no recorded origin), so callers need not pre-check.
216 217 218 219 220 |
# File 'lib/rigor/scope.rb', line 216 def with_local_origin(name, cause) return self if cause.nil? rebuild(local_origins: @local_origins.merge(name.to_sym => cause).freeze) end |
#with_method_chain_narrowing(receiver_kind, receiver_name, method_name, type) ⇒ Scope
718 719 720 721 722 723 |
# File 'lib/rigor/scope.rb', line 718 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_optimistic_ivar(name, cause) ⇒ Scope
202 203 204 205 206 |
# File 'lib/rigor/scope.rb', line 202 def with_optimistic_ivar(name, cause) return self if cause.nil? rebuild(optimistic_ivars: @optimistic_ivars.merge(name.to_sym => cause).freeze) end |
#with_optimistic_local(name, cause) ⇒ Scope
196 197 198 199 200 |
# File 'lib/rigor/scope.rb', line 196 def with_optimistic_local(name, cause) return self if cause.nil? rebuild(optimistic_locals: @optimistic_locals.merge(name.to_sym => cause).freeze) end |
#with_self_type(type) ⇒ Scope
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.
263 264 265 |
# File 'lib/rigor/scope.rb', line 263 def with_self_type(type) rebuild(self_type: type) end |
#with_source_path(path) ⇒ Scope
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.
270 271 272 |
# File 'lib/rigor/scope.rb', line 270 def with_source_path(path) rebuild(source_path: path) end |
#with_struct_fold_safe(locals) ⇒ Scope
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.
276 277 278 |
# File 'lib/rigor/scope.rb', line 276 def with_struct_fold_safe(locals) rebuild(struct_fold_safe_locals: locals) end |
#without_indexed_narrowing(receiver_kind, receiver_name, key) ⇒ Scope
692 693 694 695 696 697 698 |
# File 'lib/rigor/scope.rb', line 692 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) ⇒ Scope
700 701 702 703 704 705 |
# File 'lib/rigor/scope.rb', line 700 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_inferred_param_mark(name) ⇒ Scope
ADR-67 WD6b — explicitly clear the inferred-parameter taint on name. The mark is deliberately STICKY:
with_local (used by both narrowing and reassignment) does NOT drop it, so it survives the narrowing /
join transitions between a lower-bound value's definition and its use (v = param[i]-1; if 0<=v and v<n
— the and narrows v between the two comparisons). It is cleared only at a genuine source-level local
write whose RHS does not derive from an inferred parameter (StatementEvaluator#eval_local_write), so a
local rebound to an independent value stops being treated as a lower bound. Over-retention (a mark that
outlives a rebind the write-path did not catch) only ever suppresses a diagnostic — the FP-safe
direction — so stickiness is the conservative choice. Zero-alloc when no mark is present.
390 391 392 393 |
# File 'lib/rigor/scope.rb', line 390 def without_inferred_param_mark(name) dropped = drop_declaration_sourced_for(:inferred_param, name) dropped.equal?(@declaration_sourced) ? self : rebuild(declaration_sourced: dropped) end |
#without_method_chain_narrowing(receiver_kind, receiver_name, method_name) ⇒ Scope
725 726 727 728 729 730 731 |
# File 'lib/rigor/scope.rb', line 725 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) ⇒ Scope
733 734 735 736 737 738 |
# File 'lib/rigor/scope.rb', line 733 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 |