Module: Rigor::Inference::ClosureEscapeAnalyzer
- Defined in:
- lib/rigor/inference/closure_escape_analyzer.rb
Overview
Slice 6 phase C sub-phase 3a — closure-escape classification.
Given a ‘(receiver_type, method_name)` pair representing a block-accepting call, this analyzer answers one question: does the receiver’s method invoke its block **immediately and synchronously**, without retaining the block past the call?
The answer is one of three outcomes:
-
‘:non_escaping` — the block is proven to be invoked immediately, zero or more times, and is NOT retained past the call. The receiver does not store the block in an instance variable, return it as a value, or schedule it for later invocation. Outer-local narrowing facts that survive the block body MAY safely survive the call.
-
‘:escaping` — the block is proven to be retained past the call (stored, returned, or invoked asynchronously). Outer narrowing facts on locals the block can rebind MUST be dropped at the call boundary.
-
‘:unknown` — the analyzer cannot prove either edge. Callers MUST treat `:unknown` as conservatively as `:escaping` for the purposes of fact retention; the distinction exists so diagnostics and later RBS-Extended effect plumbing can tell “deliberately conservative” apart from “declared escape”.
## Catalogue
Sub-phase 3a is RBS-blind: it ships a hardcoded catalogue keyed by Ruby class name. A future sub-phase will replace this with an ‘RBS::Extended` call-timing effect read from method signatures. The catalogue therefore covers ONLY the core-and-stdlib surface where immediate invocation is part of the documented contract:
-
‘Array`, `Hash`, `Range`, `Integer`, `Enumerator::Lazy` iteration methods (`each`, `map`, `select`, `reject`, `flat_map`, `find`/`detect`, `any?`, `all?`, `none?`, `one?`, `count`, `inject`/`reduce`, `each_with_index`, `each_with_object`, `min_by`, `max_by`, `sort_by`, `partition`, `group_by`, `tally`, `sum`, `take_while`, `drop_while`, `chunk_while`, `slice_when`, `zip`, `collect`, `collect_concat`, `filter`, `filter_map`).
-
‘Hash`-only iteration: `each_pair`, `each_key`, `each_value`, `transform_keys`, `transform_values`.
-
‘Integer#times`, `Integer#upto`, `Integer#downto`, `Range#each`, `Range#step`.
-
‘Object#tap`, `Object#then`, `Object#yield_self`.
-
Tuple/HashShape carriers map to Array/Hash for catalogue lookup so a literal ‘[1, 2, 3].each { … }` is recognised.
Anything outside the catalogue resolves to ‘:unknown`. The catalogue is intentionally narrow: adding entries requires confirming, by reading the method’s stdlib documentation, that the block is not retained. False positives in this catalogue would silently weaken the soundness of fact retention in later sub-phases.
The analyzer is a pure query. It MUST NOT mutate the receiver type or scope, MUST NOT raise on unrecognised inputs, and MUST be deterministic for a given input.
Constant Summary collapse
- OBJECT_NON_ESCAPING =
%i[tap then yield_self].freeze
- ENUMERABLE_NON_ESCAPING =
%i[ each map collect flat_map collect_concat select filter reject filter_map find detect find_index find_all any? all? none? one? count tally sum inject reduce each_with_index each_with_object min_by max_by sort_by minmax_by partition group_by chunk chunk_while slice_when slice_before slice_after take_while drop_while zip ].freeze
- ARRAY_EXTRA =
%i[each_index].freeze
- HASH_EXTRA =
%i[ each_pair each_key each_value transform_keys transform_values delete_if keep_if any? all? none? one? ].freeze
- RANGE_EXTRA =
%i[step].freeze
- INTEGER_EXTRA =
%i[times upto downto].freeze
- NON_ESCAPING =
{ "Array" => (ENUMERABLE_NON_ESCAPING + ARRAY_EXTRA).freeze, "Hash" => (ENUMERABLE_NON_ESCAPING + HASH_EXTRA).freeze, "Range" => (ENUMERABLE_NON_ESCAPING + RANGE_EXTRA).freeze, "Set" => ENUMERABLE_NON_ESCAPING, "Integer" => INTEGER_EXTRA, "Enumerator" => ENUMERABLE_NON_ESCAPING, "Enumerator::Lazy" => ENUMERABLE_NON_ESCAPING }.freeze
- ESCAPING =
Methods that are documented to retain the block past the call. The block is stored or scheduled, so outer narrowing facts on writeable captured locals cannot survive.
{ "Module" => %i[define_method].freeze, "Class" => %i[define_method].freeze, "Thread" => %i[new start fork].freeze, "Fiber" => %i[new].freeze, "Proc" => %i[new].freeze }.freeze
Class Method Summary collapse
-
.classify(receiver_type:, method_name:, environment: nil) ⇒ Symbol
One of ‘:non_escaping`, `:escaping`, `:unknown`.
Class Method Details
.classify(receiver_type:, method_name:, environment: nil) ⇒ Symbol
Returns one of ‘:non_escaping`, `:escaping`, `:unknown`.
77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/rigor/inference/closure_escape_analyzer.rb', line 77 def classify(receiver_type:, method_name:, environment: nil) # rubocop:disable Lint/UnusedMethodArgument return :unknown if receiver_type.nil? class_name = receiver_class_name(receiver_type) return :unknown if class_name.nil? method_sym = method_name.to_sym return :non_escaping if non_escaping?(class_name, method_sym) return :escaping if escaping?(class_name, method_sym) :unknown end |