Module: Rigor::Inference::IndexedNarrowing
- Defined in:
- lib/rigor/inference/indexed_narrowing.rb
Overview
Closes the “‘params ||= []; params << x`” precision gap surfaced by the Redmine 6.1.2 `Query#as_params` survey (ROADMAP § Future cycles / Type-language / engine —“Indexed-collection narrowing through `Hash ||= default`”).
After ‘receiver ||= default` the next read at `receiver` is known non-nil, but Rigor types each `Hash#[]` independently and the subsequent `<<` / `[]=` / other mutator dispatches against the un-narrowed result —which on a `HashShape{}` carrier folds to `Constant`.
This module is the address-recogniser + invalidator shared by StatementEvaluator‘s `eval_index_or_write` handler (which RECORDS the narrowing) and `eval_call` (which INVALIDATES on intervening writes / mutators) and by ExpressionTyper’s ‘call_type_for` (which CONSUMES the narrowing when typing a follow-up `[]` read).
**Stable receivers.** A receiver is “stable” iff it is a ‘LocalVariableReadNode` or `InstanceVariableReadNode`. Method-call chains (`foo.bar`) and other shapes are rejected because a follow-up read against an identical- looking AST chain has no guarantee of resolving to the same runtime value — narrowing it would invent a fact.
**Stable keys.** A key is “stable” iff it is a literal ‘SymbolNode` / `StringNode` / `IntegerNode`. Local-variable keys (`params`) are excluded for the same invent-a-fact reason: the local could be rebound between the `||=` and the read.
Invalidation. Three conditions drop a recorded narrowing:
-
The receiver variable is rebound (handled inside ‘Scope#with_local` / `Scope#with_ivar`).
-
An intervening ‘receiver = value` writes the same slot — `:[]=` could rebind the slot to nil; conservative drop.
-
An intervening mutator from MutationWidening::HASH_MUTATORS or MutationWidening::ARRAY_MUTATORS runs against the receiver (e.g. ‘params.delete(:f)`, `params.clear`).
All three are implemented in ‘StatementEvaluator#eval_call`’s post-dispatch path through IndexedNarrowing.invalidate_after_call.
Constant Summary collapse
- STABLE_KEY_NODES =
Literal Prism nodes whose Ruby value the analyzer trusts as a stable address. Symbol / String are the dominant Hash key shapes; Integer covers numerically-keyed Hashes and Array indices.
[Prism::SymbolNode, Prism::StringNode, Prism::IntegerNode].freeze
Class Method Summary collapse
-
.invalidate_after_call(call_node:, current_scope:) ⇒ Object
Removes recorded narrowings invalidated by ‘call_node`.
-
.invalidate_chain_after_call(call_node:, current_scope:) ⇒ Object
Companion invalidator for single-hop method-chain narrowings (ROADMAP § Future cycles — “Method-call receiver narrowing across stable receivers”, B2 from the slice’s design notes).
- .invalidate_indexed_write(call_node, current_scope) ⇒ Object
- .invalidate_mutator(call_node, current_scope) ⇒ Object
-
.lookup_for_call(node, scope) ⇒ Object
Looks up a recorded narrowing for ‘receiver` against `scope`, returning the narrowed type or nil when no entry applies.
- .mutator?(method_name) ⇒ Boolean
-
.stable_address(receiver_node, key_node) ⇒ Object
Returns ‘[receiver_kind, receiver_name, key]` when the CallNode is a `receiver` read or write whose receiver and key are both stable, otherwise nil.
-
.stable_key(node) ⇒ Object
Returns the literal Ruby value when ‘node` is a stable key shape, otherwise nil.
-
.stable_receiver(node) ⇒ Object
Returns ‘[receiver_kind, receiver_name]` when `node` is a `LocalVariableReadNode` or `InstanceVariableReadNode`, otherwise nil.
Class Method Details
.invalidate_after_call(call_node:, current_scope:) ⇒ Object
Removes recorded narrowings invalidated by ‘call_node`. Two patterns:
-
‘receiver = value` (a `:[]=` against a stable address): drop the specific `(receiver, key)` entry.
-
Any mutator from ‘HASH_MUTATORS` / `ARRAY_MUTATORS` against a stable receiver: drop EVERY entry rooted at that receiver, because the mutator could rebind any slot.
Returns the updated scope. Always-safe (only forgets; never invents).
130 131 132 133 134 135 136 137 138 139 140 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 130 def invalidate_after_call(call_node:, current_scope:) return current_scope unless call_node.is_a?(Prism::CallNode) if call_node.name == :[]= invalidate_indexed_write(call_node, current_scope) elsif mutator?(call_node.name) invalidate_mutator(call_node, current_scope) else current_scope end end |
.invalidate_chain_after_call(call_node:, current_scope:) ⇒ Object
Companion invalidator for single-hop method-chain narrowings (ROADMAP § Future cycles — “Method-call receiver narrowing across stable receivers”, B2 from the slice’s design notes). Drops every ‘(receiver, *)` chain narrowing rooted at the call’s OUTER stable receiver — matching the ROADMAP’s “any intervening method call against the same receiver” criterion. A call against ‘x.last` (the OUTER receiver is a `CallNode`, not a stable root) does NOT drop narrowings keyed on `x`, so the worked-site `x.last << y` pattern correctly preserves the chain narrowing for any further `x.last` read in the same body. Always-safe (only forgets; never invents).
177 178 179 180 181 182 183 184 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 177 def invalidate_chain_after_call(call_node:, current_scope:) return current_scope unless call_node.is_a?(Prism::CallNode) receiver = stable_receiver(call_node.receiver) return current_scope if receiver.nil? current_scope.without_method_chain_narrowings_for(*receiver) end |
.invalidate_indexed_write(call_node, current_scope) ⇒ Object
147 148 149 150 151 152 153 154 155 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 147 def invalidate_indexed_write(call_node, current_scope) args = call_node.arguments&.arguments return current_scope if args.nil? || args.empty? address = stable_address(call_node.receiver, args.first) return current_scope if address.nil? current_scope.without_indexed_narrowing(*address) end |
.invalidate_mutator(call_node, current_scope) ⇒ Object
157 158 159 160 161 162 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 157 def invalidate_mutator(call_node, current_scope) receiver = stable_receiver(call_node.receiver) return current_scope if receiver.nil? current_scope.without_indexed_narrowings_for(*receiver) end |
.lookup_for_call(node, scope) ⇒ Object
Looks up a recorded narrowing for ‘receiver` against `scope`, returning the narrowed type or nil when no entry applies. Used by ExpressionTyper’s ‘[]` dispatch to refine the result of a stable indexed read.
106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 106 def lookup_for_call(node, scope) return nil unless node.is_a?(Prism::CallNode) return nil unless node.name == :[] return nil if node.arguments.nil? return nil unless node.arguments.arguments.size == 1 address = stable_address(node.receiver, node.arguments.arguments.first) return nil if address.nil? scope.indexed_narrowing(*address) end |
.mutator?(method_name) ⇒ Boolean
142 143 144 145 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 142 def mutator?(method_name) MutationWidening::HASH_MUTATORS.include?(method_name) || MutationWidening::ARRAY_MUTATORS.include?(method_name) end |
.stable_address(receiver_node, key_node) ⇒ Object
Returns ‘[receiver_kind, receiver_name, key]` when the CallNode is a `receiver` read or write whose receiver and key are both stable, otherwise nil. Used by both the recorder (for `IndexOrWriteNode`’s receiver/arguments triplet) and the invalidator (for ‘CallNode :[]=` / mutator calls). Treats only the FIRST argument as the key; `:[]=`’s second argument is the rvalue and is not part of the address.
92 93 94 95 96 97 98 99 100 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 92 def stable_address(receiver_node, key_node) receiver = stable_receiver(receiver_node) return nil if receiver.nil? key = stable_key(key_node) return nil if key.nil? [receiver.first, receiver.last, key] end |
.stable_key(node) ⇒ Object
Returns the literal Ruby value when ‘node` is a stable key shape, otherwise nil. Symbols → `Symbol`, Strings → `String` (unescaped), Integers → `Integer`.
76 77 78 79 80 81 82 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 76 def stable_key(node) case node when Prism::SymbolNode then node.unescaped.to_sym when Prism::StringNode then node.unescaped when Prism::IntegerNode then node.value end end |
.stable_receiver(node) ⇒ Object
Returns ‘[receiver_kind, receiver_name]` when `node` is a `LocalVariableReadNode` or `InstanceVariableReadNode`, otherwise nil.
66 67 68 69 70 71 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 66 def stable_receiver(node) case node when Prism::LocalVariableReadNode then [:local, node.name] when Prism::InstanceVariableReadNode then [:ivar, node.name] end end |