Class: Rigor::Scope
- Inherits:
-
Object
- Object
- Rigor::Scope
- Defined in:
- lib/rigor/scope.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
Instance Attribute Summary collapse
-
#class_cvars ⇒ Object
readonly
Returns the value of attribute class_cvars.
-
#class_ivars ⇒ Object
readonly
Returns the value of attribute class_ivars.
-
#cvars ⇒ Object
readonly
Returns the value of attribute cvars.
-
#declared_types ⇒ Object
readonly
Returns the value of attribute declared_types.
-
#discovered_classes ⇒ Object
readonly
Returns the value of attribute discovered_classes.
-
#discovered_def_nodes ⇒ Object
readonly
Returns the value of attribute discovered_def_nodes.
-
#discovered_methods ⇒ Object
readonly
Returns the value of attribute discovered_methods.
-
#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.
-
#in_source_constants ⇒ Object
readonly
Returns the value of attribute in_source_constants.
-
#ivars ⇒ Object
readonly
Returns the value of attribute ivars.
-
#locals ⇒ Object
readonly
Returns the value of attribute locals.
-
#program_globals ⇒ Object
readonly
Returns the value of attribute program_globals.
-
#self_type ⇒ Object
readonly
Returns the value of attribute self_type.
Class Method Summary collapse
Instance Method Summary collapse
-
#==(other) ⇒ Object
(also: #eql?)
rubocop:disable Metrics/CyclomaticComplexity.
-
#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_for(class_name) ⇒ Object
Slice 7 phase 2 — class-level ivar accumulator.
- #cvar(name) ⇒ Object
-
#discovered_method?(class_name, method_name, kind) ⇒ Boolean
Slice 7 phase 12 — in-source method discovery.
-
#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
- #global(name) ⇒ Object
- #hash ⇒ Object
-
#initialize(environment:, locals:, fact_store: Analysis::FactStore.empty, self_type: nil, declared_types: EMPTY_DECLARED_TYPES, ivars: EMPTY_VAR_BINDINGS, cvars: EMPTY_VAR_BINDINGS, globals: EMPTY_VAR_BINDINGS, class_ivars: EMPTY_CLASS_BINDINGS, class_cvars: EMPTY_CLASS_BINDINGS, program_globals: EMPTY_VAR_BINDINGS, discovered_classes: EMPTY_VAR_BINDINGS, in_source_constants: EMPTY_VAR_BINDINGS, discovered_methods: EMPTY_CLASS_BINDINGS, discovered_def_nodes: EMPTY_CLASS_BINDINGS) ⇒ 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
-
#top_level_def_for(method_name) ⇒ Object
v0.0.3 A — top-level def lookup for implicit-self calls.
- #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`.
- #with_class_cvars(table) ⇒ Object
- #with_class_ivars(table) ⇒ Object
- #with_cvar(name, type) ⇒ Object
-
#with_declared_types(table) ⇒ Object
Slice A-declarations.
-
#with_discovered_classes(table) ⇒ Object
Slice 7 phase 7 — in-source class discovery.
- #with_discovered_def_nodes(table) ⇒ Object
- #with_discovered_methods(table) ⇒ Object
- #with_fact(fact) ⇒ Object
- #with_global(name, type) ⇒ Object
-
#with_in_source_constants(table) ⇒ Object
Slice 7 phase 9 — in-source constant-value tracking.
- #with_ivar(name, type) ⇒ Object
- #with_local(name, type) ⇒ Object
-
#with_program_globals(table) ⇒ Object
Slice 7 phase 6 — program-level globals accumulator.
-
#with_self_type(type) ⇒ Object
Slice A-engine.
Constructor Details
#initialize(environment:, locals:, fact_store: Analysis::FactStore.empty, self_type: nil, declared_types: EMPTY_DECLARED_TYPES, ivars: EMPTY_VAR_BINDINGS, cvars: EMPTY_VAR_BINDINGS, globals: EMPTY_VAR_BINDINGS, class_ivars: EMPTY_CLASS_BINDINGS, class_cvars: EMPTY_CLASS_BINDINGS, program_globals: EMPTY_VAR_BINDINGS, discovered_classes: EMPTY_VAR_BINDINGS, in_source_constants: EMPTY_VAR_BINDINGS, discovered_methods: EMPTY_CLASS_BINDINGS, discovered_def_nodes: EMPTY_CLASS_BINDINGS) ⇒ Scope
Returns a new instance of Scope.
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/rigor/scope.rb', line 36 def initialize( environment:, locals:, fact_store: Analysis::FactStore.empty, self_type: nil, declared_types: EMPTY_DECLARED_TYPES, ivars: EMPTY_VAR_BINDINGS, cvars: EMPTY_VAR_BINDINGS, globals: EMPTY_VAR_BINDINGS, class_ivars: EMPTY_CLASS_BINDINGS, class_cvars: EMPTY_CLASS_BINDINGS, program_globals: EMPTY_VAR_BINDINGS, discovered_classes: EMPTY_VAR_BINDINGS, in_source_constants: EMPTY_VAR_BINDINGS, discovered_methods: EMPTY_CLASS_BINDINGS, discovered_def_nodes: EMPTY_CLASS_BINDINGS ) @environment = environment @locals = locals @fact_store = fact_store @self_type = self_type @declared_types = declared_types @ivars = ivars @cvars = cvars @globals = globals @class_ivars = class_ivars @class_cvars = class_cvars @program_globals = program_globals @discovered_classes = discovered_classes @in_source_constants = in_source_constants @discovered_methods = discovered_methods @discovered_def_nodes = discovered_def_nodes freeze end |
Instance Attribute Details
#class_cvars ⇒ Object (readonly)
Returns the value of attribute class_cvars.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def class_cvars @class_cvars end |
#class_ivars ⇒ Object (readonly)
Returns the value of attribute class_ivars.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def class_ivars @class_ivars end |
#cvars ⇒ Object (readonly)
Returns the value of attribute cvars.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def cvars @cvars end |
#declared_types ⇒ Object (readonly)
Returns the value of attribute declared_types.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def declared_types @declared_types end |
#discovered_classes ⇒ Object (readonly)
Returns the value of attribute discovered_classes.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def discovered_classes @discovered_classes end |
#discovered_def_nodes ⇒ Object (readonly)
Returns the value of attribute discovered_def_nodes.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def discovered_def_nodes @discovered_def_nodes end |
#discovered_methods ⇒ Object (readonly)
Returns the value of attribute discovered_methods.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def discovered_methods @discovered_methods end |
#environment ⇒ Object (readonly)
Returns the value of attribute environment.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def environment @environment end |
#fact_store ⇒ Object (readonly)
Returns the value of attribute fact_store.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def fact_store @fact_store end |
#globals ⇒ Object (readonly)
Returns the value of attribute globals.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def globals @globals end |
#in_source_constants ⇒ Object (readonly)
Returns the value of attribute in_source_constants.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def in_source_constants @in_source_constants end |
#ivars ⇒ Object (readonly)
Returns the value of attribute ivars.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def ivars @ivars end |
#locals ⇒ Object (readonly)
Returns the value of attribute locals.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def locals @locals end |
#program_globals ⇒ Object (readonly)
Returns the value of attribute program_globals.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def program_globals @program_globals end |
#self_type ⇒ Object (readonly)
Returns the value of attribute self_type.
19 20 21 |
# File 'lib/rigor/scope.rb', line 19 def self_type @self_type end |
Class Method Details
.empty(environment: Environment.default) ⇒ Object
31 32 33 |
# File 'lib/rigor/scope.rb', line 31 def empty(environment: Environment.default) new(environment: environment, locals: {}.freeze, fact_store: Analysis::FactStore.empty) end |
Instance Method Details
#==(other) ⇒ Object Also known as: eql?
rubocop:disable Metrics/CyclomaticComplexity
314 315 316 317 318 319 320 321 322 323 |
# File 'lib/rigor/scope.rb', line 314 def ==(other) # rubocop:disable Metrics/CyclomaticComplexity 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 end |
#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).
170 171 172 173 174 |
# File 'lib/rigor/scope.rb', line 170 def class_cvars_for(class_name) return EMPTY_VAR_BINDINGS if class_name.nil? @class_cvars[class_name.to_s] || EMPTY_VAR_BINDINGS end |
#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.
156 157 158 159 160 |
# File 'lib/rigor/scope.rb', line 156 def class_ivars_for(class_name) return EMPTY_VAR_BINDINGS if class_name.nil? @class_ivars[class_name.to_s] || EMPTY_VAR_BINDINGS end |
#cvar(name) ⇒ Object
123 124 125 |
# File 'lib/rigor/scope.rb', line 123 def cvar(name) @cvars[name.to_sym] end |
#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.
226 227 228 229 230 231 |
# File 'lib/rigor/scope.rb', line 226 def discovered_method?(class_name, method_name, kind) table = @discovered_methods[class_name.to_s] return false unless table table[method_name.to_sym] == kind end |
#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.
289 290 291 |
# File 'lib/rigor/scope.rb', line 289 def evaluate(node, tracer: nil) Inference::StatementEvaluator.new(scope: self, tracer: tracer).evaluate(node) end |
#facts_for(target: nil, bucket: nil) ⇒ Object
271 272 273 |
# File 'lib/rigor/scope.rb', line 271 def facts_for(target: nil, bucket: nil) fact_store.facts_for(target: target, bucket: bucket) end |
#global(name) ⇒ Object
127 128 129 |
# File 'lib/rigor/scope.rb', line 127 def global(name) @globals[name.to_sym] end |
#hash ⇒ Object
326 327 328 |
# File 'lib/rigor/scope.rb', line 326 def hash [Scope, environment.object_id, @locals, fact_store, self_type, @ivars, @cvars, @globals].hash 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.
119 120 121 |
# File 'lib/rigor/scope.rb', line 119 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.
300 301 302 303 304 305 306 307 308 309 310 311 312 |
# File 'lib/rigor/scope.rb', line 300 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
70 71 72 |
# File 'lib/rigor/scope.rb', line 70 def local(name) @locals[name.to_sym] end |
#local_facts(name, bucket: nil) ⇒ Object
275 276 277 |
# File 'lib/rigor/scope.rb', line 275 def local_facts(name, bucket: nil) facts_for(target: Analysis::FactStore::Target.local(name), bucket: bucket) 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.
260 261 262 263 264 265 |
# File 'lib/rigor/scope.rb', line 260 def top_level_def_for(method_name) table = @discovered_def_nodes[Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY] return nil unless table table[method_name.to_sym] end |
#type_of(node, tracer: nil) ⇒ Object
279 280 281 |
# File 'lib/rigor/scope.rb', line 279 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.
246 247 248 249 250 251 |
# File 'lib/rigor/scope.rb', line 246 def user_def_for(class_name, method_name) table = @discovered_def_nodes[class_name.to_s] return nil unless table table[method_name.to_sym] end |
#with_class_cvars(table) ⇒ Object
176 177 178 |
# File 'lib/rigor/scope.rb', line 176 def with_class_cvars(table) rebuild(class_cvars: table) end |
#with_class_ivars(table) ⇒ Object
162 163 164 |
# File 'lib/rigor/scope.rb', line 162 def with_class_ivars(table) rebuild(class_ivars: table) end |
#with_cvar(name, type) ⇒ Object
135 136 137 |
# File 'lib/rigor/scope.rb', line 135 def with_cvar(name, type) rebuild(cvars: @cvars.merge(name.to_sym => type).freeze) end |
#with_declared_types(table) ⇒ Object
Slice A-declarations. Returns a scope that carries an identity-comparing Hash of ‘Prism::Node => Rigor::Type` overrides. `ExpressionTyper#type_of(node)` MUST consult `declared_types` before any other dispatch and return the recorded type as-is when present. The table is populated by `ScopeIndexer` for declaration-position nodes (the `constant_path` of `Prism::ModuleNode` and `Prism::ClassNode`) so a `module Foo` / `class Bar` header types as `Singleton[<qualified path>]` instead of falling through to `Dynamic`. The table is shared by structural reference across every derived scope so `with_local` / `with_fact` / `with_self_type` carry it transparently.
106 107 108 |
# File 'lib/rigor/scope.rb', line 106 def with_declared_types(table) rebuild(declared_types: table) end |
#with_discovered_classes(table) ⇒ Object
Slice 7 phase 7 — in-source class discovery. Maps a qualified class name (e.g. ‘“Account”`) to its `Type::Singleton` so references to user-defined classes in the analyzed files resolve through `ExpressionTyper#resolve_constant_name` even when no RBS decl exists. Populated once at index time by `ScopeIndexer` from every `Prism::ClassNode` and `Prism::ModuleNode` it walks.
198 199 200 |
# File 'lib/rigor/scope.rb', line 198 def with_discovered_classes(table) rebuild(discovered_classes: table) end |
#with_discovered_def_nodes(table) ⇒ Object
267 268 269 |
# File 'lib/rigor/scope.rb', line 267 def with_discovered_def_nodes(table) rebuild(discovered_def_nodes: table) end |
#with_discovered_methods(table) ⇒ Object
233 234 235 |
# File 'lib/rigor/scope.rb', line 233 def with_discovered_methods(table) rebuild(discovered_methods: table) end |
#with_fact(fact) ⇒ Object
80 81 82 |
# File 'lib/rigor/scope.rb', line 80 def with_fact(fact) rebuild(fact_store: fact_store.with_fact(fact)) end |
#with_global(name, type) ⇒ Object
139 140 141 |
# File 'lib/rigor/scope.rb', line 139 def with_global(name, type) rebuild(globals: @globals.merge(name.to_sym => type).freeze) end |
#with_in_source_constants(table) ⇒ Object
Slice 7 phase 9 — in-source constant-value tracking. Maps a qualified constant name (e.g. ‘“BUCKETS”` or `“Rigor::Analysis::FactStore::BUCKETS”`) to the type of the rvalue assigned at its `Prism::ConstantWriteNode` / `Prism::ConstantPathWriteNode`. Populated by `ScopeIndexer` once at index time. `ExpressionTyper#resolve_constant_name` consults this map after class lookups so an in-source constant assignment overrides any RBS-declared constant of the same qualified name (matching Ruby’s runtime precedence: a constant defined in user code is the authoritative value).
213 214 215 |
# File 'lib/rigor/scope.rb', line 213 def with_in_source_constants(table) rebuild(in_source_constants: table) end |
#with_ivar(name, type) ⇒ Object
131 132 133 |
# File 'lib/rigor/scope.rb', line 131 def with_ivar(name, type) rebuild(ivars: @ivars.merge(name.to_sym => type).freeze) end |
#with_local(name, type) ⇒ Object
74 75 76 77 78 |
# File 'lib/rigor/scope.rb', line 74 def with_local(name, type) new_locals = @locals.merge(name.to_sym => type).freeze new_fact_store = fact_store.invalidate_target(Analysis::FactStore::Target.local(name)) rebuild(locals: new_locals, fact_store: new_fact_store) end |
#with_program_globals(table) ⇒ Object
Slice 7 phase 6 — program-level globals accumulator. Globals are process-wide in Ruby, so the analyzer carries a single map (‘Hash[Symbol, Type]`) keyed by the variable name and seeded into every method body (instance and singleton) plus the top-level program scope. `ScopeIndexer` populates it from a single program-wide pre-pass.
186 187 188 |
# File 'lib/rigor/scope.rb', line 186 def with_program_globals(table) rebuild(program_globals: 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.
89 90 91 |
# File 'lib/rigor/scope.rb', line 89 def with_self_type(type) rebuild(self_type: type) end |