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
|
@@ -14,7 +14,7 @@ require_relative "block_call_timing"
|
|
|
14
14
|
require_relative "block_repetition"
|
|
15
15
|
require_relative "block_parameter_binder"
|
|
16
16
|
require_relative "body_fixpoint"
|
|
17
|
-
require_relative "memo_audit"
|
|
17
|
+
require_relative "../memo_audit"
|
|
18
18
|
require_relative "callee_mutation_chain"
|
|
19
19
|
require_relative "toplevel_def_census"
|
|
20
20
|
require_relative "callee_write_census"
|
|
@@ -28,6 +28,7 @@ require_relative "../analysis/check_rules/inferred_param_guard"
|
|
|
28
28
|
require_relative "../analysis/check_rules/published_constant_guard"
|
|
29
29
|
require_relative "struct_fold_safety"
|
|
30
30
|
require_relative "closure_escape_analyzer"
|
|
31
|
+
require_relative "self_preserving_block"
|
|
31
32
|
require_relative "content_join"
|
|
32
33
|
require_relative "construction_escape"
|
|
33
34
|
require_relative "define_method_block_self"
|
|
@@ -62,6 +63,7 @@ require_relative "unknown_store_widening"
|
|
|
62
63
|
require_relative "untyped_rejoin"
|
|
63
64
|
require_relative "version_guard"
|
|
64
65
|
require_relative "../run_scoped_slots"
|
|
66
|
+
require_relative "../typing_state"
|
|
65
67
|
|
|
66
68
|
module Rigor
|
|
67
69
|
module Inference
|
|
@@ -194,13 +196,22 @@ module Rigor
|
|
|
194
196
|
# return type. The flow value of a `return` is still `Bot` (it transfers control rather than producing a value);
|
|
195
197
|
# the sink only records what the method *returns* through that edge. nil means "not collecting" — a top-level /
|
|
196
198
|
# DSL-block walk, or inside a nested `def` barrier (whose returns belong to the inner method).
|
|
197
|
-
|
|
198
|
-
|
|
199
|
+
#
|
|
200
|
+
# ADR-129 WD1 — the installation is a Frame and each record is a Resource touch.
|
|
201
|
+
RETURN_SINK = TypingState.frame(
|
|
202
|
+
:return_sink,
|
|
203
|
+
contents: :resource, reason: "installed per method body; each `return` records into it",
|
|
204
|
+
witness: "spec/fixtures/typing_state_witnesses/return_sink.rb"
|
|
205
|
+
)
|
|
206
|
+
private_constant :RETURN_SINK
|
|
199
207
|
|
|
200
208
|
# Thread-local flag: set while a side pass ({.provisional_returns}) re-types part of a body from a scope other
|
|
201
209
|
# than the one the statement walk holds, so the `return`s it reaches are filed as provisional ({ReturnSink}).
|
|
202
|
-
|
|
203
|
-
|
|
210
|
+
PROVISIONAL_RETURNS = TypingState.frame(
|
|
211
|
+
:provisional_returns, reason: "a flag installed around a side pass",
|
|
212
|
+
witness: "spec/fixtures/typing_state_witnesses/provisional_returns.rb"
|
|
213
|
+
)
|
|
214
|
+
private_constant :PROVISIONAL_RETURNS
|
|
204
215
|
|
|
205
216
|
# The `return` sink {.with_return_sink} installs. Issue #2299 — a `return` the statement walk records is typed
|
|
206
217
|
# with its real bindings; one a side pass records ({.provisional_returns}) may read a name the pass skipped the
|
|
@@ -215,8 +226,9 @@ module Rigor
|
|
|
215
226
|
@provisional = []
|
|
216
227
|
end
|
|
217
228
|
|
|
218
|
-
|
|
219
|
-
|
|
229
|
+
# `provisional` is the {PROVISIONAL_RETURNS} frame the evaluator reached the `return` under.
|
|
230
|
+
def record(node, type, provisional)
|
|
231
|
+
if provisional
|
|
220
232
|
@provisional << [node, type]
|
|
221
233
|
else
|
|
222
234
|
@types << type
|
|
@@ -234,14 +246,18 @@ module Rigor
|
|
|
234
246
|
# Thread-local sink (an Array of `[NextNode, Type]`) collecting the value each `next` carries out of the block it
|
|
235
247
|
# leaves, so `ExpressionTyper#type_block_body` can join those arms into the block's value type. Issue #841: the
|
|
236
248
|
# block-return pass modelled only the fall-through tail, so `ops.all? { |o| next false unless o; true }` read as
|
|
237
|
-
# `Constant[true]` and folded the call to always-truthy — the same defect `
|
|
249
|
+
# `Constant[true]` and folded the call to always-truthy — the same defect `RETURN_SINK` fixes one level up for
|
|
238
250
|
# a method's early `return`. The flow value of a `next` stays `Bot`.
|
|
239
251
|
#
|
|
240
252
|
# The sink also collects `next`s from nested blocks / loops / defs evaluated under the same installation (they do
|
|
241
253
|
# not install their own), so the consumer filters by node identity against a statically computed
|
|
242
254
|
# directly-targeting set, exactly as the break sink does. nil means "not collecting".
|
|
243
|
-
|
|
244
|
-
|
|
255
|
+
NEXT_SINK = TypingState.frame(
|
|
256
|
+
:next_sink,
|
|
257
|
+
contents: :resource, reason: "installed per block body; each `next` records into it",
|
|
258
|
+
witness: "spec/fixtures/typing_state_witnesses/jump_sinks.rb"
|
|
259
|
+
)
|
|
260
|
+
private_constant :NEXT_SINK
|
|
245
261
|
|
|
246
262
|
# Thread-local sink (an Array of `[BreakNode, Scope]`) collecting the scope at each `break` reached while
|
|
247
263
|
# evaluating a loop body, so `eval_loop` / `eval_for` can join a `break`-path binding (`flag = true; break`) into
|
|
@@ -249,21 +265,33 @@ module Rigor
|
|
|
249
265
|
# installs its own sink, restored on exit, so an inner loop's break does not leak to the outer one. A `break`
|
|
250
266
|
# inside a block / nested loop targets that inner construct, not the lexical loop — filtered out by the loop's
|
|
251
267
|
# {JumpTargets} set ({#loop_jumps}). See docs/notes/20260615-loop-break-binding-propagation-design.md.
|
|
252
|
-
|
|
253
|
-
|
|
268
|
+
#
|
|
269
|
+
# ADR-129 WD3 — a construct reads the sink's size before it runs and again after, to find the jumps it recorded
|
|
270
|
+
# itself ({#restoring_error_info}, {#ensure_jump_marks}), so a read of the contents is delta-internal.
|
|
271
|
+
BREAK_SINK = TypingState.frame(
|
|
272
|
+
:break_sink,
|
|
273
|
+
contents: :resource, read_exemption: :delta_internal,
|
|
274
|
+
reason: "installed per loop body; each `break` records into it, and an enclosing `ensure` rewrites them",
|
|
275
|
+
witness: "spec/fixtures/typing_state_witnesses/jump_sinks.rb"
|
|
276
|
+
)
|
|
277
|
+
private_constant :BREAK_SINK
|
|
254
278
|
|
|
255
279
|
# Thread-local sink (an Array of `[BreakNode, Type]`) collecting the value each `break` carries out of the
|
|
256
280
|
# construct it leaves. Issue #853: `break value` terminates the yielding CALL and is that call's value, so —
|
|
257
|
-
# unlike `next`, which {
|
|
281
|
+
# unlike `next`, which {NEXT_SINK} joins into the block — these arms are consumed one level up, by
|
|
258
282
|
# `ExpressionTyper#call_dispatch_type_for`. Without them `ops.all? { |o| break false unless o; true }` folded to
|
|
259
283
|
# `Constant[true]` and warned about a program that really can answer false.
|
|
260
284
|
#
|
|
261
|
-
# Separate from {
|
|
285
|
+
# Separate from {BREAK_SINK}, which records SCOPES for the loop-continuation join: the two consumers want
|
|
262
286
|
# different things from the same node, and a loop body evaluated under a call's collection must keep feeding its
|
|
263
287
|
# own sink. Both are filtered by node identity against a statically computed directly-targeting set, so a `break`
|
|
264
288
|
# that belongs to an inner loop or block never reaches the wrong consumer. nil means "not collecting".
|
|
265
|
-
|
|
266
|
-
|
|
289
|
+
BREAK_VALUE_SINK = TypingState.frame(
|
|
290
|
+
:break_value_sink,
|
|
291
|
+
contents: :resource, reason: "installed per yielding call; each `break` records into it",
|
|
292
|
+
witness: "spec/fixtures/typing_state_witnesses/jump_sinks.rb"
|
|
293
|
+
)
|
|
294
|
+
private_constant :BREAK_VALUE_SINK
|
|
267
295
|
|
|
268
296
|
# Lexical class frame: the `name:` field is the qualified class name as it would render in Ruby (e.g.,
|
|
269
297
|
# `"Foo::Bar"`); the `singleton:` field is `true` for `class << self` frames so nested defs resolve to
|
|
@@ -356,18 +384,9 @@ module Rigor
|
|
|
356
384
|
# so a `def` evaluated inside another method's body (which itself installed a sink) does not corrupt the outer
|
|
357
385
|
# one. Used by `ExpressionTyper#infer_user_method_return` to join the explicit returns into the inferred
|
|
358
386
|
# method-return type.
|
|
359
|
-
def self.with_return_sink
|
|
360
|
-
previous = Thread.current[RETURN_SINK_KEY]
|
|
361
|
-
previous_provisional = Thread.current[PROVISIONAL_RETURNS_KEY]
|
|
387
|
+
def self.with_return_sink(&)
|
|
362
388
|
sink = ReturnSink.new
|
|
363
|
-
|
|
364
|
-
Thread.current[PROVISIONAL_RETURNS_KEY] = nil
|
|
365
|
-
begin
|
|
366
|
-
result = yield
|
|
367
|
-
ensure
|
|
368
|
-
Thread.current[RETURN_SINK_KEY] = previous
|
|
369
|
-
Thread.current[PROVISIONAL_RETURNS_KEY] = previous_provisional
|
|
370
|
-
end
|
|
389
|
+
result = TypingState.current.with_pair(RETURN_SINK, sink, PROVISIONAL_RETURNS, nil, &)
|
|
371
390
|
[result, sink.types]
|
|
372
391
|
end
|
|
373
392
|
|
|
@@ -375,48 +394,54 @@ module Rigor
|
|
|
375
394
|
# on exit. Issue #2299 — {ExpressionTyper#type_block_body}'s tail-only answer types `return r if r` without the
|
|
376
395
|
# `r = yield e` before it, so `r` read `Dynamic[top]` and that went into the enclosing method's return type
|
|
377
396
|
# although the statement walk of the same block recorded `r` with its real binding.
|
|
378
|
-
def self.provisional_returns
|
|
379
|
-
|
|
380
|
-
Thread.current[PROVISIONAL_RETURNS_KEY] = true
|
|
381
|
-
begin
|
|
382
|
-
yield
|
|
383
|
-
ensure
|
|
384
|
-
Thread.current[PROVISIONAL_RETURNS_KEY] = previous
|
|
385
|
-
end
|
|
397
|
+
def self.provisional_returns(&)
|
|
398
|
+
TypingState.current.with(PROVISIONAL_RETURNS, true, &)
|
|
386
399
|
end
|
|
387
400
|
|
|
388
401
|
# Runs `block` with a fresh `next` sink installed, then yields the collected `[NextNode, Type]` pairs to the
|
|
389
402
|
# caller. Stacks like the return sink, and for the same reason: a block body evaluated inside another block body's
|
|
390
403
|
# evaluation must not spill its arms into the outer collection. Used by `ExpressionTyper#type_block_body` to join
|
|
391
404
|
# the `next` arms into the block's value type.
|
|
392
|
-
def self.with_next_sink
|
|
393
|
-
previous = Thread.current[NEXT_SINK_KEY]
|
|
405
|
+
def self.with_next_sink(&)
|
|
394
406
|
sink = []
|
|
395
|
-
|
|
396
|
-
begin
|
|
397
|
-
result = yield
|
|
398
|
-
ensure
|
|
399
|
-
Thread.current[NEXT_SINK_KEY] = previous
|
|
400
|
-
end
|
|
407
|
+
result = TypingState.current.with(NEXT_SINK, sink, &)
|
|
401
408
|
[result, sink]
|
|
402
409
|
end
|
|
403
410
|
|
|
411
|
+
# Runs `block` with none of {CALLER_SINKS} installed: a user-method body re-typed for its return starts outside
|
|
412
|
+
# its caller's constructs (ADR-129 WD1: the return memo forces these Frames at its entry). Installs only when one
|
|
413
|
+
# is set, read with `forcing_read`, so the common entry costs no frame.
|
|
414
|
+
def self.outside_caller_sinks(&)
|
|
415
|
+
state = TypingState.current
|
|
416
|
+
return yield if CALLER_SINKS.all? { |slot| state.forcing_read(slot).nil? }
|
|
417
|
+
|
|
418
|
+
state.without(CALLER_SINKS, &)
|
|
419
|
+
end
|
|
420
|
+
|
|
404
421
|
# Runs `block` with a fresh `break`-value sink installed, then yields the collected `[BreakNode, Type]` pairs to
|
|
405
422
|
# the caller. Stacks like the other sinks: a block body evaluated inside another block body's evaluation must not
|
|
406
423
|
# spill its arms into the outer collection. Used by `ExpressionTyper#call_dispatch_type_for` to union the `break`
|
|
407
424
|
# arms into the yielding call's type (issue #853).
|
|
408
|
-
def self.with_break_value_sink
|
|
409
|
-
previous = Thread.current[BREAK_VALUE_SINK_KEY]
|
|
425
|
+
def self.with_break_value_sink(&)
|
|
410
426
|
sink = []
|
|
411
|
-
|
|
412
|
-
begin
|
|
413
|
-
result = yield
|
|
414
|
-
ensure
|
|
415
|
-
Thread.current[BREAK_VALUE_SINK_KEY] = previous
|
|
416
|
-
end
|
|
427
|
+
result = TypingState.current.with(BREAK_VALUE_SINK, sink, &)
|
|
417
428
|
[result, sink]
|
|
418
429
|
end
|
|
419
430
|
|
|
431
|
+
# Issue #1246 — enters only `call`'s block, from the receiver scope, as the statement form enters it
|
|
432
|
+
# ({#evaluate_block_if_present}): the per-node scope index's evaluation of a block a value-position call holds
|
|
433
|
+
# ({ScopeIndexer#evaluate_unentered_block}). The call's receiver and arguments are left to the index walk.
|
|
434
|
+
#
|
|
435
|
+
# Such a block enters with the unknown-`self` mark only ({Scope#with_block_self_unknown}), as the walk entered
|
|
436
|
+
# it, not with the statement form's opaque-`self` mark ({Scope#entering_opaque_block}): issue #1717 keeps the
|
|
437
|
+
# latter, which gates binding a call to a cross-file top-level `def`, away from value-position blocks.
|
|
438
|
+
def evaluate_block_of(call)
|
|
439
|
+
@value_position_block = call.block
|
|
440
|
+
evaluate_block_if_present(call)
|
|
441
|
+
ensure
|
|
442
|
+
@value_position_block = nil
|
|
443
|
+
end
|
|
444
|
+
|
|
420
445
|
# Evaluate `node` under the receiver scope. Returns `[type, scope']` where `type` is the value the node produces
|
|
421
446
|
# and `scope'` is the scope observable after the node has run. The receiver scope is never mutated.
|
|
422
447
|
def evaluate(node)
|
|
@@ -810,7 +835,9 @@ module Rigor
|
|
|
810
835
|
memo = unrecorded_hand_off_memo(written)
|
|
811
836
|
key = [constructed, call_node.name]
|
|
812
837
|
unless memo.key?(key)
|
|
813
|
-
return memo[key] =
|
|
838
|
+
return memo[key] = MemoAudit.key_audit("unrecorded hand-off") do
|
|
839
|
+
compute_unrecorded_hand_off_callees(call_node, receiver, written, constructed)
|
|
840
|
+
end
|
|
814
841
|
end
|
|
815
842
|
|
|
816
843
|
MemoAudit.check("unrecorded hand-off", memo[key], written, key) do
|
|
@@ -832,14 +859,18 @@ module Rigor
|
|
|
832
859
|
# the class `self` is and its side (a constant's ancestor lookup differs between the class object and an
|
|
833
860
|
# instance), and the called name, not on the def it sits in.
|
|
834
861
|
def unrecorded_hand_off_memo(written)
|
|
835
|
-
table = (
|
|
862
|
+
table = typing_state.weak_key_map(UNRECORDED_HAND_OFFS)
|
|
836
863
|
self_type = scope.self_type
|
|
837
864
|
by_side = ((table[scope.discovery] ||= {})[self_type.is_a?(Type::Singleton)] ||= {})
|
|
838
865
|
(by_side[enclosing_class_name_for(self_type)] ||= {})[written] ||= {}
|
|
839
866
|
end
|
|
840
867
|
|
|
841
|
-
|
|
842
|
-
|
|
868
|
+
UNRECORDED_HAND_OFFS = TypingState.memo(
|
|
869
|
+
:callee_unrecorded_hand_offs,
|
|
870
|
+
run_scoped: true, audit: "unrecorded hand-off",
|
|
871
|
+
reason: "keyed by discovery, side, class and spelling, all its answer reads"
|
|
872
|
+
)
|
|
873
|
+
private_constant :UNRECORDED_HAND_OFFS
|
|
843
874
|
|
|
844
875
|
# The scopes {#unrecorded_hand_off_callees} types a receiver written as `written` in. A rooted constant and a
|
|
845
876
|
# top-level def are typed once.
|
|
@@ -1131,9 +1162,11 @@ module Rigor
|
|
|
1131
1162
|
def instance_def_owners(name)
|
|
1132
1163
|
return scope.user_instance_def_owners(name) if MemoAudit.suspended?
|
|
1133
1164
|
|
|
1134
|
-
table = (
|
|
1165
|
+
table = typing_state.weak_key_map(INSTANCE_DEF_INDEX)
|
|
1135
1166
|
by_name = (table[scope.discovery] ||= {})
|
|
1136
|
-
|
|
1167
|
+
unless by_name.key?(name)
|
|
1168
|
+
return by_name[name] = MemoAudit.key_audit("instance def owners") { scope.user_instance_def_owners(name) }
|
|
1169
|
+
end
|
|
1137
1170
|
|
|
1138
1171
|
record_instance_def_owner_reads(name, by_name[name]) if Analysis::DependencyRecorder.active?
|
|
1139
1172
|
MemoAudit.check("instance def owners", by_name[name], name) { scope.user_instance_def_owners(name) }
|
|
@@ -1144,9 +1177,9 @@ module Rigor
|
|
|
1144
1177
|
def subclassing_opaque?
|
|
1145
1178
|
return scope.subclassing_opacity.first if MemoAudit.suspended?
|
|
1146
1179
|
|
|
1147
|
-
table = (
|
|
1180
|
+
table = typing_state.weak_key_map(INSTANCE_DEF_INDEX)
|
|
1148
1181
|
by_name = (table[scope.discovery] ||= {})
|
|
1149
|
-
entry = by_name[OPACITY_ENTRY] ||= scope.subclassing_opacity
|
|
1182
|
+
entry = by_name[OPACITY_ENTRY] ||= MemoAudit.key_audit("subclassing opacity") { scope.subclassing_opacity }
|
|
1150
1183
|
Analysis::DependencyRecorder.read_keys(entry.last) if Analysis::DependencyRecorder.active?
|
|
1151
1184
|
MemoAudit.check("subclassing opacity", entry, OPACITY_ENTRY) { scope.subclassing_opacity }.first
|
|
1152
1185
|
end
|
|
@@ -1159,8 +1192,12 @@ module Rigor
|
|
|
1159
1192
|
owners.map(&:first).each { |owner| Analysis::DependencyRecorder.read_last_segment(:subclass_of, owner) }
|
|
1160
1193
|
end
|
|
1161
1194
|
|
|
1162
|
-
|
|
1163
|
-
|
|
1195
|
+
INSTANCE_DEF_INDEX = TypingState.memo(
|
|
1196
|
+
:callee_instance_def_index,
|
|
1197
|
+
run_scoped: true, audit: ["instance def owners", "subclassing opacity"],
|
|
1198
|
+
reason: "keyed by discovery and name, all its answer reads"
|
|
1199
|
+
)
|
|
1200
|
+
private_constant :INSTANCE_DEF_INDEX
|
|
1164
1201
|
|
|
1165
1202
|
def constant_node?(node) = node.is_a?(Prism::ConstantReadNode) || node.is_a?(Prism::ConstantPathNode)
|
|
1166
1203
|
|
|
@@ -1298,6 +1335,13 @@ module Rigor
|
|
|
1298
1335
|
|
|
1299
1336
|
attr_reader :scope, :tracer
|
|
1300
1337
|
|
|
1338
|
+
# ADR-129 WD2 — the calling thread's {TypingState::State}, cached on the evaluator the first time a slot is read.
|
|
1339
|
+
# An evaluator kept across threads must be kept per state instead ({ExpressionTyper#typing_state}). A frame
|
|
1340
|
+
# install whose block's value is the answer calls {TypingState.current} instead: Rigor's own analysis does not
|
|
1341
|
+
# adopt this memoised reader's type at a self-call (#525's self-purity), so the install's answer would read as
|
|
1342
|
+
# `Dynamic` through it.
|
|
1343
|
+
def typing_state = @typing_state ||= TypingState.current
|
|
1344
|
+
|
|
1301
1345
|
# Thread the scope through every child statement in declaration order. The body's value is the type of the last
|
|
1302
1346
|
# statement (or `Constant[nil]` for an empty body); intermediate statements' types are discarded, but their scope
|
|
1303
1347
|
# effects are preserved.
|
|
@@ -1307,7 +1351,7 @@ module Rigor
|
|
|
1307
1351
|
def eval_statements(node)
|
|
1308
1352
|
result_type = Type::Combinator.constant_of(nil)
|
|
1309
1353
|
current = scope
|
|
1310
|
-
raising =
|
|
1354
|
+
raising = typing_state[RETRY_FRAMES]
|
|
1311
1355
|
node.body.each do |stmt|
|
|
1312
1356
|
result_type, current = sub_eval(stmt, current)
|
|
1313
1357
|
record_raise_points(raising, stmt, current) if raising
|
|
@@ -1456,7 +1500,7 @@ module Rigor
|
|
|
1456
1500
|
def rhs_origin(value_node, scope_after_rhs, rhs_type)
|
|
1457
1501
|
return nil unless rhs_type.is_a?(Type::Dynamic)
|
|
1458
1502
|
|
|
1459
|
-
scope_after_rhs.
|
|
1503
|
+
scope_after_rhs.dynamic_origin(value_node)
|
|
1460
1504
|
end
|
|
1461
1505
|
|
|
1462
1506
|
# The scope binding the written local with ADR-58's `:local` mark when the write copies a declaration-sourced
|
|
@@ -1730,7 +1774,8 @@ module Rigor
|
|
|
1730
1774
|
# The expression value is the result type, matching Ruby's semantics: `(x = params[:f] ||= []); x` observes the
|
|
1731
1775
|
# post-`||=` value, not the rvalue alone.
|
|
1732
1776
|
def eval_index_or_write(node)
|
|
1733
|
-
|
|
1777
|
+
operands = index_write_operand_scope(node)
|
|
1778
|
+
_rhs_type, post_rhs = sub_eval(node.value, operands)
|
|
1734
1779
|
result_type = index_write_stored_type(node, scope)
|
|
1735
1780
|
|
|
1736
1781
|
# A narrowing is keyed on ONE literal slot — `a[k]` — but a multi-index `||=` reads and
|
|
@@ -1738,8 +1783,7 @@ module Rigor
|
|
|
1738
1783
|
# record `a[0]`'s type as the region answer: `a[0, 1] ||= []` would claim `a[0]` non-nil
|
|
1739
1784
|
# where the store splices nothing and `a[0]` stays nil at runtime. Decline the record for
|
|
1740
1785
|
# any form but the single-index one.
|
|
1741
|
-
|
|
1742
|
-
address = key_node && IndexedNarrowing.stable_address(node.receiver, key_node)
|
|
1786
|
+
address = recordable_slot_address(node)
|
|
1743
1787
|
# Issue #544 — a receiver with an untracked (Dynamic / Top) constituent can hold a caller-supplied
|
|
1744
1788
|
# slot value the `||=` keeps, so the recorded default would invent a fact; decline the record.
|
|
1745
1789
|
address = nil if address && !IndexedNarrowing.fully_tracked_receiver_type?(scope.type_of(node.receiver))
|
|
@@ -1748,7 +1792,9 @@ module Rigor
|
|
|
1748
1792
|
# the reverse order would trade this feature away for the widening. The two are complementary — the shape
|
|
1749
1793
|
# forgets that the collection is still empty, the narrowing remembers that THIS slot is now non-nil.
|
|
1750
1794
|
post = IndexWriteWidening.widen(node: node, current_scope: post,
|
|
1751
|
-
arg_types: index_write_arg_types(node, result_type))
|
|
1795
|
+
arg_types: index_write_arg_types(node, result_type, join_scope: operands))
|
|
1796
|
+
# Issue #1218 — a key that is not a literal (`h[k] ||= v`) may name a slot another `||=` narrowed.
|
|
1797
|
+
post = IndexedNarrowing.invalidate_indexed_write(node, post)
|
|
1752
1798
|
post = post.with_indexed_narrowing(*address, result_type) if address
|
|
1753
1799
|
|
|
1754
1800
|
# The value the expression yields takes the memoizing reading ({#index_compound_write_value}, #1155) for a
|
|
@@ -1814,14 +1860,76 @@ module Rigor
|
|
|
1814
1860
|
# expression, scope untouched — and the receiver never widened. They store through `[]=` exactly as
|
|
1815
1861
|
# `eval_index_or_write` does, so they take the same widening; the stored value is the compound result —
|
|
1816
1862
|
# `falsey(h[k]) | v` for `&&=`, the dispatched `h[k] + v` for `+=` — not the rvalue alone.
|
|
1863
|
+
#
|
|
1864
|
+
# Issue #1218 — the widening carries the receiver's slot narrowings across its rebind
|
|
1865
|
+
# ({MutationRejoin.carry_slot_narrowings}), so a `h[:e] ||= 0` narrowing outlived `h[:e] += 1` and the slot
|
|
1866
|
+
# kept reading `0`. The store drops the narrowing on the slot it overwrote, as a `[]=` call does
|
|
1867
|
+
# ({IndexedNarrowing.invalidate_indexed_write}: every narrowing on the receiver when the index is not one
|
|
1868
|
+
# literal key). A slot that held a narrowing then records the value the write stored, which the read it
|
|
1869
|
+
# performed took from that narrowing; a slot with none records nothing new.
|
|
1817
1870
|
def eval_index_write(node)
|
|
1818
|
-
|
|
1871
|
+
operands = index_write_operand_scope(node)
|
|
1872
|
+
_rhs_type, post_rhs = sub_eval(node.value, operands)
|
|
1819
1873
|
stored = index_write_stored_type(node, scope)
|
|
1820
1874
|
widened = IndexWriteWidening.widen(node: node, current_scope: post_rhs,
|
|
1821
|
-
arg_types: index_write_arg_types(node, stored))
|
|
1875
|
+
arg_types: index_write_arg_types(node, stored, join_scope: operands))
|
|
1876
|
+
widened = IndexedNarrowing.invalidate_indexed_write(node, widened)
|
|
1877
|
+
address = recordable_slot_address(node)
|
|
1878
|
+
widened = widened.with_indexed_narrowing(*address, stored) if address && scope.indexed_narrowing(*address)
|
|
1822
1879
|
[stored, forget_rebound_specials(widened, node)]
|
|
1823
1880
|
end
|
|
1824
1881
|
|
|
1882
|
+
# The `(receiver, key)` slot a compound index write may record its stored value on, or nil. A narrowing is
|
|
1883
|
+
# keyed on ONE literal slot, so a multi-index form, which reads and stores a splice region, records none. The
|
|
1884
|
+
# receiver's `[]=` must leave the stored value at that slot ({IndexedNarrowing.verbatim_slot?}: a `Hash` or
|
|
1885
|
+
# `Array`, and no negative index on an `Array`), and the rvalue must not rebind the receiver variable
|
|
1886
|
+
# ({#rvalue_rebinds_receiver?}), since the store went to the object it named before (issue #1218).
|
|
1887
|
+
def recordable_slot_address(node)
|
|
1888
|
+
key_node = single_index_argument(node)
|
|
1889
|
+
address = key_node && IndexedNarrowing.stable_address(node.receiver, key_node)
|
|
1890
|
+
return nil if address.nil?
|
|
1891
|
+
return nil unless IndexedNarrowing.verbatim_slot?(scope.type_of(node.receiver, tracer: tracer), address.last)
|
|
1892
|
+
return nil if rvalue_rebinds_receiver?(node)
|
|
1893
|
+
|
|
1894
|
+
address
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
# Whether a compound index write's rvalue may rebind its stable receiver variable: it writes the variable
|
|
1898
|
+
# (`h[k] += (h = {}; 1)`, `@h ||= …`, a multiple-assignment target, in a block too:
|
|
1899
|
+
# {OperandEffects.written_variables}), or, for an instance variable, the intervening-call reset past the
|
|
1900
|
+
# rvalue's self calls rebinds it ({InterveningIvarReset.after_operand}). That reset widens only a binding
|
|
1901
|
+
# narrower than the class-ivar seed, and only for a call not proven to leave it alone, so
|
|
1902
|
+
# `@h = { k: nil }; @h[:k] += (reset; 1)` with a `reset` that assigns `@h` is a rebind, and `@t[:k] ||=
|
|
1903
|
+
# compute(1)` on a `@t` the class binds to one shape is not. A store into another slot of the receiver
|
|
1904
|
+
# (`h[:a] ||= (h[:b] ||= [])`) is no rebind either.
|
|
1905
|
+
def rvalue_rebinds_receiver?(node)
|
|
1906
|
+
receiver = node.receiver
|
|
1907
|
+
name = receiver.name
|
|
1908
|
+
return true if OperandEffects.written_variables(node.value).include?(name)
|
|
1909
|
+
return false unless receiver.is_a?(Prism::InstanceVariableReadNode)
|
|
1910
|
+
|
|
1911
|
+
!InterveningIvarReset.after_operand(node.value, scope).ivar(name).equal?(scope.ivar(name))
|
|
1912
|
+
end
|
|
1913
|
+
|
|
1914
|
+
# The scope a compound index write's rvalue runs from: Ruby evaluates the receiver, then the index arguments,
|
|
1915
|
+
# then the rvalue, so a write in either is threaded in that order ({#thread_operand}) before the rvalue. An
|
|
1916
|
+
# index write takes no block-pass argument (a syntax error since Ruby 3.4).
|
|
1917
|
+
#
|
|
1918
|
+
# Issue #1272 — `(h ||= {})[k] ||= v`, `(@h ||= {})[k] ||= v` and `h[(i = 1)] ||= v` left `h`, `@h` and `i`
|
|
1919
|
+
# on their pre-write bindings, so a later `h.fetch(k)` on a nil-initialised `h` reported a call on `nil`. A
|
|
1920
|
+
# receiver or argument holding no write answers the entry scope unchanged.
|
|
1921
|
+
#
|
|
1922
|
+
# The receiver's joinability is read from this scope ({#index_write_arg_types}' `join_scope`), since the
|
|
1923
|
+
# binding the store widens is the one the receiver's write left (`h` is `{}`, not the entry's `nil`). The read,
|
|
1924
|
+
# the stored value and the widening's argument types are still typed from the entry scope (TODO #3100): the
|
|
1925
|
+
# receiver expression's own value (`(h ||= {})` is the hash) answers what its write produced, but an rvalue
|
|
1926
|
+
# reading a variable an index argument writes (`h[(i = 1)] ||= i + 1`) is typed at its pre-write binding.
|
|
1927
|
+
def index_write_operand_scope(node)
|
|
1928
|
+
walk = OperandWalk.new(walk_recorder)
|
|
1929
|
+
after = thread_operand(node.receiver, scope, walk, scope)
|
|
1930
|
+
thread_operand(node.arguments, after, walk, scope)
|
|
1931
|
+
end
|
|
1932
|
+
|
|
1825
1933
|
# `[index_type..., stored_value_type]` for an index-write node, shaped exactly like a `[]=`
|
|
1826
1934
|
# call's argument list so the widening seam can join it the same way (issue #560) — a
|
|
1827
1935
|
# two-index compound write (`a[0, 1] += v`) keeps BOTH index arguments ahead of the stored
|
|
@@ -1835,15 +1943,16 @@ module Rigor
|
|
|
1835
1943
|
# The index arguments are typed, and the receiver's joinability read, in `type_scope`: the
|
|
1836
1944
|
# evaluator's entry scope by default; a `for` index passes its post-collection scope and a
|
|
1837
1945
|
# rescue reference its arm's entry scope, the nearest the engine has to where Ruby evaluates
|
|
1838
|
-
# them (each iteration, the moment of the catch).
|
|
1946
|
+
# them (each iteration, the moment of the catch). A compound index write reads the joinability alone in
|
|
1947
|
+
# `join_scope`, the scope its receiver and index arguments left ({#index_write_operand_scope}).
|
|
1839
1948
|
# There is deliberately NO `rescue` here. `Scope#type_of` is a total query over well-formed Prism input,
|
|
1840
1949
|
# so a raise is an engine bug, and swallowing it would silently downgrade a live seam to "no evidence" —
|
|
1841
1950
|
# the join would quietly stop happening with nothing to show for it. Let it reach the runner's
|
|
1842
1951
|
# internal-error path, where it is visible.
|
|
1843
|
-
def index_write_arg_types(node, stored_type, type_scope: scope)
|
|
1952
|
+
def index_write_arg_types(node, stored_type, type_scope: scope, join_scope: type_scope)
|
|
1844
1953
|
args = node.arguments
|
|
1845
1954
|
return MutationWidening::NO_ARG_TYPES if args.nil? || stored_type.nil?
|
|
1846
|
-
return MutationWidening::NO_ARG_TYPES unless MutationWidening.joinable_receiver?(node.receiver,
|
|
1955
|
+
return MutationWidening::NO_ARG_TYPES unless MutationWidening.joinable_receiver?(node.receiver, join_scope)
|
|
1847
1956
|
|
|
1848
1957
|
list = args.respond_to?(:arguments) ? args.arguments : args
|
|
1849
1958
|
# A splat argument is marked `nil` — its expansion decides the store's arity at
|
|
@@ -1951,15 +2060,70 @@ module Rigor
|
|
|
1951
2060
|
# receiver is an element read into a local, that element's pin inside its container
|
|
1952
2061
|
# ({ElementReadWidening.widen_element_read}, issue #643) — and, issue #2499, the String a stdlib
|
|
1953
2062
|
# out-buffer call fills ({OutBufferMutation}), which names no receiver of the call at all.
|
|
1954
|
-
|
|
2063
|
+
#
|
|
2064
|
+
# Issue #2335 — a literal-symbol `send` (`a.send(:push, x)`) widens as the call it makes, and a mutation through a
|
|
2065
|
+
# receiver that may be another local's object floors every local whose object another holder may reach
|
|
2066
|
+
# ({AliasMutationFloor}).
|
|
2067
|
+
def widen_mutated_receivers(node, current_scope)
|
|
2068
|
+
call_node = AliasMutationFloor.literal_send_target(node) || node
|
|
1955
2069
|
arg_types = mutator_arg_types(call_node, current_scope)
|
|
1956
2070
|
widened = MutationWidening.widen_after_call(call_node: call_node, current_scope: current_scope,
|
|
1957
2071
|
arg_types: arg_types,
|
|
1958
2072
|
rewritten_element: rewritten_element(call_node, current_scope))
|
|
1959
2073
|
widened = ElementReadWidening.widen_element_read(call_node: call_node, current_scope: widened,
|
|
1960
2074
|
arg_types: arg_types)
|
|
2075
|
+
if AliasMutationFloor.candidate?(call_node)
|
|
2076
|
+
widened = AliasMutationFloor.floor_after_call(call_node, widened) do
|
|
2077
|
+
named_receiver_locals(call_node.receiver)
|
|
2078
|
+
end
|
|
2079
|
+
end
|
|
1961
2080
|
# Issue #2499 — a stdlib call that fills a String argument (`io.read(n, buf)`, `StringIO.new(s)`).
|
|
1962
|
-
OutBufferMutation.floor(call_node, widened)
|
|
2081
|
+
floor_ivar_mutating_callee(node, OutBufferMutation.floor(call_node, widened))
|
|
2082
|
+
end
|
|
2083
|
+
|
|
2084
|
+
# Issue #2335 — a call on a local that may hold another local's object (`box = Box.new(a)`) to a project method
|
|
2085
|
+
# that mutates one of its instance variables (`def add(v) = @items << v`) may mutate that object: every local
|
|
2086
|
+
# whose object another holder may reach takes one unknown store ({AliasMutationFloor.floor_after_written_local}).
|
|
2087
|
+
def floor_ivar_mutating_callee(call_node, current_scope)
|
|
2088
|
+
receiver = call_node.receiver
|
|
2089
|
+
return current_scope if MutationWidening::SHAPE_MUTATORS.include?(call_node.name) || send_hand_off?(call_node)
|
|
2090
|
+
return floor_self_ivar_callee(call_node, current_scope) if self_dispatch_call?(call_node)
|
|
2091
|
+
return current_scope unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
2092
|
+
return current_scope unless callee_bearing?(current_scope.local(receiver.name))
|
|
2093
|
+
return current_scope unless AliasMutationFloor.reaches_other_locals?(current_scope, receiver, call_node)
|
|
2094
|
+
|
|
2095
|
+
callees = receiver_callees_in(scope, call_node)
|
|
2096
|
+
return current_scope unless callees.any? { |def_node, _, _| AliasMutationFloor.mutates_ivar?(def_node) }
|
|
2097
|
+
|
|
2098
|
+
AliasMutationFloor.floor_after_written_local(current_scope, receiver, call_node)
|
|
2099
|
+
end
|
|
2100
|
+
|
|
2101
|
+
# The same for a call on `self` (`push_it(v)`, `self.push_it(v)`): its ivars hold whatever locals were handed to
|
|
2102
|
+
# them (`@items = a`, `a = @q`), so every aliased local holding a literal shape takes one unknown store.
|
|
2103
|
+
def floor_self_ivar_callee(call_node, current_scope)
|
|
2104
|
+
return current_scope unless AliasMutationFloor.holder_call_reaches?(current_scope, call_node)
|
|
2105
|
+
return current_scope unless resolve_self_callees(call_node).any? do |def_node, _, _|
|
|
2106
|
+
AliasMutationFloor.mutates_ivar?(def_node)
|
|
2107
|
+
end
|
|
2108
|
+
|
|
2109
|
+
AliasMutationFloor.floor_after_holder_call(current_scope, call_node)
|
|
2110
|
+
end
|
|
2111
|
+
|
|
2112
|
+
# A binding `receiver_callees_in` can resolve a project def on: a class or instance type, or a union of them. Any
|
|
2113
|
+
# other binding resolves none, so the floor skips the resolution for it.
|
|
2114
|
+
def callee_bearing?(type) = type.is_a?(Type::Nominal) || type.is_a?(Type::Singleton) || type.is_a?(Type::Union)
|
|
2115
|
+
|
|
2116
|
+
# The locals a mutator's receiver names ({ReceiverAlias.mutated_reads}), which the named path widens, and the
|
|
2117
|
+
# root of an element read (`c[0].shift`), whose element {ElementReadWidening} widens.
|
|
2118
|
+
def named_receiver_locals(receiver)
|
|
2119
|
+
return nil if receiver.nil?
|
|
2120
|
+
return receiver.name if receiver.is_a?(Prism::LocalVariableReadNode)
|
|
2121
|
+
|
|
2122
|
+
names = ReceiverAlias.mutated_reads(receiver).filter_map do |read|
|
|
2123
|
+
ReceiverAlias.read_name(read) if read.is_a?(Prism::LocalVariableReadNode)
|
|
2124
|
+
end
|
|
2125
|
+
root = ElementReadWidening.element_read_path(receiver)&.first
|
|
2126
|
+
root ? names << root.name : names
|
|
1963
2127
|
end
|
|
1964
2128
|
|
|
1965
2129
|
# Issue #2427 — the element an element rewriter (`map!`, `collect!`, `flatten!`) leaves in its receiver, read
|
|
@@ -2068,14 +2232,17 @@ module Rigor
|
|
|
2068
2232
|
[scope.type_of(node, tracer: tracer), forget_rebound_specials(widened, node)]
|
|
2069
2233
|
end
|
|
2070
2234
|
|
|
2071
|
-
# The scope effect of calling the writer `writer` on `receiver` outside a `CallNode`: the receiver widening,
|
|
2072
|
-
#
|
|
2235
|
+
# The scope effect of calling the writer `writer` on `receiver` outside a `CallNode`: the receiver widening, the
|
|
2236
|
+
# drop of every method-chain narrowing rooted at the receiver that `eval_call` makes after any call on it (issue
|
|
2237
|
+
# #2693: `x.r ||= v`, `x.r, y = …` and `for x.r in …` call `r=`), and the receiver-wide drop of recorded
|
|
2238
|
+
# `receiver[key]` narrowings `IndexedNarrowing` makes after a mutator call.
|
|
2073
2239
|
def widen_attribute_write(receiver, writer, current_scope)
|
|
2074
2240
|
widened = MutationWidening.widen_receiver_aliases(receiver, writer, current_scope)
|
|
2075
2241
|
stable = IndexedNarrowing.stable_receiver(receiver)
|
|
2076
|
-
return widened unless stable
|
|
2242
|
+
return widened unless stable
|
|
2077
2243
|
|
|
2078
|
-
widened.
|
|
2244
|
+
widened = widened.without_method_chain_narrowings_for(*stable)
|
|
2245
|
+
IndexedNarrowing.mutator?(writer) ? widened.without_indexed_narrowings_for(*stable) : widened
|
|
2079
2246
|
end
|
|
2080
2247
|
|
|
2081
2248
|
# The attribute targets of a multi-write (`h.default, x = 0, 1`), nested ones included, each widening its
|
|
@@ -2155,11 +2322,7 @@ module Rigor
|
|
|
2155
2322
|
return [Type::Combinator.union(then_type, else_type), then_scope] \
|
|
2156
2323
|
if branch_terminates?(node.subsequent, else_type) && node.statements
|
|
2157
2324
|
|
|
2158
|
-
[
|
|
2159
|
-
Type::Combinator.union(then_type, else_type),
|
|
2160
|
-
UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred,
|
|
2161
|
-
[node.statements, node.subsequent])
|
|
2162
|
-
]
|
|
2325
|
+
if_join(node.statements, node.subsequent, [then_type, then_scope], [else_type, else_scope], post_pred)
|
|
2163
2326
|
end
|
|
2164
2327
|
|
|
2165
2328
|
# `unless pred; t; else; e; end`. Same shape as `if`, but Prism exposes the else-branch as `else_clause` (no elsif
|
|
@@ -2191,11 +2354,22 @@ module Rigor
|
|
|
2191
2354
|
return [Type::Combinator.union(then_type, else_type), then_scope] \
|
|
2192
2355
|
if branch_terminates?(node.else_clause, else_type) && node.statements
|
|
2193
2356
|
|
|
2194
|
-
[
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2357
|
+
if_join(node.statements, node.else_clause, [then_type, then_scope], [else_type, else_scope], post_pred)
|
|
2358
|
+
end
|
|
2359
|
+
|
|
2360
|
+
# The `[type, post_scope]` an `if` / `unless` with both branches evaluated ends with, once the arms above found no
|
|
2361
|
+
# terminating branch by {#branch_terminates?}.
|
|
2362
|
+
#
|
|
2363
|
+
# Issue #1230 — the then-body (`then_node`) certainly exits beside an else (`else_node`): the only surviving path
|
|
2364
|
+
# is the else that RAN, so the continuation is the else's exit scope, the predicate's other edge PLUS the else's
|
|
2365
|
+
# writes, not a join with the exited body's writes. {#branch_certainly_exits?} is stricter than
|
|
2366
|
+
# {#branch_terminates?}, which the arms above use: a `bot`-typed helper call does not qualify (issue #3110).
|
|
2367
|
+
# Otherwise both exit scopes join with nil-injection.
|
|
2368
|
+
def if_join(then_node, else_node, (then_type, then_scope), (else_type, else_scope), post_pred)
|
|
2369
|
+
type = Type::Combinator.union(then_type, else_type)
|
|
2370
|
+
return [type, else_scope] if else_node && branch_certainly_exits?(then_node)
|
|
2371
|
+
|
|
2372
|
+
[type, UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred, [then_node, else_node])]
|
|
2199
2373
|
end
|
|
2200
2374
|
|
|
2201
2375
|
# Returns the `[type, post_scope]` of the live branch when the predicate is provably truthy / falsey, else nil so
|
|
@@ -2580,9 +2754,12 @@ module Rigor
|
|
|
2580
2754
|
# while it ran, put back as this scope binds them ({ErrorInfo.restore}): the control those carry has left every
|
|
2581
2755
|
# rescue clause the construct entered.
|
|
2582
2756
|
def restoring_error_info
|
|
2583
|
-
|
|
2757
|
+
state = typing_state
|
|
2758
|
+
marks = [@next_scope_sink&.size, state[BREAK_SINK]&.size]
|
|
2584
2759
|
type, after = yield
|
|
2585
|
-
|
|
2760
|
+
break_sink = state[BREAK_SINK]
|
|
2761
|
+
state.touch(BREAK_SINK) if break_sink && marks.last && marks.last < break_sink.size
|
|
2762
|
+
[[@next_scope_sink, marks.first], [break_sink, marks.last]].each do |sink, mark|
|
|
2586
2763
|
next if sink.nil? || mark.nil?
|
|
2587
2764
|
|
|
2588
2765
|
(mark...sink.size).each do |index|
|
|
@@ -2605,9 +2782,12 @@ module Rigor
|
|
|
2605
2782
|
# after it, or a rescue clause, may have set `$_` enters with it forgotten; a rescue clause runs after any
|
|
2606
2783
|
# prefix of the body, and so reads it forgotten whenever the body may set it.
|
|
2607
2784
|
entry = edge ? forget_guard_if_rebinds(LastLine.forget_if_set(scope, node), node) : scope
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2785
|
+
rescue_entry = rescue_entry_scope(node, entry)
|
|
2786
|
+
collector = begin_fact_collector(node, rescue_entry, edge)
|
|
2787
|
+
primary_type, primary_scope = eval_begin_primary_under(node, entry, edge: edge, collector: collector)
|
|
2788
|
+
rescue_chain = collect_rescue_chain_results(node.rescue_clause, rescue_entry,
|
|
2789
|
+
edge: edge, writes: rescue_writes(node, edge),
|
|
2790
|
+
raise_scopes: collector&.raise_scopes)
|
|
2611
2791
|
|
|
2612
2792
|
# B2.1 — retry-edge widening. When a `retry` in the rescue chain targets this `begin`, control re-enters the
|
|
2613
2793
|
# primary body carrying every rebind made before the retry: the arm's (`rescue; tries += 1; retry; end`), and
|
|
@@ -2703,7 +2883,10 @@ module Rigor
|
|
|
2703
2883
|
# none, so an `ensure` clause inside it, nested at any depth, is evaluated once rather than once per pass of
|
|
2704
2884
|
# every enclosing clause holding a jump.
|
|
2705
2885
|
def jump_sinks_installed?
|
|
2706
|
-
|
|
2886
|
+
return true unless @next_scope_sink.nil?
|
|
2887
|
+
|
|
2888
|
+
state = typing_state
|
|
2889
|
+
JUMP_SINKS.any? { |slot| state[slot] }
|
|
2707
2890
|
end
|
|
2708
2891
|
|
|
2709
2892
|
# Whether `node` holds a `return`, `next` or `break` outside a nested `def`.
|
|
@@ -2759,12 +2942,8 @@ module Rigor
|
|
|
2759
2942
|
end
|
|
2760
2943
|
|
|
2761
2944
|
# Runs `block` with no `return`, `next`, `break` or retry-frame sink installed.
|
|
2762
|
-
def without_jump_sinks
|
|
2763
|
-
|
|
2764
|
-
CLAUSE_SINK_KEYS.each { |key| Thread.current[key] = nil }
|
|
2765
|
-
yield
|
|
2766
|
-
ensure
|
|
2767
|
-
CLAUSE_SINK_KEYS.zip(previous).each { |key, value| Thread.current[key] = value } if previous
|
|
2945
|
+
def without_jump_sinks(&)
|
|
2946
|
+
TypingState.current.without(CLAUSE_SINKS, &)
|
|
2768
2947
|
end
|
|
2769
2948
|
|
|
2770
2949
|
# The scope a rescue clause of `node` enters with: it runs after any prefix of the body, so neither a `$_`
|
|
@@ -2773,6 +2952,65 @@ module Rigor
|
|
|
2773
2952
|
forget_guard_if_rebinds(LastLine.forget_if_set(entry, node.statements), node.statements)
|
|
2774
2953
|
end
|
|
2775
2954
|
|
|
2955
|
+
# Issue #2691 — a rescue clause runs after any prefix of the `begin` body raised, so a fact the clause's
|
|
2956
|
+
# entry scope holds for an indexed read (`h[:k]`) or a single-hop chain (`x.r`) survives into it only where
|
|
2957
|
+
# EVERY scope the body can raise from still binds the identical type object. `raise_scopes` is that set —
|
|
2958
|
+
# the post-scope of each statement of the body's frame and the scope it ends with
|
|
2959
|
+
# ({#eval_begin_primary_under}'s collector) — not the fall-through exit alone: `eval_if` and kin discard a
|
|
2960
|
+
# branch that terminates, so a write on `if c; @x.r = nil; raise; end` never reaches the exit scope.
|
|
2961
|
+
# `equal?` keeps only an untouched fact: a re-narrowed or re-joined one is a new object and drops (the
|
|
2962
|
+
# conservative side).
|
|
2963
|
+
#
|
|
2964
|
+
# The residual is a fact the body drops and then re-establishes as the identical (interned) type object:
|
|
2965
|
+
# raise points after the re-establishment keep it, which is correct, and the only unsound window would be a
|
|
2966
|
+
# raise point whose recorded scope still binds the object although the fact was invalidated and not yet
|
|
2967
|
+
# re-established. No producer reaches that today — a rebind or a clause widening answers `Dynamic[top]`,
|
|
2968
|
+
# never the fact's type ({ClauseWidening}), and a widened slot read floors to `Dynamic` rather than
|
|
2969
|
+
# reinstalling the recorded object.
|
|
2970
|
+
#
|
|
2971
|
+
# The filter covers rescue entries only: an `ensure` clause enters from the scope past the `begin` — the
|
|
2972
|
+
# join of the fall-through exits — so it keeps a fact a terminating-branch write voided, the pre-#2691
|
|
2973
|
+
# hole left open because collecting raise points for every `ensure`-only `begin` costs a frame walk.
|
|
2974
|
+
def clause_fact_scope(entry_scope, raise_scopes)
|
|
2975
|
+
return entry_scope if entry_scope.indexed_narrowings.empty? && entry_scope.method_chain_narrowings.empty?
|
|
2976
|
+
|
|
2977
|
+
indexed = entry_scope.indexed_narrowings.reduce(entry_scope) do |acc, (key, type)|
|
|
2978
|
+
if raise_scopes.all? { |point| point.indexed_narrowings[key].equal?(type) }
|
|
2979
|
+
acc
|
|
2980
|
+
else
|
|
2981
|
+
acc.without_indexed_narrowing(key.receiver_kind, key.receiver_name, key.key)
|
|
2982
|
+
end
|
|
2983
|
+
end
|
|
2984
|
+
indexed.method_chain_narrowings.reduce(indexed) do |acc, (key, type)|
|
|
2985
|
+
if raise_scopes.all? { |point| point.method_chain_narrowings[key].equal?(type) }
|
|
2986
|
+
acc
|
|
2987
|
+
else
|
|
2988
|
+
acc.without_method_chain_narrowing(key.receiver_kind, key.receiver_name, key.method_name)
|
|
2989
|
+
end
|
|
2990
|
+
end
|
|
2991
|
+
end
|
|
2992
|
+
|
|
2993
|
+
# Issue #2691 — the {BodyRaiseScopes} {#eval_begin_primary_under} fills while evaluating `node`'s primary
|
|
2994
|
+
# body, or nil when {#clause_fact_scope} has nothing to filter: no rescue clause, no body, or a `clause_entry`
|
|
2995
|
+
# (the scope the chain enters with — `rescue_entry_scope`'s result, or the retry-widened entry under an
|
|
2996
|
+
# edge) holding no indexed or method-chain fact. The frame walk is skipped unless a fact could drop, so a
|
|
2997
|
+
# `begin` whose entry binds none pays nothing.
|
|
2998
|
+
def begin_fact_collector(node, clause_entry, edge)
|
|
2999
|
+
return nil unless node.rescue_clause && node.statements
|
|
3000
|
+
return nil if clause_entry.indexed_narrowings.empty? && clause_entry.method_chain_narrowings.empty?
|
|
3001
|
+
|
|
3002
|
+
frame = edge ? edge.frame : primary_body_frame(node.statements)
|
|
3003
|
+
BodyRaiseScopes.new(frame: frame, raise_scopes: [])
|
|
3004
|
+
end
|
|
3005
|
+
|
|
3006
|
+
# The set {#walk_primary_frame} collects for a primary body — the nodes whose post-statement scopes are the
|
|
3007
|
+
# body's raise points — without the write-name pass a retry edge also needs.
|
|
3008
|
+
def primary_body_frame(statements)
|
|
3009
|
+
frame = Set.new.compare_by_identity
|
|
3010
|
+
walk_primary_frame(statements, true, frame, Set.new)
|
|
3011
|
+
frame
|
|
3012
|
+
end
|
|
3013
|
+
|
|
2776
3014
|
# Rescue arms that never fall through contribute neither a type fragment NOR a scope to the post-begin flow —
|
|
2777
3015
|
# control left the `begin` via that arm. That is an arm ending in `return`, `next`, `break`, `raise`, `throw`,
|
|
2778
3016
|
# `exit`, `abort` or `fail`, and one whose type is `bot` ({#branch_terminates?}): an arm ending in `retry`, or in
|
|
@@ -2791,7 +3029,7 @@ module Rigor
|
|
|
2791
3029
|
def ensure_jump_marks(node)
|
|
2792
3030
|
return nil unless node.ensure_clause
|
|
2793
3031
|
|
|
2794
|
-
[@next_scope_sink&.size,
|
|
3032
|
+
[@next_scope_sink&.size, typing_state[BREAK_SINK]&.size]
|
|
2795
3033
|
end
|
|
2796
3034
|
|
|
2797
3035
|
# A `next` or `break` inside `begin … ensure` leaves only once the `ensure` clause has run, so each jump scope the
|
|
@@ -2801,7 +3039,10 @@ module Rigor
|
|
|
2801
3039
|
def carry_jumps_through_ensure(ensure_clause, marks)
|
|
2802
3040
|
next_mark, break_mark = marks
|
|
2803
3041
|
carry_through_ensure(ensure_clause, @next_scope_sink, next_mark)
|
|
2804
|
-
|
|
3042
|
+
state = typing_state
|
|
3043
|
+
break_sink = state[BREAK_SINK]
|
|
3044
|
+
state.touch(BREAK_SINK) if break_sink && break_mark && break_mark < break_sink.size
|
|
3045
|
+
carry_through_ensure(ensure_clause, break_sink, break_mark)
|
|
2805
3046
|
end
|
|
2806
3047
|
|
|
2807
3048
|
def carry_through_ensure(ensure_clause, sink, mark)
|
|
@@ -2820,13 +3061,23 @@ module Rigor
|
|
|
2820
3061
|
# `edge`, when given, collects every scope the primary body could raise from: the scope after each statement of
|
|
2821
3062
|
# its frame ({#record_raise_points}), and the scope it ends with. The else-clause is not among them: what it
|
|
2822
3063
|
# raises is not rescued here.
|
|
2823
|
-
|
|
3064
|
+
#
|
|
3065
|
+
# `collector`, when given, is a {BodyRaiseScopes} the same recording fills for {#clause_fact_scope} — built
|
|
3066
|
+
# only when the clause entry still holds an indexed or method-chain fact a raise-point write could
|
|
3067
|
+
# invalidate ({#begin_fact_collector}). A retry `edge` collects the same scopes for its own widening; the
|
|
3068
|
+
# collector is separate so each evaluation pass filters against exactly its own raise points.
|
|
3069
|
+
#
|
|
3070
|
+
# Returns `[type, scope]`: the construct's value and exit scope (the else-clause's when present). The scope the
|
|
3071
|
+
# primary body alone ended with is recorded as a raise point before the `else` runs — the `else` runs only when
|
|
3072
|
+
# nothing raised, so its effects are no raise point's.
|
|
3073
|
+
def eval_begin_primary_under(node, entry_scope, edge: nil, collector: nil)
|
|
2824
3074
|
body_type, body_scope =
|
|
2825
3075
|
if node.statements
|
|
2826
|
-
with_raise_frame(edge) { sub_eval(node.statements, entry_scope) }
|
|
3076
|
+
with_raise_frame(edge, collector) { sub_eval(node.statements, entry_scope) }
|
|
2827
3077
|
else
|
|
2828
3078
|
[Type::Combinator.constant_of(nil), entry_scope]
|
|
2829
3079
|
end
|
|
3080
|
+
collector.raise_scopes << body_scope if collector
|
|
2830
3081
|
edge.raise_scopes << body_scope if edge
|
|
2831
3082
|
|
|
2832
3083
|
if node.else_clause
|
|
@@ -2850,6 +3101,13 @@ module Rigor
|
|
|
2850
3101
|
end
|
|
2851
3102
|
private_constant :RetryEdge
|
|
2852
3103
|
|
|
3104
|
+
# Issue #2691 — the raise-point scopes one evaluation of a `begin` primary body collects for
|
|
3105
|
+
# {#clause_fact_scope}: {RetryEdge}'s `frame` / `raise_scopes` pair without the retry bookkeeping, so
|
|
3106
|
+
# {#record_raise_points} records into it exactly as into an edge. `frame` is the body's own frame — shared
|
|
3107
|
+
# with the retry edge when one exists, since both walks collect the same nodes.
|
|
3108
|
+
BodyRaiseScopes = Data.define(:frame, :raise_scopes)
|
|
3109
|
+
private_constant :BodyRaiseScopes
|
|
3110
|
+
|
|
2853
3111
|
# The entry and edge one widening reads, whether it widens to the Nominal envelope or keeps literals, and, per
|
|
2854
3112
|
# name, the rebinds it has already weighed: a statement-by-statement body shares one binding object across every
|
|
2855
3113
|
# scope until the name is rebound, and weighing each copy again is what made a long body quadratic.
|
|
@@ -2858,17 +3116,29 @@ module Rigor
|
|
|
2858
3116
|
end
|
|
2859
3117
|
private_constant :RetryWidening
|
|
2860
3118
|
|
|
2861
|
-
# The thread-local stack of the
|
|
2862
|
-
|
|
2863
|
-
|
|
3119
|
+
# The thread-local stack of the raise-point frames whose primary body is being evaluated, innermost last, or
|
|
3120
|
+
# nil: a {RetryEdge} and/or a {BodyRaiseScopes}. The
|
|
3121
|
+
# frozen stack is the Frame; a statement's raise point recorded into a frame is a Resource touch
|
|
3122
|
+
# ({#record_raise_points}).
|
|
3123
|
+
RETRY_FRAMES = TypingState.frame(
|
|
3124
|
+
:retry_frames,
|
|
3125
|
+
contents: :resource, reason: "installed per retrying body; each raise point records into it",
|
|
3126
|
+
witness: "spec/fixtures/typing_state_witnesses/retry_frames.rb"
|
|
3127
|
+
)
|
|
3128
|
+
private_constant :RETRY_FRAMES
|
|
2864
3129
|
|
|
2865
3130
|
# The thread-local sinks a widened clause pass ({#without_jump_sinks}) leaves uninstalled.
|
|
2866
|
-
|
|
2867
|
-
private_constant :
|
|
3131
|
+
CLAUSE_SINKS = [RETURN_SINK, NEXT_SINK, BREAK_SINK, BREAK_VALUE_SINK, RETRY_FRAMES].freeze
|
|
3132
|
+
private_constant :CLAUSE_SINKS
|
|
2868
3133
|
|
|
2869
3134
|
# The thread-local sinks a `return`, `next` or `break` is collected into.
|
|
2870
|
-
|
|
2871
|
-
private_constant :
|
|
3135
|
+
JUMP_SINKS = (CLAUSE_SINKS - [RETRY_FRAMES]).freeze
|
|
3136
|
+
private_constant :JUMP_SINKS
|
|
3137
|
+
|
|
3138
|
+
# The sinks a caller's construct installs and a callee's body must not record into: a `next`, `break` or raise
|
|
3139
|
+
# point in the callee belongs to the callee's own constructs, never to the caller's block, loop or retrying body.
|
|
3140
|
+
CALLER_SINKS = [NEXT_SINK, BREAK_SINK, BREAK_VALUE_SINK, RETRY_FRAMES].freeze
|
|
3141
|
+
private_constant :CALLER_SINKS
|
|
2872
3142
|
|
|
2873
3143
|
# The retry edge of `node`, or nil when no `retry` in its rescue chain targets it. Allocation-free for a `begin`
|
|
2874
3144
|
# no `retry` targets.
|
|
@@ -2946,8 +3216,11 @@ module Rigor
|
|
|
2946
3216
|
end
|
|
2947
3217
|
|
|
2948
3218
|
def eval_begin_paths(node, entry, edge)
|
|
2949
|
-
|
|
2950
|
-
|
|
3219
|
+
collector = begin_fact_collector(node, entry, edge)
|
|
3220
|
+
primary_type, primary_scope = eval_begin_primary_under(node, entry, edge: edge, collector: collector)
|
|
3221
|
+
[primary_type, primary_scope,
|
|
3222
|
+
collect_rescue_chain_results(node.rescue_clause, entry,
|
|
3223
|
+
edge: edge, raise_scopes: collector&.raise_scopes)]
|
|
2951
3224
|
end
|
|
2952
3225
|
|
|
2953
3226
|
# B2.1 — the entry scope widened by what crosses the retry edge, or nil when nothing does. A local or ivar bound
|
|
@@ -2972,28 +3245,33 @@ module Rigor
|
|
|
2972
3245
|
widened
|
|
2973
3246
|
end
|
|
2974
3247
|
|
|
2975
|
-
# Runs `block` with `
|
|
2976
|
-
|
|
2977
|
-
|
|
3248
|
+
# Runs `block` with `frames` — a retry edge and/or a {BodyRaiseScopes}, nils skipped — on top of the
|
|
3249
|
+
# thread-local stack {#record_raise_points} reads.
|
|
3250
|
+
def with_raise_frame(first, second, &)
|
|
3251
|
+
return yield if first.nil? && second.nil?
|
|
2978
3252
|
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
ensure
|
|
2984
|
-
Thread.current[RETRY_FRAMES_KEY] = previous
|
|
2985
|
-
end
|
|
3253
|
+
frames = first && second ? [first, second] : [first || second]
|
|
3254
|
+
state = TypingState.current
|
|
3255
|
+
previous = state[RETRY_FRAMES]
|
|
3256
|
+
state.with(RETRY_FRAMES, (previous ? previous + frames : frames).freeze, &)
|
|
2986
3257
|
end
|
|
2987
3258
|
|
|
2988
|
-
# The scope after each statement of a
|
|
2989
|
-
# statement's being the one after
|
|
2990
|
-
#
|
|
2991
|
-
#
|
|
2992
|
-
#
|
|
3259
|
+
# The scope after each statement of a primary body's frame is a point the body can raise from, the next
|
|
3260
|
+
# statement's being the one after — into every frame on the stack: a retrying begin's edge, which carries
|
|
3261
|
+
# every rebind a retry can re-enter with, and a {BodyRaiseScopes}, which {#clause_fact_scope} intersects
|
|
3262
|
+
# over. Both see a rebind on a branch that then raises and so never reaches the body's exit scope
|
|
3263
|
+
# (`if bad; tries += 1; raise; end`), and a write threaded into the raising call's own operands
|
|
3264
|
+
# (`raise Retry.new(tries += 1) if flaky?`). A statement of a nested block or lambda body is not in the
|
|
3265
|
+
# frame (a block parameter can shadow the counter); the block's effect
|
|
2993
3266
|
# on this frame shows in the post-scope of the statement holding it. Recording from the evaluator rather than
|
|
2994
3267
|
# `on_enter` keeps a statement reached with the index recorder off (a threaded operand, a loop fixpoint pass).
|
|
2995
3268
|
def record_raise_points(frames, stmt, stmt_scope)
|
|
2996
|
-
frames.each
|
|
3269
|
+
frames.each do |collector|
|
|
3270
|
+
next unless collector.frame.include?(stmt)
|
|
3271
|
+
|
|
3272
|
+
typing_state.touch(RETRY_FRAMES)
|
|
3273
|
+
collector.raise_scopes << stmt_scope
|
|
3274
|
+
end
|
|
2997
3275
|
end
|
|
2998
3276
|
|
|
2999
3277
|
# An `on_enter` that records, besides forwarding to the installed one, the entry scope of each `retry` of `edge`
|
|
@@ -3150,7 +3428,14 @@ module Rigor
|
|
|
3150
3428
|
# Issue #1231 — `writes` widens the scope each clause enters with ({#widen_clause_entry}) before its
|
|
3151
3429
|
# reference is bound, so `rescue => e` still binds `e` to the exception. It is nil under a retry edge, which
|
|
3152
3430
|
# already carries every scope the primary body raises from ({#widen_entry_for_retry}).
|
|
3153
|
-
|
|
3431
|
+
# `raise_scopes`, when given, is every scope the primary body can raise from — the post-scope of each
|
|
3432
|
+
# statement of its frame and the scope it ends with ({#eval_begin_primary_under}'s collector): the rescue
|
|
3433
|
+
# entry keeps only the indexed and method-chain facts every one of them still binds verbatim
|
|
3434
|
+
# ({#clause_fact_scope}).
|
|
3435
|
+
def collect_rescue_chain_results(rescue_node, entry_scope, edge: nil, writes: nil, raise_scopes: nil)
|
|
3436
|
+
return [] if rescue_node.nil?
|
|
3437
|
+
|
|
3438
|
+
entry_scope = clause_fact_scope(entry_scope, raise_scopes) if raise_scopes
|
|
3154
3439
|
on_enter = edge ? retry_scope_recorder(edge) : @on_enter
|
|
3155
3440
|
results = []
|
|
3156
3441
|
current = rescue_node
|
|
@@ -3198,7 +3483,7 @@ module Rigor
|
|
|
3198
3483
|
# True when the clause's scope `after` still binds `$_` to the `Dynamic[top]` the entry gave it and the clause
|
|
3199
3484
|
# holds nothing that may set it ({LastLine.may_set?}).
|
|
3200
3485
|
def last_line_kept?(after, node)
|
|
3201
|
-
after.global(:$_)
|
|
3486
|
+
Type::Combinator.untyped?(after.global(:$_)) && !LastLine.may_set?(node.statements, scope)
|
|
3202
3487
|
end
|
|
3203
3488
|
|
|
3204
3489
|
# Issue #1889 — {#eval_for} with a literal-name `instance_variable_set` in the body joining its value into the
|
|
@@ -3472,15 +3757,9 @@ module Rigor
|
|
|
3472
3757
|
# Installs a fresh thread-local break sink around `yield` (a loop-body evaluation), returning `[collected,
|
|
3473
3758
|
# yield_result]`. Stacks: the previous sink is restored on exit so a nested loop's breaks do not leak to the
|
|
3474
3759
|
# enclosing loop.
|
|
3475
|
-
def collect_break_scopes
|
|
3476
|
-
previous = Thread.current[BREAK_SINK_KEY]
|
|
3760
|
+
def collect_break_scopes(&)
|
|
3477
3761
|
sink = []
|
|
3478
|
-
|
|
3479
|
-
begin
|
|
3480
|
-
result = yield
|
|
3481
|
-
ensure
|
|
3482
|
-
Thread.current[BREAK_SINK_KEY] = previous
|
|
3483
|
-
end
|
|
3762
|
+
result = TypingState.current.with(BREAK_SINK, sink, &)
|
|
3484
3763
|
[sink, result]
|
|
3485
3764
|
end
|
|
3486
3765
|
|
|
@@ -3557,7 +3836,7 @@ module Rigor
|
|
|
3557
3836
|
# from the widened binding reaches it ({#loop_break_arms}); without it the method inferred `true` and a
|
|
3558
3837
|
# caller's test on it folded always-truthy on correct code.
|
|
3559
3838
|
def later_pass_returns?(statements)
|
|
3560
|
-
!
|
|
3839
|
+
!typing_state[RETURN_SINK].nil? && ReturnBarrier.exits_method?(statements)
|
|
3561
3840
|
end
|
|
3562
3841
|
|
|
3563
3842
|
# Joins each `break` arm's body-written local bindings into the loop continuation, so a `break`-path binding the
|
|
@@ -3569,12 +3848,11 @@ module Rigor
|
|
|
3569
3848
|
def join_break_scopes(continuation, breaks, names)
|
|
3570
3849
|
return continuation if breaks.empty? || names.empty?
|
|
3571
3850
|
|
|
3572
|
-
floor = Type::Combinator.untyped
|
|
3573
3851
|
breaks.reduce(continuation) do |cont, break_scope|
|
|
3574
3852
|
names.reduce(cont) do |acc, name|
|
|
3575
3853
|
break_value = CapturedLocals.bound_type(break_scope, name)
|
|
3576
3854
|
current = CapturedLocals.bound_type(acc, name)
|
|
3577
|
-
next acc if break_value.nil? || current
|
|
3855
|
+
next acc if break_value.nil? || Type::Combinator.untyped?(current)
|
|
3578
3856
|
|
|
3579
3857
|
bind_capture(acc, name, current ? Type::Combinator.union(current, break_value) : break_value)
|
|
3580
3858
|
end
|
|
@@ -3965,7 +4243,7 @@ module Rigor
|
|
|
3965
4243
|
# signature, unknown receiver) the structural answer stands.
|
|
3966
4244
|
def for_iteration_element_type(coll_type)
|
|
3967
4245
|
structural = collection_element_type(coll_type)
|
|
3968
|
-
return structural unless
|
|
4246
|
+
return structural unless Type::Combinator.untyped?(structural)
|
|
3969
4247
|
|
|
3970
4248
|
block_params = MethodDispatcher.expected_block_param_types(
|
|
3971
4249
|
receiver_type: coll_type,
|
|
@@ -3999,13 +4277,17 @@ module Rigor
|
|
|
3999
4277
|
scope.with_ivar(index_node.name, element_type).without_ivar_guard(index_node.name)
|
|
4000
4278
|
when Prism::GlobalVariableTargetNode
|
|
4001
4279
|
scope.with_global(index_node.name, element_type)
|
|
4280
|
+
when Prism::ClassVariableTargetNode
|
|
4281
|
+
scope.with_cvar(index_node.name, element_type)
|
|
4002
4282
|
when Prism::IndexTargetNode
|
|
4003
4283
|
widen_index_target(index_node, element_type, scope, type_scope: scope)
|
|
4284
|
+
when Prism::CallTargetNode
|
|
4285
|
+
widen_attribute_write(index_node.receiver, index_node.name, scope)
|
|
4004
4286
|
when Prism::MultiTargetNode
|
|
4005
4287
|
bound = MultiTargetBinder.bind_marked(index_node, element_type, scope: scope)
|
|
4006
|
-
widen_index_targets(bound, bound.apply_to(scope), type_scope: scope)
|
|
4288
|
+
widen_attribute_targets(index_node, widen_index_targets(bound, bound.apply_to(scope), type_scope: scope))
|
|
4007
4289
|
when Prism::SplatNode
|
|
4008
|
-
bind_for_splat_index(index_node, scope)
|
|
4290
|
+
bind_for_splat_index(index_node, element_type, scope)
|
|
4009
4291
|
else
|
|
4010
4292
|
scope
|
|
4011
4293
|
end
|
|
@@ -4013,13 +4295,29 @@ module Rigor
|
|
|
4013
4295
|
|
|
4014
4296
|
# `for *h[:a] in pairs` — Prism gives a bare splat index as a `SplatNode`, not a `MultiTargetNode`, so the
|
|
4015
4297
|
# binder never sees it. The store is `*h[:a] = element`, an array of the element's `to_ary` parts; the receiver
|
|
4016
|
-
# widens with the binder's floor for a rest it cannot decompose, `Dynamic[top]`.
|
|
4017
|
-
#
|
|
4018
|
-
|
|
4298
|
+
# widens with the binder's floor for a rest it cannot decompose, `Dynamic[top]`. `for *x.r in pairs` calls `r=`
|
|
4299
|
+
# and widens as `for x.r in` does (issue #2693).
|
|
4300
|
+
#
|
|
4301
|
+
# Issue #1217 — a bare `*a` / `*@a` / `*@@a` / `*$a` binds the name to the rest
|
|
4302
|
+
# {MultiTargetBinder.decompose_slots} reads off the element: a `Tuple` element exactly, an `Array[T]` element as
|
|
4303
|
+
# `Array[T]`, and anything else — a union, a value Ruby would wrap as `[element]` — the `Dynamic[top]` floor.
|
|
4304
|
+
# Left unbound, a read after the loop folded on the pre-loop binding.
|
|
4305
|
+
def bind_for_splat_index(splat, element_type, scope)
|
|
4019
4306
|
target = splat.expression
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4307
|
+
case target
|
|
4308
|
+
when Prism::CallTargetNode
|
|
4309
|
+
widen_attribute_write(target.receiver, target.name, scope)
|
|
4310
|
+
when Prism::IndexTargetNode
|
|
4311
|
+
widen_index_target(target, Type::Combinator.untyped, scope, type_scope: scope)
|
|
4312
|
+
when Prism::LocalVariableTargetNode, Prism::InstanceVariableTargetNode,
|
|
4313
|
+
Prism::ClassVariableTargetNode, Prism::GlobalVariableTargetNode
|
|
4314
|
+
_, rest, = MultiTargetBinder.decompose_slots(
|
|
4315
|
+
element_type, front_count: 0, back_count: 0, rest_present: true, scope: scope
|
|
4316
|
+
)
|
|
4317
|
+
bind_for_index(target, rest, scope)
|
|
4318
|
+
else
|
|
4319
|
+
scope
|
|
4320
|
+
end
|
|
4023
4321
|
end
|
|
4024
4322
|
|
|
4025
4323
|
# Extracts the per-iteration element type from a collection carrier. `Tuple[T1..Tn]` yields the union of its
|
|
@@ -4356,13 +4654,9 @@ module Rigor
|
|
|
4356
4654
|
# false positives in `prism-1.9.0`'s auto-generated `copy` methods alone. A nested `def` is a return barrier:
|
|
4357
4655
|
# its body's `return`s belong to the inner method, not the one currently being inferred. Suspend the return sink
|
|
4358
4656
|
# across the nested body so `eval_return` does not record them into the outer method's return type.
|
|
4359
|
-
|
|
4360
|
-
Thread.current[RETURN_SINK_KEY] = nil
|
|
4361
|
-
begin
|
|
4657
|
+
typing_state.with(RETURN_SINK, nil) do
|
|
4362
4658
|
sub_eval(node.parameters, body_scope, class_context: @class_context) if node.parameters
|
|
4363
4659
|
sub_eval(node.body, body_scope, class_context: @class_context) if node.body
|
|
4364
|
-
ensure
|
|
4365
|
-
Thread.current[RETURN_SINK_KEY] = outer_sink
|
|
4366
4660
|
end
|
|
4367
4661
|
[Type::Combinator.constant_of(node.name), scope]
|
|
4368
4662
|
end
|
|
@@ -5156,8 +5450,15 @@ module Rigor
|
|
|
5156
5450
|
narrowed = Narrowing.safe_navigation_block_scope(node, scope)
|
|
5157
5451
|
return enter_call_block(node, block) if narrowed.equal?(scope)
|
|
5158
5452
|
|
|
5159
|
-
evaluator_at(narrowed, operand_scope: operand_scope, operand_types: @operand_types)
|
|
5160
|
-
|
|
5453
|
+
narrowed_evaluator = evaluator_at(narrowed, operand_scope: operand_scope, operand_types: @operand_types)
|
|
5454
|
+
narrowed_evaluator.send(:enter_call_block_carrying, node, block, @value_position_block)
|
|
5455
|
+
end
|
|
5456
|
+
|
|
5457
|
+
# The safe-navigation arm of {#evaluate_block_if_present}: the narrowed evaluator enters the block carrying the
|
|
5458
|
+
# value-position mark {#evaluate_block_of} set on this one, which {#block_self_entry} reads.
|
|
5459
|
+
def enter_call_block_carrying(node, block, value_position_block)
|
|
5460
|
+
@value_position_block = value_position_block
|
|
5461
|
+
enter_call_block(node, block)
|
|
5161
5462
|
end
|
|
5162
5463
|
|
|
5163
5464
|
def enter_call_block(node, block)
|
|
@@ -5215,14 +5516,8 @@ module Rigor
|
|
|
5215
5516
|
end
|
|
5216
5517
|
|
|
5217
5518
|
# Runs the block with the method's return sink suspended, for a body whose `return` is not the method's.
|
|
5218
|
-
def without_return_sink
|
|
5219
|
-
|
|
5220
|
-
Thread.current[RETURN_SINK_KEY] = nil
|
|
5221
|
-
begin
|
|
5222
|
-
yield
|
|
5223
|
-
ensure
|
|
5224
|
-
Thread.current[RETURN_SINK_KEY] = outer_sink
|
|
5225
|
-
end
|
|
5519
|
+
def without_return_sink(&)
|
|
5520
|
+
TypingState.current.with(RETURN_SINK, nil, &)
|
|
5226
5521
|
end
|
|
5227
5522
|
|
|
5228
5523
|
# Issue #963 — `define_method(:name) { ... }` in a class body defines an INSTANCE method, and Ruby runs
|
|
@@ -5693,12 +5988,13 @@ module Rigor
|
|
|
5693
5988
|
# binds to it floors: the body reads it anywhere but as a splat argument of a call (`args.first[:k] = 1`), or
|
|
5694
5989
|
# hands it on (`b(*args)`) to a call that resolves to a def which writes a positional parameter or has a rest
|
|
5695
5990
|
# parameter of its own that it hands on. A rest handed to a call that resolves to no user def hands off nothing,
|
|
5696
|
-
# as for any hand-off. A callee reached through an inherited lookup ({FLOOR_ALL})
|
|
5697
|
-
# parameters do
|
|
5991
|
+
# as for any hand-off. A callee reached through an inherited lookup ({FLOOR_ALL}) or one that may not be the def
|
|
5992
|
+
# the call runs ({FLOOR_EVERY}) reads as written, as its parameters do: neither owner is a {CalleeOwner}, so its
|
|
5993
|
+
# body's hand-offs cannot be read under it.
|
|
5698
5994
|
def rest_parameter_written?(def_node, owner)
|
|
5699
5995
|
return true if def_node.nil?
|
|
5700
5996
|
return false unless variable_arity?(def_node)
|
|
5701
|
-
return true if owner
|
|
5997
|
+
return true if [FLOOR_ALL, FLOOR_EVERY].include?(owner)
|
|
5702
5998
|
|
|
5703
5999
|
other, calls = def_census(:rest_uses, def_node) { rest_uses(def_node) }
|
|
5704
6000
|
return true if other
|
|
@@ -5774,12 +6070,15 @@ module Rigor
|
|
|
5774
6070
|
end
|
|
5775
6071
|
|
|
5776
6072
|
# `base_scope` with each local `node` passes to a parameter of `mutated` rebound to what the block answers for
|
|
5777
|
-
# its binding; a nil answer, or an answer equal to the binding, leaves it.
|
|
6073
|
+
# its binding; a nil answer, or an answer equal to the binding, leaves it. Issue #2335 — an argument that may be
|
|
6074
|
+
# another local's object (a block parameter, a copy) floors every local another holder may reach
|
|
6075
|
+
# ({AliasMutationFloor.floor_after_written_local}), a block-local one included.
|
|
5778
6076
|
def floor_arguments(node, mutated, base_scope, offset, climbing = nil)
|
|
5779
6077
|
arguments = arguments_for_slots(node, mutated, offset)
|
|
5780
6078
|
return base_scope if arguments.empty?
|
|
5781
6079
|
|
|
5782
6080
|
arguments.reduce(base_scope) do |acc, argument|
|
|
6081
|
+
acc = AliasMutationFloor.floor_after_written_local(acc, argument, node)
|
|
5783
6082
|
next acc unless acc.locals.key?(argument.name)
|
|
5784
6083
|
next acc if climbing && argument.depth <= climbing
|
|
5785
6084
|
|
|
@@ -5814,15 +6113,19 @@ module Rigor
|
|
|
5814
6113
|
def callee_chain_store
|
|
5815
6114
|
return NO_CHAIN_STORE if hand_off_memo_bypassed?
|
|
5816
6115
|
|
|
5817
|
-
table = (
|
|
6116
|
+
table = typing_state.weak_key_map(CHAIN_STORE)
|
|
5818
6117
|
self_type = scope.self_type
|
|
5819
6118
|
{ store: (table[scope.discovery] ||= {}.compare_by_identity),
|
|
5820
6119
|
ctx: [self_type.is_a?(Type::Singleton), enclosing_class_name_for(self_type)] }
|
|
5821
6120
|
end
|
|
5822
6121
|
|
|
5823
|
-
|
|
6122
|
+
CHAIN_STORE = TypingState.memo(
|
|
6123
|
+
:callee_chain_successors,
|
|
6124
|
+
run_scoped: true, audit: "successor",
|
|
6125
|
+
reason: "graph facts keyed by discovery, the same whichever census reads them"
|
|
6126
|
+
)
|
|
5824
6127
|
NO_CHAIN_STORE = {}.freeze
|
|
5825
|
-
private_constant :
|
|
6128
|
+
private_constant :CHAIN_STORE, :NO_CHAIN_STORE
|
|
5826
6129
|
|
|
5827
6130
|
# Issue #2296 — the `{ name => slot }` parameters the callee mutates inside a block in its body
|
|
5828
6131
|
# ({#own_block_content_parameters}), closed over hand-offs as {#callee_content_mutated_parameters} is.
|
|
@@ -5858,22 +6161,47 @@ module Rigor
|
|
|
5858
6161
|
# survive `with_self_type` and change what a constant in the def's body types as, so the census is no longer
|
|
5859
6162
|
# the def's alone, and the first caller's narrowing must not become every later caller's answer.
|
|
5860
6163
|
def resolved_census(kind, def_node, &)
|
|
6164
|
+
# An audit recomputation bypasses the memo but still opens the site's declaration (ADR-129 WD1).
|
|
6165
|
+
return MemoAudit.key_audit("resolved census", RESOLVED_CENSUS_KEY_AUDIT, &) if MemoAudit.suspended?
|
|
5861
6166
|
return yield if hand_off_memo_bypassed?
|
|
5862
6167
|
|
|
5863
6168
|
discovery = scope.discovery
|
|
5864
|
-
table = (
|
|
6169
|
+
table = typing_state.weak_key_map(RESOLVED_CENSUS)
|
|
5865
6170
|
slot = (table[discovery] ||= [{}, {}])
|
|
5866
6171
|
self_type = scope.self_type
|
|
5867
6172
|
by_class = (slot[self_type.is_a?(Type::Singleton) ? 1 : 0][kind] ||= {})
|
|
5868
6173
|
class_name = enclosing_class_name_for(self_type)
|
|
5869
6174
|
by_def = (by_class[class_name] ||= {}.compare_by_identity)
|
|
5870
|
-
|
|
6175
|
+
unless by_def.key?(def_node)
|
|
6176
|
+
return by_def[def_node] = MemoAudit.key_audit("resolved census", RESOLVED_CENSUS_KEY_AUDIT, &)
|
|
6177
|
+
end
|
|
5871
6178
|
|
|
5872
|
-
MemoAudit.check("resolved census", by_def[def_node], kind, class_name
|
|
6179
|
+
MemoAudit.check("resolved census", by_def[def_node], kind, class_name) do
|
|
6180
|
+
MemoAudit.key_audit("resolved census", RESOLVED_CENSUS_KEY_AUDIT, &)
|
|
6181
|
+
end
|
|
5873
6182
|
end
|
|
5874
6183
|
|
|
5875
|
-
|
|
5876
|
-
|
|
6184
|
+
RESOLVED_CENSUS = TypingState.memo(
|
|
6185
|
+
:callee_resolved_census,
|
|
6186
|
+
run_scoped: true, audit: "resolved census",
|
|
6187
|
+
reason: "keyed by discovery, side, kind, class and def, all its answer reads; audited by MemoAudit"
|
|
6188
|
+
)
|
|
6189
|
+
private_constant :RESOLVED_CENSUS
|
|
6190
|
+
|
|
6191
|
+
# ADR-129 WD1's key audit of a resolved-census miss. A census types a callee's receivers ({#receiver_callees_in})
|
|
6192
|
+
# under the frames its caller installed, and keys on none of them: a known gap, #2867.
|
|
6193
|
+
CENSUS_CALLER_FRAMES = %i[
|
|
6194
|
+
threaded_block_body key_presence_walk return_sink next_sink break_sink break_value_sink retry_frames
|
|
6195
|
+
interface_erasure interface_dispatch_off
|
|
6196
|
+
].freeze
|
|
6197
|
+
private_constant :CENSUS_CALLER_FRAMES
|
|
6198
|
+
RESOLVED_CENSUS_KEY_AUDIT = MemoAudit.key_declaration(
|
|
6199
|
+
gaps: CENSUS_CALLER_FRAMES.to_h do |name|
|
|
6200
|
+
[name, "#2867: the census types a callee's receivers under the caller's frames, and its key carries " \
|
|
6201
|
+
"none of them"]
|
|
6202
|
+
end
|
|
6203
|
+
).freeze
|
|
6204
|
+
private_constant :RESOLVED_CENSUS_KEY_AUDIT
|
|
5877
6205
|
|
|
5878
6206
|
# The parameters `def_node` mutates itself, from either channel: those escape-mutated inside a block (which may
|
|
5879
6207
|
# run later / repeatedly) AND those mutated directly in the method body during the call itself
|
|
@@ -6128,15 +6456,22 @@ module Rigor
|
|
|
6128
6456
|
|
|
6129
6457
|
# Issue #2104 — the per-thread memo of a census that reads nothing but `def_node`'s AST, under `kind`.
|
|
6130
6458
|
def def_census(kind, def_node, &)
|
|
6131
|
-
|
|
6459
|
+
# An audit recomputation bypasses the memo, so it reads no stored entry (ADR-127 WD3, #3071).
|
|
6460
|
+
return MemoAudit.key_audit("def census", &) if MemoAudit.suspended?
|
|
6461
|
+
|
|
6462
|
+
table = typing_state.weak_key_map(DEF_CENSUS)
|
|
6132
6463
|
entry = (table[def_node] ||= {})
|
|
6133
|
-
return entry[kind] =
|
|
6464
|
+
return entry[kind] = MemoAudit.key_audit("def census", &) unless entry.key?(kind)
|
|
6134
6465
|
|
|
6135
6466
|
MemoAudit.check("def census", entry[kind], kind, &)
|
|
6136
6467
|
end
|
|
6137
6468
|
|
|
6138
|
-
|
|
6139
|
-
|
|
6469
|
+
DEF_CENSUS = TypingState.memo(
|
|
6470
|
+
:callee_def_census,
|
|
6471
|
+
run_scoped: true, audit: "def census",
|
|
6472
|
+
reason: "keyed by def node, the only input it reads; audited by MemoAudit"
|
|
6473
|
+
)
|
|
6474
|
+
private_constant :DEF_CENSUS
|
|
6140
6475
|
|
|
6141
6476
|
# Issue #2104 — the {CalleeMutationChain::Edge}s along which `def_node` hands one of its parameters, unchanged, to
|
|
6142
6477
|
# a self-call resolving to a user def: a positional argument or a keyword value that reads the parameter itself
|
|
@@ -6748,14 +7083,19 @@ module Rigor
|
|
|
6748
7083
|
# True when at least one argument of `node` is a bare local-variable read (positional or keyword value) bound in
|
|
6749
7084
|
# the current scope — a cheap pre-filter so the def resolution / body scan only runs for calls that could actually
|
|
6750
7085
|
# floor something.
|
|
7086
|
+
# A local bound in the current scope, or (issue #2335) one whose object another local may share, which the
|
|
7087
|
+
# alias floor reads a callee's write through ({AliasMutationFloor.reaches_other_locals?}).
|
|
7088
|
+
def passed_local?(read, node)
|
|
7089
|
+
scope.locals.key?(read.name) || AliasMutationFloor.reaches_other_locals?(scope, read, node)
|
|
7090
|
+
end
|
|
7091
|
+
|
|
6751
7092
|
def call_passes_local_argument?(node)
|
|
6752
7093
|
args = node.arguments
|
|
6753
7094
|
return false unless args.respond_to?(:arguments)
|
|
6754
7095
|
|
|
6755
7096
|
args.arguments.any? do |arg|
|
|
6756
7097
|
case arg
|
|
6757
|
-
when Prism::LocalVariableReadNode
|
|
6758
|
-
scope.locals.key?(arg.name)
|
|
7098
|
+
when Prism::LocalVariableReadNode then passed_local?(arg, node)
|
|
6759
7099
|
when Prism::SplatNode
|
|
6760
7100
|
arg.expression.is_a?(Prism::LocalVariableReadNode) && scope.locals.key?(arg.expression.name)
|
|
6761
7101
|
when Prism::KeywordHashNode
|
|
@@ -6781,7 +7121,7 @@ module Rigor
|
|
|
6781
7121
|
while receiver.is_a?(Prism::CallNode)
|
|
6782
7122
|
if receiver.block.is_a?(Prism::BlockNode) &&
|
|
6783
7123
|
classify_closure_escape(receiver) != :non_escaping
|
|
6784
|
-
acc = widen_escaping_content_captures(receiver.block, acc)
|
|
7124
|
+
acc = widen_escaping_content_captures(receiver.block, acc, foreign_self: foreign_self_block?(receiver))
|
|
6785
7125
|
end
|
|
6786
7126
|
receiver = receiver.receiver
|
|
6787
7127
|
end
|
|
@@ -6799,7 +7139,14 @@ module Rigor
|
|
|
6799
7139
|
# `Array[Dynamic[top]]`, Hash -> `Hash[untyped, untyped]`, String -> `String`. The seed's element/key/value
|
|
6800
7140
|
# precision is forgotten; only the carrier survives. Read-only captures and locals the block merely rebinds
|
|
6801
7141
|
# (already floored by `drop_captured_narrowing`) are untouched.
|
|
6802
|
-
|
|
7142
|
+
#
|
|
7143
|
+
# An instance variable of this scope's `self` the body mutates in place is floored too (issue #1208): a callback
|
|
7144
|
+
# that runs `@opts[:format] = "json"` later left `@opts[:format]` folding to `"text"`, and so are a class variable
|
|
7145
|
+
# and a global (issue #3096). Each floors member by member ({#member_content_floor}, issue #3095), so a lazily
|
|
7146
|
+
# initialised `@buf = flag ? [1] : nil` keeps its `nil`. An instance variable under a block that runs on another
|
|
7147
|
+
# object is that object's, and so is every one when `foreign_self` says the closure itself does; a class
|
|
7148
|
+
# variable resolves lexically and a global is global, so neither depends on `self`.
|
|
7149
|
+
def widen_escaping_content_captures(block_node, post_scope, foreign_self: false)
|
|
6803
7150
|
body = block_node.body
|
|
6804
7151
|
return post_scope if body.nil?
|
|
6805
7152
|
|
|
@@ -6807,15 +7154,85 @@ module Rigor
|
|
|
6807
7154
|
# `local[k] = v` write, which `collect_content_mutations` cannot see (see below).
|
|
6808
7155
|
post_scope = floor_block_body_callee_escaped_args(body, post_scope)
|
|
6809
7156
|
|
|
6810
|
-
|
|
6811
|
-
|
|
7157
|
+
post_scope = collect_content_mutations(body).keys.reduce(post_scope) do |acc, name|
|
|
7158
|
+
floored = content_floor_for(CapturedLocals.bound_type(acc, name))
|
|
7159
|
+
floored.nil? ? acc : bind_mutated_capture(acc, name, floored)
|
|
7160
|
+
end
|
|
7161
|
+
|
|
7162
|
+
floor_non_local_contents(escaping_non_local_content_mutations(body, post_scope, foreign_self), post_scope)
|
|
7163
|
+
end
|
|
6812
7164
|
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
7165
|
+
# `scope` with each instance variable, class variable or global in `names` floored member by member.
|
|
7166
|
+
def floor_non_local_contents(names, scope)
|
|
7167
|
+
names.reduce(scope) do |acc, name|
|
|
7168
|
+
bound = CapturedLocals.bound_type(acc, name)
|
|
7169
|
+
floored = member_content_floor(bound)
|
|
7170
|
+
floored.equal?(bound) ? acc : CapturedLocals.bind(acc, name, floored)
|
|
6816
7171
|
end
|
|
6817
7172
|
end
|
|
6818
7173
|
|
|
7174
|
+
# `type` with each member {#content_floor_for} recognises floored to its bare carrier and every other member
|
|
7175
|
+
# kept (issue #3095): `Array[1]?` reads `Array[Dynamic[top]]?` and `3 | [1]` reads `3 | Array[Dynamic[top]]`,
|
|
7176
|
+
# so the floor neither drops the `nil` a callback that never runs leaves nor narrows the binding. `type` itself
|
|
7177
|
+
# when no member floors.
|
|
7178
|
+
def member_content_floor(type)
|
|
7179
|
+
members = type.is_a?(Type::Union) ? type.members : [type]
|
|
7180
|
+
floored = members.map { |member| content_floor_for(member) || member }
|
|
7181
|
+
return type if floored.each_with_index.all? { |member, index| member.equal?(members[index]) }
|
|
7182
|
+
|
|
7183
|
+
Type::Combinator.union(*floored)
|
|
7184
|
+
end
|
|
7185
|
+
|
|
7186
|
+
# The instance variables of `scope`'s `self`, the class variables and the globals an escaping body mutates in
|
|
7187
|
+
# place, on the terms {#capturable_non_local?} reads against `scope` — the continuation the floor writes into.
|
|
7188
|
+
def escaping_non_local_content_mutations(body, scope, foreign_self)
|
|
7189
|
+
names = nil
|
|
7190
|
+
Source::NodeWalker.each_with_ancestors(body) do |node, ancestors|
|
|
7191
|
+
name, content = non_local_shape_mutation(node)
|
|
7192
|
+
next if name.nil?
|
|
7193
|
+
next unless (content && capturable_non_local?(name, ancestors, scope, foreign_self: foreign_self)) ||
|
|
7194
|
+
block_defined_method_capture?(name, ancestors, scope, foreign_self)
|
|
7195
|
+
|
|
7196
|
+
(names ||= []) << name
|
|
7197
|
+
end
|
|
7198
|
+
names ? names.uniq : NO_REWRITES
|
|
7199
|
+
end
|
|
7200
|
+
|
|
7201
|
+
# `[name, content]` when `node` mutates an instance variable, class variable or global in place, else nil.
|
|
7202
|
+
# `content` says a content scan counts the mutation ({CONTENT_MUTATORS}, or an index write). A method the block
|
|
7203
|
+
# defines counts every name the straight-line widening responds to as well ({CALLEE_WRITE_MUTATORS}: `merge!`,
|
|
7204
|
+
# `map!`, `clear`, …), because its floor reads no evidence from the call (issue #3163).
|
|
7205
|
+
def non_local_shape_mutation(node)
|
|
7206
|
+
content = index_write?(node)
|
|
7207
|
+
unless content
|
|
7208
|
+
return nil unless node.is_a?(Prism::CallNode) && CALLEE_WRITE_MUTATORS.include?(node.name)
|
|
7209
|
+
|
|
7210
|
+
content = CONTENT_MUTATORS.include?(node.name)
|
|
7211
|
+
end
|
|
7212
|
+
receiver = node.receiver
|
|
7213
|
+
NON_LOCAL_READ_NODES.include?(receiver.class) ? [receiver.name, content] : nil
|
|
7214
|
+
end
|
|
7215
|
+
|
|
7216
|
+
# Issue #3163 — `post_scope` with every instance variable, class variable and global floored member by member
|
|
7217
|
+
# ({#member_content_floor}) that a method defined in `call_node`'s non-escaping block mutates in place
|
|
7218
|
+
# ({#block_defined_method_capture?}). The block, or anything it calls, may call that method before it returns:
|
|
7219
|
+
# `$buf = []; [1].each { def h; $buf << 1; end; h }` leaves `$buf` as `[1]`, and the content join reads no
|
|
7220
|
+
# evidence from a method body, which it types in no scope of the block's. No call is followed, so a method the
|
|
7221
|
+
# block defines and nothing calls floors too. An escaping block floors the same names
|
|
7222
|
+
# ({#escaping_non_local_content_mutations}).
|
|
7223
|
+
def floor_block_defined_method_mutations(call_node, block, post_scope)
|
|
7224
|
+
foreign = foreign_self_block?(call_node)
|
|
7225
|
+
names = nil
|
|
7226
|
+
Source::NodeWalker.each_with_ancestors(block.body) do |node, ancestors|
|
|
7227
|
+
name, = non_local_shape_mutation(node)
|
|
7228
|
+
# Read against the continuation: a variable the block itself first binds has no pre-call binding.
|
|
7229
|
+
next if name.nil? || !block_defined_method_capture?(name, ancestors, post_scope, foreign)
|
|
7230
|
+
|
|
7231
|
+
(names ||= []) << name
|
|
7232
|
+
end
|
|
7233
|
+
names.nil? ? post_scope : floor_non_local_contents(names.uniq, post_scope)
|
|
7234
|
+
end
|
|
7235
|
+
|
|
6819
7236
|
# Inside an ESCAPING block body, a captured outer local can be content- mutated transitively: the body is (or
|
|
6820
7237
|
# contains) a self-call that escape-mutates one of its arguments. The canonical shape is the CLI's own
|
|
6821
7238
|
# `OptionParser.new { |opts| define_options(opts, options) }` — the block body is a bare `define_options(opts,
|
|
@@ -6939,7 +7356,7 @@ module Rigor
|
|
|
6939
7356
|
# continuation holds each rebound capture at `Dynamic[top]` ({#drop_captured_narrowing}), and the pass reads it
|
|
6940
7357
|
# so: `seen = 0; yielder { return :ret if (seen += 1) > 1 }` reaches the `return`.
|
|
6941
7358
|
def collect_repeated_block_returns(call_node, post_scope)
|
|
6942
|
-
return if
|
|
7359
|
+
return if typing_state[RETURN_SINK].nil?
|
|
6943
7360
|
|
|
6944
7361
|
block = call_node.block
|
|
6945
7362
|
return unless repeated_block_return_candidate?(call_node, block)
|
|
@@ -7076,16 +7493,16 @@ module Rigor
|
|
|
7076
7493
|
end
|
|
7077
7494
|
end
|
|
7078
7495
|
|
|
7079
|
-
# Binds a name from `CapturedLocals.writes`. An ivar
|
|
7080
|
-
# #286 optimistic mark. A local keeps the plain `with_local`
|
|
7081
|
-
# rebinds no ivar leaves the locals exactly as before.
|
|
7496
|
+
# Binds a name from `CapturedLocals.writes`. An ivar (or any other non-local) goes through
|
|
7497
|
+
# {CapturedLocals.bind}, which keeps an ivar's issue #286 optimistic mark. A local keeps the plain `with_local`
|
|
7498
|
+
# these seams have always used, so a block that rebinds no ivar leaves the locals exactly as before.
|
|
7082
7499
|
#
|
|
7083
7500
|
# Issue #1402 — `optimistic` is a mark a write-back pass's exit held ({#with_capture_exit_marks}); a name that has
|
|
7084
7501
|
# one binds through {CapturedLocals.bind} whatever its kind, which adds it.
|
|
7085
7502
|
def bind_capture(scope, name, type, optimistic: nil)
|
|
7086
7503
|
return CapturedLocals.bind(scope, name, type, optimistic: optimistic) if optimistic
|
|
7087
7504
|
|
|
7088
|
-
CapturedLocals.
|
|
7505
|
+
CapturedLocals.local_name?(name) ? scope.with_local(name, type) : CapturedLocals.bind(scope, name, type)
|
|
7089
7506
|
end
|
|
7090
7507
|
|
|
7091
7508
|
# A block-level `break` ends the CALL, so the binding it leaves with starts no further iteration and is no input
|
|
@@ -7113,9 +7530,8 @@ module Rigor
|
|
|
7113
7530
|
end
|
|
7114
7531
|
return converged if arms.empty?
|
|
7115
7532
|
|
|
7116
|
-
floor = Type::Combinator.untyped
|
|
7117
7533
|
converged.to_h do |name, type|
|
|
7118
|
-
next [name, type] if type
|
|
7534
|
+
next [name, type] if Type::Combinator.untyped?(type)
|
|
7119
7535
|
|
|
7120
7536
|
[name, Type::Combinator.union(type, *arms.filter_map { |arm| CapturedLocals.bound_type(arm, name) })]
|
|
7121
7537
|
end
|
|
@@ -7174,22 +7590,48 @@ module Rigor
|
|
|
7174
7590
|
block = call_node.block
|
|
7175
7591
|
return post_scope unless block.is_a?(Prism::BlockNode)
|
|
7176
7592
|
return post_scope unless classify_closure_escape(call_node) == :non_escaping
|
|
7593
|
+
return post_scope if block.body.nil?
|
|
7177
7594
|
|
|
7178
|
-
|
|
7179
|
-
|
|
7595
|
+
post_scope = join_block_content_captures(call_node, block, post_scope, seed_scope)
|
|
7596
|
+
# Issue #3163 — a method the block defines may run during the block, so its in-place mutations of an instance
|
|
7597
|
+
# variable, class variable or global floor that binding. The join reads no evidence from a method body.
|
|
7598
|
+
floor_block_defined_method_mutations(call_node, block, post_scope)
|
|
7599
|
+
end
|
|
7180
7600
|
|
|
7601
|
+
# The join {#content_writeback_block_captures} documents, for a non-escaping `block` with a body.
|
|
7602
|
+
def join_block_content_captures(call_node, block, post_scope, seed_scope)
|
|
7603
|
+
body = block.body
|
|
7181
7604
|
shadows = {}.compare_by_identity
|
|
7182
|
-
mutations = captured_content_mutations(block, shadows)
|
|
7605
|
+
mutations = captured_content_mutations(block, shadows, call_node)
|
|
7183
7606
|
return post_scope if mutations.empty?
|
|
7184
7607
|
|
|
7185
7608
|
seeds = mutations.to_h do |name, _calls|
|
|
7186
|
-
|
|
7609
|
+
seed = CapturedLocals.bound_type(seed_scope, name)
|
|
7610
|
+
[name, lookup_mutated_seed(body, name, seed) { |depth, nesting| depth > nesting }]
|
|
7187
7611
|
end
|
|
7188
|
-
shadow_rebound_reads(block, mutations, seeds, shadows)
|
|
7612
|
+
shadow_rebound_reads(block, mutations, seeds, shadows, call_node)
|
|
7189
7613
|
joined = join_content_to_fixpoint(mutations, seeds, build_block_entry_scope(call_node, block), shadows)
|
|
7190
7614
|
rewrites = local_rewrites(block.body) { |receiver, ancestors| receiver.depth > scope_nesting(ancestors) }
|
|
7191
7615
|
joined.reduce(post_scope) do |acc, (name, type)|
|
|
7192
|
-
acc
|
|
7616
|
+
bind_mutated_capture(acc, name, rewritten_capture(type, seeds[name], rewrites.fetch(name, NO_REWRITES)))
|
|
7617
|
+
end
|
|
7618
|
+
end
|
|
7619
|
+
|
|
7620
|
+
# `scope` with a capture the block mutated in place bound to its joined or floored content: a local through
|
|
7621
|
+
# `Scope#with_mutated_local`, which keeps the marks a source-level write drops, and an instance variable, class
|
|
7622
|
+
# variable or global through {CapturedLocals.bind}, which keeps an ivar's optimistic nil-freeness mark. The
|
|
7623
|
+
# binding still names the same object.
|
|
7624
|
+
def bind_mutated_capture(scope, name, type)
|
|
7625
|
+
CapturedLocals.local_name?(name) ? scope.with_mutated_local(name, type) : CapturedLocals.bind(scope, name, type)
|
|
7626
|
+
end
|
|
7627
|
+
|
|
7628
|
+
# `scope` with a content seam's working binding for `name` — its join so far, or `Dynamic[top]` for a read the
|
|
7629
|
+
# body rebinds — whatever kind of variable it is.
|
|
7630
|
+
def bind_capture_slot(scope, name, type)
|
|
7631
|
+
case CapturedLocals.variable_kind(name)
|
|
7632
|
+
when :local then scope.with_local(name, type)
|
|
7633
|
+
when :ivar then scope.with_ivar(name, type)
|
|
7634
|
+
else CapturedLocals.bind(scope, name, type)
|
|
7193
7635
|
end
|
|
7194
7636
|
end
|
|
7195
7637
|
|
|
@@ -7198,19 +7640,29 @@ module Rigor
|
|
|
7198
7640
|
|
|
7199
7641
|
# The {RewriteMutation} names `root` calls on each local its block admits — `a.map!(&:to_s)` beside an `a << x`.
|
|
7200
7642
|
# The receiver test is the one the caller's content-mutation walk applies.
|
|
7201
|
-
def local_rewrites(root)
|
|
7643
|
+
def local_rewrites(root, &)
|
|
7202
7644
|
rewrites = {}
|
|
7203
7645
|
Source::NodeWalker.each_with_ancestors(root) do |node, ancestors|
|
|
7204
7646
|
next unless node.is_a?(Prism::CallNode) && RewriteMutation.rewriter?(node.name)
|
|
7205
7647
|
|
|
7206
7648
|
receiver = node.receiver
|
|
7207
|
-
next unless
|
|
7649
|
+
next unless rewritten_capture_receiver?(receiver, ancestors, &)
|
|
7208
7650
|
|
|
7209
7651
|
(rewrites[receiver.name] ||= []) << node.name
|
|
7210
7652
|
end
|
|
7211
7653
|
rewrites
|
|
7212
7654
|
end
|
|
7213
7655
|
|
|
7656
|
+
# A local the caller's test admits, or an instance variable, class variable or global outside a body that makes
|
|
7657
|
+
# it another variable ({#under_scope_body?}): the names the content walks can collect.
|
|
7658
|
+
def rewritten_capture_receiver?(receiver, ancestors)
|
|
7659
|
+
case receiver
|
|
7660
|
+
when Prism::LocalVariableReadNode then yield(receiver, ancestors)
|
|
7661
|
+
when *NON_LOCAL_READ_NODES then !under_scope_body?(ancestors, CapturedLocals.variable_kind(receiver.name))
|
|
7662
|
+
else false
|
|
7663
|
+
end
|
|
7664
|
+
end
|
|
7665
|
+
|
|
7214
7666
|
# The slice-C join (and the loop seam's) rebuilds a collection from its SEED, so the rewrite the body's own
|
|
7215
7667
|
# widening applied is gone from it: `a = [1]; [0].each { a.map!(&:to_s); a << "x" }` read `Array["x" | 1]`, and
|
|
7216
7668
|
# `a[0] == "1"` folded always-falsey. Each rewrite the body makes on the local is re-applied here, on the seam's
|
|
@@ -7237,10 +7689,14 @@ module Rigor
|
|
|
7237
7689
|
# - joined with the block-entry typing, it still stores exit values no store reads;
|
|
7238
7690
|
# - one more walk of the body to the store inherits every gap in the engine's in-body flow.
|
|
7239
7691
|
#
|
|
7692
|
+
# An instance variable the body rebinds is one too (issue #1235): `@total += x; out << @total` stored the
|
|
7693
|
+
# pre-call `@total`, and so are a class variable and a global (issue #3096). Each counts on the terms the content
|
|
7694
|
+
# walk takes one ({#capturable_non_local?}).
|
|
7695
|
+
#
|
|
7240
7696
|
# See ADR-56 WD2.13.
|
|
7241
|
-
def shadow_rebound_reads(block, sites, seeds, shadows)
|
|
7697
|
+
def shadow_rebound_reads(block, sites, seeds, shadows, call_node)
|
|
7242
7698
|
entry_names = scope.locals.keys | CapturedLocals.introduced_locals(block).to_a
|
|
7243
|
-
written = scope_local_writes(block) & entry_names
|
|
7699
|
+
written = (scope_local_writes(block) & entry_names) | rebound_capturable_non_locals(block, call_node)
|
|
7244
7700
|
return if written.empty?
|
|
7245
7701
|
|
|
7246
7702
|
sites.each do |name, nodes|
|
|
@@ -7254,6 +7710,23 @@ module Rigor
|
|
|
7254
7710
|
end
|
|
7255
7711
|
end
|
|
7256
7712
|
|
|
7713
|
+
# The instance variables, class variables and globals the block body writes that {#capturable_non_local?}
|
|
7714
|
+
# accepts; no instance variable for a block that runs on another object. Every self-rebinding call classifies
|
|
7715
|
+
# `:unknown` today, so neither content seam that calls this meets one; the guard keeps the answer right should
|
|
7716
|
+
# the escape analysis learn to call one non-escaping.
|
|
7717
|
+
def rebound_capturable_non_locals(block, call_node)
|
|
7718
|
+
return NO_REWRITES if block.body.nil?
|
|
7719
|
+
|
|
7720
|
+
foreign = foreign_self_block?(call_node)
|
|
7721
|
+
names = []
|
|
7722
|
+
Source::NodeWalker.each_with_ancestors(block.body) do |node, ancestors|
|
|
7723
|
+
next unless CapturedLocals::NON_LOCAL_WRITE_NODES.include?(node.class)
|
|
7724
|
+
|
|
7725
|
+
names << node.name if capturable_non_local?(node.name, ancestors, scope, foreign_self: foreign)
|
|
7726
|
+
end
|
|
7727
|
+
names.uniq
|
|
7728
|
+
end
|
|
7729
|
+
|
|
7257
7730
|
# The locals a store reads to build what it stores. An Array index write's index arguments are left out, and so
|
|
7258
7731
|
# is any name they read: the join classifies the store as an element or a splice from the index's type, and a
|
|
7259
7732
|
# `Dynamic` index reads as both, so `grid[i] = [x, x]` would join `x` itself as a member of `grid` beside the
|
|
@@ -7292,6 +7765,29 @@ module Rigor
|
|
|
7292
7765
|
SCOPE_BODY_NODES = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
7293
7766
|
private_constant :SCOPE_BODY_NODES
|
|
7294
7767
|
|
|
7768
|
+
CLASS_BODY_NODES = [Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode].freeze
|
|
7769
|
+
private_constant :CLASS_BODY_NODES
|
|
7770
|
+
|
|
7771
|
+
# The bodies under a block body where a non-local of each kind is no longer the one the block's caller sees, or
|
|
7772
|
+
# does not run while the block does. A `def` body runs only when called. A class or module body is a class of
|
|
7773
|
+
# its own for an instance variable and a class variable, but runs at once and reads the one global. `class <<
|
|
7774
|
+
# self` keeps the enclosing class's class variables, so only an instance variable leaves there.
|
|
7775
|
+
NON_LOCAL_SCOPE_BODY_NODES = {
|
|
7776
|
+
ivar: SCOPE_BODY_NODES,
|
|
7777
|
+
cvar: [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze,
|
|
7778
|
+
global: [Prism::DefNode].freeze
|
|
7779
|
+
}.freeze
|
|
7780
|
+
private_constant :NON_LOCAL_SCOPE_BODY_NODES
|
|
7781
|
+
|
|
7782
|
+
# True when a non-local of `kind` (`:ivar`, `:cvar` or `:global`) at `ancestors` under a block body sits in a body
|
|
7783
|
+
# the block defines that makes it another variable or does not run during the block
|
|
7784
|
+
# ({NON_LOCAL_SCOPE_BODY_NODES}): for an instance variable, any method, class or module body, where `self` is that
|
|
7785
|
+
# body's own.
|
|
7786
|
+
def under_scope_body?(ancestors, kind = :ivar)
|
|
7787
|
+
bodies = NON_LOCAL_SCOPE_BODY_NODES.fetch(kind)
|
|
7788
|
+
ancestors.any? { |a| bodies.any? { |k| a.is_a?(k) } }
|
|
7789
|
+
end
|
|
7790
|
+
|
|
7295
7791
|
# True when the local `node` names lives in the scope the walk started in or an outer one.
|
|
7296
7792
|
def same_scope_local?(node, ancestors)
|
|
7297
7793
|
return false if ancestors.any? { |ancestor| SCOPE_BODY_NODES.any? { |klass| ancestor.is_a?(klass) } }
|
|
@@ -7308,16 +7804,37 @@ module Rigor
|
|
|
7308
7804
|
].freeze
|
|
7309
7805
|
private_constant :LOCAL_READ_NODES
|
|
7310
7806
|
|
|
7311
|
-
#
|
|
7312
|
-
|
|
7807
|
+
# The same for an instance variable, a class variable and a global.
|
|
7808
|
+
NON_LOCAL_VALUE_READ_NODES = Set[
|
|
7809
|
+
Prism::InstanceVariableReadNode,
|
|
7810
|
+
Prism::InstanceVariableOperatorWriteNode,
|
|
7811
|
+
Prism::InstanceVariableOrWriteNode,
|
|
7812
|
+
Prism::InstanceVariableAndWriteNode,
|
|
7813
|
+
Prism::ClassVariableReadNode,
|
|
7814
|
+
Prism::ClassVariableOperatorWriteNode,
|
|
7815
|
+
Prism::ClassVariableOrWriteNode,
|
|
7816
|
+
Prism::ClassVariableAndWriteNode,
|
|
7817
|
+
Prism::GlobalVariableReadNode,
|
|
7818
|
+
Prism::GlobalVariableOperatorWriteNode,
|
|
7819
|
+
Prism::GlobalVariableOrWriteNode,
|
|
7820
|
+
Prism::GlobalVariableAndWriteNode
|
|
7821
|
+
].freeze
|
|
7822
|
+
private_constant :NON_LOCAL_VALUE_READ_NODES
|
|
7823
|
+
|
|
7824
|
+
# Every local `node` reads from the scope it sits in, and every instance variable, class variable or global
|
|
7825
|
+
# outside a nested method or class body: a read inside an inner block of a name that block introduces is a
|
|
7826
|
+
# different variable. The caller intersects the answer with the names the body writes, which
|
|
7827
|
+
# {#rebound_capturable_non_locals} has already filtered.
|
|
7313
7828
|
def local_reads(node)
|
|
7314
7829
|
return [] if node.nil?
|
|
7315
7830
|
|
|
7316
7831
|
names = []
|
|
7317
7832
|
Source::NodeWalker.each_with_ancestors(node) do |n, ancestors|
|
|
7318
|
-
|
|
7319
|
-
|
|
7320
|
-
|
|
7833
|
+
if LOCAL_READ_NODES.any? { |klass| n.is_a?(klass) }
|
|
7834
|
+
names << n.name if same_scope_local?(n, ancestors)
|
|
7835
|
+
elsif NON_LOCAL_VALUE_READ_NODES.include?(n.class)
|
|
7836
|
+
names << n.name unless under_scope_body?(ancestors, CapturedLocals.variable_kind(n.name))
|
|
7837
|
+
end
|
|
7321
7838
|
end
|
|
7322
7839
|
names.uniq
|
|
7323
7840
|
end
|
|
@@ -7403,7 +7920,7 @@ module Rigor
|
|
|
7403
7920
|
|
|
7404
7921
|
def bind_content_joins(scope, kinds, seeds, evidence)
|
|
7405
7922
|
kinds.reduce(scope) do |acc, (name, kind)|
|
|
7406
|
-
acc
|
|
7923
|
+
bind_capture_slot(acc, name, content_entry_binding(seeds[name], kind, name, evidence))
|
|
7407
7924
|
end
|
|
7408
7925
|
end
|
|
7409
7926
|
|
|
@@ -7436,10 +7953,23 @@ module Rigor
|
|
|
7436
7953
|
#
|
|
7437
7954
|
# Each site nested in an inner block or lambda is recorded in `shadows` with the names those blocks bind
|
|
7438
7955
|
# themselves (see {#join_content_to_fixpoint}).
|
|
7439
|
-
|
|
7956
|
+
#
|
|
7957
|
+
# Issue #1208 — an instance variable is a capture too: every block made in a method shares its `self`, so `@out
|
|
7958
|
+
# << x` in the body grows the method's `@out` exactly as `out << x` grows `out`. It counts on the terms
|
|
7959
|
+
# {CapturedLocals.writes} takes a rebound one: bound at the call site and not on its ADR-58 declaration seed
|
|
7960
|
+
# ({CapturedLocals.rebindable_ivar?}), outside a nested method or class body, and not in a block that runs on
|
|
7961
|
+
# another object ({CapturedLocals.under_foreign_self_block?}, or `call_node`'s own block).
|
|
7962
|
+
#
|
|
7963
|
+
# Issue #3096 — a class variable and a global are captures on the same terms, less the foreign-self one: Ruby
|
|
7964
|
+
# resolves a class variable lexically, and a global is the same one whatever `self` is.
|
|
7965
|
+
def captured_content_mutations(block, shadows, call_node)
|
|
7440
7966
|
mutations = Hash.new { |h, k| h[k] = [] }
|
|
7967
|
+
# Every self-rebinding call classifies `:unknown` today, so this seam does not meet one; the guard keeps the
|
|
7968
|
+
# ivar arm right should the escape analysis learn to call one non-escaping.
|
|
7969
|
+
foreign = foreign_self_block?(call_node)
|
|
7441
7970
|
Source::NodeWalker.each_with_ancestors(block.body) do |node, ancestors|
|
|
7442
7971
|
name, site = content_mutation_target(node) { |receiver| receiver.depth > scope_nesting(ancestors) }
|
|
7972
|
+
name, site = non_local_content_mutation_target(node, ancestors, foreign) if name.nil?
|
|
7443
7973
|
next if name.nil?
|
|
7444
7974
|
|
|
7445
7975
|
mutations[name] << site
|
|
@@ -7448,6 +7978,89 @@ module Rigor
|
|
|
7448
7978
|
mutations
|
|
7449
7979
|
end
|
|
7450
7980
|
|
|
7981
|
+
# `[name, node]` when `node` mutates the content of an instance variable of this scope's `self`, a class
|
|
7982
|
+
# variable or a global that the content seams may join ({#captured_content_mutations}), else
|
|
7983
|
+
# {NO_CONTENT_MUTATION}. `foreign_self` says the seam's block runs on another object.
|
|
7984
|
+
def non_local_content_mutation_target(node, ancestors, foreign_self)
|
|
7985
|
+
is_call_mutator = node.is_a?(Prism::CallNode) && CONTENT_MUTATORS.include?(node.name)
|
|
7986
|
+
return NO_CONTENT_MUTATION unless is_call_mutator || index_write?(node)
|
|
7987
|
+
|
|
7988
|
+
receiver = node.receiver
|
|
7989
|
+
return NO_CONTENT_MUTATION unless NON_LOCAL_READ_NODES.include?(receiver.class)
|
|
7990
|
+
return NO_CONTENT_MUTATION unless capturable_non_local?(receiver.name, ancestors, scope, foreign_self:)
|
|
7991
|
+
return NO_CONTENT_MUTATION unless collection_only?(CapturedLocals.bound_type(scope, receiver.name))
|
|
7992
|
+
|
|
7993
|
+
[receiver.name, node]
|
|
7994
|
+
end
|
|
7995
|
+
|
|
7996
|
+
# The receiver reads of a variable the content seams take besides a local.
|
|
7997
|
+
NON_LOCAL_READ_NODES = Set[
|
|
7998
|
+
Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
|
|
7999
|
+
].freeze
|
|
8000
|
+
private_constant :NON_LOCAL_READ_NODES
|
|
8001
|
+
|
|
8002
|
+
# True when every member of `type` is an Array, a Hash or a String carrier. The non-escaping join takes an
|
|
8003
|
+
# instance variable only then: it drops a seed's `nil` after a guarded mutation (#1219), so `@buf = flag ? [1] :
|
|
8004
|
+
# nil; [1].each { @buf << 2 if @buf }` would lose the `nil` the runtime keeps when the block never runs it. The
|
|
8005
|
+
# local spelling has the gap; an instance variable, the common lazily-initialised spelling, keeps its pre-call
|
|
8006
|
+
# binding until it closes. The escaping floor takes every instance variable ({#member_content_floor}).
|
|
8007
|
+
def collection_only?(type)
|
|
8008
|
+
return false if type.nil?
|
|
8009
|
+
return type.members.all? { |member| collection_only?(member) } if type.is_a?(Type::Union)
|
|
8010
|
+
|
|
8011
|
+
arrayish?(type) || hashish?(type) || UnknownStoreWidening.carrier_class(type) == "String"
|
|
8012
|
+
end
|
|
8013
|
+
|
|
8014
|
+
# True when the non-local `name`, read or written at `ancestors` under a block body, is one the content seams
|
|
8015
|
+
# take against `in_scope`: bound there ({CapturedLocals.rebindable_non_local?}, which keeps an instance
|
|
8016
|
+
# variable on its declaration seed out) and outside a body the block defines that makes it another variable or
|
|
8017
|
+
# runs later ({#under_scope_body?}).
|
|
8018
|
+
#
|
|
8019
|
+
# - An instance variable must be `self`'s: not under a block that runs on another object, nor in a seam whose
|
|
8020
|
+
# block does (`foreign_self`), nor in any method, class or module body.
|
|
8021
|
+
# - A class variable resolves lexically and a global is global, so `self` does not matter to either (issue
|
|
8022
|
+
# #3096). A `def` body runs only when called, so neither counts there. A class or module body runs at once:
|
|
8023
|
+
# it reads its own class variables but the one global, so a global counts there. `class << self` keeps the
|
|
8024
|
+
# enclosing class's class variables, so a class variable counts there.
|
|
8025
|
+
# - A global Ruby's own signatures declare (`$stdout`, `$;`, `$0`, the frame-local `$~` and `$_`, …) is left
|
|
8026
|
+
# alone: the interpreter owns it, and some of them read through a getter (`$0` answers a frozen copy).
|
|
8027
|
+
def capturable_non_local?(name, ancestors, in_scope, foreign_self: false)
|
|
8028
|
+
return false if under_scope_body?(ancestors, CapturedLocals.variable_kind(name))
|
|
8029
|
+
|
|
8030
|
+
non_local_of_scope?(name, ancestors, in_scope, foreign_self)
|
|
8031
|
+
end
|
|
8032
|
+
|
|
8033
|
+
# Issue #3163 — true when the non-local `name`, mutated at `ancestors` under a block body, sits in a method the
|
|
8034
|
+
# block defines and is one the content floor takes against `in_scope`. {#capturable_non_local?} leaves a `def`
|
|
8035
|
+
# body out because it runs only when called, but the block may call it. The terms are that method's, less the
|
|
8036
|
+
# `def` boundary:
|
|
8037
|
+
#
|
|
8038
|
+
# - A class or module body or a `class << self` between the block and the mutation still leaves every kind out:
|
|
8039
|
+
# a method defined there is another class's, and the walks do not ask who calls it.
|
|
8040
|
+
# - An instance variable in a method defined on an explicit receiver other than `self` (`def other.m`) is that
|
|
8041
|
+
# object's. A plain `def` counts wherever it is written, so one in a class body's block, whose instance
|
|
8042
|
+
# variables are an instance's and not the class's, floors the class's too.
|
|
8043
|
+
def block_defined_method_capture?(name, ancestors, in_scope, foreign_self)
|
|
8044
|
+
defs = ancestors.grep(Prism::DefNode)
|
|
8045
|
+
return false if defs.empty? || ancestors.any? { |a| CLASS_BODY_NODES.any? { |k| a.is_a?(k) } }
|
|
8046
|
+
|
|
8047
|
+
foreign_def = defs.any? { |d| !d.receiver.nil? && !d.receiver.is_a?(Prism::SelfNode) }
|
|
8048
|
+
return false if foreign_def && CapturedLocals.variable_kind(name) == :ivar
|
|
8049
|
+
|
|
8050
|
+
non_local_of_scope?(name, ancestors, in_scope, foreign_self)
|
|
8051
|
+
end
|
|
8052
|
+
|
|
8053
|
+
# The half of {#capturable_non_local?} that does not read the bodies between the block and the non-local.
|
|
8054
|
+
def non_local_of_scope?(name, ancestors, in_scope, foreign_self)
|
|
8055
|
+
case CapturedLocals.variable_kind(name)
|
|
8056
|
+
when :ivar
|
|
8057
|
+
return false if foreign_self || CapturedLocals.under_foreign_self_block?(ancestors, in_scope)
|
|
8058
|
+
when :global
|
|
8059
|
+
return false if in_scope.environment.global_for_name(name, builtin: true)
|
|
8060
|
+
end
|
|
8061
|
+
CapturedLocals.rebindable_non_local?(in_scope, name)
|
|
8062
|
+
end
|
|
8063
|
+
|
|
7451
8064
|
def scope_nesting(ancestors)
|
|
7452
8065
|
ancestors.count { |ancestor| scope_nesting_node?(ancestor) }
|
|
7453
8066
|
end
|
|
@@ -7468,7 +8081,7 @@ module Rigor
|
|
|
7468
8081
|
names = shadows[site]
|
|
7469
8082
|
return scope if names.nil?
|
|
7470
8083
|
|
|
7471
|
-
names.reduce(scope) { |acc, name| acc
|
|
8084
|
+
names.reduce(scope) { |acc, name| bind_capture_slot(acc, name, Type::Combinator.untyped) }
|
|
7472
8085
|
end
|
|
7473
8086
|
|
|
7474
8087
|
# `base` binds every fixed name to its join; the moving names are rebound on each pass.
|
|
@@ -7480,7 +8093,7 @@ module Rigor
|
|
|
7480
8093
|
widen: Type::Combinator.method(:widen_value_pinned),
|
|
7481
8094
|
evaluate_body: lambda do |assumption|
|
|
7482
8095
|
pass_entry = kinds.reduce(base) do |acc, (name, kind)|
|
|
7483
|
-
acc
|
|
8096
|
+
bind_capture_slot(acc, name, content_carrier_under(seeds[name], kind, name, assumption))
|
|
7484
8097
|
end
|
|
7485
8098
|
content_evidence(sites, kinds, pass_entry, shadows)
|
|
7486
8099
|
end
|
|
@@ -7591,11 +8204,19 @@ module Rigor
|
|
|
7591
8204
|
type.nil? || type.is_a?(Type::Bot) ? [] : [type]
|
|
7592
8205
|
end
|
|
7593
8206
|
|
|
8207
|
+
# A joined name is a local, an instance variable (#1208), a class variable or a global (#3096), so a store
|
|
8208
|
+
# reading any of them moves.
|
|
8209
|
+
CAPTURE_READ_NODES = Set[
|
|
8210
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode,
|
|
8211
|
+
Prism::GlobalVariableReadNode
|
|
8212
|
+
].freeze
|
|
8213
|
+
private_constant :CAPTURE_READ_NODES
|
|
8214
|
+
|
|
7594
8215
|
def store_arguments_read?(node, names)
|
|
7595
8216
|
arguments = node.arguments
|
|
7596
8217
|
return false if arguments.nil?
|
|
7597
8218
|
|
|
7598
|
-
Source::NodeWalker.each(arguments).any? { |n|
|
|
8219
|
+
Source::NodeWalker.each(arguments).any? { |n| CAPTURE_READ_NODES.include?(n.class) && names.include?(n.name) }
|
|
7599
8220
|
end
|
|
7600
8221
|
|
|
7601
8222
|
# ADR-56 slice C (B3). For `recv.each_with_object(memo) { |x, acc| … }` the return is the memo object after the
|
|
@@ -7854,8 +8475,14 @@ module Rigor
|
|
|
7854
8475
|
next if ancestors.any?(Prism::DefNode)
|
|
7855
8476
|
|
|
7856
8477
|
receiver = node.receiver
|
|
7857
|
-
|
|
7858
|
-
|
|
8478
|
+
case receiver
|
|
8479
|
+
when Prism::LocalVariableReadNode
|
|
8480
|
+
next unless receiver.name == name && yield(receiver.depth, scope_nesting(ancestors))
|
|
8481
|
+
when Prism::InstanceVariableReadNode, Prism::ClassVariableReadNode, Prism::GlobalVariableReadNode
|
|
8482
|
+
next unless receiver.name == name && capturable_non_local?(name, ancestors, scope)
|
|
8483
|
+
else
|
|
8484
|
+
next
|
|
8485
|
+
end
|
|
7859
8486
|
|
|
7860
8487
|
seed = MutationWidening.widen_for_mutator(seed, node.name) || seed
|
|
7861
8488
|
end
|
|
@@ -7868,11 +8495,11 @@ module Rigor
|
|
|
7868
8495
|
# write.
|
|
7869
8496
|
def join_memo_content(call_node, memo_param, calls, pre_state, shadows)
|
|
7870
8497
|
block = call_node.block
|
|
7871
|
-
captured = captured_content_mutations(block, shadows)
|
|
7872
|
-
seeds = captured.keys.to_h { |name| [name,
|
|
8498
|
+
captured = captured_content_mutations(block, shadows, call_node)
|
|
8499
|
+
seeds = captured.keys.to_h { |name| [name, CapturedLocals.bound_type(scope, name)] }
|
|
7873
8500
|
seeds[memo_param] = pre_state
|
|
7874
8501
|
sites = captured.merge(memo_param => calls)
|
|
7875
|
-
shadow_rebound_reads(block, sites, seeds, shadows)
|
|
8502
|
+
shadow_rebound_reads(block, sites, seeds, shadows, call_node)
|
|
7876
8503
|
join_content_to_fixpoint(sites, seeds, build_block_entry_scope(call_node, block), shadows)[memo_param]
|
|
7877
8504
|
end
|
|
7878
8505
|
|
|
@@ -8429,7 +9056,8 @@ module Rigor
|
|
|
8429
9056
|
# ({#record_closure_escape_if_any}) and a lambda literal ({#eval_lambda}) — so they cannot drift.
|
|
8430
9057
|
def escaping_closure_captures(closure_node, post_scope, foreign_self: false)
|
|
8431
9058
|
widen_escaping_content_captures(closure_node,
|
|
8432
|
-
drop_captured_narrowing(closure_node, post_scope, foreign_self: foreign_self)
|
|
9059
|
+
drop_captured_narrowing(closure_node, post_scope, foreign_self: foreign_self),
|
|
9060
|
+
foreign_self: foreign_self)
|
|
8433
9061
|
end
|
|
8434
9062
|
|
|
8435
9063
|
# Issue #1402 — whether `call_node`'s block runs on an object that is provably not `self`
|
|
@@ -8439,6 +9067,30 @@ module Rigor
|
|
|
8439
9067
|
InterveningIvarReset.foreign_self_block?(call_node, operand_scope)
|
|
8440
9068
|
end
|
|
8441
9069
|
|
|
9070
|
+
# The scope a block body's `self` marks start from: the statement form's opaque `self` (issue #316), or the
|
|
9071
|
+
# walk's unknown-`self` mark alone (issue #1717) for the value-position block {#evaluate_block_of} enters and for
|
|
9072
|
+
# every block entered beneath it. A scope carrying the unknown-`self` mark without the opaque one comes only from
|
|
9073
|
+
# the index walk ({Scope#with_block_self_unknown}), so it is how a nested block knows it sits under one: the
|
|
9074
|
+
# opaque mark, which declines a cross-file top-level `def`, stays off the whole value-position subtree.
|
|
9075
|
+
#
|
|
9076
|
+
# Issue #2861 — a block whose call provably runs it under the caller's `self` ({SelfPreservingBlock}:
|
|
9077
|
+
# `rows.map { … }`) enters with no opaque mark the caller's scope does not already carry
|
|
9078
|
+
# ({Scope#entering_self_preserving_block}).
|
|
9079
|
+
def block_self_entry(block_node, call_node = nil)
|
|
9080
|
+
walked = block_node.equal?(@value_position_block) || scope.walked_block_self_unknown?
|
|
9081
|
+
return scope.with_block_self_unknown(true) if walked
|
|
9082
|
+
return scope.entering_self_preserving_block if self_preserving_block?(call_node)
|
|
9083
|
+
|
|
9084
|
+
scope.entering_opaque_block
|
|
9085
|
+
end
|
|
9086
|
+
|
|
9087
|
+
def self_preserving_block?(call_node)
|
|
9088
|
+
return false if call_node.nil? || call_node.receiver.nil?
|
|
9089
|
+
|
|
9090
|
+
SelfPreservingBlock.call?(call_node, explicit_receiver_type(call_node), scope,
|
|
9091
|
+
classification: classify_closure_escape(call_node))
|
|
9092
|
+
end
|
|
9093
|
+
|
|
8442
9094
|
# Builds the entry scope for a block body. The block sees the outer scope's locals (Ruby's lexical scoping rule)
|
|
8443
9095
|
# and adds bindings for every named block parameter on top. Parameter types come from the receiving method's RBS
|
|
8444
9096
|
# signature when one is available; the rest default to `Dynamic[Top]`.
|
|
@@ -8453,7 +9105,8 @@ module Rigor
|
|
|
8453
9105
|
# yielding method, not the lexical context, decides what `self` is, and Rigor does not track it.
|
|
8454
9106
|
# Issue #1358 — a body that may run a match reads the match globals an earlier iteration may have rebound
|
|
8455
9107
|
# ({MatchRebinding.block_entry}).
|
|
8456
|
-
entry = MatchRebinding.block_entry(
|
|
9108
|
+
entry = MatchRebinding.block_entry(block_self_entry(block_node, call_node), block_node, call_node)
|
|
9109
|
+
entry = entry.entering_block_without_gradual_marks(block_node)
|
|
8457
9110
|
scope_with_params = BlockParameterBinder.new(expected_param_types: expected).bind_onto(block_node, entry)
|
|
8458
9111
|
# ADR-16 Tier A — a plugin `block_as_methods:` entry that matches `(receiver, name)` narrows the
|
|
8459
9112
|
# body's `self` to the object the DSL `instance_eval`s the block on (`params` on
|
|
@@ -8589,7 +9242,8 @@ module Rigor
|
|
|
8589
9242
|
# def's contract; it binds its parameters as an undeclared method does.
|
|
8590
9243
|
class_path: @class_context.last&.refinement ? nil : current_class_path,
|
|
8591
9244
|
singleton: singleton,
|
|
8592
|
-
source_path: scope.source_path
|
|
9245
|
+
source_path: scope.source_path,
|
|
9246
|
+
scope: scope
|
|
8593
9247
|
)
|
|
8594
9248
|
bindings = binder.bind(def_node)
|
|
8595
9249
|
# ADR-67 WD3 — override an undeclared parameter with its call-site inferred type (precision-additive; an
|
|
@@ -8674,7 +9328,7 @@ module Rigor
|
|
|
8674
9328
|
# True for the `Dynamic[Top]` carrier `MethodParameterBinder` leaves on an undeclared parameter — the only
|
|
8675
9329
|
# bindings ADR-67 WD3 overrides.
|
|
8676
9330
|
def untyped_binding?(type)
|
|
8677
|
-
|
|
9331
|
+
Type::Combinator.untyped?(type)
|
|
8678
9332
|
end
|
|
8679
9333
|
|
|
8680
9334
|
def seed_instance_ivars(body_scope, singleton:)
|
|
@@ -8886,8 +9540,13 @@ module Rigor
|
|
|
8886
9540
|
# nested body, so this handler only ever appends a return that genuinely exits the method currently being
|
|
8887
9541
|
# inferred.
|
|
8888
9542
|
def eval_return(node)
|
|
8889
|
-
|
|
8890
|
-
sink
|
|
9543
|
+
state = typing_state
|
|
9544
|
+
sink = state[RETURN_SINK]
|
|
9545
|
+
if sink
|
|
9546
|
+
type = jump_value_type(node)
|
|
9547
|
+
state.touch(RETURN_SINK)
|
|
9548
|
+
sink.record(node, type, state[PROVISIONAL_RETURNS])
|
|
9549
|
+
end
|
|
8891
9550
|
[Type::Combinator.bot, scope]
|
|
8892
9551
|
end
|
|
8893
9552
|
|
|
@@ -8901,8 +9560,13 @@ module Rigor
|
|
|
8901
9560
|
# dead arm), so the join is flow-sensitive for free: `[1, 2].map { |x| next nil if x.nil?; x.to_s }` keeps its
|
|
8902
9561
|
# exact per-element answer.
|
|
8903
9562
|
def eval_next(node)
|
|
8904
|
-
|
|
8905
|
-
sink
|
|
9563
|
+
state = typing_state
|
|
9564
|
+
sink = state[NEXT_SINK]
|
|
9565
|
+
if sink
|
|
9566
|
+
type = jump_value_type(node)
|
|
9567
|
+
state.touch(NEXT_SINK)
|
|
9568
|
+
sink << [node, type]
|
|
9569
|
+
end
|
|
8906
9570
|
@next_scope_sink << [node, jump_scope(node)] if @next_scope_sink
|
|
8907
9571
|
[Type::Combinator.bot, scope]
|
|
8908
9572
|
end
|
|
@@ -8928,10 +9592,19 @@ module Rigor
|
|
|
8928
9592
|
# break-value sink for `ExpressionTyper#call_dispatch_type_for` to union in. Both sinks are optional and
|
|
8929
9593
|
# independent — a loop body collects scopes while an enclosing call collects values from the same walk.
|
|
8930
9594
|
def eval_break(node)
|
|
8931
|
-
|
|
8932
|
-
sink
|
|
8933
|
-
|
|
8934
|
-
|
|
9595
|
+
state = typing_state
|
|
9596
|
+
sink = state[BREAK_SINK]
|
|
9597
|
+
if sink
|
|
9598
|
+
jump = jump_scope(node)
|
|
9599
|
+
state.touch(BREAK_SINK)
|
|
9600
|
+
sink << [node, jump]
|
|
9601
|
+
end
|
|
9602
|
+
value_sink = state[BREAK_VALUE_SINK]
|
|
9603
|
+
if value_sink
|
|
9604
|
+
type = jump_value_type(node)
|
|
9605
|
+
state.touch(BREAK_VALUE_SINK)
|
|
9606
|
+
value_sink << [node, type]
|
|
9607
|
+
end
|
|
8935
9608
|
[Type::Combinator.bot, scope]
|
|
8936
9609
|
end
|
|
8937
9610
|
|
|
@@ -9037,6 +9710,39 @@ module Rigor
|
|
|
9037
9710
|
branch_type.is_a?(Type::Bot)
|
|
9038
9711
|
end
|
|
9039
9712
|
|
|
9713
|
+
# Issue #1230 — a strict, syntactic-only exit test for the then-exits-beside-an-else arm ({#if_join}). The
|
|
9714
|
+
# branch's last statement is a `return` / `next` / `break`; a receiver-less `raise` / `fail` /
|
|
9715
|
+
# `exit` / `exit!` / `abort` / `throw` whose name no project file defines on any class or module, at the top level
|
|
9716
|
+
# or by `define_method` ({Scope#project_method_name?}); or an if / unless / case with an else whose every arm
|
|
9717
|
+
# certainly exits. A `redo` / `retry` does not: it re-runs a loop body or a `begin` that reads the branch's
|
|
9718
|
+
# writes. Unlike {#branch_terminates?} it reads no type, so a `bot`-typed helper call does not qualify:
|
|
9719
|
+
# the typer types a call to an overridable or Kernel-shadowing project method `bot` (issue #3110).
|
|
9720
|
+
def branch_certainly_exits?(node)
|
|
9721
|
+
case node
|
|
9722
|
+
when Prism::ReturnNode, Prism::NextNode, Prism::BreakNode then true
|
|
9723
|
+
when Prism::CallNode then certain_exit_call?(node)
|
|
9724
|
+
when Prism::StatementsNode then branch_certainly_exits?(node.body.last)
|
|
9725
|
+
when Prism::ParenthesesNode then branch_certainly_exits?(node.body)
|
|
9726
|
+
when Prism::ElseNode then branch_certainly_exits?(node.statements)
|
|
9727
|
+
when Prism::IfNode, Prism::UnlessNode
|
|
9728
|
+
branch_certainly_exits?(node.statements) && branch_certainly_exits?(node_else_branch(node))
|
|
9729
|
+
when Prism::CaseNode, Prism::CaseMatchNode then case_certainly_exits?(node)
|
|
9730
|
+
else false
|
|
9731
|
+
end
|
|
9732
|
+
end
|
|
9733
|
+
|
|
9734
|
+
def certain_exit_call?(node)
|
|
9735
|
+
node.receiver.nil? && CERTAIN_EXIT_NAMES.include?(node.name) && !scope.project_method_name?(node.name)
|
|
9736
|
+
end
|
|
9737
|
+
|
|
9738
|
+
def case_certainly_exits?(node)
|
|
9739
|
+
!node.else_clause.nil? && branch_certainly_exits?(node.else_clause) &&
|
|
9740
|
+
node.conditions.all? { |arm| branch_certainly_exits?(arm.statements) }
|
|
9741
|
+
end
|
|
9742
|
+
|
|
9743
|
+
CERTAIN_EXIT_NAMES = %i[raise fail exit exit! abort throw].freeze
|
|
9744
|
+
private_constant :CERTAIN_EXIT_NAMES
|
|
9745
|
+
|
|
9040
9746
|
def eval_branch_or_nil(branch_node, branch_scope, on_enter: @on_enter)
|
|
9041
9747
|
return [Type::Combinator.constant_of(nil), branch_scope] if branch_node.nil?
|
|
9042
9748
|
|
|
@@ -9089,6 +9795,8 @@ module Rigor
|
|
|
9089
9795
|
scope.with_ivar(ref.name, exception_type).without_ivar_guard(ref.name)
|
|
9090
9796
|
when Prism::GlobalVariableTargetNode
|
|
9091
9797
|
scope.with_global(ref.name, exception_type)
|
|
9798
|
+
when Prism::ClassVariableTargetNode
|
|
9799
|
+
scope.with_cvar(ref.name, exception_type)
|
|
9092
9800
|
when Prism::IndexTargetNode
|
|
9093
9801
|
widen_index_target(ref, exception_type, scope, type_scope: scope)
|
|
9094
9802
|
else
|
|
@@ -9096,20 +9804,11 @@ module Rigor
|
|
|
9096
9804
|
end
|
|
9097
9805
|
end
|
|
9098
9806
|
|
|
9099
|
-
# Derives the exception instance type for a `RescueNode
|
|
9100
|
-
#
|
|
9101
|
-
#
|
|
9807
|
+
# Derives the exception instance type for a `RescueNode` ({ErrorInfo.rescue_exception_type}): `StandardError` for
|
|
9808
|
+
# a bare `rescue`, else the union of the named classes' instances, with `Dynamic[top]` for one that does not
|
|
9809
|
+
# resolve to a `Singleton`.
|
|
9102
9810
|
def rescue_exception_type(rescue_node, scope)
|
|
9103
|
-
|
|
9104
|
-
if exceptions.empty?
|
|
9105
|
-
Type::Combinator.nominal_of("StandardError")
|
|
9106
|
-
else
|
|
9107
|
-
types = exceptions.map do |exc_node|
|
|
9108
|
-
singleton_type, = sub_eval(exc_node, scope)
|
|
9109
|
-
singleton_to_nominal(singleton_type)
|
|
9110
|
-
end
|
|
9111
|
-
Type::Combinator.union(*types)
|
|
9112
|
-
end
|
|
9811
|
+
ErrorInfo.rescue_exception_type(rescue_node) { |exception_node| sub_eval(exception_node, scope).first }
|
|
9113
9812
|
end
|
|
9114
9813
|
|
|
9115
9814
|
# --------------------------------------------------------------- `case/in` pattern variable binding helpers
|
|
@@ -9121,7 +9820,27 @@ module Rigor
|
|
|
9121
9820
|
# `deconstruct` / `deconstruct_keys` dispatch reads for its freshness gate.
|
|
9122
9821
|
def apply_in_pattern_bindings(subject_type, subject_node, pattern, scope)
|
|
9123
9822
|
bindings = collect_in_pattern_bindings(subject_type, pattern, scope, subject_node: subject_node)
|
|
9124
|
-
bindings.reduce(scope) { |s, (name, type)| s.with_local(name, type) }
|
|
9823
|
+
bound = bindings.reduce(scope) { |s, (name, type)| s.with_local(name, type) }
|
|
9824
|
+
marked = subject_node.is_a?(Prism::LocalVariableReadNode) && scope.gradual_narrowing?(subject_node.name)
|
|
9825
|
+
mark_gradual_class_capture(subject_type, pattern, bound, marked: marked)
|
|
9826
|
+
end
|
|
9827
|
+
|
|
9828
|
+
# Issue #2656 — `in C => y` over a `Dynamic` subject binds `y` to `C`'s `Nominal`, as a class guard's edge binds
|
|
9829
|
+
# the subject, and marks it as that edge marks a local ({Narrowing.mark_gradual_local}). Only a capture of
|
|
9830
|
+
# the whole subject by a class or an alternation of classes is marked.
|
|
9831
|
+
def mark_gradual_class_capture(subject_type, pattern, scope, marked: false)
|
|
9832
|
+
return scope unless pattern.is_a?(Prism::CapturePatternNode) && class_capture_value?(pattern.value)
|
|
9833
|
+
|
|
9834
|
+
name = pattern.target.name
|
|
9835
|
+
Narrowing.mark_gradual_local(scope, name, subject_type, scope.local(name), marked: marked)
|
|
9836
|
+
end
|
|
9837
|
+
|
|
9838
|
+
def class_capture_value?(value)
|
|
9839
|
+
case value
|
|
9840
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode then true
|
|
9841
|
+
when Prism::AlternationPatternNode then class_capture_value?(value.left) && class_capture_value?(value.right)
|
|
9842
|
+
else false
|
|
9843
|
+
end
|
|
9125
9844
|
end
|
|
9126
9845
|
|
|
9127
9846
|
# Returns an array of `[Symbol, Rigor::Type]` pairs for every variable captured by `pattern`, typed against the
|
|
@@ -9501,7 +10220,8 @@ module Rigor
|
|
|
9501
10220
|
floor = Type::Combinator.untyped
|
|
9502
10221
|
walks.first.map do |name, _type|
|
|
9503
10222
|
types = walks.map { |walk| walk.assoc(name)&.last }
|
|
9504
|
-
|
|
10223
|
+
floored = types.any? { |type| type.nil? || Type::Combinator.untyped?(type) }
|
|
10224
|
+
[name, floored ? floor : Type::Combinator.union(*types)]
|
|
9505
10225
|
end
|
|
9506
10226
|
end
|
|
9507
10227
|
|