Module: Rigor::Inference::IndexedNarrowing
- Defined in:
- lib/rigor/inference/indexed_narrowing.rb
Overview
Closes the "params[:f] ||= []; params[:f] << 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[k] ||= default").
After receiver[key] ||= default the next read at receiver[key] 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[nil].
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[:k]) 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[field]) 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[key] = valuewrites 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[key]againstscope, 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 areceiver[key]read or write whose receiver and key are both stable, otherwise nil. -
.stable_key(node) ⇒ Object
Returns the literal Ruby value when
nodeis a stable key shape, otherwise nil. -
.stable_receiver(node) ⇒ Object
Returns
[receiver_kind, receiver_name]whennodeis aLocalVariableReadNodeorInstanceVariableReadNode, otherwise nil.
Class Method Details
.invalidate_after_call(call_node:, current_scope:) ⇒ Object
Removes recorded narrowings invalidated by call_node. Two patterns:
receiver[key] = value(a:[]=against a stable address): drop the specific(receiver, key)entry.- Any mutator from
HASH_MUTATORS/ARRAY_MUTATORSagainst 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).
109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 109 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).
151 152 153 154 155 156 157 158 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 151 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
126 127 128 129 130 131 132 133 134 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 126 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
136 137 138 139 140 141 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 136 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[key] 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.
89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 89 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
121 122 123 124 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 121 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[key] 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.
76 77 78 79 80 81 82 83 84 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 76 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.
63 64 65 66 67 68 69 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 63 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.
54 55 56 57 58 59 |
# File 'lib/rigor/inference/indexed_narrowing.rb', line 54 def stable_receiver(node) case node when Prism::LocalVariableReadNode then [:local, node.name] when Prism::InstanceVariableReadNode then [:ivar, node.name] end end |