Class: Rigor::Scope

Inherits:
Object
  • Object
show all
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 Method Summary collapse

Instance Method Summary collapse

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_cvarsObject (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_ivarsObject (readonly)

Returns the value of attribute class_ivars.



19
20
21
# File 'lib/rigor/scope.rb', line 19

def class_ivars
  @class_ivars
end

#cvarsObject (readonly)

Returns the value of attribute cvars.



19
20
21
# File 'lib/rigor/scope.rb', line 19

def cvars
  @cvars
end

#declared_typesObject (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_classesObject (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_nodesObject (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_methodsObject (readonly)

Returns the value of attribute discovered_methods.



19
20
21
# File 'lib/rigor/scope.rb', line 19

def discovered_methods
  @discovered_methods
end

#environmentObject (readonly)

Returns the value of attribute environment.



19
20
21
# File 'lib/rigor/scope.rb', line 19

def environment
  @environment
end

#fact_storeObject (readonly)

Returns the value of attribute fact_store.



19
20
21
# File 'lib/rigor/scope.rb', line 19

def fact_store
  @fact_store
end

#globalsObject (readonly)

Returns the value of attribute globals.



19
20
21
# File 'lib/rigor/scope.rb', line 19

def globals
  @globals
end

#in_source_constantsObject (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

#ivarsObject (readonly)

Returns the value of attribute ivars.



19
20
21
# File 'lib/rigor/scope.rb', line 19

def ivars
  @ivars
end

#localsObject (readonly)

Returns the value of attribute locals.



19
20
21
# File 'lib/rigor/scope.rb', line 19

def locals
  @locals
end

#program_globalsObject (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_typeObject (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



300
301
302
303
304
305
306
307
308
309
# File 'lib/rigor/scope.rb', line 300

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.

Returns:

  • (Boolean)


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.



275
276
277
# File 'lib/rigor/scope.rb', line 275

def evaluate(node, tracer: nil)
  Inference::StatementEvaluator.new(scope: self, tracer: tracer).evaluate(node)
end

#facts_for(target: nil, bucket: nil) ⇒ Object



257
258
259
# File 'lib/rigor/scope.rb', line 257

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

#hashObject



312
313
314
# File 'lib/rigor/scope.rb', line 312

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.

Raises:

  • (ArgumentError)


286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/rigor/scope.rb', line 286

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



261
262
263
# File 'lib/rigor/scope.rb', line 261

def local_facts(name, bucket: nil)
  facts_for(target: Analysis::FactStore::Target.local(name), bucket: bucket)
end

#type_of(node, tracer: nil) ⇒ Object



265
266
267
# File 'lib/rigor/scope.rb', line 265

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



253
254
255
# File 'lib/rigor/scope.rb', line 253

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