Module: Rigor::Inference::ClosureEscapeAnalyzer
- Defined in:
- lib/rigor/inference/closure_escape_analyzer.rb,
sig/rigor/inference.rbs
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:unknownas conservatively as:escapingfor 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::Lazyiteration 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.
Instance Method Summary collapse
Class Method Details
.classify(receiver_type:, method_name:, environment: nil) ⇒ Symbol
Returns one of :non_escaping, :escaping, :unknown.
59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/rigor/inference/closure_escape_analyzer.rb', line 59 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 |
Instance Method Details
#self?.classify ⇒ closure_classification
128 |
# File 'sig/rigor/inference.rbs', line 128
def self?.classify: (receiver_type: Type::t?, method_name: Symbol, ?environment: Environment?) -> closure_classification
|