rigortype 0.4.7 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/data/core_overlay/bigdecimal.rbs +33 -0
- data/data/core_overlay/resolv.rbs +30 -0
- data/data/gem_overlay/activesupport/core_ext.rbs +29 -0
- data/data/gem_overlay/addressable/addressable.rbs +40 -0
- data/docs/handbook/01-getting-started.md +1 -1
- data/docs/handbook/11-sig-gen.md +35 -0
- data/docs/install.md +20 -0
- data/docs/manual/01-installation.md +67 -26
- data/docs/manual/02-cli-reference.md +307 -91
- data/docs/manual/03-configuration.md +41 -3
- data/docs/manual/04-diagnostics.md +5 -4
- data/docs/manual/08-skills.md +11 -0
- data/docs/manual/09-editor-integration.md +8 -0
- data/docs/manual/14-rails-quickstart.md +1 -1
- data/docs/manual/19-effect-labels.md +3 -4
- data/docs/manual/plugins/rigor-actionpack.md +15 -6
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +3 -0
- data/docs/manual/plugins/rigor-sorbet.md +17 -3
- data/lib/rigor/analysis/check_rules/always_raises_shield.rb +147 -0
- data/lib/rigor/analysis/check_rules/call_facts.rb +58 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +8 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +13 -2
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +6 -0
- data/lib/rigor/analysis/check_rules/plugin_api_surface.rb +195 -0
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +40 -11
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +27 -1
- data/lib/rigor/analysis/check_rules/replaced_singleton_defs.rb +96 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +8 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +1 -1
- data/lib/rigor/analysis/check_rules.rb +598 -99
- data/lib/rigor/analysis/crash_signature.rb +43 -1
- data/lib/rigor/analysis/dependency_recorder.rb +233 -76
- data/lib/rigor/analysis/incremental.rb +11 -1
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +279 -37
- data/lib/rigor/analysis/reachability/plugin_roots.rb +6 -3
- data/lib/rigor/analysis/rule_catalog.rb +37 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -3
- data/lib/rigor/analysis/run_disclosure_credit.rb +26 -6
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +4 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +7 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +72 -20
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +3 -8
- data/lib/rigor/analysis/runner/pool_coordinator.rb +26 -5
- data/lib/rigor/analysis/runner/project_pre_passes.rb +4 -37
- data/lib/rigor/analysis/runner.rb +222 -39
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +61 -22
- data/lib/rigor/analysis/worker_session.rb +42 -41
- data/lib/rigor/bleeding_edge.rb +19 -4
- data/lib/rigor/cache/file_digest.rb +39 -31
- data/lib/rigor/cache/incremental_snapshot.rb +66 -7
- data/lib/rigor/cache/rbs_descriptor.rb +37 -2
- data/lib/rigor/cache/rbs_environment.rb +5 -1
- data/lib/rigor/cli/baseline_command.rb +15 -2
- data/lib/rigor/cli/check_command.rb +4 -2
- data/lib/rigor/cli/effects_snapshot_command.rb +8 -1
- data/lib/rigor/cli/lens_command.rb +17 -2
- data/lib/rigor/cli/sig_gen_command.rb +8 -0
- data/lib/rigor/cli/skill_describe.rb +1 -0
- data/lib/rigor/cli.rb +5 -2
- data/lib/rigor/config_audit.rb +32 -18
- data/lib/rigor/configuration/severity_profile.rb +9 -0
- data/lib/rigor/configuration.rb +32 -5
- data/lib/rigor/effects/collector.rb +53 -21
- data/lib/rigor/environment/bundle_sig_discovery.rb +63 -28
- data/lib/rigor/environment/constant_type_cache_holder.rb +5 -0
- data/lib/rigor/environment/default_libraries.rb +17 -0
- data/lib/rigor/environment/deferred_signature_plan.rb +104 -6
- data/lib/rigor/environment/hkt_registry_holder.rb +6 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +123 -8
- data/lib/rigor/environment/rbs_coverage_report.rb +12 -11
- data/lib/rigor/environment/rbs_hierarchy.rb +6 -0
- data/lib/rigor/environment/rbs_loader.rb +312 -63
- data/lib/rigor/environment.rb +131 -35
- data/lib/rigor/inference/acceptance.rb +50 -15
- data/lib/rigor/inference/alias_mutation_floor/foreign_locals.rb +117 -0
- data/lib/rigor/inference/alias_mutation_floor/memos.rb +85 -0
- data/lib/rigor/inference/alias_mutation_floor/reach.rb +119 -0
- data/lib/rigor/inference/alias_mutation_floor/site_collector.rb +252 -0
- data/lib/rigor/inference/alias_mutation_floor.rb +339 -0
- data/lib/rigor/inference/anonymous_meta_class.rb +18 -0
- data/lib/rigor/inference/block_auto_splat.rb +1 -1
- data/lib/rigor/inference/block_run_count.rb +9 -2
- data/lib/rigor/inference/builtins/array_catalog.rb +2 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +1 -1
- data/lib/rigor/inference/captured_locals.rb +2 -0
- data/lib/rigor/inference/census_foreign_block.rb +354 -0
- data/lib/rigor/inference/clause_widening.rb +1 -1
- data/lib/rigor/inference/closure_escape_analyzer.rb +11 -4
- data/lib/rigor/inference/construction_escape.rb +56 -15
- data/lib/rigor/inference/declaration_walk.rb +4 -0
- data/lib/rigor/inference/def_node_resolver.rb +32 -14
- data/lib/rigor/inference/def_return_typer.rb +1 -1
- data/lib/rigor/inference/definer_resolution.rb +15 -7
- data/lib/rigor/inference/definers_index.rb +72 -16
- data/lib/rigor/inference/error_info.rb +16 -0
- data/lib/rigor/inference/expression_typer.rb +1459 -287
- data/lib/rigor/inference/flow_tracer.rb +31 -14
- data/lib/rigor/inference/fork_map.rb +3 -0
- data/lib/rigor/inference/fresh_frame_blocks.rb +52 -1
- data/lib/rigor/inference/global_write_census.rb +8 -6
- data/lib/rigor/inference/in_effect_refinements.rb +7 -0
- data/lib/rigor/inference/indexed_narrowing.rb +57 -8
- data/lib/rigor/inference/ivar_writer_census.rb +106 -11
- data/lib/rigor/inference/key_presence_guard.rb +51 -25
- data/lib/rigor/inference/last_line/self_evidence.rb +30 -0
- data/lib/rigor/inference/last_line.rb +5 -2
- data/lib/rigor/inference/loop_value_record.rb +63 -15
- data/lib/rigor/inference/match_rebinding/frame.rb +11 -0
- data/lib/rigor/inference/match_rebinding/yielding_match.rb +74 -18
- data/lib/rigor/inference/match_rebinding.rb +21 -8
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +43 -29
- data/lib/rigor/inference/method_dispatcher/class_keyed_block_params.rb +65 -0
- data/lib/rigor/inference/method_dispatcher/core_enumerable_receiver.rb +44 -0
- data/lib/rigor/inference/method_dispatcher/declared_constructor.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/facet_distribution.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/interface_shape.rb +181 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +12 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +409 -67
- data/lib/rigor/inference/method_dispatcher/self_substitute.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +384 -64
- data/lib/rigor/inference/method_dispatcher/singleton_type_params.rb +104 -0
- data/lib/rigor/inference/method_dispatcher.rb +66 -18
- data/lib/rigor/inference/method_parameter_binder.rb +136 -56
- data/lib/rigor/inference/module_function_state/spellings.rb +2 -2
- data/lib/rigor/inference/module_function_state.rb +13 -2
- data/lib/rigor/inference/multi_target_binder.rb +1 -1
- data/lib/rigor/inference/mutation_widening.rb +44 -4
- data/lib/rigor/inference/narrowing.rb +201 -27
- data/lib/rigor/inference/operand_effects.rb +43 -2
- data/lib/rigor/inference/optimistic_origin.rb +133 -8
- data/lib/rigor/inference/origin_lookup.rb +1 -1
- data/lib/rigor/inference/precision_scanner.rb +1 -1
- data/lib/rigor/inference/project_method_ownership.rb +28 -10
- data/lib/rigor/inference/project_patched_methods.rb +65 -13
- data/lib/rigor/inference/project_patched_scanner.rb +120 -25
- data/lib/rigor/inference/project_patched_visibility.rb +39 -3
- data/lib/rigor/inference/rbs_type_translator.rb +68 -15
- data/lib/rigor/inference/receiver_writer_call.rb +67 -0
- data/lib/rigor/inference/recursion_rebinding.rb +157 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +56 -18
- data/lib/rigor/inference/refined_dispatch/own_method.rb +51 -17
- data/lib/rigor/inference/refined_dispatch.rb +13 -0
- data/lib/rigor/inference/reflective_ivar_write.rb +19 -11
- data/lib/rigor/inference/remover_holder_scan.rb +76 -22
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +13 -6
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +12 -11
- data/lib/rigor/inference/scope_indexer/ivar_contributions.rb +188 -28
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +136 -9
- data/lib/rigor/inference/scope_indexer.rb +2079 -183
- data/lib/rigor/inference/self_call_ivar_writes.rb +26 -7
- data/lib/rigor/inference/self_preserving_block.rb +87 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +41 -6
- data/lib/rigor/inference/singleton_object_constant.rb +32 -2
- data/lib/rigor/inference/statement_evaluator.rb +970 -250
- data/lib/rigor/inference/string_mutation.rb +14 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +35 -11
- data/lib/rigor/inference/toplevel_def_census.rb +14 -6
- data/lib/rigor/inference/unknown_store_widening.rb +1 -1
- data/lib/rigor/inference/untyped_rejoin.rb +1 -1
- data/lib/rigor/inference/void_tail_summary.rb +29 -10
- data/lib/rigor/lens/claims/comparison.rb +78 -0
- data/lib/rigor/lens/claims/request.rb +131 -0
- data/lib/rigor/lens/claims/type_reader.rb +216 -0
- data/lib/rigor/lens/claims.rb +208 -0
- data/lib/rigor/lens/contract_reader.rb +5 -0
- data/lib/rigor/lens/document.rb +1 -1
- data/lib/rigor/lens/file_reader.rb +21 -8
- data/lib/rigor/lens/run.rb +8 -0
- data/lib/rigor/lens.rb +1 -1
- data/lib/rigor/library_probe.rb +68 -0
- data/lib/rigor/memo_audit.rb +282 -0
- data/lib/rigor/plugin/base.rb +15 -2
- data/lib/rigor/plugin/io_boundary.rb +25 -3
- data/lib/rigor/plugin/manifest.rb +7 -3
- data/lib/rigor/plugin/registry.rb +13 -2
- data/lib/rigor/plugin/trust_policy.rb +46 -0
- data/lib/rigor/protection/mutation_cache.rb +4 -1
- data/lib/rigor/reflection/constant_ancestors.rb +79 -22
- data/lib/rigor/reflection/declaration_collectors.rb +55 -0
- data/lib/rigor/reflection/declarations.rb +58 -0
- data/lib/rigor/reflection/project_method.rb +177 -0
- data/lib/rigor/reflection.rb +2 -0
- data/lib/rigor/rooted_glob.rb +18 -1
- data/lib/rigor/run_scoped_slots.rb +20 -20
- data/lib/rigor/scope/discovery_index.rb +149 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -1
- data/lib/rigor/scope/resolution_chain.rb +900 -55
- data/lib/rigor/scope.rb +307 -34
- data/lib/rigor/sig_gen/bot_proposal_guard.rb +306 -0
- data/lib/rigor/sig_gen/classification.rb +14 -1
- data/lib/rigor/sig_gen/generator.rb +222 -13
- data/lib/rigor/sig_gen/method_candidate.rb +14 -0
- data/lib/rigor/sig_gen/observation_collector.rb +11 -2
- data/lib/rigor/sig_gen/platform_guarded_files.rb +184 -0
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +47 -0
- data/lib/rigor/sig_gen/writer.rb +21 -0
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/source/forwardable_delegators.rb +122 -0
- data/lib/rigor/source/node_children.rb +13 -0
- data/lib/rigor/type/combinator.rb +164 -8
- data/lib/rigor/type/interface.rb +60 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/type_of_memo/serving.rb +99 -0
- data/lib/rigor/type_of_memo/stats.rb +43 -0
- data/lib/rigor/type_of_memo.rb +337 -0
- data/lib/rigor/typing_state.rb +1094 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +2 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +57 -25
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +3 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +25 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +16 -6
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +130 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_table.rb +20 -5
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +7 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +49 -13
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +19 -5
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +4 -2
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +13 -5
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +29 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +15 -21
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +4 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +12 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +6 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/analyzer.rb +199 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +3 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +4 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +209 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +146 -7
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +52 -34
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +80 -3
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +16 -16
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +1 -6
- data/sig/rigor/analysis/check_rules/rule_walk.rbs +6 -0
- data/sig/rigor/analysis/fact_store.rbs +2 -3
- data/sig/rigor/cache/descriptor.rbs +14 -0
- data/sig/rigor/environment.rbs +4 -1
- data/sig/rigor/plugin/base.rbs +53 -19
- data/sig/rigor/plugin/interfaces.rbs +51 -0
- data/sig/rigor/plugin/io_boundary.rbs +2 -3
- data/sig/rigor/plugin/node_context.rbs +21 -0
- data/sig/rigor/reflection.rbs +37 -0
- data/sig/rigor/scope.rbs +22 -3
- data/sig/rigor/trinary.rbs +1 -0
- data/sig/rigor/type.rbs +9 -3
- data/sig/rigor.rbs +3 -2
- data/skills/rigor-ask/SKILL.md +1 -0
- data/skills/rigor-project-init/references/01-detect.md +1 -0
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +5 -1
- data/skills/rigor-project-init/references/06-agent-contract.md +4 -4
- data/skills/rigor-rbs-contract/SKILL.md +147 -0
- data/skills/rigor-rbs-contract/references/01-finding-candidates.md +212 -0
- data/skills/rigor-rbs-contract/references/02-contract-rules.md +157 -0
- data/skills/rigor-rbs-contract/references/03-target-own-code.md +90 -0
- data/skills/rigor-rbs-contract/references/04-target-gem-rbs-collection.md +210 -0
- data/skills/rigor-rbs-contract/references/05-declaration-mutants.md +182 -0
- data/skills/rigor-rbs-setup/SKILL.md +3 -1
- data/skills/rigor-type-oracle/SKILL.md +26 -1
- data/skills/rigor-type-oracle/references/02-agents-md-fragment.md +4 -3
- metadata +48 -2
- data/lib/rigor/inference/memo_audit.rb +0 -55
|
@@ -11,6 +11,8 @@ require_relative "../source/node_children"
|
|
|
11
11
|
require_relative "../source/node_walker"
|
|
12
12
|
require_relative "../analysis/self_call_resolution_recorder"
|
|
13
13
|
require_relative "../run_scoped_slots"
|
|
14
|
+
require_relative "../typing_state"
|
|
15
|
+
require_relative "../type_of_memo"
|
|
14
16
|
require_relative "block_call_timing"
|
|
15
17
|
require_relative "block_repetition"
|
|
16
18
|
require_relative "project_yielder"
|
|
@@ -20,6 +22,7 @@ require_relative "body_fixpoint"
|
|
|
20
22
|
require_relative "budget_trace"
|
|
21
23
|
require_relative "captured_locals"
|
|
22
24
|
require_relative "closure_escape_analyzer"
|
|
25
|
+
require_relative "self_preserving_block"
|
|
23
26
|
require_relative "receiver_blind_block"
|
|
24
27
|
require_relative "def_node_resolver"
|
|
25
28
|
require_relative "dynamic_origin"
|
|
@@ -41,6 +44,7 @@ require_relative "mutation_widening"
|
|
|
41
44
|
require_relative "narrowing"
|
|
42
45
|
require_relative "optimistic_origin"
|
|
43
46
|
require_relative "receiver_alias"
|
|
47
|
+
require_relative "recursion_rebinding"
|
|
44
48
|
require_relative "repeated_or_writes"
|
|
45
49
|
require_relative "singleton_object_constant"
|
|
46
50
|
require_relative "refined_dispatch"
|
|
@@ -260,17 +264,36 @@ module Rigor
|
|
|
260
264
|
@typing_node = typing_node
|
|
261
265
|
end
|
|
262
266
|
|
|
267
|
+
# ADR-129 WD2 — the calling thread's {TypingState::State}, cached on the typer the first time a slot is read, so
|
|
268
|
+
# a later read is an ivar and an array index rather than a `Thread.current` lookup. A typer lives for one query on
|
|
269
|
+
# one thread; an object kept across threads must not keep one (see {Analysis::IncrementalSession}'s effects
|
|
270
|
+
# evaluator), or it would write into a state whose thread's {RunScopedSlots.release} has already run. A frame
|
|
271
|
+
# install whose block's value is the answer calls {TypingState.current} instead: Rigor's own analysis does not
|
|
272
|
+
# adopt this memoised reader's type at a self-call (#525's self-purity), so the install's answer would read as
|
|
273
|
+
# `Dynamic` through it.
|
|
274
|
+
def typing_state = @typing_state ||= TypingState.current
|
|
275
|
+
|
|
263
276
|
def type_of(node)
|
|
264
277
|
previous = @typing_node
|
|
265
278
|
@typing_node = node
|
|
279
|
+
# ADR-129 G1′: under the audit, the Frame declarations are asserted around every `type_of`.
|
|
280
|
+
audited = MemoAudit.enabled?
|
|
281
|
+
MemoAudit.enter_type_of if audited
|
|
266
282
|
result = if FlowTracer.active?
|
|
267
283
|
FlowTracer.trace_node(node) { untraced_type_of(node) }
|
|
284
|
+
elsif TypeOfMemo.enabled? && @tracer.nil? && @operand_types.nil?
|
|
285
|
+
# ADR-129 stage 3 — the shadow memo (G1); shut under a tracer and for a typer with instance state.
|
|
286
|
+
state = typing_state
|
|
287
|
+
TypeOfMemo.shadow(state, scope, node, callee: !state.stack(INFERENCE_GUARD).empty?) do
|
|
288
|
+
untraced_type_of(node)
|
|
289
|
+
end
|
|
268
290
|
else
|
|
269
291
|
untraced_type_of(node)
|
|
270
292
|
end
|
|
271
293
|
result
|
|
272
294
|
ensure
|
|
273
295
|
@typing_node = previous
|
|
296
|
+
MemoAudit.leave_type_of if audited
|
|
274
297
|
end
|
|
275
298
|
|
|
276
299
|
def untraced_type_of(node)
|
|
@@ -299,8 +322,10 @@ module Rigor
|
|
|
299
322
|
# unchanged before skipping the callee's symbol dependents. nil for an abstract / bodyless def, or when
|
|
300
323
|
# the memo refuses a transient result — the caller treats either as "not provably stable" (keeps the
|
|
301
324
|
# dependents), the conservative direction.
|
|
302
|
-
|
|
303
|
-
|
|
325
|
+
#
|
|
326
|
+
# Issue #1237 — `positionals_unknown:` re-drives a key a splat call observed, binding it the way that call did.
|
|
327
|
+
def return_type_for(def_node, receiver, arg_types, positionals_unknown: false)
|
|
328
|
+
infer_user_method_return(def_node, receiver, arg_types, positionals_unknown: positionals_unknown)
|
|
304
329
|
end
|
|
305
330
|
|
|
306
331
|
# The `receiver[args]` read a compound index write performs before it stores (`c[k] += v` reads `c[k]`),
|
|
@@ -317,7 +342,7 @@ module Rigor
|
|
|
317
342
|
# `name` anyway. Minting a `Prism::CallNode` for the read would hand plugins a node that is not in the
|
|
318
343
|
# tree, through a constructor whose field list changed inside the `prism` range the gemspec accepts.
|
|
319
344
|
def implicit_index_read_type(node, receiver, arg_types)
|
|
320
|
-
return splat_index_read_type(node, receiver, arg_types) if
|
|
345
|
+
return splat_index_read_type(node, receiver, arg_types) if splat_arguments?(node)
|
|
321
346
|
|
|
322
347
|
try_overriding_def_dispatch(node, receiver, arg_types, method_name: :[]) ||
|
|
323
348
|
index_read_dispatch(node, receiver, arg_types) ||
|
|
@@ -342,36 +367,59 @@ module Rigor
|
|
|
342
367
|
recorded = self.class.recorded_inject_accumulator(block, scope)
|
|
343
368
|
return recorded if recorded
|
|
344
369
|
|
|
345
|
-
|
|
370
|
+
# ADR-129 WD1 — a read-through Table's miss: writing another Table from it touches.
|
|
371
|
+
typing_state.memo_miss(INJECT_ACCUMULATOR) { try_block_inject_fold(call_node, receiver, arg_types) }
|
|
346
372
|
self.class.recorded_inject_accumulator(block, scope)
|
|
347
373
|
end
|
|
348
374
|
|
|
349
|
-
|
|
350
|
-
|
|
375
|
+
# ADR-129 WD1 — a Table, and a read-through cache of the computation's own value: a record read back answers
|
|
376
|
+
# what typing the call again from the same scope would compute.
|
|
377
|
+
INJECT_ACCUMULATOR = TypingState.table(
|
|
378
|
+
:inject_accumulator,
|
|
379
|
+
run_scoped: true, read_through: true, entries: TypingState::WeakKeyMapEntries,
|
|
380
|
+
reason: "keyed by block node; a read-through cache of the fold that the call's own typing computes"
|
|
381
|
+
)
|
|
382
|
+
private_constant :INJECT_ACCUMULATOR
|
|
351
383
|
|
|
352
384
|
# The accumulator type {#record_inject_accumulator} stored for `block` when the call was typed from `scope`
|
|
353
|
-
# itself (by identity), else nil.
|
|
385
|
+
# itself (by identity) in the current ADR-55 summary generation (#2767, {LoopValueRecord.generation}), else nil.
|
|
354
386
|
def self.recorded_inject_accumulator(block, scope)
|
|
355
|
-
|
|
356
|
-
slot
|
|
387
|
+
state = TypingState.current
|
|
388
|
+
slot = state[INJECT_ACCUMULATOR]&.[](block)
|
|
389
|
+
state.log_read(INJECT_ACCUMULATOR, block, slot || TypingState::ABSENT)
|
|
390
|
+
slot && slot[0].equal?(scope) && slot[2] == LoopValueRecord.generation ? slot[1] : nil
|
|
357
391
|
end
|
|
358
392
|
|
|
359
393
|
# ADR-89 WD2 — the current run's return memo bucket as `{ def_node => [MemoEntry, …] }` (only entries
|
|
360
394
|
# that carry a call descriptor, i.e. every stored entry). Read by the incremental session right after a
|
|
361
395
|
# recording run to harvest each analyzed callee's observed call keys → return descriptors. Returns an
|
|
362
|
-
# empty hash when no bucket exists (a run that memoised nothing). Class method: the bucket lives
|
|
363
|
-
#
|
|
396
|
+
# empty hash when no bucket exists (a run that memoised nothing). Class method: the bucket lives in the thread's
|
|
397
|
+
# {TypingState}, independent of any one `ExpressionTyper` instance.
|
|
364
398
|
#
|
|
365
399
|
# `bucket` defaults to the calling thread's current one; `Runner#run` keeps the bucket its run filled
|
|
366
400
|
# ({.current_return_memo}) and passes it, since the run releases the thread's slot when it ends.
|
|
401
|
+
#
|
|
402
|
+
# Issue #3160 — the entries are what the memo STORED. A def one of whose calls it refused to store was observed
|
|
403
|
+
# under a key no entry lists; {.return_memo_refused?} says which.
|
|
367
404
|
def self.harvest_return_memo(bucket = current_return_memo)
|
|
368
405
|
return {} if bucket.nil?
|
|
369
406
|
|
|
370
|
-
bucket.transform_values(
|
|
407
|
+
bucket.transform_values { |per_def| per_def.except(RETURN_MEMO_REFUSED).values }
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Issue #3160 — whether `bucket` served a result of `def_node` that it refused to store (ADR-84 WD3). That
|
|
411
|
+
# result reached its caller, so the def's stored entries are not every key it was observed under.
|
|
412
|
+
def self.return_memo_refused?(bucket, def_node)
|
|
413
|
+
bucket&.[](def_node)&.key?(RETURN_MEMO_REFUSED) || false
|
|
371
414
|
end
|
|
372
415
|
|
|
416
|
+
# Issue #3160 — the per-def key under which {#consult_and_store_return_memo} notes that it served a result it
|
|
417
|
+
# did not store. No call descriptor is a Symbol, so it never meets a real key.
|
|
418
|
+
RETURN_MEMO_REFUSED = :refused
|
|
419
|
+
private_constant :RETURN_MEMO_REFUSED
|
|
420
|
+
|
|
373
421
|
# The calling thread's return memo bucket (`{ def_node => { descriptor => MemoEntry } }`), or nil.
|
|
374
|
-
def self.current_return_memo =
|
|
422
|
+
def self.current_return_memo = TypingState.current[RETURN_MEMO]&.last
|
|
375
423
|
|
|
376
424
|
# Issue #720 — the value the block passed by the call site currently being re-typed evaluates to, or
|
|
377
425
|
# `nil` outside such a frame. Read by {#type_of_yield}.
|
|
@@ -381,22 +429,19 @@ module Rigor
|
|
|
381
429
|
# inference rebuilds freely. It is installed unconditionally — with `nil` — at every user-method
|
|
382
430
|
# inference frame, so a callee reached WITHOUT a block from inside a yielding body cannot read its
|
|
383
431
|
# caller's block.
|
|
384
|
-
|
|
385
|
-
|
|
432
|
+
YIELD_VALUE = TypingState.frame(
|
|
433
|
+
:yield_value_type, reason: "the caller's block value, installed per frame",
|
|
434
|
+
witness: "spec/fixtures/typing_state_witnesses/yield_value_type.rb"
|
|
435
|
+
)
|
|
436
|
+
private_constant :YIELD_VALUE
|
|
386
437
|
|
|
387
438
|
def self.current_yield_value_type
|
|
388
|
-
|
|
439
|
+
TypingState.current[YIELD_VALUE]
|
|
389
440
|
end
|
|
390
441
|
|
|
391
442
|
# Runs `block` with `type` installed as the frame's yield value, restoring the previous frame on exit.
|
|
392
|
-
def self.with_yield_value_type(type)
|
|
393
|
-
|
|
394
|
-
Thread.current[YIELD_VALUE_KEY] = type
|
|
395
|
-
begin
|
|
396
|
-
yield
|
|
397
|
-
ensure
|
|
398
|
-
Thread.current[YIELD_VALUE_KEY] = previous
|
|
399
|
-
end
|
|
443
|
+
def self.with_yield_value_type(type, &)
|
|
444
|
+
TypingState.current.with(YIELD_VALUE, type, &)
|
|
400
445
|
end
|
|
401
446
|
|
|
402
447
|
# Issue #1125 — the argument types of the call currently being re-typed into the body being walked,
|
|
@@ -404,37 +449,48 @@ module Rigor
|
|
|
404
449
|
# `m` itself was called with, so {#call_arg_types} expands the forwarding node to THIS list instead of
|
|
405
450
|
# the `Dynamic[top]` a bare `Prism::ForwardingArgumentsNode` types as.
|
|
406
451
|
#
|
|
407
|
-
# A thread-local for the same reason the yield value is (see {
|
|
452
|
+
# A thread-local for the same reason the yield value is (see {YIELD_VALUE}): `...` names the
|
|
408
453
|
# frame's caller, not a binding, and the body is walked through scopes the inference rebuilds freely.
|
|
409
454
|
# It is installed unconditionally — with nil — at every user-method inference frame, so a body reached
|
|
410
455
|
# without `...` cannot read its caller's argument list. Because each frame installs its OWN list, a
|
|
411
456
|
# chain `a(...) -> b(...) -> c(...)` threads through: `b`'s frame holds the types `a` expanded and
|
|
412
457
|
# `c`'s holds `b`'s. The list is the callee's call-site `arg_types`, which is what keeps the ADR-84
|
|
413
458
|
# return memo's `(def_node, receiver, arg_types)` key complete for a forwarding def.
|
|
414
|
-
|
|
415
|
-
|
|
459
|
+
FORWARDED_ARGS = TypingState.frame(:forwarded_call_arg_types,
|
|
460
|
+
reason: "the caller's frozen argument list, installed per frame",
|
|
461
|
+
witness: "spec/fixtures/typing_state_witnesses/forwarded_call_arg_types.rb")
|
|
462
|
+
private_constant :FORWARDED_ARGS
|
|
416
463
|
|
|
417
464
|
def self.current_forwarded_arg_types
|
|
418
|
-
|
|
465
|
+
TypingState.current[FORWARDED_ARGS]
|
|
419
466
|
end
|
|
420
467
|
|
|
421
|
-
# Runs `block` with `types` installed as the frame's forwarded argument list, restoring the previous
|
|
468
|
+
# Runs `block` with `types` (frozen) installed as the frame's forwarded argument list, restoring the previous
|
|
422
469
|
# frame on exit.
|
|
423
|
-
def self.with_forwarded_arg_types(types, &
|
|
424
|
-
|
|
425
|
-
Thread.current[FORWARDED_ARGS_KEY] = types
|
|
426
|
-
begin
|
|
427
|
-
block.call
|
|
428
|
-
ensure
|
|
429
|
-
Thread.current[FORWARDED_ARGS_KEY] = previous
|
|
430
|
-
end
|
|
470
|
+
def self.with_forwarded_arg_types(types, &)
|
|
471
|
+
TypingState.current.with(FORWARDED_ARGS, types, &)
|
|
431
472
|
end
|
|
432
473
|
|
|
433
474
|
# The two call-site channels a user-method body may read from its caller — the block a `yield`
|
|
434
475
|
# reaches ({#with_yield_value_type}) and the argument list `...` re-supplies
|
|
435
476
|
# ({#with_forwarded_arg_types}) — installed together, both even when nil, for exactly one frame.
|
|
477
|
+
#
|
|
478
|
+
# The frame also clears the reflective-ivar repeat depth ({ReflectiveIvarWrite.in_repeating_body}): it says
|
|
479
|
+
# the CALLER's loop body is walked once for many runs, while the callee's body starts from its own scope and
|
|
480
|
+
# reads its own write. The depth only widens and the return memo's key has no term for it, so a body first
|
|
481
|
+
# reached from a `for` body stored a wider return that every later caller was served. It clears the caller's
|
|
482
|
+
# jump sinks and retry frames too ({StatementEvaluator.outside_caller_sinks}): a `next`, `break` or raise point
|
|
483
|
+
# in the callee's body is never the caller's, and a recursive re-walk of the caller's own block otherwise
|
|
484
|
+
# recorded the callee's arms into it. The block-body
|
|
485
|
+
# threading suppression is the same kind of caller flag and is NOT cleared here (issue #2782): it is what
|
|
486
|
+
# keeps a fold from nesting through callee frames, and clearing it re-walked unstorable frames
|
|
487
|
+
# exponentially, while keying the memo on it cost a fifth of `lib`'s allocations.
|
|
436
488
|
def self.with_call_site_frame(yield_type, forwarded_arg_types, &)
|
|
437
|
-
|
|
489
|
+
ReflectiveIvarWrite.outside_repeating_body do
|
|
490
|
+
StatementEvaluator.outside_caller_sinks do
|
|
491
|
+
TypingState.current.with_pair(YIELD_VALUE, yield_type, FORWARDED_ARGS, forwarded_arg_types, &)
|
|
492
|
+
end
|
|
493
|
+
end
|
|
438
494
|
end
|
|
439
495
|
|
|
440
496
|
private
|
|
@@ -1058,7 +1114,7 @@ module Rigor
|
|
|
1058
1114
|
# optimistic-carrier decline) and one terminating-arm rule, and cannot drift apart again. Only the
|
|
1059
1115
|
# value is read; the post-scope belongs to whichever walk owns the statement.
|
|
1060
1116
|
def type_of_conditional(node)
|
|
1061
|
-
LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
|
|
1117
|
+
LoopValueRecord.fetch(node, scope) || LoopValueRecord.miss { scope.evaluate(node, tracer: tracer).first }
|
|
1062
1118
|
end
|
|
1063
1119
|
|
|
1064
1120
|
def type_of_else(node)
|
|
@@ -1289,7 +1345,7 @@ module Rigor
|
|
|
1289
1345
|
def type_of_loop(node)
|
|
1290
1346
|
return Type::Combinator.constant_of(nil) if break_free_condition_loop?(node)
|
|
1291
1347
|
|
|
1292
|
-
LoopValueRecord.fetch(node, scope) || scope.evaluate(node, tracer: tracer).first
|
|
1348
|
+
LoopValueRecord.fetch(node, scope) || LoopValueRecord.miss { scope.evaluate(node, tracer: tracer).first }
|
|
1293
1349
|
end
|
|
1294
1350
|
|
|
1295
1351
|
def break_free_condition_loop?(node)
|
|
@@ -1326,7 +1382,7 @@ module Rigor
|
|
|
1326
1382
|
# to bare `Nominal[Range]` when no endpoint contributes a typable shape.
|
|
1327
1383
|
def nominal_range_for_endpoints(left_node, right_node)
|
|
1328
1384
|
endpoints = [left_node, right_node].compact.map { |n| range_endpoint_element_type(n) }
|
|
1329
|
-
endpoints.reject! { |t|
|
|
1385
|
+
endpoints.reject! { |t| Type::Combinator.untyped?(t) }
|
|
1330
1386
|
return Type::Combinator.nominal_of("Range") if endpoints.empty?
|
|
1331
1387
|
|
|
1332
1388
|
Type::Combinator.nominal_of("Range", type_args: [Type::Combinator.union(*endpoints)])
|
|
@@ -1562,13 +1618,20 @@ module Rigor
|
|
|
1562
1618
|
# engine walks can enumerate: a plugin's `dynamic_return` answer and the ADR-16 synthetic-method
|
|
1563
1619
|
# index. They are members of `self` exactly as an `attr_reader` is, so a top-level `def` must not
|
|
1564
1620
|
# bind ahead of them either.
|
|
1621
|
+
#
|
|
1622
|
+
# Issue #2861 — a bind the #316 gate declines answers `Dynamic[Top]`, never the RBS method of the same name: the
|
|
1623
|
+
# block's real `self` reaches either a same-named method of its own or the top-level `def`, which shadows
|
|
1624
|
+
# `Kernel#format` and every other `Object` / `Kernel` method on the way. Falling through typed
|
|
1625
|
+
# `format(r).bit_length` against `Kernel#format`'s `String` on code that runs the project's `def format`.
|
|
1565
1626
|
def try_local_def_dispatch(node, receiver, arg_types, block_type = nil)
|
|
1566
|
-
|
|
1567
|
-
|
|
1627
|
+
return nil unless node.receiver.nil?
|
|
1628
|
+
|
|
1629
|
+
local_def = scope.bindable_top_level_def_for(node.name)
|
|
1630
|
+
return declined_top_level_def_answer(node) if local_def.nil?
|
|
1568
1631
|
return nil if self_type_answers?(node.name)
|
|
1569
1632
|
return nil if plugin_member_answers?(node, receiver)
|
|
1570
1633
|
|
|
1571
|
-
local_inference = infer_top_level_user_method(local_def, receiver, arg_types, block_type)
|
|
1634
|
+
local_inference = infer_top_level_user_method(local_def, receiver, node, arg_types, block_type)
|
|
1572
1635
|
return local_inference if local_inference
|
|
1573
1636
|
|
|
1574
1637
|
# The local def matches by name but the inference was disqualified — the parameter shape is too
|
|
@@ -1580,6 +1643,16 @@ module Rigor
|
|
|
1580
1643
|
dynamic_top
|
|
1581
1644
|
end
|
|
1582
1645
|
|
|
1646
|
+
# Issue #2861 — the answer for an implicit-self call whose top-level `def` the #316 gate declined: nil (dispatch
|
|
1647
|
+
# as before) when the project defines no top-level `def` of the name, else `Dynamic[Top]`. The gate declines only
|
|
1648
|
+
# where `self_type` is nil, so no member of `self` ({#self_type_answers?}, {#plugin_member_answers?}) can answer.
|
|
1649
|
+
def declined_top_level_def_answer(node)
|
|
1650
|
+
return nil unless scope.opaque_block_self? && scope.self_type.nil?
|
|
1651
|
+
return nil if scope.top_level_def_for(node.name).nil?
|
|
1652
|
+
|
|
1653
|
+
dynamic_top
|
|
1654
|
+
end
|
|
1655
|
+
|
|
1583
1656
|
# ADR-110 WD1 / #856 — the receiver's own `def` outranks a declaration that only an ANCESTOR carries.
|
|
1584
1657
|
#
|
|
1585
1658
|
# RBS resolves through the fully built method table, so `RbsDispatch` answers a subclass call with a
|
|
@@ -1712,8 +1785,15 @@ module Rigor
|
|
|
1712
1785
|
return true if scope.discovered_method_through_ancestors?(class_name, method_name, :instance)
|
|
1713
1786
|
return true if meta_member?(class_name, method_name)
|
|
1714
1787
|
return true if resolve_user_def_through_ancestors(class_name, method_name)
|
|
1788
|
+
return true if rbs_ancestor_answers?(class_name, method_name)
|
|
1715
1789
|
|
|
1716
|
-
|
|
1790
|
+
# Issue #2968 — a class on the chain defines methods under names no table holds (`attr_accessor(*FLAGS)`, a
|
|
1791
|
+
# splatted `Struct.new(*FIELDS)`), so the name may be one of them, ahead of the top-level `def`.
|
|
1792
|
+
return true if Scope::DiscoveryIndex.open_names_on_chain?(scope, class_name, :instance)
|
|
1793
|
+
|
|
1794
|
+
# Issue #2994 — a class on the chain mixes in a module no table names (`include(*MODS)`, `include helper`), so
|
|
1795
|
+
# the name may be that module's, ahead of the top-level `def`.
|
|
1796
|
+
Scope::DiscoveryIndex.unnamed_mixin_on_chain?(scope, class_name)
|
|
1717
1797
|
end
|
|
1718
1798
|
|
|
1719
1799
|
# The RBS arm of {#instance_self_answers?}. A project class is usually absent from the RBS
|
|
@@ -1740,7 +1820,13 @@ module Rigor
|
|
|
1740
1820
|
return true if scope.discovered_method_through_ancestors?(class_name, method_name, :singleton)
|
|
1741
1821
|
return true unless scope.singleton_def_through_ancestors(class_name, method_name).first.nil?
|
|
1742
1822
|
|
|
1743
|
-
|
|
1823
|
+
declared = safe_rbs_method_definition(class_name, method_name, :singleton)
|
|
1824
|
+
return true if rbs_declared_on_class?(declared, class_name)
|
|
1825
|
+
|
|
1826
|
+
# Issue #2994 — a class object on the chain that a module no table names may extend through a hook (the
|
|
1827
|
+
# `:include_hook` open mark of a class with `include(*MODS)`), a class that `extend(*EMODS)`s, or an extended
|
|
1828
|
+
# module whose own module set is unknown, may answer any name ahead of the top-level `def`.
|
|
1829
|
+
Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods).unnamed_mixin?
|
|
1744
1830
|
end
|
|
1745
1831
|
|
|
1746
1832
|
# A `Struct.new(:a, :b)` / `Data.define(:a, :b)` member accessor. The layouts are a discovery table of
|
|
@@ -1775,7 +1861,148 @@ module Rigor
|
|
|
1775
1861
|
def call_type_for(node)
|
|
1776
1862
|
return safe_navigation_call_type(node) if node.safe_navigation?
|
|
1777
1863
|
|
|
1778
|
-
memo_object_value(node, attribute_write_value(node, call_dispatch_type_for(node)))
|
|
1864
|
+
memo_object_value(node, class_test_find_value(node, attribute_write_value(node, call_dispatch_type_for(node))))
|
|
1865
|
+
end
|
|
1866
|
+
|
|
1867
|
+
CLASS_TEST_FINDERS = Set[:find, :detect, :rfind].freeze
|
|
1868
|
+
CLASS_TEST_PREDICATES = Set[:is_a?, :kind_of?, :instance_of?].freeze
|
|
1869
|
+
private_constant :CLASS_TEST_FINDERS, :CLASS_TEST_PREDICATES
|
|
1870
|
+
|
|
1871
|
+
# Issue #2719 — `find` / `detect` / `rfind` whose block is one class test on its one parameter
|
|
1872
|
+
# (`ancestors.rfind { |n| n.is_a?(Prism::DefNode) }`) returns an element that passed the test, or `nil`. The
|
|
1873
|
+
# dispatched `E?` is narrowed by that test, through the same {Narrowing} rule an `if n.is_a?(C)` edge takes, and
|
|
1874
|
+
# keeps its `nil`. Only an answer that already carries `nil` is narrowed, so an untyped receiver's `Dynamic`
|
|
1875
|
+
# stays as it was; a test that leaves no element (`Bot`, a class the element type is disjoint from) keeps the
|
|
1876
|
+
# dispatched answer too, since `nil` alone would be a verdict that the guard never matches. Any other block
|
|
1877
|
+
# shape, an `ifnone` argument, and a block with more than one parameter or statement keep today's answer.
|
|
1878
|
+
def class_test_find_value(node, result)
|
|
1879
|
+
param = class_test_find_parameter(node)
|
|
1880
|
+
return result if param.nil?
|
|
1881
|
+
return result unless result.is_a?(Type::Union) && result.members.any? { |m| nil_constant?(m) }
|
|
1882
|
+
return result unless element_or_nil?(node, result)
|
|
1883
|
+
# A refinement in effect may replace the finder with a body whose answer only happens to read as an element
|
|
1884
|
+
# or nil (`refine(Array) { def find = [2.5, 1, nil].sample }`), so the call keeps its answer.
|
|
1885
|
+
return result unless scope.in_effect_refinements(node).empty?
|
|
1886
|
+
|
|
1887
|
+
truthy, = Narrowing.predicate_scopes(node.block.body.body.first, scope.with_local(param, result))
|
|
1888
|
+
narrowed = truthy.local(param)
|
|
1889
|
+
return result if narrowed.nil?
|
|
1890
|
+
|
|
1891
|
+
non_nil = Narrowing.narrow_non_nil(narrowed)
|
|
1892
|
+
return result if non_nil.is_a?(Type::Bot) || introduces_dynamic?(non_nil, result)
|
|
1893
|
+
|
|
1894
|
+
Type::Combinator.union(non_nil, Type::Combinator.constant_of(nil))
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
def nil_constant?(type) = type.is_a?(Type::Constant) && type.value.nil?
|
|
1898
|
+
|
|
1899
|
+
# True when the dispatched answer is exactly the receiver's element type or nil: the call really answers "an
|
|
1900
|
+
# element the block accepted, or nil". Whatever else wins the dispatch (a refinement, a reopened `Array` or
|
|
1901
|
+
# `Enumerable`, a gem's override) and answers anything else keeps its answer, and so does a receiver whose
|
|
1902
|
+
# element type cannot be read.
|
|
1903
|
+
def element_or_nil?(node, result)
|
|
1904
|
+
type = type_of(node.receiver)
|
|
1905
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
1906
|
+
elements = members.map { |member| MethodDispatcher::IteratorDispatch.element_type_of(member) }
|
|
1907
|
+
return false if elements.include?(nil)
|
|
1908
|
+
|
|
1909
|
+
result == Type::Combinator.union(*elements, Type::Combinator.constant_of(nil))
|
|
1910
|
+
end
|
|
1911
|
+
|
|
1912
|
+
# A narrowing that cannot order an element's class against the tested one answers `Dynamic[top]` for it (a
|
|
1913
|
+
# project class with no RBS, typically); that is the test's "cannot tell", not a narrower element, so the
|
|
1914
|
+
# dispatched answer stands.
|
|
1915
|
+
def introduces_dynamic?(narrowed, result)
|
|
1916
|
+
members = narrowed.is_a?(Type::Union) ? narrowed.members : [narrowed]
|
|
1917
|
+
members.any? { |member| member.is_a?(Type::Dynamic) && !result.members.include?(member) }
|
|
1918
|
+
end
|
|
1919
|
+
|
|
1920
|
+
# The block parameter's name when `node` is a block-only `find` / `detect` / `rfind` whose block takes exactly
|
|
1921
|
+
# one plain parameter and whose body is one `is_a?` / `kind_of?` / `instance_of?` call on that parameter with a
|
|
1922
|
+
# single constant argument; nil otherwise.
|
|
1923
|
+
def class_test_find_parameter(node)
|
|
1924
|
+
# `detect(ifnone)` answers `ifnone.call` when nothing matches, which is not an element. A guard: today the
|
|
1925
|
+
# dispatched answer of an argument-bearing `detect` never carries the `nil` the narrowing needs, so the
|
|
1926
|
+
# call would already keep its answer.
|
|
1927
|
+
return nil unless CLASS_TEST_FINDERS.include?(node.name) && node.arguments.nil?
|
|
1928
|
+
|
|
1929
|
+
block = node.block
|
|
1930
|
+
return nil unless block.is_a?(Prism::BlockNode)
|
|
1931
|
+
|
|
1932
|
+
name = single_block_parameter(block)
|
|
1933
|
+
return nil if name.nil?
|
|
1934
|
+
|
|
1935
|
+
body = block.body
|
|
1936
|
+
return nil unless body.is_a?(Prism::StatementsNode) && body.body.size == 1
|
|
1937
|
+
|
|
1938
|
+
return nil unless class_test_on?(body.body.first, name)
|
|
1939
|
+
|
|
1940
|
+
core_finder_call?(node) ? name : nil
|
|
1941
|
+
end
|
|
1942
|
+
|
|
1943
|
+
# The owners whose `find` / `detect` / `rfind` return an element the block accepted: core `Array` and
|
|
1944
|
+
# `Enumerable` (`Hash`, `Set` and `Range` take `Enumerable`'s).
|
|
1945
|
+
CORE_FINDER_OWNERS = %w[Array Enumerable].freeze
|
|
1946
|
+
private_constant :CORE_FINDER_OWNERS
|
|
1947
|
+
|
|
1948
|
+
# True when every member of the call's receiver dispatches `node.name` to core `Array` or `Enumerable`: the
|
|
1949
|
+
# method's RBS owner is one of {CORE_FINDER_OWNERS} and the project defines no method by that name on the
|
|
1950
|
+
# class or an ancestor. A project or gem `find` may return anything its block did not test
|
|
1951
|
+
# (`def find = yield(1) ? 2.0 : nil`), so the name alone is not enough. {#element_or_nil?} closes the spellings
|
|
1952
|
+
# this cannot see (a refinement, a reopened module the discovery walk does not reach).
|
|
1953
|
+
def core_finder_call?(node)
|
|
1954
|
+
receiver = node.receiver
|
|
1955
|
+
return false if receiver.nil?
|
|
1956
|
+
|
|
1957
|
+
type = type_of(receiver)
|
|
1958
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
1959
|
+
members.all? { |member| core_finder_member?(member, node.name) }
|
|
1960
|
+
end
|
|
1961
|
+
|
|
1962
|
+
def core_finder_member?(member, method_name)
|
|
1963
|
+
class_name =
|
|
1964
|
+
case member
|
|
1965
|
+
when Type::Nominal then member.class_name
|
|
1966
|
+
when Type::Tuple then "Array"
|
|
1967
|
+
when Type::HashShape then "Hash"
|
|
1968
|
+
end
|
|
1969
|
+
return false if class_name.nil?
|
|
1970
|
+
return false if scope.discovered_method_through_ancestors?(class_name, method_name, :instance)
|
|
1971
|
+
|
|
1972
|
+
defined_in = safe_rbs_method_definition(class_name, method_name, :instance)&.defined_in
|
|
1973
|
+
return false if defined_in.nil?
|
|
1974
|
+
|
|
1975
|
+
owner = defined_in.to_s.delete_prefix("::")
|
|
1976
|
+
return false unless CORE_FINDER_OWNERS.include?(owner)
|
|
1977
|
+
|
|
1978
|
+
# A reopened core owner (`module Enumerable; def find = 2.5; end`) replaces the method for every includer,
|
|
1979
|
+
# which the ancestor walk above does not reach for a class whose ancestry RBS alone records (`Set`).
|
|
1980
|
+
!scope.discovered_method?(owner, method_name, :instance)
|
|
1981
|
+
end
|
|
1982
|
+
|
|
1983
|
+
def single_block_parameter(block)
|
|
1984
|
+
params = block.parameters
|
|
1985
|
+
return nil unless params.is_a?(Prism::BlockParametersNode) && params.locals.empty?
|
|
1986
|
+
|
|
1987
|
+
list = params.parameters
|
|
1988
|
+
return nil if list.nil? || list.requireds.size != 1
|
|
1989
|
+
return nil unless list.optionals.empty? && list.rest.nil? && list.posts.empty? &&
|
|
1990
|
+
list.keywords.empty? && list.keyword_rest.nil? && list.block.nil?
|
|
1991
|
+
|
|
1992
|
+
required = list.requireds.first
|
|
1993
|
+
required.is_a?(Prism::RequiredParameterNode) ? required.name : nil
|
|
1994
|
+
end
|
|
1995
|
+
|
|
1996
|
+
def class_test_on?(test, name)
|
|
1997
|
+
return false unless test.is_a?(Prism::CallNode) && CLASS_TEST_PREDICATES.include?(test.name)
|
|
1998
|
+
return false if test.block || test.safe_navigation?
|
|
1999
|
+
|
|
2000
|
+
receiver = test.receiver
|
|
2001
|
+
return false unless receiver.is_a?(Prism::LocalVariableReadNode) && receiver.name == name
|
|
2002
|
+
|
|
2003
|
+
args = test.arguments&.arguments
|
|
2004
|
+
!args.nil? && args.size == 1 &&
|
|
2005
|
+
(args.first.is_a?(Prism::ConstantReadNode) || args.first.is_a?(Prism::ConstantPathNode))
|
|
1779
2006
|
end
|
|
1780
2007
|
|
|
1781
2008
|
# Issue #2411 — `recv.each_with_object(memo) { |x, acc| acc[k] = v }` returns the memo as the block's
|
|
@@ -1938,7 +2165,7 @@ module Rigor
|
|
|
1938
2165
|
return type unless type.is_a?(Type::Constant) && [true, false].include?(type.value)
|
|
1939
2166
|
# The carrier read itself keeps its declared answer even when that is a literal `true` /
|
|
1940
2167
|
# `false` — the mark says its nil-freeness is a bet, not that the value's class widened.
|
|
1941
|
-
return type if scope.
|
|
2168
|
+
return type if scope.optimistic_origin(node)
|
|
1942
2169
|
return type if OptimisticOrigin.resolve(node, scope).nil?
|
|
1943
2170
|
|
|
1944
2171
|
# Widen only toward what a miss really answers: a safe-navigation call answers `nil`, and
|
|
@@ -2068,7 +2295,7 @@ module Rigor
|
|
|
2068
2295
|
return dynamic_top if name && refined_name_for?(receiver, name, list)
|
|
2069
2296
|
|
|
2070
2297
|
block_name = symbol_block_argument(node)
|
|
2071
|
-
dynamic_top if block_name && refined_anywhere?(block_name, list)
|
|
2298
|
+
dynamic_top if block_name && RefinedDispatch.refined_anywhere?(scope, block_name, list)
|
|
2072
2299
|
end
|
|
2073
2300
|
|
|
2074
2301
|
REFINED_NAMING_CALLS = Set[:send, :__send__, :public_send, :respond_to?, :method, :public_method].freeze
|
|
@@ -2104,17 +2331,6 @@ module Rigor
|
|
|
2104
2331
|
end
|
|
2105
2332
|
end
|
|
2106
2333
|
|
|
2107
|
-
# ADR-121 WD7 — a names-wildcard row counts as a refinement of every name.
|
|
2108
|
-
def refined_anywhere?(name, list)
|
|
2109
|
-
refinements = scope.discovered_refinements
|
|
2110
|
-
wildcard = Scope::DiscoveryIndex::REFINEMENT_WILDCARD
|
|
2111
|
-
return true if list.include?(InEffectRefinements::UNKNOWN) &&
|
|
2112
|
-
refinements.any? { |_refined, methods| methods.key?(name) || methods.key?(wildcard) }
|
|
2113
|
-
|
|
2114
|
-
!RefinedDispatch.targets(refinements, name, list).nil? ||
|
|
2115
|
-
!RefinedDispatch.targets(refinements, wildcard, list).nil?
|
|
2116
|
-
end
|
|
2117
|
-
|
|
2118
2334
|
# ADR-121 WD7 — does the call's lexical list (a plugin-declared module is the plugin's declaration and never
|
|
2119
2335
|
# opaque) hold an opaque module? `list` is the full list, which is the lexical one when nothing is declared.
|
|
2120
2336
|
def opaque_refinement_span?(node, list)
|
|
@@ -2152,7 +2368,7 @@ module Rigor
|
|
|
2152
2368
|
return nil if winner.nil?
|
|
2153
2369
|
return dynamic_top if winner == RefinedDispatch::UNKNOWN
|
|
2154
2370
|
|
|
2155
|
-
refined_body_return(winner, node
|
|
2371
|
+
refined_body_return(winner, node, Type::Combinator.nominal_of(class_name), arg_types, block_type) ||
|
|
2156
2372
|
dynamic_top
|
|
2157
2373
|
end
|
|
2158
2374
|
|
|
@@ -2171,15 +2387,17 @@ module Rigor
|
|
|
2171
2387
|
#
|
|
2172
2388
|
# A body another file wrote is re-typed with that file's in-effect refinements at the `def` (its own module
|
|
2173
2389
|
# among them), so a sibling override it calls answers from its refine body too.
|
|
2174
|
-
def refined_body_return(winner,
|
|
2390
|
+
def refined_body_return(winner, call_node, self_type, arg_types, block_type)
|
|
2175
2391
|
def_node, foreign =
|
|
2176
|
-
InEffectRefinements.refinement_def_with_query(scope, winner.module_name, winner.refined_class,
|
|
2392
|
+
InEffectRefinements.refinement_def_with_query(scope, winner.module_name, winner.refined_class, call_node.name)
|
|
2177
2393
|
return nil if def_node.nil?
|
|
2178
2394
|
|
|
2179
2395
|
refinements = foreign&.at(def_node.location.start_offset) do |name|
|
|
2180
2396
|
InEffectRefinements.activated_modules(scope, name)
|
|
2181
2397
|
end
|
|
2182
|
-
|
|
2398
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
2399
|
+
infer_user_method_return(def_node, self_type, arg_types, yield_type: block_type, refinements: refinements,
|
|
2400
|
+
positionals_unknown: positionals_unknown)
|
|
2183
2401
|
rescue StandardError
|
|
2184
2402
|
nil
|
|
2185
2403
|
end
|
|
@@ -2199,6 +2417,151 @@ module Rigor
|
|
|
2199
2417
|
end
|
|
2200
2418
|
end
|
|
2201
2419
|
|
|
2420
|
+
# Issue #1237 — whether the call's argument list leaves the positional correspondence unknown: it holds a
|
|
2421
|
+
# `*splat`, or a `...` with no forwarding frame to expand it from. `call_arg_types` carries either as ONE
|
|
2422
|
+
# argument, so a body re-typed from that list bound it to one parameter: `def count(*keys) = keys.size`
|
|
2423
|
+
# called as `r.count(*[0, 1])` folded to `1` and drew an always-falsey `== 2`. A Tuple splat of known
|
|
2424
|
+
# arity is not expanded either.
|
|
2425
|
+
def splat_arguments?(node)
|
|
2426
|
+
arguments = node.arguments&.arguments
|
|
2427
|
+
return false if arguments.nil?
|
|
2428
|
+
|
|
2429
|
+
arguments.any?(Prism::SplatNode) ||
|
|
2430
|
+
(forwards_arguments?(arguments) && ExpressionTyper.current_forwarded_arg_types.nil?)
|
|
2431
|
+
end
|
|
2432
|
+
|
|
2433
|
+
# The argument list and the `positionals_unknown:` flag a body tier re-types a user `def` with. Under
|
|
2434
|
+
# {#splat_arguments?} the body is still re-typed, so a body that always raises still answers `bot` and a
|
|
2435
|
+
# return that does not read its arguments is kept, but with no positional correspondence
|
|
2436
|
+
# ({#bind_params_without_correspondence}): the list is the call's keyword shape alone. Only a trailing
|
|
2437
|
+
# keyword-hash argument the call writes and {#call_arg_type} types as a `HashShape` supplies one. Any
|
|
2438
|
+
# other call leaves the keywords unknown (an open empty shape), because a splatted array's last element
|
|
2439
|
+
# is passed as keywords when it is a `ruby2_keywords` hash.
|
|
2440
|
+
#
|
|
2441
|
+
# Issue #3157 — a call whose every splat {#expand_tuple_splats} can expand is bound as the equivalent
|
|
2442
|
+
# positional call. Issue #3099 — a literal `**{}` passes no argument and is dropped. Issue #3191 — any other
|
|
2443
|
+
# trailing keyword hash holding a double splat is bound with no positional correspondence unless it is typed
|
|
2444
|
+
# as a non-empty closed shape ({#unknown_double_splat?}).
|
|
2445
|
+
def positional_correspondence(call_node, arg_types)
|
|
2446
|
+
arguments = call_node.arguments&.arguments
|
|
2447
|
+
unless splat_arguments?(call_node)
|
|
2448
|
+
return [arg_types[0...-1], false] if literal_empty_double_splat?(arguments&.last)
|
|
2449
|
+
return [[UNKNOWN_KEYWORDS], true] if unknown_double_splat?(arguments&.last, arg_types.last)
|
|
2450
|
+
|
|
2451
|
+
return [arg_types, false]
|
|
2452
|
+
end
|
|
2453
|
+
|
|
2454
|
+
expanded = expand_tuple_splats(arguments, arg_types)
|
|
2455
|
+
return [expanded, false] if expanded
|
|
2456
|
+
|
|
2457
|
+
last = arguments.last
|
|
2458
|
+
keywords = arg_types.last if last.is_a?(Prism::KeywordHashNode) && keyword_shape?(arg_types.last)
|
|
2459
|
+
[[keywords || UNKNOWN_KEYWORDS], true]
|
|
2460
|
+
end
|
|
2461
|
+
|
|
2462
|
+
UNKNOWN_KEYWORDS = Type::Combinator.hash_shape_of({}, extra_keys: :open)
|
|
2463
|
+
private_constant :UNKNOWN_KEYWORDS
|
|
2464
|
+
|
|
2465
|
+
# Issue #3099 — whether `argument` is a keyword hash built only of literal empty double splats (`**{}`),
|
|
2466
|
+
# which Ruby passes as no argument at all.
|
|
2467
|
+
def literal_empty_double_splat?(argument)
|
|
2468
|
+
argument.is_a?(Prism::KeywordHashNode) &&
|
|
2469
|
+
argument.elements.all? do |element|
|
|
2470
|
+
element.is_a?(Prism::AssocSplatNode) && element.value.is_a?(Prism::HashNode) &&
|
|
2471
|
+
element.value.elements.empty?
|
|
2472
|
+
end
|
|
2473
|
+
end
|
|
2474
|
+
|
|
2475
|
+
# Issues #3099 / #3191 — whether `argument` is a keyword hash holding a double splat whose type (`type`) is
|
|
2476
|
+
# not a non-empty closed shape. Ruby passes such a hash as a trailing positional `Hash` to a method that
|
|
2477
|
+
# declares no keywords when it is non-empty, and passes nothing when it is empty, so the call does not say
|
|
2478
|
+
# how many positionals the callee receives. An empty closed shape (`**h`, `h = {}`) may be a stale read of
|
|
2479
|
+
# a hash a callee filled; an open or shapeless one (`**opts`), a mixed `k: 1, **h`, two splats `**h, **h`
|
|
2480
|
+
# and `**nil` are not typed as a shape at all ({#double_splat_hash_shape}). Bound as given, each left a
|
|
2481
|
+
# positional `Hash`: `def count(*keys) = keys.size` called `r.count(1, 2, **opts)` folded to `3`.
|
|
2482
|
+
def unknown_double_splat?(argument, type)
|
|
2483
|
+
argument.is_a?(Prism::KeywordHashNode) && argument.elements.any?(Prism::AssocSplatNode) &&
|
|
2484
|
+
!keyword_shape?(type)
|
|
2485
|
+
end
|
|
2486
|
+
|
|
2487
|
+
# Issue #3157 — the argument list with each `*splat` replaced by its elements, or nil to keep the
|
|
2488
|
+
# no-correspondence binding. It expands only when every splat's operand is an array literal, so its arity is
|
|
2489
|
+
# syntactic, and every element is {#provably_not_hash?}: a splatted array's last element is passed as
|
|
2490
|
+
# keywords when it is a `ruby2_keywords` hash. A splat of a local or any other expression is not expanded,
|
|
2491
|
+
# because a `Tuple` read there may be a stale fold of an array that has since grown or shrunk. A trailing
|
|
2492
|
+
# keyword hash must be a literal `**{}` (dropped) or a non-empty shape; an open, mixed or empty non-literal
|
|
2493
|
+
# one leaves the keywords unknown. A list holding `...` is not expanded: its argument types do not line up
|
|
2494
|
+
# with its argument nodes. The splat's own entry in `arg_types` is `Dynamic[top]` ({#type_of_non_value}),
|
|
2495
|
+
# so the literal's elements are typed here.
|
|
2496
|
+
#
|
|
2497
|
+
# Only a call outside every re-typed body frame expands (the main walk's call sites). Inside a frame a rest
|
|
2498
|
+
# parameter holds the caller's arguments, so a chain `m1(*a) = m2(*[a, 1]) + m2(*[a, a])` handed each level
|
|
2499
|
+
# a deeper element type and walked every combination of every level
|
|
2500
|
+
# (spec/rigor/inference/splat_chain_growth_spec.rb).
|
|
2501
|
+
def expand_tuple_splats(arguments, arg_types)
|
|
2502
|
+
return nil if forwards_arguments?(arguments)
|
|
2503
|
+
return nil unless typing_state.stack(INFERENCE_GUARD).empty?
|
|
2504
|
+
|
|
2505
|
+
last = arguments.last
|
|
2506
|
+
trailing_keywords = last.is_a?(Prism::KeywordHashNode)
|
|
2507
|
+
return nil if trailing_keywords && !literal_empty_double_splat?(last) && !keyword_shape?(arg_types.last)
|
|
2508
|
+
|
|
2509
|
+
expanded = []
|
|
2510
|
+
arguments.each_with_index do |argument, index|
|
|
2511
|
+
next if argument.equal?(last) && trailing_keywords && literal_empty_double_splat?(last)
|
|
2512
|
+
next expanded << arg_types[index] unless argument.is_a?(Prism::SplatNode)
|
|
2513
|
+
|
|
2514
|
+
elements = literal_splat_elements(argument)
|
|
2515
|
+
return nil if elements.nil?
|
|
2516
|
+
|
|
2517
|
+
expanded.concat(elements)
|
|
2518
|
+
end
|
|
2519
|
+
expanded
|
|
2520
|
+
end
|
|
2521
|
+
|
|
2522
|
+
# Whether a trailing keyword-hash argument's type is a shape with at least one pair. {#call_arg_type} types a
|
|
2523
|
+
# keyword hash as a shape only when it is closed.
|
|
2524
|
+
def keyword_shape?(type)
|
|
2525
|
+
type.is_a?(Type::HashShape) && !type.pairs.empty?
|
|
2526
|
+
end
|
|
2527
|
+
|
|
2528
|
+
# The element types of `*[a, b]`, each typed on its own, or nil unless the operand is an array literal whose
|
|
2529
|
+
# every element is {#provably_not_hash?}. A splat element (`*[*t]`) types `Dynamic[top]`
|
|
2530
|
+
# ({#type_of_non_value}), so it declines too.
|
|
2531
|
+
def literal_splat_elements(splat)
|
|
2532
|
+
literal = unparenthesized(splat.expression)
|
|
2533
|
+
return nil unless literal.is_a?(Prism::ArrayNode)
|
|
2534
|
+
|
|
2535
|
+
types = literal.elements.map { |element| type_of(element) }
|
|
2536
|
+
types if types.all? { |type| provably_not_hash?(type) }
|
|
2537
|
+
end
|
|
2538
|
+
|
|
2539
|
+
# Classes no instance of which is a `Hash`. A `Nominal` of any other class may be a `Hash` subclass.
|
|
2540
|
+
NON_HASH_CLASSES = %w[
|
|
2541
|
+
Integer Float Rational Complex String Symbol Array Range Regexp NilClass TrueClass FalseClass Proc
|
|
2542
|
+
].to_set.freeze
|
|
2543
|
+
private_constant :NON_HASH_CLASSES
|
|
2544
|
+
|
|
2545
|
+
# `*([1, 2])`'s array literal, or the node itself when it is not a parenthesised single expression.
|
|
2546
|
+
def unparenthesized(node)
|
|
2547
|
+
return node unless node.is_a?(Prism::ParenthesesNode) && node.body.is_a?(Prism::StatementsNode)
|
|
2548
|
+
|
|
2549
|
+
statements = node.body.body
|
|
2550
|
+
statements.size == 1 ? statements.first : node
|
|
2551
|
+
end
|
|
2552
|
+
|
|
2553
|
+
# Whether no value of `type` is a `Hash`. Anything this does not recognise answers false. A `Constant` never
|
|
2554
|
+
# carries a `Hash` (it holds scalars only).
|
|
2555
|
+
def provably_not_hash?(type)
|
|
2556
|
+
case type
|
|
2557
|
+
when Type::Constant, Type::Tuple, Type::IntegerRange then true
|
|
2558
|
+
when Type::Nominal then NON_HASH_CLASSES.include?(type.class_name)
|
|
2559
|
+
when Type::Union then type.members.all? { |member| provably_not_hash?(member) }
|
|
2560
|
+
when Type::Refined, Type::Difference then provably_not_hash?(type.base)
|
|
2561
|
+
else false
|
|
2562
|
+
end
|
|
2563
|
+
end
|
|
2564
|
+
|
|
2202
2565
|
# The post-dispatch tiers for a call `MethodDispatcher` could not answer, in their historical
|
|
2203
2566
|
# order; extracted from {#call_dispatch_type_for} whole.
|
|
2204
2567
|
def dispatch_miss_result(node, receiver, arg_types, block_type = nil)
|
|
@@ -2414,8 +2777,10 @@ module Rigor
|
|
|
2414
2777
|
# `scope.self_type` (or implicit `Object`) as the receiver carrier so the body's own self is
|
|
2415
2778
|
# consistent with the call site's. Returns nil when the parameter shape disqualifies the def, when the
|
|
2416
2779
|
# body is empty, or when the inference raises.
|
|
2417
|
-
def infer_top_level_user_method(def_node, receiver, arg_types, block_type = nil)
|
|
2418
|
-
|
|
2780
|
+
def infer_top_level_user_method(def_node, receiver, call_node, arg_types, block_type = nil)
|
|
2781
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
2782
|
+
infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type,
|
|
2783
|
+
positionals_unknown: positionals_unknown)
|
|
2419
2784
|
rescue StandardError
|
|
2420
2785
|
nil
|
|
2421
2786
|
end
|
|
@@ -2467,6 +2832,9 @@ module Rigor
|
|
|
2467
2832
|
case type
|
|
2468
2833
|
when Type::Constant then true
|
|
2469
2834
|
when Type::Tuple then type.elements.all? { |element| fully_value_pinned?(element) }
|
|
2835
|
+
when Type::HashShape
|
|
2836
|
+
# Issue #3115 — the shape {#pinned_shape_descriptor} keys an unroll on is as pinned as a result.
|
|
2837
|
+
type.closed? && type.optional_keys.empty? && type.pairs.each_value.all? { |value| fully_value_pinned?(value) }
|
|
2470
2838
|
else false
|
|
2471
2839
|
end
|
|
2472
2840
|
end
|
|
@@ -2553,9 +2921,10 @@ module Rigor
|
|
|
2553
2921
|
def_node, owner, raising = resolve_typing_def_with_owner(receiver.class_name, method_name)
|
|
2554
2922
|
return every_definer_raises_type(raising, receiver, call_node, arg_types, block_type) if def_node.nil?
|
|
2555
2923
|
|
|
2924
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
2556
2925
|
result = infer_user_method_return(def_node, receiver, arg_types,
|
|
2557
2926
|
self_fold_safe: fold_safe_call_receiver?(call_node),
|
|
2558
|
-
yield_type: block_type)
|
|
2927
|
+
yield_type: block_type, positionals_unknown: positionals_unknown)
|
|
2559
2928
|
return result if result.nil?
|
|
2560
2929
|
|
|
2561
2930
|
degrade_if_overridable(result, owner, method_name, :instance)
|
|
@@ -2637,7 +3006,9 @@ module Rigor
|
|
|
2637
3006
|
# proven to be the method Ruby calls, so the call falls through to `Dynamic`.
|
|
2638
3007
|
return nil if scope.discovery.contested?(:discovered_singleton_def_nodes, [owner, method_name.to_sym])
|
|
2639
3008
|
|
|
2640
|
-
|
|
3009
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
3010
|
+
result = infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type,
|
|
3011
|
+
positionals_unknown: positionals_unknown)
|
|
2641
3012
|
return result if result.nil?
|
|
2642
3013
|
|
|
2643
3014
|
degrade_if_overridable(result, owner, method_name, :singleton)
|
|
@@ -2659,13 +3030,185 @@ module Rigor
|
|
|
2659
3030
|
cache = (by_singleton[scope.discovered_singleton_def_nodes] ||= {})
|
|
2660
3031
|
table = (cache[class_name.to_s] ||= {})
|
|
2661
3032
|
key = method_name.to_sym
|
|
2662
|
-
|
|
3033
|
+
if table.key?(key)
|
|
3034
|
+
return MemoAudit.check("class graph singleton def", table[key], class_name, key) do
|
|
3035
|
+
compute_singleton_def_with_owner(class_name, method_name)
|
|
3036
|
+
end
|
|
3037
|
+
end
|
|
3038
|
+
|
|
3039
|
+
table[key] = MemoAudit.key_audit("class graph singleton def") do
|
|
3040
|
+
compute_singleton_def_with_owner(class_name, method_name)
|
|
3041
|
+
end
|
|
3042
|
+
end
|
|
2663
3043
|
|
|
3044
|
+
def compute_singleton_def_with_owner(class_name, method_name)
|
|
2664
3045
|
found = scope.singleton_def_through_ancestors(class_name, method_name)
|
|
2665
3046
|
owner = found.last
|
|
2666
3047
|
found = [nil, nil] if owner && SingletonExternalAhead.withhold?(scope, class_name, method_name, owner)
|
|
2667
3048
|
found = [nil, nil] if found.last && dynamic_singleton_definer_ahead?(class_name, method_name, found.last)
|
|
2668
|
-
|
|
3049
|
+
found = [nil, nil] if found.last && singleton_hook_order_split?(class_name, method_name)
|
|
3050
|
+
found = [nil, nil] if found.last && singleton_mixin_split?(class_name, method_name, found)
|
|
3051
|
+
found = [nil, nil] if found.last && unseen_definer_split?(class_name, method_name, found)
|
|
3052
|
+
found
|
|
3053
|
+
end
|
|
3054
|
+
|
|
3055
|
+
# A definer no def table holds that Ruby may call instead of `found`: a hook-placed one, or a foreign `def C.x`.
|
|
3056
|
+
def unseen_definer_split?(class_name, method_name, found)
|
|
3057
|
+
hidden_hook_split?(class_name, method_name, found.last) || foreign_definer_behind_copy?(method_name, found)
|
|
3058
|
+
end
|
|
3059
|
+
|
|
3060
|
+
# Issue #3155 — a `def C.x` outside C's body (in the extended module, another class or the file) defines C's
|
|
3061
|
+
# own `x`, which no def table holds, only its object-singleton mark. A fold copy found on C may then be a
|
|
3062
|
+
# body Ruby does not call, so the typing read withholds.
|
|
3063
|
+
def foreign_definer_behind_copy?(method_name, found)
|
|
3064
|
+
node, owner = found
|
|
3065
|
+
return false unless singleton_copy_head(owner, node) != owner
|
|
3066
|
+
|
|
3067
|
+
SingletonHookDecline.foreign_singleton_definer?(scope, owner, method_name)
|
|
3068
|
+
end
|
|
3069
|
+
|
|
3070
|
+
# The hook-placed definers a found singleton `def` may not be the one Ruby calls through.
|
|
3071
|
+
def hidden_hook_split?(class_name, method_name, owner)
|
|
3072
|
+
hidden_hook_definer?(class_name, method_name, owner) || hook_block_rival?(class_name, method_name, owner)
|
|
3073
|
+
end
|
|
3074
|
+
|
|
3075
|
+
# Issue #3005 — a class object's own `def self.x` and a `def self.x` a concern's `included` block defines on it
|
|
3076
|
+
# replace each other in the order the `def` and the `include` run, which the tables do not hold, so the typing
|
|
3077
|
+
# read withholds when the answer is a class object's own `def` and its level's hook-block module defines the
|
|
3078
|
+
# name too.
|
|
3079
|
+
def hook_block_rival?(class_name, method_name, owner)
|
|
3080
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3081
|
+
index = chain.entries.index { |entry| !entry.external? && entry.side == :singleton && entry.name == owner }
|
|
3082
|
+
!index.nil? && chain.hook_block_rival?(scope, index, method_name)
|
|
3083
|
+
end
|
|
3084
|
+
|
|
3085
|
+
# Issue #1592 — a module a hook extends the class with may define the name private (`class_methods do private;
|
|
3086
|
+
# def x`), which Ruby refuses on an explicit receiver. The visibility rule does not reach that module, so the
|
|
3087
|
+
# typing read withholds rather than answer a call Ruby raises on. A module the class `extend`s itself is left to
|
|
3088
|
+
# that rule.
|
|
3089
|
+
def hidden_hook_definer?(class_name, method_name, owner)
|
|
3090
|
+
visibility = scope.discovered_method_visibility(owner, method_name)
|
|
3091
|
+
return false if visibility.nil? || visibility == :public
|
|
3092
|
+
|
|
3093
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3094
|
+
return false unless chain.entries.any? { |entry| entry.name == owner && entry.side == :instance }
|
|
3095
|
+
|
|
3096
|
+
scope.singleton_extends_of(class_name).none? do |raw|
|
|
3097
|
+
written = raw.delete_prefix("::")
|
|
3098
|
+
owner == written || owner.end_with?("::#{written}")
|
|
3099
|
+
end
|
|
3100
|
+
end
|
|
3101
|
+
|
|
3102
|
+
# Issue #1592 — whether to withhold `method_name`'s class-side answer: a class object on the chain has
|
|
3103
|
+
# hook-extended modules whose order against its own `extend`s is not a fact, and the orders give different
|
|
3104
|
+
# definers (the retro world's against the chain's, or, with no retro world, the class objects' alone against
|
|
3105
|
+
# the chain's). A `def` the extends fold copied onto a class object is read where Ruby finds it, at the extended
|
|
3106
|
+
# module's own entry, since the copy sits ahead of every module in both orders.
|
|
3107
|
+
def singleton_hook_order_split?(class_name, method_name)
|
|
3108
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3109
|
+
return false unless chain.hook_order_marked?
|
|
3110
|
+
|
|
3111
|
+
answer = positioned_singleton_definer(chain, method_name)
|
|
3112
|
+
retro = chain.retro_world
|
|
3113
|
+
return answer != positioned_singleton_definer(retro, method_name) if retro
|
|
3114
|
+
|
|
3115
|
+
answer != positioned_singleton_definer(chain, method_name, copies: true)
|
|
3116
|
+
end
|
|
3117
|
+
|
|
3118
|
+
# Issues #2998 and #3020 — whether a singleton-side mixin of a class object on the chain may answer ahead of
|
|
3119
|
+
# `found`: a named prepend master's order does not place, or a module no table names.
|
|
3120
|
+
def singleton_mixin_split?(class_name, method_name, found)
|
|
3121
|
+
singleton_prepend_split?(class_name, method_name, found) ||
|
|
3122
|
+
unnamed_singleton_mixin_split?(class_name, method_name)
|
|
3123
|
+
end
|
|
3124
|
+
|
|
3125
|
+
# Issue #2998 — whether to withhold the class-side answer `found` on a chain whose class objects have
|
|
3126
|
+
# singleton-side prepends of their own: master's order, which a chain that does not stand answers, places no
|
|
3127
|
+
# such prepend, so its definer may not be the one Ruby's order reaches first. A `def` the extends fold copied
|
|
3128
|
+
# onto a class object is compared as its module's. A definer from a prepend region whose order is not a fact
|
|
3129
|
+
# is withheld whichever order answers.
|
|
3130
|
+
def singleton_prepend_split?(class_name, method_name, found)
|
|
3131
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3132
|
+
return false unless chain.singleton_prepend_marked?
|
|
3133
|
+
|
|
3134
|
+
answer = positioned_singleton_definer(chain, method_name)
|
|
3135
|
+
return true if unordered_prepend_definer?(chain, answer)
|
|
3136
|
+
|
|
3137
|
+
node, owner = found
|
|
3138
|
+
head = scope.singleton_def_for(owner, method_name).equal?(node) ? singleton_copy_head(owner, node) : owner
|
|
3139
|
+
head != answer
|
|
3140
|
+
end
|
|
3141
|
+
|
|
3142
|
+
# Whether `answer` is a module in the prepend region of a class object on the chain whose prepends have no known
|
|
3143
|
+
# order: the entries between the start of that class's level and its own entry.
|
|
3144
|
+
def unordered_prepend_definer?(chain, answer)
|
|
3145
|
+
chain.prepend_order_nodes.any? do |node|
|
|
3146
|
+
level = chain.level_classes.index(node)
|
|
3147
|
+
next true if level.nil?
|
|
3148
|
+
|
|
3149
|
+
start = chain.level_starts[level]
|
|
3150
|
+
own = chain.entries.index { |entry| entry.side == :singleton && entry.name == node }
|
|
3151
|
+
next true if own.nil?
|
|
3152
|
+
|
|
3153
|
+
chain.entries[start...own].any? { |entry| entry.name == answer }
|
|
3154
|
+
end
|
|
3155
|
+
end
|
|
3156
|
+
|
|
3157
|
+
# Issue #3020 — whether to withhold the class-side answer on a chain whose class objects mix a module no table
|
|
3158
|
+
# names into their singleton side, which may define the name. A prepend (`class << self; prepend(*MODS)`) sits
|
|
3159
|
+
# ahead of the class object's own `def self.`, so a definer at or behind that class object's level is withheld;
|
|
3160
|
+
# an `extend(*MODS)` or a `class << self` `include(*MODS)` sits right behind it, so a definer behind the class
|
|
3161
|
+
# object's own entry is withheld and its own `def self.` still answers.
|
|
3162
|
+
def unnamed_singleton_mixin_split?(class_name, method_name)
|
|
3163
|
+
chain = Scope::ResolutionChain.for(scope, class_name.to_s, :singleton, :methods)
|
|
3164
|
+
prepends = chain.unnamed_prepend_nodes
|
|
3165
|
+
extends = chain.unnamed_extend_nodes
|
|
3166
|
+
return false if prepends.empty? && extends.empty?
|
|
3167
|
+
|
|
3168
|
+
answer = positioned_singleton_definer(chain, method_name)
|
|
3169
|
+
prepends.any? { |node| !answered_ahead?(chain, answer, level_start_of(chain, node)) } ||
|
|
3170
|
+
extends.any? { |node| !answered_ahead?(chain, answer, own_singleton_index(chain, node)&.succ) }
|
|
3171
|
+
end
|
|
3172
|
+
|
|
3173
|
+
# Whether `answer` owns an entry before `stop` (nil when the position is unknown: never). A nil `answer` (no class
|
|
3174
|
+
# object's own def) owns none.
|
|
3175
|
+
def answered_ahead?(chain, answer, stop)
|
|
3176
|
+
!stop.nil? && chain.entries[0...stop].any? { |entry| entry.name == answer }
|
|
3177
|
+
end
|
|
3178
|
+
|
|
3179
|
+
def level_start_of(chain, node)
|
|
3180
|
+
level = chain.level_classes.index(node)
|
|
3181
|
+
level && chain.level_starts[level]
|
|
3182
|
+
end
|
|
3183
|
+
|
|
3184
|
+
def own_singleton_index(chain, node)
|
|
3185
|
+
chain.entries.index { |entry| entry.side == :singleton && entry.name == node }
|
|
3186
|
+
end
|
|
3187
|
+
|
|
3188
|
+
# The owner of the first `def` of the name along the chain: a fold copy is skipped, or with `copies:` read as
|
|
3189
|
+
# its module's, on the class objects alone (the answer master gives).
|
|
3190
|
+
def positioned_singleton_definer(chain, method_name, copies: false)
|
|
3191
|
+
chain.search(scope, side: copies ? :singleton : nil) do |entry|
|
|
3192
|
+
next if entry.external?
|
|
3193
|
+
|
|
3194
|
+
name = entry.name
|
|
3195
|
+
if entry.side == :singleton
|
|
3196
|
+
node = scope.singleton_def_for(name, method_name)
|
|
3197
|
+
next nil if node.nil?
|
|
3198
|
+
|
|
3199
|
+
head = singleton_copy_head(name, node)
|
|
3200
|
+
copies ? head : (name if head == name)
|
|
3201
|
+
elsif scope.user_def_for(name, method_name)
|
|
3202
|
+
name
|
|
3203
|
+
end
|
|
3204
|
+
end
|
|
3205
|
+
end
|
|
3206
|
+
|
|
3207
|
+
# The module a receiverless singleton `def` was written in when the extends fold copied it onto `owner`, else
|
|
3208
|
+
# `owner`.
|
|
3209
|
+
def singleton_copy_head(owner, node)
|
|
3210
|
+
head = node.receiver.nil? ? scope.discovery.recorded_def_nesting(node)&.first : nil
|
|
3211
|
+
head.nil? ? owner : head
|
|
2669
3212
|
end
|
|
2670
3213
|
|
|
2671
3214
|
SINGLETON_WILDCARD_SLOT = %i[singleton *].freeze
|
|
@@ -2693,6 +3236,8 @@ module Rigor
|
|
|
2693
3236
|
next if entry.external?
|
|
2694
3237
|
return false if entry.name == owner.to_s
|
|
2695
3238
|
return true if entry_defines_without_def?(entry, name)
|
|
3239
|
+
# Issue #1592 — a hook of a module the class includes may have extended it with a module no table names.
|
|
3240
|
+
return true if entry.side == :singleton && chain.hook_open_nodes.include?(entry.name)
|
|
2696
3241
|
end
|
|
2697
3242
|
false
|
|
2698
3243
|
end
|
|
@@ -2744,7 +3289,9 @@ module Rigor
|
|
|
2744
3289
|
def_node = SingletonObjectConstant.def_node_for(call_node, receiver, method_name, scope)
|
|
2745
3290
|
return nil if def_node.nil?
|
|
2746
3291
|
|
|
2747
|
-
|
|
3292
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
3293
|
+
infer_user_method_return(def_node, receiver, arg_types, yield_type: block_type,
|
|
3294
|
+
positionals_unknown: positionals_unknown)
|
|
2748
3295
|
rescue StandardError
|
|
2749
3296
|
nil
|
|
2750
3297
|
end
|
|
@@ -2756,8 +3303,14 @@ module Rigor
|
|
|
2756
3303
|
# external entries this lookup passes over. Cross-file: the chain is built from the tables the runner
|
|
2757
3304
|
# seeds from the project-wide pre-pass, cycle-guarded and capped at `Scope::ANCESTOR_WALK_LIMIT`.
|
|
2758
3305
|
|
|
2759
|
-
|
|
2760
|
-
|
|
3306
|
+
CLASS_GRAPH_CACHE = TypingState.memo(
|
|
3307
|
+
:class_graph_cache,
|
|
3308
|
+
run_scoped: true,
|
|
3309
|
+
audit: ["class graph user def", "class graph typing def", "class graph singleton def", "class graph yields",
|
|
3310
|
+
"class graph self pure"],
|
|
3311
|
+
reason: "pure functions of the frozen discovery index it keys on"
|
|
3312
|
+
)
|
|
3313
|
+
private_constant :CLASS_GRAPH_CACHE
|
|
2761
3314
|
|
|
2762
3315
|
# Memo for the static class-graph resolvers below. They are pure functions of the scope's *frozen*
|
|
2763
3316
|
# discovery tables — `user_def_for` / `superclass_of` / `includes_of` and the ancestor-name resolver
|
|
@@ -2775,8 +3328,8 @@ module Rigor
|
|
|
2775
3328
|
#
|
|
2776
3329
|
# ONE slot, replaced rather than accumulated — the same shape, and now the same key, as
|
|
2777
3330
|
# {MethodDispatcher::RbsDispatch}'s `core_stdlib_memo`. `ExpressionTyper` is rebuilt per
|
|
2778
|
-
# `Scope#type_of`, so the slot lives
|
|
2779
|
-
# file's discovery with the project pre-pass, so every analysed file arrives with a fresh index: an
|
|
3331
|
+
# `Scope#type_of`, so the slot lives in the thread's {TypingState} rather than on `self`. A `Scope` merges
|
|
3332
|
+
# its file's discovery with the project pre-pass, so every analysed file arrives with a fresh index: an
|
|
2780
3333
|
# identity-keyed *store* grew one bucket per file and, because the keys ARE the tables, pinned every
|
|
2781
3334
|
# file's whole discovery index for the length of the run. What that bought, measured over
|
|
2782
3335
|
# `lib`+`plugins`: 552 buckets holding 1,834 entries between them, and 58 of 80,106 calls answered
|
|
@@ -2786,23 +3339,32 @@ module Rigor
|
|
|
2786
3339
|
# because one `equal?` is cheaper than three identity-hash lookups.
|
|
2787
3340
|
#
|
|
2788
3341
|
# Steady-state cost is one `equal?` check and zero allocation; the array and the buckets are
|
|
2789
|
-
# allocated once per index. (Pool mode forks per worker, so the
|
|
3342
|
+
# allocated once per index. (Pool mode forks per worker, so the per-thread slot is
|
|
2790
3343
|
# process-local and never crosses a project boundary — and note that the parent's reported
|
|
2791
3344
|
# `Memory peak` therefore does not see a worker's share of this at all.)
|
|
3345
|
+
#
|
|
3346
|
+
# ADR-129 WD1 — every bucket's hit goes through `MemoAudit.check`; an audit recomputation gets fresh buckets that
|
|
3347
|
+
# nothing stores, so it reads no entry and leaves none.
|
|
2792
3348
|
def class_graph_buckets
|
|
3349
|
+
return fresh_class_graph_buckets if MemoAudit.enabled? && MemoAudit.suspended?
|
|
3350
|
+
|
|
2793
3351
|
discovery = scope.discovery
|
|
2794
|
-
|
|
3352
|
+
state = typing_state
|
|
3353
|
+
slot = state[CLASS_GRAPH_CACHE]
|
|
2795
3354
|
unless slot && slot[0].equal?(discovery)
|
|
2796
3355
|
# `self_pure` is issue #525's grant scan (identity-keyed by def node); it belongs here because it
|
|
2797
3356
|
# is a pure function of the same frozen index — the sibling resolver it walks reads nothing else.
|
|
2798
3357
|
# `singleton_def` is added lazily by {#singleton_def_through_ancestors}'s caller.
|
|
2799
|
-
slot = [discovery,
|
|
2800
|
-
|
|
2801
|
-
Thread.current[CLASS_GRAPH_CACHE_KEY] = slot
|
|
3358
|
+
slot = [discovery, fresh_class_graph_buckets]
|
|
3359
|
+
state[CLASS_GRAPH_CACHE] = slot
|
|
2802
3360
|
end
|
|
2803
3361
|
slot[1]
|
|
2804
3362
|
end
|
|
2805
3363
|
|
|
3364
|
+
def fresh_class_graph_buckets
|
|
3365
|
+
{ user_def: {}, typing_def: {}, self_pure: {}.compare_by_identity, yields: {}.compare_by_identity }
|
|
3366
|
+
end
|
|
3367
|
+
|
|
2806
3368
|
# ADR-119 C2-b1 — the TYPING read of {#resolve_user_def_with_owner}: the same `[def_node, owner]` pair, but
|
|
2807
3369
|
# answered through {DefinerResolution} so a chain that does not stand for the name (a conditional definer,
|
|
2808
3370
|
# a conditional include, a fork, an external ancestor that may answer first) is `[nil, nil]` and the call
|
|
@@ -2817,17 +3379,26 @@ module Rigor
|
|
|
2817
3379
|
cache = class_graph_buckets[:typing_def]
|
|
2818
3380
|
table = (cache[class_name.to_s] ||= {})
|
|
2819
3381
|
key = method_name.to_sym
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
case DefinerResolution.resolve(scope, class_name, method_name, :instance, question: :definer)
|
|
2824
|
-
in DefinerResolution::Known(answer: [node, owner])
|
|
2825
|
-
[node, owner]
|
|
2826
|
-
in DefinerResolution::UNKNOWN
|
|
2827
|
-
[nil, nil, every_project_definer(class_name, method_name)]
|
|
2828
|
-
in DefinerResolution::ABSENT
|
|
2829
|
-
[nil, nil]
|
|
3382
|
+
if table.key?(key)
|
|
3383
|
+
return MemoAudit.check("class graph typing def", table[key], class_name, key) do
|
|
3384
|
+
compute_typing_def_with_owner(class_name, method_name)
|
|
2830
3385
|
end
|
|
3386
|
+
end
|
|
3387
|
+
|
|
3388
|
+
table[key] = MemoAudit.key_audit("class graph typing def") do
|
|
3389
|
+
compute_typing_def_with_owner(class_name, method_name)
|
|
3390
|
+
end
|
|
3391
|
+
end
|
|
3392
|
+
|
|
3393
|
+
def compute_typing_def_with_owner(class_name, method_name)
|
|
3394
|
+
case DefinerResolution.resolve(scope, class_name, method_name, :instance, question: :definer)
|
|
3395
|
+
in DefinerResolution::Known(answer: [node, owner])
|
|
3396
|
+
[node, owner]
|
|
3397
|
+
in DefinerResolution::UNKNOWN
|
|
3398
|
+
[nil, nil, every_project_definer(class_name, method_name)]
|
|
3399
|
+
in DefinerResolution::ABSENT
|
|
3400
|
+
[nil, nil]
|
|
3401
|
+
end
|
|
2831
3402
|
end
|
|
2832
3403
|
|
|
2833
3404
|
# ADR-119 C2-b1 errata (the `bot` exception) — an UNKNOWN chain does not say WHICH definer Ruby calls, but
|
|
@@ -2841,9 +3412,11 @@ module Rigor
|
|
|
2841
3412
|
return nil if defs.nil?
|
|
2842
3413
|
|
|
2843
3414
|
self_fold_safe = fold_safe_call_receiver?(call_node)
|
|
3415
|
+
arg_types, positionals_unknown = positional_correspondence(call_node, arg_types)
|
|
2844
3416
|
defs.each do |def_node|
|
|
2845
3417
|
result = infer_user_method_return(def_node, receiver, arg_types,
|
|
2846
|
-
self_fold_safe: self_fold_safe, yield_type: block_type
|
|
3418
|
+
self_fold_safe: self_fold_safe, yield_type: block_type,
|
|
3419
|
+
positionals_unknown: positionals_unknown)
|
|
2847
3420
|
return nil unless result.is_a?(Type::Bot)
|
|
2848
3421
|
end
|
|
2849
3422
|
Type::Combinator.bot
|
|
@@ -2918,9 +3491,15 @@ module Rigor
|
|
|
2918
3491
|
cache = class_graph_buckets[:user_def]
|
|
2919
3492
|
table = (cache[class_name.to_s] ||= {})
|
|
2920
3493
|
key = method_name.to_sym
|
|
2921
|
-
|
|
3494
|
+
if table.key?(key)
|
|
3495
|
+
return MemoAudit.check("class graph user def", table[key], class_name, key) do
|
|
3496
|
+
compute_user_def_with_owner(class_name, method_name)
|
|
3497
|
+
end
|
|
3498
|
+
end
|
|
2922
3499
|
|
|
2923
|
-
table[key] =
|
|
3500
|
+
table[key] = MemoAudit.key_audit("class graph user def") do
|
|
3501
|
+
compute_user_def_with_owner(class_name, method_name)
|
|
3502
|
+
end
|
|
2924
3503
|
end
|
|
2925
3504
|
|
|
2926
3505
|
# ADR-24 slice 2 — the walk itself lives on `Scope` ({Scope#user_def_through_ancestors}); it reads
|
|
@@ -2957,8 +3536,12 @@ module Rigor
|
|
|
2957
3536
|
dynamic_top
|
|
2958
3537
|
end
|
|
2959
3538
|
|
|
2960
|
-
|
|
2961
|
-
|
|
3539
|
+
OVERRIDE_GATE_CACHE = TypingState.memo(
|
|
3540
|
+
:overridable_method_gate,
|
|
3541
|
+
run_scoped: true, audit: "overridable method gate",
|
|
3542
|
+
reason: "pure functions of the frozen discovery index it keys on"
|
|
3543
|
+
)
|
|
3544
|
+
private_constant :OVERRIDE_GATE_CACHE
|
|
2962
3545
|
|
|
2963
3546
|
# Memo for {#overridden_in_project?}, nested `kind → owner → method_name`. The predicate is a pure
|
|
2964
3547
|
# function of the scope's frozen discovery tables, so that index's *identity* is what says whether a
|
|
@@ -2988,10 +3571,11 @@ module Rigor
|
|
|
2988
3571
|
# keyed and bounded differently for exactly that reason.
|
|
2989
3572
|
def override_gate_buckets
|
|
2990
3573
|
discovery = scope.discovery
|
|
2991
|
-
|
|
3574
|
+
state = typing_state
|
|
3575
|
+
slot = state[OVERRIDE_GATE_CACHE]
|
|
2992
3576
|
unless slot && slot[0].equal?(discovery)
|
|
2993
3577
|
slot = [discovery, { instance: {}, singleton: {} }]
|
|
2994
|
-
|
|
3578
|
+
state[OVERRIDE_GATE_CACHE] = slot
|
|
2995
3579
|
end
|
|
2996
3580
|
slot[1]
|
|
2997
3581
|
end
|
|
@@ -3002,10 +3586,18 @@ module Rigor
|
|
|
3002
3586
|
# reopen of `owner` itself is NOT an override (monkey-patch reopen shares the owner identity). Memoized
|
|
3003
3587
|
# per `(owner, method_name, kind)`.
|
|
3004
3588
|
def overridden_in_project?(owner, method_name, kind)
|
|
3589
|
+
return compute_overridden_in_project?(owner, method_name, kind) if MemoAudit.enabled? && MemoAudit.suspended?
|
|
3590
|
+
|
|
3005
3591
|
by_owner = (override_gate_buckets[kind][owner] ||= {})
|
|
3006
|
-
|
|
3592
|
+
if by_owner.key?(method_name)
|
|
3593
|
+
return MemoAudit.check("overridable method gate", by_owner[method_name], owner, method_name) do
|
|
3594
|
+
compute_overridden_in_project?(owner, method_name, kind)
|
|
3595
|
+
end
|
|
3596
|
+
end
|
|
3007
3597
|
|
|
3008
|
-
by_owner[method_name] =
|
|
3598
|
+
by_owner[method_name] = MemoAudit.key_audit("overridable method gate") do
|
|
3599
|
+
compute_overridden_in_project?(owner, method_name, kind)
|
|
3600
|
+
end
|
|
3009
3601
|
end
|
|
3010
3602
|
|
|
3011
3603
|
def compute_overridden_in_project?(owner, method_name, kind)
|
|
@@ -3030,16 +3622,23 @@ module Rigor
|
|
|
3030
3622
|
EMPTY_REDEFINERS = [].freeze
|
|
3031
3623
|
private_constant :EMPTY_REDEFINERS
|
|
3032
3624
|
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
#
|
|
3041
|
-
|
|
3042
|
-
|
|
3625
|
+
METHOD_DEFINERS_INDEX = TypingState.memo(
|
|
3626
|
+
:method_definers_index,
|
|
3627
|
+
run_scoped: true, audit: "method definers index",
|
|
3628
|
+
reason: "pure functions of the def tables it keys on"
|
|
3629
|
+
)
|
|
3630
|
+
private_constant :METHOD_DEFINERS_INDEX
|
|
3631
|
+
|
|
3632
|
+
# The second way. Two flat slots rather than one slot holding an array of ways: a `slot ||= […]` seeds the
|
|
3633
|
+
# store with an array LITERAL, which folds to a tuple whose elements are pinned, and the engine then reads the
|
|
3634
|
+
# `way[0].equal?(…)` guards below as always-falsey (`make check` fires `flow.always-truthy-condition` on this
|
|
3635
|
+
# very file). Two slots read as two plain reads and keep the guards analysable.
|
|
3636
|
+
METHOD_DEFINERS_INDEX_ALT = TypingState.memo(
|
|
3637
|
+
:method_definers_index_alt,
|
|
3638
|
+
run_scoped: true, audit: "method definers index alt",
|
|
3639
|
+
reason: "pure functions of the def tables it keys on"
|
|
3640
|
+
)
|
|
3641
|
+
private_constant :METHOD_DEFINERS_INDEX_ALT
|
|
3043
3642
|
|
|
3044
3643
|
# `method_name (Symbol) → [owner names]` inverted index over the instance / singleton def tables,
|
|
3045
3644
|
# valid for exactly as long as the table it inverts: a new analysis generation, or a `Scope` that swaps
|
|
@@ -3080,19 +3679,52 @@ module Rigor
|
|
|
3080
3679
|
# def-table-keyed one misses 201 — it would rebuild the seed's 934-entry table once per file and hand
|
|
3081
3680
|
# back the +22.6 % this bound exists to avoid. Check that number before widening this key to match
|
|
3082
3681
|
# its siblings.
|
|
3682
|
+
#
|
|
3683
|
+
# ADR-129 WD1 — a hit in either way is audited against an index built with every memo bypassed, compared in
|
|
3684
|
+
# {DefinersIndex.canonical} form (a `Layered` index and a whole one hold the same owners, in any order).
|
|
3083
3685
|
def method_definers_index(kind)
|
|
3084
|
-
def_nodes = scope.discovered_def_nodes
|
|
3085
|
-
singleton_def_nodes = scope.discovered_singleton_def_nodes
|
|
3086
|
-
way = Thread.current[METHOD_DEFINERS_INDEX_KEY]
|
|
3087
|
-
unless way && way[0].equal?(def_nodes) && way[1].equal?(singleton_def_nodes)
|
|
3088
|
-
alt = Thread.current[METHOD_DEFINERS_INDEX_ALT_KEY]
|
|
3089
|
-
alt = nil unless alt && alt[0].equal?(def_nodes) && alt[1].equal?(singleton_def_nodes)
|
|
3090
|
-
Thread.current[METHOD_DEFINERS_INDEX_ALT_KEY] = way
|
|
3091
|
-
way = alt || [def_nodes, singleton_def_nodes, nil, nil]
|
|
3092
|
-
Thread.current[METHOD_DEFINERS_INDEX_KEY] = way
|
|
3093
|
-
end
|
|
3094
3686
|
singleton = kind == :singleton
|
|
3095
|
-
|
|
3687
|
+
table = singleton ? scope.discovered_singleton_def_nodes : scope.discovered_def_nodes
|
|
3688
|
+
return DefinersIndex.for(table) if MemoAudit.enabled? && MemoAudit.suspended?
|
|
3689
|
+
|
|
3690
|
+
state = typing_state
|
|
3691
|
+
primary = current_definers_way?(state[METHOD_DEFINERS_INDEX])
|
|
3692
|
+
way = primary ? state[METHOD_DEFINERS_INDEX] : rotate_definers_ways(state)
|
|
3693
|
+
position = singleton ? 3 : 2
|
|
3694
|
+
index = way[position]
|
|
3695
|
+
return way[position] = MemoAudit.key_audit("method definers index") { DefinersIndex.for(table) } if index.nil?
|
|
3696
|
+
|
|
3697
|
+
audit_definers_index(primary, index, kind, table) if MemoAudit.enabled?
|
|
3698
|
+
index
|
|
3699
|
+
end
|
|
3700
|
+
|
|
3701
|
+
# Whether `way` was built over this scope's two def tables.
|
|
3702
|
+
def current_definers_way?(way)
|
|
3703
|
+
!way.nil? && way[0].equal?(scope.discovered_def_nodes) && way[1].equal?(scope.discovered_singleton_def_nodes)
|
|
3704
|
+
end
|
|
3705
|
+
|
|
3706
|
+
# Makes the alternate way current when it was built over this scope's def tables, or a fresh way otherwise; the
|
|
3707
|
+
# way it replaces becomes the alternate.
|
|
3708
|
+
def rotate_definers_ways(state)
|
|
3709
|
+
alt = state[METHOD_DEFINERS_INDEX_ALT]
|
|
3710
|
+
alt = nil unless current_definers_way?(alt)
|
|
3711
|
+
state[METHOD_DEFINERS_INDEX_ALT] = state[METHOD_DEFINERS_INDEX]
|
|
3712
|
+
state[METHOD_DEFINERS_INDEX] =
|
|
3713
|
+
alt || [scope.discovered_def_nodes, scope.discovered_singleton_def_nodes, nil, nil]
|
|
3714
|
+
end
|
|
3715
|
+
|
|
3716
|
+
# A hit in the current way (`primary`) or the alternate one, checked in {DefinersIndex.canonical} form.
|
|
3717
|
+
def audit_definers_index(primary, index, kind, table)
|
|
3718
|
+
canonical = DefinersIndex.canonical(index)
|
|
3719
|
+
if primary
|
|
3720
|
+
MemoAudit.check("method definers index", canonical, kind) do
|
|
3721
|
+
DefinersIndex.canonical(DefinersIndex.for(table))
|
|
3722
|
+
end
|
|
3723
|
+
else
|
|
3724
|
+
MemoAudit.check("method definers index alt", canonical, kind) do
|
|
3725
|
+
DefinersIndex.canonical(DefinersIndex.for(table))
|
|
3726
|
+
end
|
|
3727
|
+
end
|
|
3096
3728
|
end
|
|
3097
3729
|
|
|
3098
3730
|
# True when `candidate`'s ancestor chain (superclasses + included/prepended modules) reaches `owner` —
|
|
@@ -3105,18 +3737,30 @@ module Rigor
|
|
|
3105
3737
|
!chain.search(scope) { |entry| entry.name == owner }.nil?
|
|
3106
3738
|
end
|
|
3107
3739
|
|
|
3108
|
-
|
|
3109
|
-
|
|
3740
|
+
# ADR-129 WD3 — an id-determined Resource: {TypingState::State#push} and {TypingState::State#pop} each take a
|
|
3741
|
+
# fresh id, so the stack's contents are fixed while the id is and a read of it refuses nothing.
|
|
3742
|
+
INFERENCE_GUARD = TypingState.resource(
|
|
3743
|
+
:user_method_inference_stack,
|
|
3744
|
+
read_exemption: :id_determined,
|
|
3745
|
+
reason: "the ADR-55 recursion guard, pushed and popped in place by each inference frame"
|
|
3746
|
+
)
|
|
3747
|
+
private_constant :INFERENCE_GUARD
|
|
3110
3748
|
|
|
3111
|
-
|
|
3112
|
-
|
|
3749
|
+
INFERENCE_UNROLL_FUEL = TypingState.resource(
|
|
3750
|
+
:user_method_unroll_fuel,
|
|
3751
|
+
reason: "ADR-55 unroll fuel, consumed in place per outermost entry"
|
|
3752
|
+
)
|
|
3753
|
+
private_constant :INFERENCE_UNROLL_FUEL
|
|
3113
3754
|
|
|
3114
3755
|
# ADR-55 slice 2 — thread-local fixpoint return-summary table, keyed by the plain `(receiver, method)`
|
|
3115
3756
|
# signature (NOT the value-extended signature: extended frames from slice 1 share the same summary).
|
|
3116
3757
|
# Each entry is `{ assumption:, consulted: }` where `assumption` is the current Kleene iterate (seeded
|
|
3117
3758
|
# `bot`) and `consulted` flips true when an in-cycle re-entry returns it.
|
|
3118
|
-
|
|
3119
|
-
|
|
3759
|
+
INFERENCE_SUMMARY = TypingState.resource(
|
|
3760
|
+
:user_method_return_summary,
|
|
3761
|
+
reason: "ADR-55 Kleene summaries and their `:consulted` flag, rewritten in place"
|
|
3762
|
+
)
|
|
3763
|
+
private_constant :INFERENCE_SUMMARY
|
|
3120
3764
|
|
|
3121
3765
|
# Per-thread append-only log of the seed depths of every in-flight summary `consult_summary` read
|
|
3122
3766
|
# (ADR-55 slice 2 mutual-recursion soundness fix, 2026-06-12). Each fixpoint owner records the guard
|
|
@@ -3127,8 +3771,11 @@ module Rigor
|
|
|
3127
3771
|
# `untyped` rather than fold one branch's seed into a "final" constant. Own-signature consults log
|
|
3128
3772
|
# depth == own depth, and a nested fixpoint that completes within the evaluation logs depths > own
|
|
3129
3773
|
# depth; neither is foreign. Cleared with the summary table when the guard stack drains to empty.
|
|
3130
|
-
|
|
3131
|
-
|
|
3774
|
+
SUMMARY_CONSULT_DEPTHS = TypingState.resource(
|
|
3775
|
+
:user_method_summary_consult_depths,
|
|
3776
|
+
reason: "ADR-55 consult log, appended in place and read as a delta"
|
|
3777
|
+
)
|
|
3778
|
+
private_constant :SUMMARY_CONSULT_DEPTHS
|
|
3132
3779
|
|
|
3133
3780
|
# ADR-57 follow-up — run-scoped memo for resolved user-method return types. The ADR-57 gate-open made
|
|
3134
3781
|
# every resolved in-body self-call adopt the callee's inferred return, which re-types the callee body
|
|
@@ -3151,7 +3798,7 @@ module Rigor
|
|
|
3151
3798
|
# post-hoc (ADR-84 WD3): candidacy only requires that the plain signature not already be on the
|
|
3152
3799
|
# recursion guard stack (a frame inside its own cycle returns a Kleene iterate by construction), and
|
|
3153
3800
|
# the store gate brackets the compute over the transient-event depth log
|
|
3154
|
-
# (
|
|
3801
|
+
# (TRANSIENT_EVENT_DEPTHS), refusing only when an event referenced a frame BELOW the bracket's
|
|
3155
3802
|
# entry depth — proof the ancestor context (an in-flight ancestor summary, the shared unroll fuel, a
|
|
3156
3803
|
# possibly-ancestor-caused clamp) influenced the result. Events at-or-above the entry depth are the
|
|
3157
3804
|
# compute's own deterministic machinery — a recursive method's own converged fixpoint stores from a
|
|
@@ -3175,10 +3822,68 @@ module Rigor
|
|
|
3175
3822
|
# parse). Only the current generation's bucket is retained (single slot) — a rolled-over run's entries
|
|
3176
3823
|
# become garbage instead of accumulating across LSP runs, and the run that filled the slot releases it
|
|
3177
3824
|
# when it ends ({RunScopedSlots}; `Runner#run` keeps its bucket for `#return_summaries`).
|
|
3178
|
-
# `ExpressionTyper` is rebuilt per `Scope#type_of`, so the store lives
|
|
3825
|
+
# `ExpressionTyper` is rebuilt per `Scope#type_of`, so the store lives in the thread's {TypingState}; fork-pool
|
|
3179
3826
|
# workers are separate processes, so it never crosses a project boundary (ADR-84 WD5).
|
|
3180
|
-
|
|
3181
|
-
|
|
3827
|
+
#
|
|
3828
|
+
# ADR-129 WD1 — a Table, not a Memo: its WD3 serves a standalone result where a fresh compute inside a cycle
|
|
3829
|
+
# would be tainted, and its WD6 serves a stored `untyped`, so it is not extensional. Its reads and stores are
|
|
3830
|
+
# journaled ({#log_return_memo}).
|
|
3831
|
+
# ADR-129 WD3 — the return memo's entry protocol. The slot holds `[generation, bucket]` ({#return_memo_bucket}),
|
|
3832
|
+
# and a journal record keys an entry on `[def_node, memo_key]`. A replay writes into the bucket the slot holds: a
|
|
3833
|
+
# `type_of` memo entry lives no longer than its file's analysis, inside one run generation, so a slot with no
|
|
3834
|
+
# bucket means the entry outlived its run, which is a defect to surface rather than a bucket to invent.
|
|
3835
|
+
module ReturnMemoEntries
|
|
3836
|
+
module_function
|
|
3837
|
+
|
|
3838
|
+
def entry(slot_value, key)
|
|
3839
|
+
per_def = slot_value&.[](1)&.[](key[0])
|
|
3840
|
+
per_def.nil? ? TypingState::ABSENT : per_def.fetch(key[1], TypingState::ABSENT)
|
|
3841
|
+
end
|
|
3842
|
+
|
|
3843
|
+
def put(slot_value, key, value)
|
|
3844
|
+
(slot_value[1][key[0]] ||= {})[key[1]] = value
|
|
3845
|
+
end
|
|
3846
|
+
|
|
3847
|
+
def delete(slot_value, key)
|
|
3848
|
+
per_def = slot_value && slot_value[1][key[0]]
|
|
3849
|
+
per_def&.delete(key[1])
|
|
3850
|
+
end
|
|
3851
|
+
|
|
3852
|
+
def container(state, slot)
|
|
3853
|
+
state[slot] || raise(ArgumentError, "no return memo bucket to replay into: its entry outlived its run")
|
|
3854
|
+
end
|
|
3855
|
+
end
|
|
3856
|
+
|
|
3857
|
+
RETURN_MEMO = TypingState.table(
|
|
3858
|
+
:user_method_return_memo,
|
|
3859
|
+
run_scoped: true, computed: "return memo", entries: ReturnMemoEntries,
|
|
3860
|
+
reason: "ADR-84 return memo; not extensional (its WD3 and WD6), so its reads and stores are journaled"
|
|
3861
|
+
)
|
|
3862
|
+
private_constant :RETURN_MEMO
|
|
3863
|
+
|
|
3864
|
+
# ADR-129 WD1's key audit of a return-memo miss. The key carries the yield value ({YIELD_VALUE}, read through
|
|
3865
|
+
# `key_frame`). {.with_call_site_frame} installs the forwarded-argument list, derived from `arg_types`, repeat
|
|
3866
|
+
# depth zero and none of the caller's jump sinks or retry frames before the key is built, and
|
|
3867
|
+
# {#infer_user_method_return} installs the key-presence walk off, so the computation reads those forms. The
|
|
3868
|
+
# bucket keys on the run generation ({#return_memo_bucket}), within which ADR-130's run-level flags are fixed, so
|
|
3869
|
+
# the key carries them. Two Frames are read without a key term, each a known gap: the block-body threading
|
|
3870
|
+
# suppression (#2782) and Acceptance's unread-shape refusal (#2866).
|
|
3871
|
+
RETURN_MEMO_KEY_AUDIT = MemoAudit.key_declaration(
|
|
3872
|
+
forced: %i[forwarded_call_arg_types reflective_ivar_write_repeating key_presence_walk next_sink break_sink
|
|
3873
|
+
break_value_sink retry_frames],
|
|
3874
|
+
carried: {
|
|
3875
|
+
interface_erasure: "installed around a whole run; the bucket keys on the run generation",
|
|
3876
|
+
interface_dispatch_off: "installed around a whole run; the bucket keys on the run generation"
|
|
3877
|
+
},
|
|
3878
|
+
gaps: {
|
|
3879
|
+
threaded_block_body: "#2782: the return memo reads the block-body threading suppression without keying on " \
|
|
3880
|
+
"it; keying on it cost a fifth of lib's allocations, and clearing it per callee " \
|
|
3881
|
+
"frame re-walked unstorable frames exponentially",
|
|
3882
|
+
acceptance_unread_shape_refused: "#2866: a callee body typed inside Acceptance.refusing_unread " \
|
|
3883
|
+
"reads the caller's refusal, and the return memo does not key on it"
|
|
3884
|
+
}
|
|
3885
|
+
).freeze
|
|
3886
|
+
private_constant :RETURN_MEMO_KEY_AUDIT
|
|
3182
3887
|
|
|
3183
3888
|
# ADR-84 WD2 — one memo entry: the final inferred return plus, when the entry was computed under
|
|
3184
3889
|
# ADR-46 dependency recording, the frozen `Analysis::DependencyRecorder::ReadSet` its body walk
|
|
@@ -3190,7 +3895,10 @@ module Rigor
|
|
|
3190
3895
|
# when every return is unchanged, skips its symbol dependents. Carrying two frozen type refs is
|
|
3191
3896
|
# negligible on the memo-store path (already in scope) and never enters memo equality (entries are
|
|
3192
3897
|
# keyed by `memo_key`, retrieved by `.result` / `.read_set`).
|
|
3193
|
-
|
|
3898
|
+
#
|
|
3899
|
+
# Issue #1237 — `positionals_unknown` marks an entry a splat call stored ({#positional_correspondence}): the
|
|
3900
|
+
# session re-drives its key the same way ({#return_type_for}).
|
|
3901
|
+
MemoEntry = Data.define(:result, :read_set, :receiver, :arg_types, :positionals_unknown)
|
|
3194
3902
|
private_constant :MemoEntry
|
|
3195
3903
|
|
|
3196
3904
|
# ADR-84 WD3 — thread-local transient-machinery event log, appended by `note_transient_fallback` at
|
|
@@ -3246,8 +3954,11 @@ module Rigor
|
|
|
3246
3954
|
# sense above — position 0 is always a sound (conservative) choice; spec/rigor/inference/
|
|
3247
3955
|
# return_memo_taint_spec.rb pins this (no raw `BudgetTrace.hit` on the recursion categories outside
|
|
3248
3956
|
# the helper).
|
|
3249
|
-
|
|
3250
|
-
|
|
3957
|
+
TRANSIENT_EVENT_DEPTHS = TypingState.resource(
|
|
3958
|
+
:user_method_transient_event_depths,
|
|
3959
|
+
reason: "ADR-84 WD3 event log, appended in place and read as a delta"
|
|
3960
|
+
)
|
|
3961
|
+
private_constant :TRANSIENT_EVENT_DEPTHS
|
|
3251
3962
|
|
|
3252
3963
|
# Per-inference recursion context threaded through the guard / fixpoint helpers (ADR-55 slice 2).
|
|
3253
3964
|
# Bundles the call descriptor (`receiver`, `arg_types`, `plain_signature`), the thread-local summary
|
|
@@ -3255,7 +3966,7 @@ module Rigor
|
|
|
3255
3966
|
# carried separately (it is the body owner, not call context).
|
|
3256
3967
|
RecursionContext = Data.define(
|
|
3257
3968
|
:receiver, :arg_types, :plain_signature, :summaries, :would_have_been_guarded,
|
|
3258
|
-
:self_fold_safe
|
|
3969
|
+
:self_fold_safe, :positionals_unknown
|
|
3259
3970
|
)
|
|
3260
3971
|
private_constant :RecursionContext
|
|
3261
3972
|
|
|
@@ -3295,24 +4006,27 @@ module Rigor
|
|
|
3295
4006
|
# The return memo and the recursion guard do not key on them, which holds because such a node comes only from
|
|
3296
4007
|
# `DefNodeResolver.refinement_query`'s own parse and always carries that file's one list; a caller that types
|
|
3297
4008
|
# the same node under another list must add it to the key.
|
|
4009
|
+
#
|
|
4010
|
+
# Issue #1237 — `positionals_unknown:` re-types the body with no positional correspondence
|
|
4011
|
+
# ({#positional_correspondence}): `arg_types` is then the call's keyword shape alone. Such a frame reads and
|
|
4012
|
+
# stores the return memo under a key of its own ({#return_memo_key}), which the incremental session re-drives
|
|
4013
|
+
# with the same flag ({#return_type_for}), and installs no `...` forwarding list.
|
|
3298
4014
|
def infer_user_method_return(def_node, receiver, arg_types, self_fold_safe: false, yield_type: nil,
|
|
3299
|
-
refinements: nil)
|
|
4015
|
+
refinements: nil, positionals_unknown: false)
|
|
3300
4016
|
return nil if def_node.body.nil?
|
|
3301
|
-
unless KeyPresenceGuard.active?
|
|
3302
|
-
return infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type,
|
|
3303
|
-
refinements)
|
|
3304
|
-
end
|
|
3305
4017
|
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
4018
|
+
call = [def_node, receiver, arg_types, self_fold_safe, yield_type, refinements, positionals_unknown]
|
|
4019
|
+
return infer_user_method_return_unguarded(*call) unless KeyPresenceGuard.forcing_off?
|
|
4020
|
+
|
|
4021
|
+
KeyPresenceGuard.without_guards { infer_user_method_return_unguarded(*call) }
|
|
3309
4022
|
end
|
|
3310
4023
|
|
|
3311
4024
|
def infer_user_method_return_unguarded(def_node, receiver, arg_types, self_fold_safe, yield_type,
|
|
3312
|
-
refinements
|
|
4025
|
+
refinements, positionals_unknown)
|
|
3313
4026
|
yield_type = nil unless yield_type && body_yields?(def_node)
|
|
3314
4027
|
body_scope = build_user_method_body_scope(def_node, receiver, arg_types,
|
|
3315
|
-
self_fold_safe: self_fold_safe
|
|
4028
|
+
self_fold_safe: self_fold_safe,
|
|
4029
|
+
positionals_unknown: positionals_unknown)
|
|
3316
4030
|
return nil if body_scope.nil?
|
|
3317
4031
|
|
|
3318
4032
|
body_scope = body_scope.with_declared_refinements(refinements) if refinements
|
|
@@ -3325,8 +4039,7 @@ module Rigor
|
|
|
3325
4039
|
# resolving during the main walk. `describe(:short)` keeps non-Nominal receivers (the implicit
|
|
3326
4040
|
# `Object` carrier for top-level / DSL-block defs) printable.
|
|
3327
4041
|
plain_signature = [receiver.describe(:short), def_node.name]
|
|
3328
|
-
stack = (
|
|
3329
|
-
summaries = (Thread.current[INFERENCE_SUMMARY_KEY] ||= {})
|
|
4042
|
+
stack = typing_state.stack(INFERENCE_GUARD)
|
|
3330
4043
|
|
|
3331
4044
|
# WD0 (RIGOR_BUDGET_TRACE) — one entry into user-method return inference. No-op when disabled.
|
|
3332
4045
|
BudgetTrace.hit(BudgetTrace::MEMO_ENTRIES)
|
|
@@ -3349,11 +4062,12 @@ module Rigor
|
|
|
3349
4062
|
# below reads it back from here rather than taking a second parameter, so key and frame cannot
|
|
3350
4063
|
# disagree). It is installed even when nil, which is what stops a blockless callee reached from
|
|
3351
4064
|
# inside a yielding body from inheriting the outer caller's block.
|
|
3352
|
-
|
|
4065
|
+
forwarded = positionals_unknown ? nil : forwarded_frame_types(def_node, arg_types)
|
|
4066
|
+
ExpressionTyper.with_call_site_frame(yield_type, forwarded) do
|
|
3353
4067
|
unless memo_candidate?(stack, plain_signature)
|
|
3354
4068
|
trace_memo_refusal(stack, plain_signature)
|
|
3355
|
-
next compute_user_method_return(def_node, body_scope, stack,
|
|
3356
|
-
|
|
4069
|
+
next compute_user_method_return(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4070
|
+
positionals_unknown: positionals_unknown)
|
|
3357
4071
|
end
|
|
3358
4072
|
|
|
3359
4073
|
# INVARIANT (ADR-46 recording soundness, ADR-84 WD2) — the deep cross-file dependency edges (the
|
|
@@ -3370,8 +4084,8 @@ module Rigor
|
|
|
3370
4084
|
# (ActiveStorage video_analyzer.rb's subtree, 0.43s -> >90s; PR #79) and stays rejected. Pinned by
|
|
3371
4085
|
# spec/rigor/inference/return_memo_recording_spec.rb (cross-file replay completeness) and
|
|
3372
4086
|
# dependency_recorder_spec.rb's transitive deep-edge example.
|
|
3373
|
-
consult_and_store_return_memo(def_node, body_scope, stack,
|
|
3374
|
-
|
|
4087
|
+
consult_and_store_return_memo(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4088
|
+
positionals_unknown: positionals_unknown)
|
|
3375
4089
|
end
|
|
3376
4090
|
end
|
|
3377
4091
|
|
|
@@ -3392,9 +4106,15 @@ module Rigor
|
|
|
3392
4106
|
# replaced with the analysis generation cannot outlive the nodes it holds.
|
|
3393
4107
|
def body_yields?(def_node)
|
|
3394
4108
|
cache = class_graph_buckets[:yields]
|
|
3395
|
-
|
|
4109
|
+
if cache.key?(def_node)
|
|
4110
|
+
return MemoAudit.check("class graph yields", cache[def_node], def_node.name) do
|
|
4111
|
+
yield_reachable?(def_node.body, def_node.parameters&.block&.name)
|
|
4112
|
+
end
|
|
4113
|
+
end
|
|
3396
4114
|
|
|
3397
|
-
cache[def_node] =
|
|
4115
|
+
cache[def_node] = MemoAudit.key_audit("class graph yields") do
|
|
4116
|
+
yield_reachable?(def_node.body, def_node.parameters&.block&.name)
|
|
4117
|
+
end
|
|
3398
4118
|
end
|
|
3399
4119
|
|
|
3400
4120
|
def yield_reachable?(node, block_name)
|
|
@@ -3425,7 +4145,7 @@ module Rigor
|
|
|
3425
4145
|
# The candidate-frame memo path: consult the current run generation's bucket, and on a miss compute
|
|
3426
4146
|
# and store a FINAL result. Reached only when `memo_candidate?` held (see `infer_user_method_return`).
|
|
3427
4147
|
# The store gate (ADR-84 WD3): a result is stored when no transient-machinery event during the
|
|
3428
|
-
# bracket referenced a stack frame BELOW the bracket's entry depth (see
|
|
4148
|
+
# bracket referenced a stack frame BELOW the bracket's entry depth (see TRANSIENT_EVENT_DEPTHS).
|
|
3429
4149
|
# A separate ADR-55 fixpoint-consult bracket counter used to sit in front of this test; it is gone
|
|
3430
4150
|
# (issue #875) because the event log already subsumes it — the sole `consult_summary` call site is
|
|
3431
4151
|
# the in-cycle guard hit, which logs its event at the consulted owner's position first, so a consult
|
|
@@ -3439,8 +4159,11 @@ module Rigor
|
|
|
3439
4159
|
# is already `Dynamic[top]` is stored anyway — see `top_result?`. A hit under ADR-46 recording
|
|
3440
4160
|
# replays the entry's captured read-set into the current consumer (see the INVARIANT comment at the
|
|
3441
4161
|
# call site).
|
|
3442
|
-
|
|
3443
|
-
|
|
4162
|
+
#
|
|
4163
|
+
# ADR-129 WD3 — the ADR-55 summaries are not read here: a hit neither reads nor writes them, and a miss's
|
|
4164
|
+
# compute takes them ({#return_summaries}).
|
|
4165
|
+
def consult_and_store_return_memo(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4166
|
+
positionals_unknown: false)
|
|
3444
4167
|
per_def = (return_memo_bucket[def_node] ||= {})
|
|
3445
4168
|
# Issue #525 — the `:self` fold-safety grant is a THIRD call-site-varying dimension: the same
|
|
3446
4169
|
# `(receiver, arg_types)` body returns a folded member type when the caller's receiver expression
|
|
@@ -3451,11 +4174,8 @@ module Rigor
|
|
|
3451
4174
|
# passed in for the same reason: a yielding callee's return depends on the block THIS call site
|
|
3452
4175
|
# hands it, so two call sites passing differently-typed blocks must not share an entry. It is nil
|
|
3453
4176
|
# for every def that cannot reach a `yield`, which is where the memo's mass is.
|
|
3454
|
-
memo_key =
|
|
3455
|
-
|
|
3456
|
-
body_scope.struct_fold_safe?(:self),
|
|
3457
|
-
ExpressionTyper.current_yield_value_type&.describe(:short)]
|
|
3458
|
-
if (entry = per_def[memo_key])
|
|
4177
|
+
memo_key = return_memo_key(body_scope, receiver, arg_types, positionals_unknown: positionals_unknown)
|
|
4178
|
+
if (entry = recall_return_memo(per_def, def_node, memo_key))
|
|
3459
4179
|
BudgetTrace.hit(BudgetTrace::MEMO_HITS)
|
|
3460
4180
|
Analysis::DependencyRecorder.replay(entry.read_set) if Analysis::DependencyRecorder.active?
|
|
3461
4181
|
return entry.result
|
|
@@ -3465,20 +4185,65 @@ module Rigor
|
|
|
3465
4185
|
|
|
3466
4186
|
entry_depth = stack.size
|
|
3467
4187
|
event_mark = transient_event_mark
|
|
3468
|
-
result, read_set =
|
|
3469
|
-
|
|
4188
|
+
result, read_set = MemoAudit.key_audit("return memo", RETURN_MEMO_KEY_AUDIT, memo_key) do
|
|
4189
|
+
compute_with_read_capture(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4190
|
+
positionals_unknown)
|
|
4191
|
+
end
|
|
3470
4192
|
|
|
3471
4193
|
if context_tainted?(event_mark, entry_depth) && !top_result?(result)
|
|
3472
4194
|
BudgetTrace.hit(BudgetTrace::MEMO_REFUSE_TRANSIENT)
|
|
4195
|
+
per_def[RETURN_MEMO_REFUSED] = true
|
|
3473
4196
|
else
|
|
3474
|
-
per_def
|
|
3475
|
-
|
|
4197
|
+
store_return_memo(per_def, def_node, memo_key,
|
|
4198
|
+
MemoEntry.new(result: result, read_set: read_set, receiver: receiver, arg_types: arg_types,
|
|
4199
|
+
positionals_unknown: positionals_unknown))
|
|
3476
4200
|
end
|
|
3477
4201
|
result
|
|
3478
4202
|
end
|
|
3479
4203
|
|
|
4204
|
+
# The return memo's key: the call descriptor, the body scope's `:self` grant, and the yield-value Frame, read as a
|
|
4205
|
+
# key term (ADR-129 WD1's key audit). Issue #1237 — a frame with no positional correspondence keys one term more,
|
|
4206
|
+
# {POSITIONALS_UNKNOWN_KEY}, so it never shares an entry with a positional call whose list reads the same; every
|
|
4207
|
+
# other key is as it was.
|
|
4208
|
+
def return_memo_key(body_scope, receiver, arg_types, positionals_unknown: false)
|
|
4209
|
+
MemoAudit.key do
|
|
4210
|
+
key = [receiver.describe(:short),
|
|
4211
|
+
arg_types.map { |type| type.describe(:short) },
|
|
4212
|
+
body_scope.struct_fold_safe?(:self),
|
|
4213
|
+
typing_state.key_frame(YIELD_VALUE)&.describe(:short)]
|
|
4214
|
+
positionals_unknown ? key << POSITIONALS_UNKNOWN_KEY : key
|
|
4215
|
+
end
|
|
4216
|
+
end
|
|
4217
|
+
|
|
4218
|
+
POSITIONALS_UNKNOWN_KEY = :positionals_unknown
|
|
4219
|
+
private_constant :POSITIONALS_UNKNOWN_KEY
|
|
4220
|
+
|
|
4221
|
+
# The entry `per_def` holds for `memo_key`, or nil, journaled as a Table read (ADR-129 WD3).
|
|
4222
|
+
def recall_return_memo(per_def, def_node, memo_key)
|
|
4223
|
+
entry = per_def[memo_key]
|
|
4224
|
+
log_return_memo(:read, def_node, memo_key, entry || TypingState::ABSENT)
|
|
4225
|
+
entry
|
|
4226
|
+
end
|
|
4227
|
+
|
|
4228
|
+
# Stores `entry` for `memo_key`, journaled as a Table write (ADR-129 WD3), logged before the store so a priors
|
|
4229
|
+
# window notes the entry it replaces.
|
|
4230
|
+
def store_return_memo(per_def, def_node, memo_key, entry)
|
|
4231
|
+
log_return_memo(:write, def_node, memo_key, entry)
|
|
4232
|
+
per_def[memo_key] = entry
|
|
4233
|
+
end
|
|
4234
|
+
|
|
4235
|
+
# ADR-129 WD3 — journals a return-memo read or store under `[def_node, memo_key]`. The composite key is built
|
|
4236
|
+
# only while a capture is open, so a run with the `type_of` memo off allocates nothing here.
|
|
4237
|
+
def log_return_memo(kind, def_node, memo_key, value)
|
|
4238
|
+
state = typing_state
|
|
4239
|
+
return unless state.capturing?
|
|
4240
|
+
|
|
4241
|
+
key = [def_node, memo_key].freeze
|
|
4242
|
+
kind == :read ? state.log_read(RETURN_MEMO, key, value) : state.log_write(RETURN_MEMO, key, value)
|
|
4243
|
+
end
|
|
4244
|
+
|
|
3480
4245
|
def transient_event_mark
|
|
3481
|
-
|
|
4246
|
+
typing_state.read(TRANSIENT_EVENT_DEPTHS)&.size || 0
|
|
3482
4247
|
end
|
|
3483
4248
|
|
|
3484
4249
|
# ADR-84 WD3 — true when a transient-machinery event logged during the bracket (entries past
|
|
@@ -3490,7 +4255,7 @@ module Rigor
|
|
|
3490
4255
|
# outermost entry, so the suffix slice allocated an array per candidate compute — a term quadratic in
|
|
3491
4256
|
# the event count, and the `ary_ensure_room_for_push` / GC-sweep profile issue #870 sampled.
|
|
3492
4257
|
def context_tainted?(event_mark, entry_depth)
|
|
3493
|
-
log =
|
|
4258
|
+
log = typing_state.read(TRANSIENT_EVENT_DEPTHS)
|
|
3494
4259
|
return false if log.nil? || log.size <= event_mark
|
|
3495
4260
|
|
|
3496
4261
|
index = event_mark
|
|
@@ -3522,31 +4287,29 @@ module Rigor
|
|
|
3522
4287
|
# happen in practice is the byte-identical `--format json` runs over `lib`, `plugins/*/lib`,
|
|
3523
4288
|
# `examples/*/lib` and the survey projects recorded on issue #872.
|
|
3524
4289
|
def top_result?(result)
|
|
3525
|
-
|
|
4290
|
+
Type::Combinator.untyped?(result)
|
|
3526
4291
|
end
|
|
3527
4292
|
|
|
3528
4293
|
# ADR-84 WD2 — the memo-miss compute, wrapped in a `DependencyRecorder.capture` window when ADR-46
|
|
3529
4294
|
# recording is active so the entry can carry its replayable read-set. Returns `[result, read_set]`
|
|
3530
4295
|
# (`read_set` nil when not recording).
|
|
3531
|
-
def compute_with_read_capture(def_node, body_scope, stack,
|
|
3532
|
-
|
|
4296
|
+
def compute_with_read_capture(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4297
|
+
positionals_unknown)
|
|
4298
|
+
call = [def_node, body_scope, stack, receiver, arg_types, plain_signature]
|
|
3533
4299
|
unless Analysis::DependencyRecorder.active?
|
|
3534
|
-
return [compute_user_method_return(
|
|
3535
|
-
receiver, arg_types, plain_signature), nil]
|
|
4300
|
+
return [compute_user_method_return(*call, positionals_unknown: positionals_unknown), nil]
|
|
3536
4301
|
end
|
|
3537
4302
|
|
|
3538
|
-
Analysis::DependencyRecorder.capture
|
|
3539
|
-
compute_user_method_return(def_node, body_scope, stack, summaries,
|
|
3540
|
-
receiver, arg_types, plain_signature)
|
|
3541
|
-
end
|
|
4303
|
+
Analysis::DependencyRecorder.capture { compute_user_method_return(*call, positionals_unknown: positionals_unknown) }
|
|
3542
4304
|
end
|
|
3543
4305
|
|
|
3544
4306
|
# The ADR-55 recursion-guard + value-unroll + fixpoint body of user-method return inference, factored
|
|
3545
4307
|
# out so `infer_user_method_return` is a thin memo wrapper (the memo is the ADR-57 follow-up; this is
|
|
3546
4308
|
# unchanged from pre-memo behaviour).
|
|
3547
|
-
def compute_user_method_return(def_node, body_scope, stack,
|
|
3548
|
-
|
|
4309
|
+
def compute_user_method_return(def_node, body_scope, stack, receiver, arg_types, plain_signature,
|
|
4310
|
+
positionals_unknown: false)
|
|
3549
4311
|
trace_body_eval(plain_signature)
|
|
4312
|
+
summaries = return_summaries
|
|
3550
4313
|
# ADR-55 slice 1: when every bound argument is value-pinned, extend the guard key with a stable
|
|
3551
4314
|
# descriptor of the argument *values* so distinct constant frames may recurse (e.g. `factorial(5)`
|
|
3552
4315
|
# folds to `Constant[120]`). Distinct constant frames are bounded by `RECURSION_UNROLL_FUEL` per
|
|
@@ -3566,7 +4329,11 @@ module Rigor
|
|
|
3566
4329
|
# `bot`) instead of bare `untyped`. The fixpoint loop below seeds the entry on the outermost frame;
|
|
3567
4330
|
# if a re-entry beats it here the entry already exists. The WD4 composition: slice 1's clamp/fuel
|
|
3568
4331
|
# fallbacks also route here when a summary is active.
|
|
3569
|
-
|
|
4332
|
+
# Issue #3162 — the summary is read only where its frame covers the re-entry's own parameter
|
|
4333
|
+
# bindings and block. A value-keyed re-entry is tested like any other: the unroll frame it
|
|
4334
|
+
# re-enters holds its constants, but the summary it reads is the outermost frame's.
|
|
4335
|
+
floor = uncovered_reentry_floor(summaries[plain_signature], def_node, body_scope, positionals_unknown)
|
|
4336
|
+
return floor || consult_summary(summaries, plain_signature)
|
|
3570
4337
|
end
|
|
3571
4338
|
|
|
3572
4339
|
# ADR-55 WD1 clamp (governing rule): the constant-arg unroll may only ever surface a fully
|
|
@@ -3586,11 +4353,16 @@ module Rigor
|
|
|
3586
4353
|
# Issue #525 — read off the built scope so the bot-collapse retry rebuilds a widened scope with
|
|
3587
4354
|
# the SAME `:self` polarity the first attempt used; a retry that silently dropped the grant would
|
|
3588
4355
|
# return a different type for the same call.
|
|
3589
|
-
self_fold_safe: body_scope.struct_fold_safe?(:self)
|
|
4356
|
+
self_fold_safe: body_scope.struct_fold_safe?(:self),
|
|
4357
|
+
positionals_unknown: positionals_unknown
|
|
3590
4358
|
)
|
|
3591
4359
|
evaluate_guarded_user_method_body(def_node, body_scope, stack, signature, context)
|
|
3592
4360
|
end
|
|
3593
4361
|
|
|
4362
|
+
# ADR-55 — the thread's in-flight fixpoint summaries, created on first use. Taken only by a compute, never on a
|
|
4363
|
+
# return-memo hit (ADR-129 WD3), and cleared when the guard stack drains.
|
|
4364
|
+
def return_summaries = typing_state.acquire(INFERENCE_SUMMARY) { {} }
|
|
4365
|
+
|
|
3594
4366
|
# Issue #1125 — the argument list a `def m(...)` body's `...` re-supplies, or nil for a def without the
|
|
3595
4367
|
# forwarding parameter (whose body cannot contain a `...` call at all) and for a call that does not even
|
|
3596
4368
|
# satisfy `m`'s own required positionals (that call raises at runtime; the body is not worth re-typing).
|
|
@@ -3609,8 +4381,9 @@ module Rigor
|
|
|
3609
4381
|
kw_shape = positional.pop if takes_keywords?(params) && positional.last.is_a?(Type::HashShape)
|
|
3610
4382
|
return nil if positional.size < params.requireds.size
|
|
3611
4383
|
|
|
4384
|
+
# Frozen: a Frame value does not change while it is installed (ADR-129 WD1).
|
|
3612
4385
|
tail = positional[(params.requireds.size + params.optionals.size)..] || []
|
|
3613
|
-
kw_shape ? tail + [kw_shape] : tail
|
|
4386
|
+
(kw_shape ? tail + [kw_shape] : tail).freeze
|
|
3614
4387
|
end
|
|
3615
4388
|
|
|
3616
4389
|
# True when this frame's result is a candidate for the return memo: the one structural precondition,
|
|
@@ -3631,13 +4404,14 @@ module Rigor
|
|
|
3631
4404
|
# `def_nodes` table identity for runner-less scopes (single-file probes) — the pre-WD2 per-file
|
|
3632
4405
|
# scope. A generation mismatch drops the previous bucket whole, so a re-run in one process (LSP,
|
|
3633
4406
|
# ADR-62 warm loop) can neither hit stale entries nor accumulate them. The bucket maps `def_node`
|
|
3634
|
-
# (object identity, held strongly — see
|
|
4407
|
+
# (object identity, held strongly — see RETURN_MEMO) to its per-descriptor entries.
|
|
3635
4408
|
def return_memo_bucket
|
|
3636
4409
|
generation = scope.run_generation || scope.discovered_def_nodes
|
|
3637
|
-
|
|
4410
|
+
state = typing_state
|
|
4411
|
+
slot = state[RETURN_MEMO]
|
|
3638
4412
|
unless slot && slot[0].equal?(generation)
|
|
3639
4413
|
slot = [generation, {}.compare_by_identity]
|
|
3640
|
-
|
|
4414
|
+
state[RETURN_MEMO] = slot
|
|
3641
4415
|
end
|
|
3642
4416
|
slot[1]
|
|
3643
4417
|
end
|
|
@@ -3687,10 +4461,10 @@ module Rigor
|
|
|
3687
4461
|
# referenced frame's stack position to the load-bearing thread-local event log (regardless of
|
|
3688
4462
|
# RIGOR_BUDGET_TRACE) and forwards the category to `BudgetTrace`. The taint spec pins that no
|
|
3689
4463
|
# recursion-machinery `BudgetTrace.hit` exists outside this helper, so a future fallback cannot
|
|
3690
|
-
# silently join unlogged (see the audit table at
|
|
4464
|
+
# silently join unlogged (see the audit table at TRANSIENT_EVENT_DEPTHS; `context_depth` 0 is
|
|
3691
4465
|
# always a sound conservative choice).
|
|
3692
4466
|
def note_transient_fallback(category, context_depth)
|
|
3693
|
-
(
|
|
4467
|
+
typing_state.append(TRANSIENT_EVENT_DEPTHS, context_depth)
|
|
3694
4468
|
BudgetTrace.hit(category)
|
|
3695
4469
|
end
|
|
3696
4470
|
|
|
@@ -3702,25 +4476,53 @@ module Rigor
|
|
|
3702
4476
|
# The outermost frame for this plain signature owns the summary entry and runs the fixpoint loop.
|
|
3703
4477
|
# ADR-55 WD2.
|
|
3704
4478
|
outermost = stack.none? { |frame| plain_part(frame) == context.plain_signature }
|
|
3705
|
-
stack
|
|
4479
|
+
first_unpinned = first_unpinned_frame?(stack, signature, context)
|
|
4480
|
+
state = typing_state
|
|
4481
|
+
state.push(INFERENCE_GUARD, signature)
|
|
3706
4482
|
begin
|
|
3707
4483
|
if outermost
|
|
3708
4484
|
fixpoint_user_method_return(def_node, body_scope, context)
|
|
4485
|
+
elsif first_unpinned
|
|
4486
|
+
nested_fixpoint_user_method_return(def_node, body_scope, context)
|
|
3709
4487
|
else
|
|
3710
4488
|
type, = evaluate_body_with_returns(body_scope, def_node.body)
|
|
3711
4489
|
clamp_unroll_result(type, context.would_have_been_guarded)
|
|
3712
4490
|
end
|
|
3713
4491
|
ensure
|
|
3714
|
-
|
|
4492
|
+
state.pop(INFERENCE_GUARD)
|
|
3715
4493
|
if stack.empty?
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
4494
|
+
state.write(INFERENCE_UNROLL_FUEL, nil)
|
|
4495
|
+
state.write(INFERENCE_SUMMARY, nil)
|
|
4496
|
+
state.write(SUMMARY_CONSULT_DEPTHS, nil)
|
|
4497
|
+
state.write(TRANSIENT_EVENT_DEPTHS, nil)
|
|
3720
4498
|
end
|
|
3721
4499
|
end
|
|
3722
4500
|
end
|
|
3723
4501
|
|
|
4502
|
+
# Issue #3162 — whether this frame is the first of its signature with an unpinned argument: every
|
|
4503
|
+
# frame of the signature below it is a constant-argument unroll frame, and the recursion has now
|
|
4504
|
+
# passed something that is no constant (`rec(k - 1, *rest)` out of `rec(3, 1)`). The outermost
|
|
4505
|
+
# frame's summary is the return of a body typed for its constants, which does not cover this
|
|
4506
|
+
# frame's re-entries, so this frame owns a summary of its own. A frame that is all constants but
|
|
4507
|
+
# out of unroll fuel is not one: its re-entries keep reading the outermost frame's summary, whose
|
|
4508
|
+
# `bot` sends the outermost frame to the widened retry. Asked before the frame is pushed.
|
|
4509
|
+
def first_unpinned_frame?(stack, signature, context)
|
|
4510
|
+
plain_signature = context.plain_signature
|
|
4511
|
+
signature.equal?(plain_signature) && stack.none?(plain_signature) &&
|
|
4512
|
+
constant_argument_value_key(context.arg_types).nil?
|
|
4513
|
+
end
|
|
4514
|
+
|
|
4515
|
+
# The fixpoint of a {#first_unpinned_frame?} frame, run over its own summary entry; the outermost
|
|
4516
|
+
# frame's entry is put back afterwards.
|
|
4517
|
+
def nested_fixpoint_user_method_return(def_node, body_scope, context)
|
|
4518
|
+
summaries = context.summaries
|
|
4519
|
+
outer = summaries[context.plain_signature]
|
|
4520
|
+
fixpoint_user_method_return(def_node, body_scope, context)
|
|
4521
|
+
ensure
|
|
4522
|
+
summaries[context.plain_signature] = outer
|
|
4523
|
+
LoopValueRecord.advance_generation
|
|
4524
|
+
end
|
|
4525
|
+
|
|
3724
4526
|
# Evaluates a method body and joins the value types of every explicit `return value` reached during
|
|
3725
4527
|
# the walk with the body's tail type.
|
|
3726
4528
|
#
|
|
@@ -3749,14 +4551,11 @@ module Rigor
|
|
|
3749
4551
|
def fixpoint_user_method_return(def_node, body_scope, context, widened: false)
|
|
3750
4552
|
plain_signature = context.plain_signature
|
|
3751
4553
|
summaries = context.summaries
|
|
3752
|
-
depth = seed_fixpoint_summary(summaries, plain_signature)
|
|
3753
|
-
consult_depths = (Thread.current[SUMMARY_CONSULT_DEPTHS_KEY] ||= [])
|
|
4554
|
+
depth = seed_fixpoint_summary(summaries, plain_signature, body_scope, context.receiver)
|
|
3754
4555
|
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
type, = evaluate_body_with_returns(body_scope, def_node.body)
|
|
3759
|
-
computed = clamp_unroll_result(type, context.would_have_been_guarded)
|
|
4556
|
+
iteration = 0
|
|
4557
|
+
while iteration < RECURSION_FIXPOINT_CAP
|
|
4558
|
+
computed, entangled = evaluate_fixpoint_iterate(def_node, body_scope, context, depth)
|
|
3760
4559
|
|
|
3761
4560
|
# Cross-signature mutual recursion (ADR-55 soundness fix, 2026-06-12): the evaluation consulted an
|
|
3762
4561
|
# ANCESTOR signature's in-flight summary (seed depth shallower than this frame's), so `computed`
|
|
@@ -3764,9 +4563,7 @@ module Rigor
|
|
|
3764
4563
|
# `Constant[false]`. The per-signature iteration below cannot converge such an entangled pair (each
|
|
3765
4564
|
# side's iterate is conditioned on the other's unfinished assumption), so degrade this frame to the
|
|
3766
4565
|
# sound `untyped` floor instead of surfacing a one-sided value.
|
|
3767
|
-
|
|
3768
|
-
return degrade_entangled_fixpoint(summaries, plain_signature)
|
|
3769
|
-
end
|
|
4566
|
+
return degrade_entangled_fixpoint(summaries, plain_signature) if entangled
|
|
3770
4567
|
|
|
3771
4568
|
# The summary was never consulted — the method did not recurse on this evaluation, so there is no
|
|
3772
4569
|
# fixpoint to chase. Return the computed type directly (pre-fixpoint behaviour for non-recursive
|
|
@@ -3784,26 +4581,153 @@ module Rigor
|
|
|
3784
4581
|
return resolved unless resolved.nil?
|
|
3785
4582
|
end
|
|
3786
4583
|
|
|
4584
|
+
# Issue #3162 — a re-entry read the summary for arguments this frame's parameters do not cover
|
|
4585
|
+
# (`acc(n - 1, k + 1)` against `k = 1`), so `computed` holds this frame's base case as that
|
|
4586
|
+
# call's answer. The evaluation is discarded and the body typed again with such a re-entry
|
|
4587
|
+
# answering `Dynamic[top]`; it is not a Kleene step.
|
|
4588
|
+
next if floored_uncovered_reentries?(summaries[plain_signature])
|
|
4589
|
+
|
|
3787
4590
|
converged = fixpoint_step(summaries, plain_signature, computed, iteration)
|
|
3788
4591
|
return converged if converged
|
|
4592
|
+
|
|
4593
|
+
iteration += 1
|
|
3789
4594
|
end
|
|
3790
4595
|
|
|
3791
4596
|
# Out of iterations and still unstable. The collapse is the loop's own tail rather than a branch
|
|
3792
|
-
# inside `fixpoint_step`:
|
|
3793
|
-
# tail-less loop reads as a path that returns an Integer summary (the `def.return-type-mismatch`
|
|
3794
|
-
# on `return_type_for` this shape retired), and the loop, not the step, owns its cap.
|
|
4597
|
+
# inside `fixpoint_step`: the loop, not the step, owns its cap.
|
|
3795
4598
|
collapse_fixpoint_cap(summaries, plain_signature)
|
|
3796
4599
|
end
|
|
3797
4600
|
|
|
4601
|
+
# One body evaluation of the fixpoint loop, as `[computed, entangled]`: the clamped return, and
|
|
4602
|
+
# whether the evaluation consulted an ancestor signature's in-flight summary (one seeded shallower
|
|
4603
|
+
# than `depth`). Clears the summary's per-evaluation record first.
|
|
4604
|
+
def evaluate_fixpoint_iterate(def_node, body_scope, context, depth)
|
|
4605
|
+
entry = context.summaries[context.plain_signature]
|
|
4606
|
+
entry[:consulted] = false
|
|
4607
|
+
entry[:uncovered] = false
|
|
4608
|
+
consult_depths = typing_state.acquire(SUMMARY_CONSULT_DEPTHS) { [] }
|
|
4609
|
+
consult_mark = consult_depths.size
|
|
4610
|
+
type, = evaluate_body_with_returns(body_scope, def_node.body)
|
|
4611
|
+
computed = clamp_unroll_result(type, context.would_have_been_guarded)
|
|
4612
|
+
[computed, consult_depths[consult_mark..].any? { |consulted| consulted < depth }]
|
|
4613
|
+
end
|
|
4614
|
+
|
|
4615
|
+
# Switches the summary to its floor after an evaluation in which an uncovered re-entry read it, and
|
|
4616
|
+
# says whether it did. Once on the floor such a re-entry never reads the summary again, so the
|
|
4617
|
+
# switch happens at most once per fixpoint.
|
|
4618
|
+
#
|
|
4619
|
+
# A `bot` iterate has been to the widened retry first. When that retry found no exit either
|
|
4620
|
+
# (`spin`), the entry here is the retry's own, which noted nothing, and the answer stays `bot`.
|
|
4621
|
+
def floored_uncovered_reentries?(entry)
|
|
4622
|
+
return false unless entry[:uncovered] && !entry[:floor]
|
|
4623
|
+
|
|
4624
|
+
entry[:floor] = true
|
|
4625
|
+
LoopValueRecord.advance_generation
|
|
4626
|
+
true
|
|
4627
|
+
end
|
|
4628
|
+
|
|
4629
|
+
# Issue #3162 — the answer of a re-entry the summary's frame does not cover, or nil for one that
|
|
4630
|
+
# reads the summary. The uncovered re-entry is noted on the entry; until the fixpoint owner has
|
|
4631
|
+
# switched the summary to its floor ({#floored_uncovered_reentries?}) it still reads the summary,
|
|
4632
|
+
# so a body whose iterate is `bot` reaches the widened retry as before. On the floor it answers
|
|
4633
|
+
# `untyped`, the pre-slice-2 observable for an in-cycle call.
|
|
4634
|
+
def uncovered_reentry_floor(entry, def_node, body_scope, positionals_unknown)
|
|
4635
|
+
return nil if entry.nil?
|
|
4636
|
+
return nil if summary_covers_reentry?(entry, def_node, body_scope.locals, positionals_unknown)
|
|
4637
|
+
|
|
4638
|
+
entry[:uncovered] = true
|
|
4639
|
+
return nil unless entry[:floor]
|
|
4640
|
+
|
|
4641
|
+
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
4642
|
+
Type::Combinator.untyped
|
|
4643
|
+
end
|
|
4644
|
+
|
|
4645
|
+
# Issue #3162 — whether the summary's frame covers a re-entry's parameter bindings (`locals`, as the
|
|
4646
|
+
# guard site built them): every parameter of the frame holds a type that includes what the re-entry
|
|
4647
|
+
# bound it to. The summary is the return of the frame's body under the frame's bindings, so it is
|
|
4648
|
+
# the re-entry's return only then; a parameter passed through unchanged is the same type object.
|
|
4649
|
+
#
|
|
4650
|
+
# A re-entry through a call-site `*splat` has no positional correspondence, and its binder holds
|
|
4651
|
+
# `Dynamic[top]` for every positional. There the def's own calls are read instead
|
|
4652
|
+
# ({RecursionRebinding}): a positional every one of them passes through unchanged (`*rest` as
|
|
4653
|
+
# `*rest`) is covered, and any other is not.
|
|
4654
|
+
#
|
|
4655
|
+
# The block is part of the frame too: `yield` answers the frame's block ({#reentry_block_covered?}).
|
|
4656
|
+
def summary_covers_reentry?(entry, def_node, locals, positionals_unknown)
|
|
4657
|
+
return false unless reentry_block_covered?(entry, def_node)
|
|
4658
|
+
|
|
4659
|
+
kept = (entry[:kept] ||= kept_positional_parameters(def_node, entry[:receiver])) if positionals_unknown
|
|
4660
|
+
frame = entry[:locals]
|
|
4661
|
+
locals.all? do |name, argument|
|
|
4662
|
+
bound = frame[name]
|
|
4663
|
+
bound.nil? || kept&.include?(name) || binding_covers?(bound, argument)
|
|
4664
|
+
end
|
|
4665
|
+
end
|
|
4666
|
+
|
|
4667
|
+
# Whether the re-entry runs with a block the summary's frame covers. A re-entry with no block, or
|
|
4668
|
+
# one that passes the def's own block parameter on (`&blk`, or the anonymous `&`), does; `&blk` is
|
|
4669
|
+
# the frame's block only while the body never assigns `blk` and no block or lambda in it declares
|
|
4670
|
+
# the name ({RecursionRebinding}'s write and block-local scan, as for a positional). One that
|
|
4671
|
+
# passes another `&expression` does not. A block literal does when the value it yields is covered
|
|
4672
|
+
# by the value the frame's block yields, which is nil on both sides for a def that never `yield`s.
|
|
4673
|
+
# The re-entry's call is the node being typed; with none to read, only the yield values are compared.
|
|
4674
|
+
def reentry_block_covered?(entry, def_node)
|
|
4675
|
+
block = @typing_node.block if @typing_node.is_a?(Prism::CallNode)
|
|
4676
|
+
return true if @typing_node.is_a?(Prism::CallNode) && block.nil?
|
|
4677
|
+
return own_block_passed?(def_node, block) if block.is_a?(Prism::BlockArgumentNode)
|
|
4678
|
+
|
|
4679
|
+
yielded = ExpressionTyper.current_yield_value_type
|
|
4680
|
+
return true if yielded.nil?
|
|
4681
|
+
|
|
4682
|
+
frame_yield = entry[:yield]
|
|
4683
|
+
!frame_yield.nil? && binding_covers?(frame_yield, yielded)
|
|
4684
|
+
end
|
|
4685
|
+
|
|
4686
|
+
def own_block_passed?(def_node, block)
|
|
4687
|
+
params = def_node.parameters
|
|
4688
|
+
own = params.block if params.is_a?(Prism::ParametersNode)
|
|
4689
|
+
return false if own.nil?
|
|
4690
|
+
|
|
4691
|
+
expression = block.expression
|
|
4692
|
+
return own.name.nil? if expression.nil?
|
|
4693
|
+
return false unless expression.is_a?(Prism::LocalVariableReadNode) && expression.name == own.name
|
|
4694
|
+
|
|
4695
|
+
written = RecursionRebinding.rebound_parameters(def_node, Set.new)
|
|
4696
|
+
written != RecursionRebinding::ALL && !written.include?(own.name)
|
|
4697
|
+
end
|
|
4698
|
+
|
|
4699
|
+
# `2` is covered by `Integer` though their union is spelled `2 | Integer`, so the argument is also
|
|
4700
|
+
# tried with its value pins widened.
|
|
4701
|
+
def binding_covers?(bound, argument)
|
|
4702
|
+
return true if argument.equal?(bound) || Type::Combinator.untyped?(bound) || argument == bound
|
|
4703
|
+
return false if Type::Combinator.untyped?(argument)
|
|
4704
|
+
return true if Type::Combinator.union(bound, argument) == bound
|
|
4705
|
+
|
|
4706
|
+
Type::Combinator.union(bound, widen_bound_argument(argument)) == bound
|
|
4707
|
+
end
|
|
4708
|
+
|
|
4709
|
+
def kept_positional_parameters(def_node, receiver)
|
|
4710
|
+
params = def_node.parameters
|
|
4711
|
+
rebound = recursion_rebound_parameters(def_node, receiver)
|
|
4712
|
+
return [] if rebound == RecursionRebinding::ALL || !params.is_a?(Prism::ParametersNode)
|
|
4713
|
+
|
|
4714
|
+
positionals = params.requireds + params.optionals + params.posts + [params.rest]
|
|
4715
|
+
positionals.filter_map { |param| param.name if param.respond_to?(:name) && param.name } - rebound.to_a
|
|
4716
|
+
end
|
|
4717
|
+
|
|
3798
4718
|
# Seeds the thread-local summary entry for a fixpoint owner: the `bot` Kleene seed plus the
|
|
3799
4719
|
# guard-stack depth at seed time (the frame for this signature is already pushed), which
|
|
3800
4720
|
# `consult_summary` logs so nested fixpoints can detect a foreign in-flight (ancestor) consult.
|
|
3801
4721
|
# Returns the seed depth. ADR-55 slice 2.
|
|
3802
|
-
def seed_fixpoint_summary(summaries, plain_signature)
|
|
3803
|
-
depth =
|
|
4722
|
+
def seed_fixpoint_summary(summaries, plain_signature, body_scope, receiver)
|
|
4723
|
+
depth = typing_state[INFERENCE_GUARD]&.size || 0
|
|
3804
4724
|
summaries[plain_signature] = {
|
|
3805
|
-
assumption: Type::Combinator.bot, consulted: false, depth: depth
|
|
4725
|
+
assumption: Type::Combinator.bot, consulted: false, depth: depth,
|
|
4726
|
+
# Issue #3162 — the frame the summary is the return of, for {#summary_covers_reentry?}.
|
|
4727
|
+
locals: body_scope.locals, yield: ExpressionTyper.current_yield_value_type, receiver: receiver,
|
|
4728
|
+
uncovered: false, floor: false
|
|
3806
4729
|
}
|
|
4730
|
+
LoopValueRecord.advance_generation
|
|
3807
4731
|
depth
|
|
3808
4732
|
end
|
|
3809
4733
|
|
|
@@ -3816,13 +4740,14 @@ module Rigor
|
|
|
3816
4740
|
note_transient_fallback(BudgetTrace::RECURSION_GUARD, own_guard_frame_position)
|
|
3817
4741
|
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
3818
4742
|
summaries[plain_signature][:assumption] = Type::Combinator.untyped
|
|
4743
|
+
LoopValueRecord.advance_generation
|
|
3819
4744
|
Type::Combinator.untyped
|
|
3820
4745
|
end
|
|
3821
4746
|
|
|
3822
4747
|
# The stack position of the currently-evaluating owner frame (the guard stack's top) — the
|
|
3823
4748
|
# self-referential `context_depth` for events that are deterministic per owner (ADR-84 WD3).
|
|
3824
4749
|
def own_guard_frame_position
|
|
3825
|
-
size =
|
|
4750
|
+
size = typing_state[INFERENCE_GUARD]&.size || 0
|
|
3826
4751
|
size.positive? ? size - 1 : 0
|
|
3827
4752
|
end
|
|
3828
4753
|
|
|
@@ -3843,6 +4768,7 @@ module Rigor
|
|
|
3843
4768
|
return candidate if joined == assumption
|
|
3844
4769
|
|
|
3845
4770
|
summaries[plain_signature][:assumption] = joined
|
|
4771
|
+
LoopValueRecord.advance_generation
|
|
3846
4772
|
nil
|
|
3847
4773
|
end
|
|
3848
4774
|
|
|
@@ -3854,17 +4780,67 @@ module Rigor
|
|
|
3854
4780
|
note_transient_fallback(BudgetTrace::RECURSION_FIXPOINT_CAP, own_guard_frame_position)
|
|
3855
4781
|
scope.record_dynamic_origin(@typing_node, DynamicOrigin::ANALYZER_BUDGET_CUTOFF) if @typing_node
|
|
3856
4782
|
summaries[plain_signature][:assumption] = Type::Combinator.untyped
|
|
4783
|
+
LoopValueRecord.advance_generation
|
|
3857
4784
|
Type::Combinator.untyped
|
|
3858
4785
|
end
|
|
3859
4786
|
|
|
3860
4787
|
# Rebuilds the user-method body scope with every bound positional parameter widened to its nominal
|
|
3861
4788
|
# base (`1 | 2 | 3` → `Integer`, `Constant[:x]` → `Symbol`). Used by the bot-collapse retry in
|
|
3862
4789
|
# `fixpoint_user_method_return`: call-site argument narrowing can prune a recursive method's base
|
|
3863
|
-
# case, and widening restores the declared-type view under which the base case is reachable.
|
|
3864
|
-
#
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
4790
|
+
# case, and widening restores the declared-type view under which the base case is reachable. A
|
|
4791
|
+
# parameter the def's own recursive calls rebind is bound `Dynamic[top]` instead ({RecursionRebinding}).
|
|
4792
|
+
# Returns `nil` when the parameter shape is not inferable (mirrors `build_user_method_body_scope`).
|
|
4793
|
+
def widened_user_method_body_scope(def_node, receiver, arg_types, self_fold_safe: false,
|
|
4794
|
+
positionals_unknown: false)
|
|
4795
|
+
widened_args = arg_types.map { |arg_type| widen_bound_argument(arg_type) }
|
|
4796
|
+
build_user_method_body_scope(def_node, receiver, widened_args,
|
|
4797
|
+
self_fold_safe: self_fold_safe, positionals_unknown: positionals_unknown,
|
|
4798
|
+
rebound: recursion_rebound_parameters(def_node, receiver))
|
|
4799
|
+
end
|
|
4800
|
+
|
|
4801
|
+
# Issue #3115 — {RecursionRebinding.rebound_parameters} for `def_node`, told the def's other names: a name
|
|
4802
|
+
# the body calls is an alias when `receiver`'s class resolves it to this same def (`alias al2 al1` makes
|
|
4803
|
+
# `al2(n - 1, !f)` a recursive call). A receiver whose class cannot be asked rebinds every parameter.
|
|
4804
|
+
def recursion_rebound_parameters(def_node, receiver)
|
|
4805
|
+
names = Set[def_node.name]
|
|
4806
|
+
RecursionRebinding.called_names(def_node.body).each do |name|
|
|
4807
|
+
next if name == def_node.name
|
|
4808
|
+
|
|
4809
|
+
resolved = recursion_def_for(receiver, name)
|
|
4810
|
+
return RecursionRebinding::ALL if resolved == :unknown
|
|
4811
|
+
|
|
4812
|
+
names << name if resolved.equal?(def_node)
|
|
4813
|
+
end
|
|
4814
|
+
RecursionRebinding.rebound_parameters(def_node, names)
|
|
4815
|
+
end
|
|
4816
|
+
|
|
4817
|
+
def recursion_def_for(receiver, method_name)
|
|
4818
|
+
return scope.singleton_def_for(receiver.class_name, method_name) if receiver.is_a?(Type::Singleton)
|
|
4819
|
+
return :unknown unless user_inference_receiver?(receiver)
|
|
4820
|
+
|
|
4821
|
+
resolve_user_def_through_ancestors(receiver.class_name, method_name)
|
|
4822
|
+
end
|
|
4823
|
+
|
|
4824
|
+
# Issue #3115 — one argument of the widened retry. A keyword argument reaches the binder inside the call's
|
|
4825
|
+
# trailing `HashShape`, which {#widen_value_pinned} leaves as it is (a shape is not a value pin), so a
|
|
4826
|
+
# keyword parameter kept its call-site pin through the retry and the base case stayed pruned: the body
|
|
4827
|
+
# then converged on the `bot` seed. A shape carrier keeps its keys, policy and arity, and the values it
|
|
4828
|
+
# holds widen at every depth: a `HashShape`'s values, a `Tuple`'s elements and a `Union`'s members.
|
|
4829
|
+
def widen_bound_argument(arg_type)
|
|
4830
|
+
case arg_type
|
|
4831
|
+
when Type::HashShape
|
|
4832
|
+
Type::HashShape.new(
|
|
4833
|
+
arg_type.pairs.transform_values { |value| widen_bound_argument(value) },
|
|
4834
|
+
required_keys: arg_type.required_keys, optional_keys: arg_type.optional_keys,
|
|
4835
|
+
read_only_keys: arg_type.read_only_keys, extra_keys: arg_type.extra_keys
|
|
4836
|
+
)
|
|
4837
|
+
when Type::Tuple
|
|
4838
|
+
Type::Combinator.tuple_of(*arg_type.elements.map { |element| widen_bound_argument(element) })
|
|
4839
|
+
when Type::Union
|
|
4840
|
+
Type::Combinator.union(*arg_type.members.map { |member| widen_bound_argument(member) })
|
|
4841
|
+
else
|
|
4842
|
+
widen_value_pinned(arg_type)
|
|
4843
|
+
end
|
|
3868
4844
|
end
|
|
3869
4845
|
|
|
3870
4846
|
# ADR-55 slice 2 bot-collapse resolution (2026-06-11). Called when a fixpoint iteration computed `bot`
|
|
@@ -3885,7 +4861,8 @@ module Rigor
|
|
|
3885
4861
|
def resolve_bot_collapse(def_node, context, widened:)
|
|
3886
4862
|
unless widened
|
|
3887
4863
|
widened_scope = widened_user_method_body_scope(def_node, context.receiver, context.arg_types,
|
|
3888
|
-
self_fold_safe: context.self_fold_safe
|
|
4864
|
+
self_fold_safe: context.self_fold_safe,
|
|
4865
|
+
positionals_unknown: context.positionals_unknown)
|
|
3889
4866
|
return fixpoint_user_method_return(def_node, widened_scope, context, widened: true) unless widened_scope.nil?
|
|
3890
4867
|
end
|
|
3891
4868
|
|
|
@@ -3927,7 +4904,7 @@ module Rigor
|
|
|
3927
4904
|
return Type::Combinator.untyped if entry.nil?
|
|
3928
4905
|
|
|
3929
4906
|
entry[:consulted] = true
|
|
3930
|
-
(
|
|
4907
|
+
typing_state.append(SUMMARY_CONSULT_DEPTHS, entry[:depth])
|
|
3931
4908
|
entry[:assumption]
|
|
3932
4909
|
end
|
|
3933
4910
|
|
|
@@ -3966,10 +4943,11 @@ module Rigor
|
|
|
3966
4943
|
# drains back to empty. On exhaustion (return 0) it records a `RECURSION_UNROLL_FUEL` hit so the caller
|
|
3967
4944
|
# keeps the plain `(receiver, method)` signature — today's behaviour.
|
|
3968
4945
|
def unroll_fuel_remaining(stack)
|
|
3969
|
-
|
|
4946
|
+
state = typing_state
|
|
4947
|
+
remaining = state.read(INFERENCE_UNROLL_FUEL)
|
|
3970
4948
|
remaining = RECURSION_UNROLL_FUEL if remaining.nil? || stack.empty?
|
|
3971
4949
|
if remaining.positive?
|
|
3972
|
-
|
|
4950
|
+
state.write(INFERENCE_UNROLL_FUEL, remaining - 1)
|
|
3973
4951
|
else
|
|
3974
4952
|
# ADR-84 WD3 — position 0: fuel is one shared resource seeded at the stack bottom, so its
|
|
3975
4953
|
# exhaustion is context-dependent for every nested bracket.
|
|
@@ -4018,9 +4996,28 @@ module Rigor
|
|
|
4018
4996
|
total += descriptor.last
|
|
4019
4997
|
end
|
|
4020
4998
|
[["t", parts], total]
|
|
4999
|
+
when Type::HashShape then pinned_shape_descriptor(arg)
|
|
4021
5000
|
end
|
|
4022
5001
|
end
|
|
4023
5002
|
|
|
5003
|
+
# Issue #3115 — a call's keyword arguments arrive as one closed `HashShape`, so `m(n: 2)` is as
|
|
5004
|
+
# value-pinned as `m(2)` when every entry is. An open shape, or one with an optional key, does not name
|
|
5005
|
+
# one argument list and stays on the type-keyed path.
|
|
5006
|
+
def pinned_shape_descriptor(shape)
|
|
5007
|
+
return nil unless shape.closed? && shape.optional_keys.empty?
|
|
5008
|
+
|
|
5009
|
+
parts = []
|
|
5010
|
+
total = 1
|
|
5011
|
+
shape.pairs.each do |key, value|
|
|
5012
|
+
descriptor = pinned_value_descriptor(value)
|
|
5013
|
+
return nil if descriptor.nil?
|
|
5014
|
+
|
|
5015
|
+
parts << [key.inspect, descriptor.first]
|
|
5016
|
+
total += descriptor.last
|
|
5017
|
+
end
|
|
5018
|
+
[["h", parts], total]
|
|
5019
|
+
end
|
|
5020
|
+
|
|
4024
5021
|
# Builds the body scope for a user-defined instance method call: a fresh `Scope` with `self_type` set
|
|
4025
5022
|
# to the receiver's nominal type, the project-wide accumulators inherited (so the body sees the same
|
|
4026
5023
|
# `discovered_classes` / `class_ivars` / etc. the caller does), and the call's `arg_types` bound to
|
|
@@ -4042,11 +5039,25 @@ module Rigor
|
|
|
4042
5039
|
# through it. Record a per-file write kind in the census instead, as {ScopeIndexer::CONSTANT_MEMO} does
|
|
4043
5040
|
# (#1276). Setting `source_path` here was rejected: plugins read it, and the return memo would become
|
|
4044
5041
|
# caller-dependent.
|
|
4045
|
-
|
|
5042
|
+
#
|
|
5043
|
+
# Issue #3115 — `rebound:` is the widened retry's set ({#widened_user_method_body_scope}): the parameters
|
|
5044
|
+
# the def's own recursive calls rebind ({RecursionRebinding}), each bound `Dynamic[top]` whether the call
|
|
5045
|
+
# wrote the slot or a default filled it. The retry types the body for every call the recursion makes,
|
|
5046
|
+
# and such a slot holds whatever a recursive call passes (`tog(n: n - 1, f: !f)` against `f: false`).
|
|
5047
|
+
def build_user_method_body_scope(def_node, receiver, arg_types, self_fold_safe: false,
|
|
5048
|
+
positionals_unknown: false, rebound: nil)
|
|
4046
5049
|
params = def_node.parameters
|
|
4047
|
-
locals =
|
|
5050
|
+
locals =
|
|
5051
|
+
if positionals_unknown
|
|
5052
|
+
bind_params_without_correspondence(params, arg_types.last)
|
|
5053
|
+
else
|
|
5054
|
+
bind_params_from_call_types(params, arg_types)
|
|
5055
|
+
end
|
|
4048
5056
|
return nil if locals.nil?
|
|
4049
5057
|
|
|
5058
|
+
rebound = locals.keys if rebound == RecursionRebinding::ALL
|
|
5059
|
+
rebound&.each { |name| locals[name] = dynamic_top if locals.key?(name) }
|
|
5060
|
+
|
|
4050
5061
|
# Construct the body scope in a SINGLE Scope allocation (plus the issue #1358 frame it carries, whose
|
|
4051
5062
|
# scans run only on demand) — the previous `Scope.empty.with_*.with_*…` chain
|
|
4052
5063
|
# allocated a fresh frozen Scope per field, run per user-method-call inference (ADR-44). The
|
|
@@ -4111,9 +5122,15 @@ module Rigor
|
|
|
4111
5122
|
def self_fold_safe_grant?(def_node, receiver)
|
|
4112
5123
|
per_def = (class_graph_buckets[:self_pure][def_node] ||= {})
|
|
4113
5124
|
key = [receiver.class_name, receiver.member_names]
|
|
4114
|
-
|
|
5125
|
+
if per_def.key?(key)
|
|
5126
|
+
return MemoAudit.check("class graph self pure", per_def[key], def_node.name, key) do
|
|
5127
|
+
self_fold_safe_body?(def_node.body, receiver, [def_node])
|
|
5128
|
+
end
|
|
5129
|
+
end
|
|
4115
5130
|
|
|
4116
|
-
per_def[key] =
|
|
5131
|
+
per_def[key] = MemoAudit.key_audit("class graph self pure") do
|
|
5132
|
+
self_fold_safe_body?(def_node.body, receiver, [def_node])
|
|
5133
|
+
end
|
|
4117
5134
|
end
|
|
4118
5135
|
|
|
4119
5136
|
# Whether `body`'s every use of `self` is a pure read, resolving each unrecognised self-call against
|
|
@@ -4148,6 +5165,11 @@ module Rigor
|
|
|
4148
5165
|
locals = bind_positional_params(params, positional)
|
|
4149
5166
|
return nil if locals.nil?
|
|
4150
5167
|
|
|
5168
|
+
bind_keyword_and_block_params(params, kw_shape, locals)
|
|
5169
|
+
end
|
|
5170
|
+
|
|
5171
|
+
# The keyword, `**rest` and block parameters' bindings, added to `locals`, which it returns.
|
|
5172
|
+
def bind_keyword_and_block_params(params, kw_shape, locals)
|
|
4151
5173
|
bind_keyword_params(params, kw_shape, locals)
|
|
4152
5174
|
rest_name = keyword_rest_name(params)
|
|
4153
5175
|
locals[rest_name.to_sym] = keyword_rest_type(params, kw_shape) if rest_name
|
|
@@ -4155,6 +5177,25 @@ module Rigor
|
|
|
4155
5177
|
locals
|
|
4156
5178
|
end
|
|
4157
5179
|
|
|
5180
|
+
# Issue #1237 — the locals table for a call whose positional correspondence is unknown (a `*splat` at the
|
|
5181
|
+
# call site). Every positional parameter (required, optional, trailing) binds `Dynamic[top]` and the rest
|
|
5182
|
+
# parameter an `Array[untyped]`; the keyword parameters bind from `keywords` as {#bind_keyword_params} does
|
|
5183
|
+
# for any call, so an open shape leaves them `Dynamic[top]`. An optional's default is not used: the splat
|
|
5184
|
+
# may supply the slot.
|
|
5185
|
+
def bind_params_without_correspondence(params, keywords)
|
|
5186
|
+
return {} if params.nil?
|
|
5187
|
+
return nil unless params.is_a?(Prism::ParametersNode)
|
|
5188
|
+
|
|
5189
|
+
locals = {}
|
|
5190
|
+
(params.requireds + params.posts).each { |param| bind_positional_param(locals, param, dynamic_top) }
|
|
5191
|
+
params.optionals.each { |param| locals[param.name.to_sym] = dynamic_top }
|
|
5192
|
+
rest = params.rest
|
|
5193
|
+
if rest.respond_to?(:name) && rest.name
|
|
5194
|
+
locals[rest.name.to_sym] = Type::Combinator.nominal_of("Array", type_args: [dynamic_top])
|
|
5195
|
+
end
|
|
5196
|
+
bind_keyword_and_block_params(params, keywords, locals)
|
|
5197
|
+
end
|
|
5198
|
+
|
|
4158
5199
|
# Trailing required positionals after a rest (`def f(a, *m, z)`) shift the correspondence and stay
|
|
4159
5200
|
# declined — correspondence, not width, is the issue. Issue #1125: `def m(...)`'s forwarding slot is no
|
|
4160
5201
|
# longer a decline. Its NAMED parameters (a leading `def m(a, ...)` required) bind exactly as before;
|
|
@@ -4300,7 +5341,7 @@ module Rigor
|
|
|
4300
5341
|
# now carry what the callee needs:
|
|
4301
5342
|
#
|
|
4302
5343
|
# - `f(...)` inside `def m(...)` re-supplies the arguments `m` was called with, read from the frame
|
|
4303
|
-
# {#infer_user_method_return} installed ({
|
|
5344
|
+
# {#infer_user_method_return} installed ({FORWARDED_ARGS}) — so it expands to a whole LIST, not
|
|
4304
5345
|
# one type, which is why the map became a flat_map.
|
|
4305
5346
|
# - a keyword hash built ENTIRELY from a double splat (`f(**h)`) is the shape of `h`, so `f(**h)`
|
|
4306
5347
|
# binds the same parameters the literal form `f(a: 1, b: 2)` does.
|
|
@@ -4396,7 +5437,7 @@ module Rigor
|
|
|
4396
5437
|
)
|
|
4397
5438
|
block_return_for(
|
|
4398
5439
|
block_arg, expected,
|
|
4399
|
-
call_node: call_node,
|
|
5440
|
+
call_node: call_node, receiver_type: receiver_type,
|
|
4400
5441
|
narrowing: block_body_self_narrowing(call_node, receiver_type),
|
|
4401
5442
|
repeats: !BLOCK_VALUE_DISCARDING.include?(call_node.name) && block_may_repeat?(call_node, receiver_type)
|
|
4402
5443
|
)
|
|
@@ -4469,7 +5510,7 @@ module Rigor
|
|
|
4469
5510
|
param_types = break_arm_param_types(call_node, receiver)
|
|
4470
5511
|
block_scope = block_entry_scope(
|
|
4471
5512
|
block_node, param_types,
|
|
4472
|
-
call_node: call_node,
|
|
5513
|
+
call_node: call_node, receiver_type: receiver,
|
|
4473
5514
|
narrowing: block_body_self_narrowing(call_node, receiver),
|
|
4474
5515
|
captured: repeating_captured_bindings(block_node, param_types, block_may_repeat?(call_node, receiver))
|
|
4475
5516
|
)
|
|
@@ -4512,12 +5553,13 @@ module Rigor
|
|
|
4512
5553
|
)
|
|
4513
5554
|
end
|
|
4514
5555
|
|
|
4515
|
-
def block_return_for(block_arg, expected, call_node: nil, narrowing: nil, repeats: false)
|
|
5556
|
+
def block_return_for(block_arg, expected, call_node: nil, narrowing: nil, repeats: false, receiver_type: nil)
|
|
4516
5557
|
case block_arg
|
|
4517
5558
|
when Prism::BlockNode
|
|
4518
5559
|
captured = repeating_captured_bindings(block_arg, expected, repeats)
|
|
4519
5560
|
entry = block_entry_scope(
|
|
4520
|
-
block_arg, expected,
|
|
5561
|
+
block_arg, expected,
|
|
5562
|
+
call_node: call_node, narrowing: narrowing, captured: captured, receiver_type: receiver_type
|
|
4521
5563
|
)
|
|
4522
5564
|
type_block_body(block_arg, entry, captured: captured)
|
|
4523
5565
|
when Prism::BlockArgumentNode
|
|
@@ -4545,8 +5587,16 @@ module Rigor
|
|
|
4545
5587
|
# Issue #316 — mirrors `StatementEvaluator#build_block_entry_scope`: the block body's `self` is the
|
|
4546
5588
|
# yielding method's business, so the return-typing pass must see the same unmodelled-self mark. Issue #1358
|
|
4547
5589
|
# — and the same match-global view ({MatchRebinding.block_entry}), which reads the owning `call_node`.
|
|
4548
|
-
|
|
4549
|
-
|
|
5590
|
+
#
|
|
5591
|
+
# Issue #2861 — and the same self-preserving entry ({SelfPreservingBlock}), read off `receiver_type`.
|
|
5592
|
+
def block_entry_scope(block_node, expected, call_node: nil, narrowing: nil, captured: nil, receiver_type: nil)
|
|
5593
|
+
self_entry = if SelfPreservingBlock.call?(call_node, receiver_type, scope)
|
|
5594
|
+
scope.entering_self_preserving_block
|
|
5595
|
+
else
|
|
5596
|
+
scope.entering_opaque_block
|
|
5597
|
+
end
|
|
5598
|
+
entry = MatchRebinding.block_entry(self_entry, block_node, call_node)
|
|
5599
|
+
entry = entry.entering_block_without_gradual_marks(block_node)
|
|
4550
5600
|
entry = captured.lay(entry) if captured
|
|
4551
5601
|
block_scope = BlockParameterBinder.new(expected_param_types: expected).bind_onto(block_node, entry)
|
|
4552
5602
|
return block_scope unless narrowing
|
|
@@ -4816,23 +5866,20 @@ module Rigor
|
|
|
4816
5866
|
# Suppression flag for {#threaded_block_body_type} — set while a fold is running (so a fold never nests)
|
|
4817
5867
|
# and while the per-element Tuple fold is over its arity cap. Thread-local because block typing is
|
|
4818
5868
|
# re-entrant within one thread and the fork-pool workers each own their own.
|
|
4819
|
-
|
|
4820
|
-
|
|
5869
|
+
THREADED_BLOCK_BODY = TypingState.frame(
|
|
5870
|
+
:threaded_block_body, reason: "a suppression flag installed per fold",
|
|
5871
|
+
witness: "spec/fixtures/typing_state_witnesses/threaded_block_body.rb"
|
|
5872
|
+
)
|
|
5873
|
+
private_constant :THREADED_BLOCK_BODY
|
|
4821
5874
|
|
|
4822
5875
|
def block_body_threading_suppressed?
|
|
4823
|
-
|
|
5876
|
+
typing_state[THREADED_BLOCK_BODY] ? true : false
|
|
4824
5877
|
end
|
|
4825
5878
|
|
|
4826
5879
|
# Runs the block with the scope-threading fold suppressed, restoring the previous state (not clearing
|
|
4827
5880
|
# it) on the way out, so nesting a suppressed region inside another cannot re-enable threading.
|
|
4828
|
-
def without_block_body_threading
|
|
4829
|
-
|
|
4830
|
-
Thread.current[THREADED_BLOCK_BODY_KEY] = true
|
|
4831
|
-
begin
|
|
4832
|
-
yield
|
|
4833
|
-
ensure
|
|
4834
|
-
Thread.current[THREADED_BLOCK_BODY_KEY] = previous
|
|
4835
|
-
end
|
|
5881
|
+
def without_block_body_threading(&)
|
|
5882
|
+
TypingState.current.with(THREADED_BLOCK_BODY, true, &)
|
|
4836
5883
|
end
|
|
4837
5884
|
|
|
4838
5885
|
# Every variable-write form whose name `StatementEvaluator` threads into the following statement's
|
|
@@ -5027,11 +6074,13 @@ module Rigor
|
|
|
5027
6074
|
# position matches. `r = [1, 2].find(-> { 0 }) { |e| e > 5 }` is `0` at runtime; the walk answered `nil`,
|
|
5028
6075
|
# and `r + 1` then reported a nil receiver on correct code. The other supported methods take no argument.
|
|
5029
6076
|
PER_ELEMENT_TUPLE_METHODS = Set[
|
|
5030
|
-
:map, :collect, :filter_map, :flat_map,
|
|
5031
|
-
:select, :filter, :reject,
|
|
5032
|
-
:find, :detect, :find_index, :index
|
|
6077
|
+
:map, :collect, :filter_map, :flat_map, :collect_concat,
|
|
6078
|
+
:select, :filter, :find_all, :reject, :take_while, :drop_while,
|
|
6079
|
+
:partition, :count, :find, :detect, :rfind, :find_index, :index, :rindex
|
|
5033
6080
|
].freeze
|
|
5034
|
-
|
|
6081
|
+
ARRAY_ONLY_PER_ELEMENT_METHODS = Set[:index, :rindex, :rfind].freeze
|
|
6082
|
+
REVERSE_PER_ELEMENT_METHODS = Set[:rfind, :rindex].freeze
|
|
6083
|
+
private_constant :PER_ELEMENT_TUPLE_METHODS, :ARRAY_ONLY_PER_ELEMENT_METHODS, :REVERSE_PER_ELEMENT_METHODS
|
|
5035
6084
|
|
|
5036
6085
|
HASH_SHAPE_TRANSFORM_METHODS = Set[
|
|
5037
6086
|
:transform_keys, :transform_keys!,
|
|
@@ -5050,23 +6099,35 @@ module Rigor
|
|
|
5050
6099
|
private_constant :PER_ELEMENT_RANGE_LIMIT
|
|
5051
6100
|
|
|
5052
6101
|
def try_per_element_block_fold(call_node, receiver_type)
|
|
5053
|
-
|
|
6102
|
+
name = call_node.name
|
|
6103
|
+
return nil unless PER_ELEMENT_TUPLE_METHODS.include?(name)
|
|
6104
|
+
return nil if ARRAY_ONLY_PER_ELEMENT_METHODS.include?(name) && !receiver_type.is_a?(Type::Tuple)
|
|
5054
6105
|
return nil unless call_node.arguments.nil?
|
|
5055
6106
|
|
|
5056
6107
|
element_types = per_element_elements_of(receiver_type)
|
|
5057
6108
|
return nil if element_types.nil? || element_types.empty?
|
|
5058
6109
|
|
|
5059
|
-
per_position =
|
|
6110
|
+
per_position = evaluate_per_element_positions(call_node.block, element_types, name)
|
|
6111
|
+
return nil if per_position.nil?
|
|
6112
|
+
|
|
6113
|
+
assemble_per_element_result(name, per_position, element_types) ||
|
|
6114
|
+
undecided_fold_floor(name, element_types, receiver_type)
|
|
6115
|
+
end
|
|
6116
|
+
|
|
6117
|
+
def evaluate_per_element_positions(block, element_types, method_name)
|
|
6118
|
+
reverse = REVERSE_PER_ELEMENT_METHODS.include?(method_name)
|
|
6119
|
+
eval_elements = reverse ? element_types.reverse : element_types
|
|
6120
|
+
per_position = per_element_block_results(block, eval_elements)
|
|
5060
6121
|
return nil if per_position.nil? || per_position.any?(&:nil?)
|
|
5061
6122
|
|
|
5062
|
-
|
|
5063
|
-
undecided_fold_floor(call_node.name, element_types, receiver_type)
|
|
6123
|
+
reverse ? per_position.reverse : per_position
|
|
5064
6124
|
end
|
|
5065
6125
|
|
|
5066
|
-
# The honest answer for the find family (`find` / `detect` / `find_index` / `index`
|
|
5067
|
-
# (`select` / `filter` / `
|
|
5068
|
-
#
|
|
5069
|
-
#
|
|
6126
|
+
# The honest answer for the find family (`find` / `detect` / `rfind` / `find_index` / `index` / `rindex`),
|
|
6127
|
+
# the filter family (`select` / `filter` / `find_all` / `reject` / `take_while` / `drop_while`), and `count`
|
|
6128
|
+
# when this fold walked every position and the assembler still could not decide — which happens for
|
|
6129
|
+
# exactly one reason: some position's predicate is not a `Constant`, so "the matching one" or "the ones
|
|
6130
|
+
# kept" is not a static fact.
|
|
5070
6131
|
#
|
|
5071
6132
|
# Falling through to the dispatcher was WRONG for this family, and issue #617 residue (1) is the bill:
|
|
5072
6133
|
# `seen = 0; [1, 2].find do |e| seen += 1; seen == 2 end` answered `nil` where the runtime answers `2`.
|
|
@@ -5076,30 +6137,34 @@ module Rigor
|
|
|
5076
6137
|
# typed the predicate per position and saw that it does not fold. Answering here is what keeps that
|
|
5077
6138
|
# knowledge from being thrown away in favour of a worse-informed tier.
|
|
5078
6139
|
#
|
|
5079
|
-
# The floor is what `find` can return and no tighter: one of the receiver's own elements, or
|
|
5080
|
-
# no element matches. Value pinning survives because it is still true of every candidate —
|
|
5081
|
-
# back an element, it does not compute one. `find_index` / `index` answer a
|
|
5082
|
-
# `Integer?` is their floor.
|
|
5083
|
-
#
|
|
5084
|
-
# `select` / `filter` / `reject` take a floor for the same
|
|
5085
|
-
# wider: `BlockFolding`'s filter folds DO answer on a `Constant`
|
|
5086
|
-
# them one, so `seen = 0; [1, 2].select do |e| seen += 1; seen == 2
|
|
5087
|
-
# where Ruby answers `[2]`. Nested inside a threaded body, where a
|
|
5088
|
-
# ({#tail_only_body_floor}), `[[], []].select do |a| a << w; a.any? end`
|
|
5089
|
-
# Array of the receiver's own elements: which of them survive is
|
|
5090
|
-
# A range receiver's elements are widened first
|
|
6140
|
+
# The floor is what `find` / `rfind` can return and no tighter: one of the receiver's own elements, or
|
|
6141
|
+
# `nil` when no element matches. Value pinning survives because it is still true of every candidate —
|
|
6142
|
+
# `find` hands back an element, it does not compute one. `find_index` / `index` / `rindex` answer a
|
|
6143
|
+
# position in the receiver, so `Integer?` is their floor.
|
|
6144
|
+
#
|
|
6145
|
+
# `select` / `filter` / `find_all` / `reject` / `take_while` / `drop_while` take a floor for the same
|
|
6146
|
+
# reason. Their fall-through is not merely wider: `BlockFolding`'s filter folds DO answer on a `Constant`
|
|
6147
|
+
# block, and the entry-scope pin hands them one, so `seen = 0; [1, 2].select do |e| seen += 1; seen == 2
|
|
6148
|
+
# end` answered a provably-empty `[]` where Ruby answers `[2]`. Nested inside a threaded body, where a
|
|
6149
|
+
# position can be typed tail-only ({#tail_only_body_floor}), `[[], []].select do |a| a << w; a.any? end`
|
|
6150
|
+
# did the same. Their floor is an Array of the receiver's own elements: which of them survive is
|
|
6151
|
+
# undecided, and what they are is not. A range receiver's elements are widened first
|
|
6152
|
+
# ({#filter_family_floor}). `count` likewise floors to `Integer` so an undecided block never falls through
|
|
6153
|
+
# to `BlockFolding`'s entry-scope `0` or `size`.
|
|
5091
6154
|
#
|
|
5092
6155
|
# Nothing else in {PER_ELEMENT_TUPLE_METHODS} takes a floor. `map`'s assembler cannot decline, and
|
|
5093
|
-
# `BlockFolding` never folds `filter_map` / `flat_map`, so their fall-through is the RBS
|
|
6156
|
+
# `BlockFolding` never folds `filter_map` / `flat_map` / `partition`, so their fall-through is the RBS
|
|
5094
6157
|
# projection.
|
|
5095
6158
|
def undecided_fold_floor(method_name, element_types, receiver_type)
|
|
5096
6159
|
case method_name
|
|
5097
|
-
when :find, :detect
|
|
6160
|
+
when :find, :detect, :rfind
|
|
5098
6161
|
Type::Combinator.union(*element_types, Type::Combinator.constant_of(nil))
|
|
5099
|
-
when :select, :filter, :reject
|
|
6162
|
+
when :select, :filter, :find_all, :reject, :take_while, :drop_while
|
|
5100
6163
|
filter_family_floor(element_types, receiver_type)
|
|
5101
|
-
when :find_index, :index
|
|
6164
|
+
when :find_index, :index, :rindex
|
|
5102
6165
|
Type::Combinator.union(Type::Combinator.nominal_of("Integer"), Type::Combinator.constant_of(nil))
|
|
6166
|
+
when :count
|
|
6167
|
+
Type::Combinator.nominal_of("Integer")
|
|
5103
6168
|
end
|
|
5104
6169
|
end
|
|
5105
6170
|
|
|
@@ -5637,9 +6702,13 @@ module Rigor
|
|
|
5637
6702
|
# Records `type` as the accumulator `block` receives when typed from this typer's scope
|
|
5638
6703
|
# ({.recorded_inject_accumulator}). Prism nodes compare by identity. The keys are weak but the values strong,
|
|
5639
6704
|
# and a recorded scope reaches the parse tree that holds the key, so a record never goes on its own (issue
|
|
5640
|
-
# #2412): the map is a run-scoped slot ({
|
|
6705
|
+
# #2412): the map is a run-scoped slot ({TypingState.release_run_scoped}), emptied when the run that filled it
|
|
6706
|
+
# ends.
|
|
5641
6707
|
def record_inject_accumulator(block, type)
|
|
5642
|
-
|
|
6708
|
+
record = [scope, type, LoopValueRecord.generation].freeze
|
|
6709
|
+
state = typing_state
|
|
6710
|
+
state.log_write(INJECT_ACCUMULATOR, block, record)
|
|
6711
|
+
state.weak_key_map(INJECT_ACCUMULATOR)[block] = record
|
|
5643
6712
|
end
|
|
5644
6713
|
|
|
5645
6714
|
# Issue #1332 — the #587 (b) captured-block binding ({#captured_block_bindings}) both inject arms lay under
|
|
@@ -5754,18 +6823,44 @@ module Rigor
|
|
|
5754
6823
|
result
|
|
5755
6824
|
end
|
|
5756
6825
|
|
|
6826
|
+
PER_ELEMENT_ASSEMBLERS = {
|
|
6827
|
+
map: :assemble_map_result,
|
|
6828
|
+
collect: :assemble_map_result,
|
|
6829
|
+
filter_map: :assemble_filter_map_result,
|
|
6830
|
+
flat_map: :assemble_flat_map_result,
|
|
6831
|
+
collect_concat: :assemble_flat_map_result,
|
|
6832
|
+
select: :assemble_select_result,
|
|
6833
|
+
filter: :assemble_select_result,
|
|
6834
|
+
find_all: :assemble_select_result,
|
|
6835
|
+
reject: :assemble_reject_result,
|
|
6836
|
+
take_while: :assemble_take_while_result,
|
|
6837
|
+
drop_while: :assemble_drop_while_result,
|
|
6838
|
+
partition: :assemble_partition_result,
|
|
6839
|
+
count: :assemble_count_result,
|
|
6840
|
+
find: :assemble_find_result,
|
|
6841
|
+
detect: :assemble_find_result,
|
|
6842
|
+
rfind: :assemble_rfind_result,
|
|
6843
|
+
find_index: :assemble_find_index_result,
|
|
6844
|
+
index: :assemble_find_index_result,
|
|
6845
|
+
rindex: :assemble_rindex_result
|
|
6846
|
+
}.freeze
|
|
6847
|
+
private_constant :PER_ELEMENT_ASSEMBLERS
|
|
6848
|
+
|
|
5757
6849
|
def assemble_per_element_result(method_name, per_position, element_types)
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
6850
|
+
handler = PER_ELEMENT_ASSEMBLERS[method_name]
|
|
6851
|
+
send(handler, per_position, element_types) if handler
|
|
6852
|
+
end
|
|
6853
|
+
|
|
6854
|
+
def assemble_map_result(per_position, _element_types)
|
|
6855
|
+
Type::Combinator.tuple_of(*per_position)
|
|
6856
|
+
end
|
|
6857
|
+
|
|
6858
|
+
def assemble_select_result(per_position, element_types)
|
|
6859
|
+
assemble_filter_result(per_position, element_types, keep_on_truthy: true)
|
|
6860
|
+
end
|
|
6861
|
+
|
|
6862
|
+
def assemble_reject_result(per_position, element_types)
|
|
6863
|
+
assemble_filter_result(per_position, element_types, keep_on_truthy: false)
|
|
5769
6864
|
end
|
|
5770
6865
|
|
|
5771
6866
|
# `select` / `filter` / `reject`: keeps each receiver element whose per-position predicate result
|
|
@@ -5785,10 +6880,51 @@ module Rigor
|
|
|
5785
6880
|
Type::Combinator.tuple_of(*kept)
|
|
5786
6881
|
end
|
|
5787
6882
|
|
|
6883
|
+
# `take_while` / `drop_while`: stops at the first falsey `Constant` position when every preceding
|
|
6884
|
+
# position is a truthy `Constant` (or takes/drops all elements when every position is a truthy
|
|
6885
|
+
# `Constant`). Declines when an undecided position precedes the stopping point.
|
|
6886
|
+
def assemble_take_while_result(per_position, element_types)
|
|
6887
|
+
cutoff = leading_truthy_cutoff(per_position)
|
|
6888
|
+
return nil if cutoff.nil?
|
|
6889
|
+
|
|
6890
|
+
Type::Combinator.tuple_of(*element_types[0...cutoff])
|
|
6891
|
+
end
|
|
6892
|
+
|
|
6893
|
+
def assemble_drop_while_result(per_position, element_types)
|
|
6894
|
+
cutoff = leading_truthy_cutoff(per_position)
|
|
6895
|
+
return nil if cutoff.nil?
|
|
6896
|
+
|
|
6897
|
+
Type::Combinator.tuple_of(*element_types[cutoff..])
|
|
6898
|
+
end
|
|
6899
|
+
|
|
6900
|
+
def leading_truthy_cutoff(per_position)
|
|
6901
|
+
first_falsey_index = per_position.index { |type| falsey_constant?(type) }
|
|
6902
|
+
return (per_position.all?(Type::Constant) ? per_position.size : nil) if first_falsey_index.nil?
|
|
6903
|
+
return nil unless per_position[0...first_falsey_index].all?(Type::Constant)
|
|
6904
|
+
|
|
6905
|
+
first_falsey_index
|
|
6906
|
+
end
|
|
6907
|
+
|
|
6908
|
+
# `partition`: returns `Tuple[truthy_tuple, falsey_tuple]` when every position folds to a `Constant`.
|
|
6909
|
+
def assemble_partition_result(per_position, element_types)
|
|
6910
|
+
return nil unless per_position.all?(Type::Constant)
|
|
6911
|
+
|
|
6912
|
+
truthy = assemble_filter_result(per_position, element_types, keep_on_truthy: true)
|
|
6913
|
+
falsey = assemble_filter_result(per_position, element_types, keep_on_truthy: false)
|
|
6914
|
+
Type::Combinator.tuple_of(truthy, falsey)
|
|
6915
|
+
end
|
|
6916
|
+
|
|
6917
|
+
# `count`: returns `Constant[Integer]` of the truthy positions when every position folds to a `Constant`.
|
|
6918
|
+
def assemble_count_result(per_position, _element_types)
|
|
6919
|
+
return nil unless per_position.all?(Type::Constant)
|
|
6920
|
+
|
|
6921
|
+
Type::Combinator.constant_of(per_position.count { |type| truthy_constant?(type) })
|
|
6922
|
+
end
|
|
6923
|
+
|
|
5788
6924
|
# `filter_map` folds tightly only when every per-position result is a `Constant`: positions whose
|
|
5789
6925
|
# value is `nil` or `false` drop, the rest survive in declaration order. When any position is
|
|
5790
6926
|
# non-Constant the dispatcher declines (returns nil) so the RBS tier widens to `Array[U]`.
|
|
5791
|
-
def assemble_filter_map_result(per_position)
|
|
6927
|
+
def assemble_filter_map_result(per_position, _element_types = nil)
|
|
5792
6928
|
return nil unless per_position.all?(Type::Constant)
|
|
5793
6929
|
|
|
5794
6930
|
kept = per_position.reject { |type| type.value.nil? || type.value == false }
|
|
@@ -5803,7 +6939,7 @@ module Rigor
|
|
|
5803
6939
|
#
|
|
5804
6940
|
# `Type::Constant` only ever holds non-Array scalars (the carrier rejects Array literals), so a single
|
|
5805
6941
|
# `Constant` safely contributes itself as a single Tuple element.
|
|
5806
|
-
def assemble_flat_map_result(per_position)
|
|
6942
|
+
def assemble_flat_map_result(per_position, _element_types = nil)
|
|
5807
6943
|
flattened = per_position.flat_map { |type| flat_map_contribution(type) }
|
|
5808
6944
|
return nil if flattened.nil? || flattened.any?(&:nil?)
|
|
5809
6945
|
|
|
@@ -5833,15 +6969,32 @@ module Rigor
|
|
|
5833
6969
|
Type::Combinator.union(*candidates.map { |index| element_types[index] })
|
|
5834
6970
|
end
|
|
5835
6971
|
|
|
6972
|
+
# `rfind`: reverse of `find` — returns the last receiver element whose block result is Ruby-truthy,
|
|
6973
|
+
# joined with any undecided positions to its right.
|
|
6974
|
+
def assemble_rfind_result(per_position, element_types)
|
|
6975
|
+
candidates = last_match_candidates(per_position)
|
|
6976
|
+
return candidates if candidates.nil? || candidates.equal?(NO_MATCH)
|
|
6977
|
+
|
|
6978
|
+
Type::Combinator.union(*candidates.map { |index| element_types[index] })
|
|
6979
|
+
end
|
|
6980
|
+
|
|
5836
6981
|
# `find_index` / `index`: returns the index of the first truthy position, or `Constant[nil]` when
|
|
5837
6982
|
# nothing matches, on {#assemble_find_result}'s terms.
|
|
5838
|
-
def assemble_find_index_result(per_position)
|
|
6983
|
+
def assemble_find_index_result(per_position, _element_types = nil)
|
|
5839
6984
|
candidates = first_match_candidates(per_position)
|
|
5840
6985
|
return candidates if candidates.nil? || candidates.equal?(NO_MATCH)
|
|
5841
6986
|
|
|
5842
6987
|
Type::Combinator.union(*candidates.map { |index| Type::Combinator.constant_of(index) })
|
|
5843
6988
|
end
|
|
5844
6989
|
|
|
6990
|
+
# `rindex`: returns the index of the last truthy position, or `Constant[nil]` when nothing matches.
|
|
6991
|
+
def assemble_rindex_result(per_position, _element_types = nil)
|
|
6992
|
+
candidates = last_match_candidates(per_position)
|
|
6993
|
+
return candidates if candidates.nil? || candidates.equal?(NO_MATCH)
|
|
6994
|
+
|
|
6995
|
+
Type::Combinator.union(*candidates.map { |index| Type::Combinator.constant_of(index) })
|
|
6996
|
+
end
|
|
6997
|
+
|
|
5845
6998
|
NO_MATCH = Type::Combinator.constant_of(nil)
|
|
5846
6999
|
private_constant :NO_MATCH
|
|
5847
7000
|
|
|
@@ -5857,8 +7010,27 @@ module Rigor
|
|
|
5857
7010
|
undecided + [first_truthy_index]
|
|
5858
7011
|
end
|
|
5859
7012
|
|
|
7013
|
+
# The positions `rfind` / `rindex` may stop at — the last decisive truthy one and every undecided one
|
|
7014
|
+
# after it — `NO_MATCH` when every position folds falsey, or nil when an undecided position has no
|
|
7015
|
+
# truthy one before it.
|
|
7016
|
+
def last_match_candidates(per_position)
|
|
7017
|
+
last_truthy_index = per_position.rindex { |type| truthy_constant?(type) }
|
|
7018
|
+
if last_truthy_index.nil?
|
|
7019
|
+
return per_position.all?(Type::Constant) ? NO_MATCH : nil
|
|
7020
|
+
end
|
|
7021
|
+
|
|
7022
|
+
undecided = ((last_truthy_index + 1)...per_position.size).reject do |index|
|
|
7023
|
+
per_position[index].is_a?(Type::Constant)
|
|
7024
|
+
end
|
|
7025
|
+
[last_truthy_index] + undecided
|
|
7026
|
+
end
|
|
7027
|
+
|
|
5860
7028
|
def truthy_constant?(type)
|
|
5861
|
-
type.is_a?(Type::Constant) && type.value && type.value != false
|
|
7029
|
+
type.is_a?(Type::Constant) && !type.value.nil? && type.value != false
|
|
7030
|
+
end
|
|
7031
|
+
|
|
7032
|
+
def falsey_constant?(type)
|
|
7033
|
+
type.is_a?(Type::Constant) && (type.value.nil? || type.value == false)
|
|
5862
7034
|
end
|
|
5863
7035
|
|
|
5864
7036
|
# Per-pair block fold for `HashShape#transform_keys` and `HashShape#transform_values` (and their bang
|