rigortype 0.4.4 → 0.4.6
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 +5 -5
- data/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +59 -7
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -10,9 +10,11 @@ require_relative "../source/node_children"
|
|
|
10
10
|
require_relative "../source/constant_path"
|
|
11
11
|
require_relative "anonymous_meta_class"
|
|
12
12
|
require_relative "definer_resolution"
|
|
13
|
+
require_relative "block_call_timing"
|
|
13
14
|
require_relative "block_repetition"
|
|
14
15
|
require_relative "block_parameter_binder"
|
|
15
16
|
require_relative "body_fixpoint"
|
|
17
|
+
require_relative "callee_mutation_chain"
|
|
16
18
|
require_relative "captured_locals"
|
|
17
19
|
require_relative "dynamic_origin"
|
|
18
20
|
require_relative "error_info"
|
|
@@ -28,7 +30,9 @@ require_relative "define_method_block_self"
|
|
|
28
30
|
require_relative "macro_block_self_type"
|
|
29
31
|
require_relative "guard_rebinding"
|
|
30
32
|
require_relative "self_call_ivar_writes"
|
|
33
|
+
require_relative "intervening_ivar_reset"
|
|
31
34
|
require_relative "match_rebinding"
|
|
35
|
+
require_relative "clause_widening"
|
|
32
36
|
require_relative "element_read_widening"
|
|
33
37
|
require_relative "hash_lookup_mutation"
|
|
34
38
|
require_relative "indexed_narrowing"
|
|
@@ -42,6 +46,8 @@ require_relative "narrowing"
|
|
|
42
46
|
require_relative "operand_effects"
|
|
43
47
|
require_relative "operand_walk"
|
|
44
48
|
require_relative "optimistic_origin"
|
|
49
|
+
require_relative "loop_value_record"
|
|
50
|
+
require_relative "project_yielder"
|
|
45
51
|
require_relative "return_barrier"
|
|
46
52
|
require_relative "repeated_or_writes"
|
|
47
53
|
require_relative "rewrite_mutation"
|
|
@@ -114,8 +120,8 @@ module Rigor
|
|
|
114
120
|
# the default expression pair, so routing the or-writes here adds the body entry and nothing else.
|
|
115
121
|
Prism::ConstantOrWriteNode => :eval_constant_write,
|
|
116
122
|
Prism::ConstantPathOrWriteNode => :eval_constant_write,
|
|
117
|
-
Prism::IfNode => :
|
|
118
|
-
Prism::UnlessNode => :
|
|
123
|
+
Prism::IfNode => :eval_recorded_if,
|
|
124
|
+
Prism::UnlessNode => :eval_recorded_unless,
|
|
119
125
|
Prism::ElseNode => :eval_else,
|
|
120
126
|
Prism::CaseNode => :eval_case,
|
|
121
127
|
Prism::CaseMatchNode => :eval_case,
|
|
@@ -124,9 +130,9 @@ module Rigor
|
|
|
124
130
|
Prism::BeginNode => :eval_begin_node,
|
|
125
131
|
Prism::RescueNode => :eval_rescue,
|
|
126
132
|
Prism::EnsureNode => :eval_ensure,
|
|
127
|
-
Prism::WhileNode => :
|
|
128
|
-
Prism::UntilNode => :
|
|
129
|
-
Prism::ForNode => :
|
|
133
|
+
Prism::WhileNode => :eval_recorded_loop,
|
|
134
|
+
Prism::UntilNode => :eval_recorded_loop,
|
|
135
|
+
Prism::ForNode => :eval_recorded_for,
|
|
130
136
|
Prism::AndNode => :eval_and_or,
|
|
131
137
|
Prism::OrNode => :eval_and_or,
|
|
132
138
|
Prism::ParenthesesNode => :eval_parentheses,
|
|
@@ -183,6 +189,40 @@ module Rigor
|
|
|
183
189
|
RETURN_SINK_KEY = :rigor_return_sink
|
|
184
190
|
private_constant :RETURN_SINK_KEY
|
|
185
191
|
|
|
192
|
+
# Thread-local flag: set while a side pass ({.provisional_returns}) re-types part of a body from a scope other
|
|
193
|
+
# than the one the statement walk holds, so the `return`s it reaches are filed as provisional ({ReturnSink}).
|
|
194
|
+
PROVISIONAL_RETURNS_KEY = :rigor_provisional_returns
|
|
195
|
+
private_constant :PROVISIONAL_RETURNS_KEY
|
|
196
|
+
|
|
197
|
+
# The `return` sink {.with_return_sink} installs. Issue #2299 — a `return` the statement walk records is typed
|
|
198
|
+
# with its real bindings; one a side pass records ({.provisional_returns}) may read a name the pass skipped the
|
|
199
|
+
# binding of as `Dynamic[top]`. A provisional record of a `ReturnNode` therefore yields to the walk's record of
|
|
200
|
+
# the SAME node, and stands only where the walk never reaches that node: a block in expression position (a call
|
|
201
|
+
# argument, an array element, a chain receiver, a `return` value, an endless-def body), whose tail the side pass
|
|
202
|
+
# is the only walk to type.
|
|
203
|
+
class ReturnSink
|
|
204
|
+
def initialize
|
|
205
|
+
@types = []
|
|
206
|
+
@recorded = Set.new.compare_by_identity
|
|
207
|
+
@provisional = []
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def record(node, type)
|
|
211
|
+
if Thread.current[PROVISIONAL_RETURNS_KEY]
|
|
212
|
+
@provisional << [node, type]
|
|
213
|
+
else
|
|
214
|
+
@types << type
|
|
215
|
+
@recorded << node
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# The walk's records, then each provisional record whose node the walk did not record.
|
|
220
|
+
def types
|
|
221
|
+
@types + @provisional.filter_map { |node, type| type unless @recorded.include?(node) }
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
private_constant :ReturnSink
|
|
225
|
+
|
|
186
226
|
# Thread-local sink (an Array of `[NextNode, Type]`) collecting the value each `next` carries out of the block it
|
|
187
227
|
# leaves, so `ExpressionTyper#type_block_body` can join those arms into the block's value type. Issue #841: the
|
|
188
228
|
# block-return pass modelled only the fall-through tail, so `ops.all? { |o| next false unless o; true }` read as
|
|
@@ -310,14 +350,31 @@ module Rigor
|
|
|
310
350
|
# method-return type.
|
|
311
351
|
def self.with_return_sink
|
|
312
352
|
previous = Thread.current[RETURN_SINK_KEY]
|
|
313
|
-
|
|
353
|
+
previous_provisional = Thread.current[PROVISIONAL_RETURNS_KEY]
|
|
354
|
+
sink = ReturnSink.new
|
|
314
355
|
Thread.current[RETURN_SINK_KEY] = sink
|
|
356
|
+
Thread.current[PROVISIONAL_RETURNS_KEY] = nil
|
|
315
357
|
begin
|
|
316
358
|
result = yield
|
|
317
359
|
ensure
|
|
318
360
|
Thread.current[RETURN_SINK_KEY] = previous
|
|
361
|
+
Thread.current[PROVISIONAL_RETURNS_KEY] = previous_provisional
|
|
362
|
+
end
|
|
363
|
+
[result, sink.types]
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Runs `block` with the `return`s it reaches filed as provisional ({ReturnSink}), restoring the previous state
|
|
367
|
+
# on exit. Issue #2299 — {ExpressionTyper#type_block_body}'s tail-only answer types `return r if r` without the
|
|
368
|
+
# `r = yield e` before it, so `r` read `Dynamic[top]` and that went into the enclosing method's return type
|
|
369
|
+
# although the statement walk of the same block recorded `r` with its real binding.
|
|
370
|
+
def self.provisional_returns
|
|
371
|
+
previous = Thread.current[PROVISIONAL_RETURNS_KEY]
|
|
372
|
+
Thread.current[PROVISIONAL_RETURNS_KEY] = true
|
|
373
|
+
begin
|
|
374
|
+
yield
|
|
375
|
+
ensure
|
|
376
|
+
Thread.current[PROVISIONAL_RETURNS_KEY] = previous
|
|
319
377
|
end
|
|
320
|
-
[result, sink]
|
|
321
378
|
end
|
|
322
379
|
|
|
323
380
|
# Runs `block` with a fresh `next` sink installed, then yields the collected `[NextNode, Type]` pairs to the
|
|
@@ -365,7 +422,18 @@ module Rigor
|
|
|
365
422
|
# Default: the node is treated as a pure expression. Type it through the existing expression typer (which
|
|
366
423
|
# observes the current scope's locals) and leave the scope unchanged, but for the match globals a call in it
|
|
367
424
|
# may rebind (`super(line.sub(re, ""))`, issue #1365).
|
|
368
|
-
|
|
425
|
+
after = reset_ivars_past_unknown_code(node, forget_rebound_specials(@scope, node))
|
|
426
|
+
[@scope.type_of(node, tracer: @tracer), after]
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Issue #1402 — `after`, past a statement `yield` or `super`, with each narrowed instance variable widened over
|
|
430
|
+
# its class-ivar seed ({InterveningIvarReset.after_operand}): the block `yield` runs, and the superclass method
|
|
431
|
+
# `super` runs on `self`, may write any of them, as an implicit-self call may. Inside an operand the statement
|
|
432
|
+
# that holds it answers instead.
|
|
433
|
+
def reset_ivars_past_unknown_code(node, after)
|
|
434
|
+
return after if @in_operand || !InterveningIvarReset.candidate?(node)
|
|
435
|
+
|
|
436
|
+
InterveningIvarReset.after_operand(node, after)
|
|
369
437
|
end
|
|
370
438
|
|
|
371
439
|
# Issue #1429 — a compound write or a `for` loop calls a method its syntax does not spell (`r += r` calls `r.+`,
|
|
@@ -438,19 +506,28 @@ module Rigor
|
|
|
438
506
|
[type, fall_through, join_jump_scopes(fall_through, sink, JumpTargets.of(body, Prism::NextNode))]
|
|
439
507
|
end
|
|
440
508
|
|
|
441
|
-
# ADR-89 WD2 — the sorted
|
|
509
|
+
# ADR-89 WD2 — the sorted slots of the parameters (positional, then keyword) whose CONTENT `def_node` mutates
|
|
442
510
|
# (`callee_content_mutated_parameters`, the ADR-56 arg-flooring surface a caller consumes). A per-def
|
|
443
511
|
# static property of the AST — this is a pure exposure of the existing private computation (it never
|
|
444
512
|
# reads the scope), so the incremental session can compute it on any `StatementEvaluator` (an empty
|
|
445
513
|
# scope suffices), persist it in a return summary, and re-check a callee's symbol dependents when it
|
|
446
514
|
# moves (a caller's arg flooring changes even if the callee's return does not).
|
|
515
|
+
#
|
|
516
|
+
# Issue #2104 — a mutated keyword parameter reports its keyword symbol, after the positions. A parameter the
|
|
517
|
+
# def hands on to a self-call counts whether or not the callee resolves: the session's empty scope resolves no
|
|
518
|
+
# callee, and without it an edit that starts or stops forwarding a parameter to a mutating callee would leave
|
|
519
|
+
# the effects equal and the callers' floors stale.
|
|
447
520
|
def content_mutated_parameter_positions(def_node)
|
|
448
|
-
|
|
521
|
+
slots = content_parameter_slots(def_node)
|
|
522
|
+
census = callee_content_mutated_parameters(def_node).values
|
|
523
|
+
each_forwarding_call(def_node, slots) { |_, forwarded| census += forwarded.map { |name, _| slots[name] } }
|
|
524
|
+
positions, keywords = census.uniq.partition { |slot| slot.is_a?(Integer) }
|
|
525
|
+
positions.sort + keywords.sort
|
|
449
526
|
end
|
|
450
527
|
|
|
451
|
-
# The local-variable reads among `call_node`'s positional
|
|
452
|
-
# content-mutates, when `call_node` is a self-dispatch call resolving to a user def in this evaluator's
|
|
453
|
-
# (`callee_content_mutated_parameters`); empty for any other call. These are the locals the straight-line
|
|
528
|
+
# The local-variable reads among `call_node`'s arguments (positional, or a keyword's value) whose parameter the
|
|
529
|
+
# callee content-mutates, when `call_node` is a self-dispatch call resolving to a user def in this evaluator's
|
|
530
|
+
# scope (`callee_content_mutated_parameters`); empty for any other call. These are the locals the straight-line
|
|
454
531
|
# callee floor ({#widen_callee_escaped_argument_captures}) floors after the call, and the ones
|
|
455
532
|
# {CapturedLocals.content_mutations} reports as a block's callee-mutated captures.
|
|
456
533
|
def content_mutated_arguments(call_node)
|
|
@@ -466,12 +543,25 @@ module Rigor
|
|
|
466
543
|
return NO_ARGUMENT_READS if mutated.empty?
|
|
467
544
|
|
|
468
545
|
argument_nodes = call_node.arguments.arguments
|
|
469
|
-
mutated.values.uniq.filter_map do |
|
|
470
|
-
argument = argument_nodes[
|
|
546
|
+
mutated.values.uniq.filter_map do |slot|
|
|
547
|
+
argument = slot.is_a?(Symbol) ? keyword_argument(argument_nodes, slot) : argument_nodes[slot]
|
|
548
|
+
argument = argument.value if argument.is_a?(Prism::ImplicitNode)
|
|
471
549
|
argument if argument.is_a?(Prism::LocalVariableReadNode)
|
|
472
550
|
end
|
|
473
551
|
end
|
|
474
552
|
|
|
553
|
+
# The value `argument_nodes` pass for the keyword `name` (`poke(acc: a)`), or nil.
|
|
554
|
+
def keyword_argument(argument_nodes, name)
|
|
555
|
+
hash = argument_nodes.last
|
|
556
|
+
return unless hash.is_a?(Prism::KeywordHashNode)
|
|
557
|
+
|
|
558
|
+
pair = hash.elements.find do |element|
|
|
559
|
+
element.is_a?(Prism::AssocNode) && element.key.is_a?(Prism::SymbolNode) &&
|
|
560
|
+
element.key.unescaped.to_sym == name
|
|
561
|
+
end
|
|
562
|
+
pair&.value
|
|
563
|
+
end
|
|
564
|
+
|
|
475
565
|
NO_ARGUMENT_READS = [].freeze
|
|
476
566
|
private_constant :NO_ARGUMENT_READS
|
|
477
567
|
|
|
@@ -664,7 +754,19 @@ module Rigor
|
|
|
664
754
|
# stamped by {#seed_instance_ivars}.
|
|
665
755
|
bound = bound.with_published_constant_mark(:ivar, node.name) if
|
|
666
756
|
published_constant_copy?(node.value, rhs_type, post_rhs)
|
|
667
|
-
[rhs_type, bound]
|
|
757
|
+
[rhs_type, mark_inferred_param_ivar(bound, node)]
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
# Issue #2189 — the ivar twin of {#bind_local_write}'s ADR-67 WD6b stamp: `@x = param; @x.upcase` reads the
|
|
761
|
+
# parameter's open-call-site lower bound back, so the ivar carries the `:inferred_param` mark under its
|
|
762
|
+
# `@`-prefixed name and {Analysis::CheckRules::InferredParamGuard} declines on the read. A write from any
|
|
763
|
+
# other RHS clears it. Method-local only: the class-ivar census seeding a sibling method types the RHS
|
|
764
|
+
# without the inference seed, so no lower bound crosses the method boundary to guard.
|
|
765
|
+
def mark_inferred_param_ivar(bound, node)
|
|
766
|
+
return bound.with_inferred_param_mark(node.name) if
|
|
767
|
+
Analysis::CheckRules::InferredParamGuard.rooted?(node.value, scope)
|
|
768
|
+
|
|
769
|
+
bound.without_inferred_param_mark(node.name)
|
|
668
770
|
end
|
|
669
771
|
|
|
670
772
|
def eval_cvar_write(node)
|
|
@@ -818,11 +920,24 @@ module Rigor
|
|
|
818
920
|
|
|
819
921
|
def compound_eval(node, kind:, op:) # rubocop:disable Naming/MethodParameterName
|
|
820
922
|
current_type = current_type_for(kind, node.name)
|
|
821
|
-
rhs_type, post_rhs = sub_eval(node.value,
|
|
923
|
+
rhs_type, post_rhs = sub_eval(node.value, compound_value_scope(kind, node.name, op))
|
|
822
924
|
result_type = compound_result_type(current_type, rhs_type, op)
|
|
823
925
|
[result_type, rebind_variable(post_rhs, kind, node.name, result_type)]
|
|
824
926
|
end
|
|
825
927
|
|
|
928
|
+
# Issue #2214 — the scope a compound write's value operand runs in. `x &&= v` runs `v` only when `x` is truthy,
|
|
929
|
+
# as `x && (x = v)` does, so `v` reads `x` narrowed to its truthy part: `c &&= c + 1` on an `Integer?` `c` reads
|
|
930
|
+
# `Integer`, not the `nil` that drew `call.possible-nil-receiver`. `x ||= v` and an operator write run `v` from
|
|
931
|
+
# the scope they start in; a variable the scope does not bind is left alone.
|
|
932
|
+
def compound_value_scope(kind, name, operator)
|
|
933
|
+
return scope unless operator == :and
|
|
934
|
+
|
|
935
|
+
current = scope.public_send(VAR_KIND_GETTERS.fetch(kind), name)
|
|
936
|
+
return scope if current.nil?
|
|
937
|
+
|
|
938
|
+
scope.public_send(VAR_KIND_BUILDERS.fetch(kind), name, Narrowing.narrow_truthy(current))
|
|
939
|
+
end
|
|
940
|
+
|
|
826
941
|
VAR_KIND_GETTERS = {
|
|
827
942
|
local: :local, ivar: :ivar, cvar: :cvar, global: :global
|
|
828
943
|
}.freeze
|
|
@@ -892,7 +1007,40 @@ module Rigor
|
|
|
892
1007
|
arg_types: index_write_arg_types(node, result_type))
|
|
893
1008
|
post = post.with_indexed_narrowing(*address, result_type) if address
|
|
894
1009
|
|
|
895
|
-
|
|
1010
|
+
# The value the expression yields takes the memoizing reading ({#index_compound_write_value}, #1155) for a
|
|
1011
|
+
# fresh container or construction rvalue, with a container's literal precision erased
|
|
1012
|
+
# ({#memo_bound_value}).
|
|
1013
|
+
[memo_bound_value(node, result_type), forget_rebound_specials(post, node)]
|
|
1014
|
+
end
|
|
1015
|
+
|
|
1016
|
+
# What `x = (tbl[k] ||= v)` binds `x` to. Where {#index_compound_write_value} reads the memoizing idiom, the
|
|
1017
|
+
# slot may hold ANY earlier value of the table, so `x` is the rvalue only as far as its CLASS is known: a
|
|
1018
|
+
# `{}` / `[]` rvalue (a shape or tuple) erases to `Hash[untyped, untyped]` / `Array[untyped]`, because the
|
|
1019
|
+
# stored table is not empty after the first store (`entry[:name].upcase` is no call on nil), and a
|
|
1020
|
+
# construction (`new(...)`, a nominal) is kept. Every other rvalue (a Constant, a union) keeps the stored
|
|
1021
|
+
# `Dynamic | v`, since a literal would read the slot as freshly created at every later read.
|
|
1022
|
+
def memo_bound_value(node, stored)
|
|
1023
|
+
return stored unless node.is_a?(Prism::IndexOrWriteNode)
|
|
1024
|
+
|
|
1025
|
+
value = index_compound_write_value(node)
|
|
1026
|
+
return stored if value == stored
|
|
1027
|
+
|
|
1028
|
+
erased = erase_memo_container(value)
|
|
1029
|
+
erased || stored
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
def erase_memo_container(type)
|
|
1033
|
+
any = Type::Combinator.untyped
|
|
1034
|
+
case type
|
|
1035
|
+
when Type::HashShape then Type::Combinator.nominal_of("Hash", type_args: [any, any])
|
|
1036
|
+
when Type::Tuple then Type::Combinator.nominal_of("Array", type_args: [any])
|
|
1037
|
+
when Type::Nominal
|
|
1038
|
+
case type.class_name
|
|
1039
|
+
when "Hash" then Type::Combinator.nominal_of("Hash", type_args: [any, any])
|
|
1040
|
+
when "Array" then Type::Combinator.nominal_of("Array", type_args: [any])
|
|
1041
|
+
else type
|
|
1042
|
+
end
|
|
1043
|
+
end
|
|
896
1044
|
end
|
|
897
1045
|
|
|
898
1046
|
# `h[k] &&= v` / `h[k] += v`. Neither had a handler, so both fell to `evaluate`'s default — typed as a pure
|
|
@@ -1349,6 +1497,9 @@ module Rigor
|
|
|
1349
1497
|
def eval_case_when_branches(subject_type, subject, conditions, entry_scope)
|
|
1350
1498
|
results = []
|
|
1351
1499
|
falsey_scope = entry_scope
|
|
1500
|
+
# Issue #1963 — set once a `when` test may have rebound the subject variable, which from there on no longer
|
|
1501
|
+
# holds the value the later tests match ({#subject_when_scopes}).
|
|
1502
|
+
rebound = false
|
|
1352
1503
|
conditions.each do |branch|
|
|
1353
1504
|
# Issue #1359 — a clause's conditions, or its pattern's pins and guard, run before its body and before
|
|
1354
1505
|
# every later clause, and the walk types them without evaluating them, so a reader there forgets `$_` here.
|
|
@@ -1359,7 +1510,11 @@ module Rigor
|
|
|
1359
1510
|
# recursion) so no condition sub-expression is newly typed; `propagate` preserves the entry because it already
|
|
1360
1511
|
# keys the node.
|
|
1361
1512
|
record_clause_entry_scope(branch, falsey_scope)
|
|
1362
|
-
|
|
1513
|
+
if subject && branch.is_a?(Prism::WhenNode)
|
|
1514
|
+
body_scope, falsey_scope, rebound = subject_when_scopes(subject, branch.conditions, falsey_scope, rebound)
|
|
1515
|
+
else
|
|
1516
|
+
body_scope, falsey_scope = branch_body_and_falsey_scopes(subject_type, subject, branch, falsey_scope)
|
|
1517
|
+
end
|
|
1363
1518
|
results << sub_eval(branch, body_scope)
|
|
1364
1519
|
end
|
|
1365
1520
|
[results, falsey_scope]
|
|
@@ -1399,6 +1554,142 @@ module Rigor
|
|
|
1399
1554
|
end
|
|
1400
1555
|
end
|
|
1401
1556
|
|
|
1557
|
+
# Issue #1963 — a `when` clause of a `case` with a subject. Ruby runs each test in order (`test === subject`)
|
|
1558
|
+
# until one matches, so a test that runs code — a call, a write, a lambda literal, whose `===` runs its body —
|
|
1559
|
+
# runs before the clause's body and every later clause. Such a test is evaluated ({#run_when_test}), so a call
|
|
1560
|
+
# in it resets the instance variables and the guard narrowings it may rebind, as a statement call does, and a
|
|
1561
|
+
# write in it binds. The body is reached after any prefix of the tests ran, so it enters from the join of the
|
|
1562
|
+
# scopes each test leaves, the next clause from the scope the last one leaves.
|
|
1563
|
+
#
|
|
1564
|
+
# The subject is evaluated once, before every test, so a test that may rebind the subject variable
|
|
1565
|
+
# ({#when_test_rebinds_subject?}) leaves the variable holding something other than the value the later tests
|
|
1566
|
+
# match: from that test on, no clause narrows it (`case @x when String then 1 when reset! then @x.upcase`, with
|
|
1567
|
+
# `reset!` assigning `@x`, reads `@x` as `reset!` may leave it, not as `nil`). `rebound` carries that across
|
|
1568
|
+
# the clauses; the answer is `[body_scope, falsey_scope, rebound]`. A clause whose tests run no code, before
|
|
1569
|
+
# any test rebound the subject, keeps {Narrowing.case_when_scopes}' narrowing unchanged.
|
|
1570
|
+
def subject_when_scopes(subject, conditions, entry, rebound)
|
|
1571
|
+
unless rebound || conditions.any? { |condition| when_test_runs_code?(condition) }
|
|
1572
|
+
return [*Narrowing.case_when_scopes(subject, conditions, entry), false]
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
after = entry
|
|
1576
|
+
test_exits = conditions.map do |condition|
|
|
1577
|
+
before = after
|
|
1578
|
+
after = run_when_test(condition, before)
|
|
1579
|
+
rebound ||= when_test_rebinds_subject?(subject, condition, before, after)
|
|
1580
|
+
after
|
|
1581
|
+
end
|
|
1582
|
+
body_entry = reduce_scopes_with_nil_injection(test_exits)
|
|
1583
|
+
if rebound
|
|
1584
|
+
body_entry, after = [body_entry, after].map { |s| forget_called_global_subject(subject, s) }
|
|
1585
|
+
return [Narrowing.apply_when_regex_globals(conditions, body_entry), after, true]
|
|
1586
|
+
end
|
|
1587
|
+
|
|
1588
|
+
[Narrowing.case_when_scopes(subject, conditions, body_entry).first,
|
|
1589
|
+
Narrowing.case_when_scopes(subject, conditions, after).last, false]
|
|
1590
|
+
end
|
|
1591
|
+
|
|
1592
|
+
# Whether a `when` test runs code beyond reading a value: a call, `super`, `yield`, a lambda literal or a write.
|
|
1593
|
+
WHEN_TEST_CODE_NODES = [
|
|
1594
|
+
Prism::CallNode, Prism::SuperNode, Prism::ForwardingSuperNode, Prism::YieldNode, Prism::LambdaNode
|
|
1595
|
+
].freeze
|
|
1596
|
+
private_constant :WHEN_TEST_CODE_NODES
|
|
1597
|
+
|
|
1598
|
+
def when_test_runs_code?(condition)
|
|
1599
|
+
return true if OperandEffects.any?(condition)
|
|
1600
|
+
|
|
1601
|
+
found = false
|
|
1602
|
+
Source::NodeWalker.each(condition) do |descendant|
|
|
1603
|
+
found ||= WHEN_TEST_CODE_NODES.any? { |klass| descendant.is_a?(klass) }
|
|
1604
|
+
end
|
|
1605
|
+
found
|
|
1606
|
+
end
|
|
1607
|
+
|
|
1608
|
+
# The scope a `when` test leaves. The instance variables its implicit-self calls may write are widened first,
|
|
1609
|
+
# a lambda literal's body included, since its `===` runs it ({InterveningIvarReset.after_operand}, which
|
|
1610
|
+
# marks a binding whose narrowing had ruled `nil` out). The test is then evaluated, which binds its writes
|
|
1611
|
+
# and drops the narrowing of a variable a lambda literal captures and writes ({#eval_lambda}). A guard's
|
|
1612
|
+
# narrowing that a lambda body may rebind is restored as a call's would be, since its `===` runs the body.
|
|
1613
|
+
def run_when_test(condition, before)
|
|
1614
|
+
before = InterveningIvarReset.after_operand(condition, before, run_lambdas: true)
|
|
1615
|
+
_type, after = sub_eval(condition, before)
|
|
1616
|
+
body = when_lambda_body(condition)
|
|
1617
|
+
return after unless body && after.guard_narrowed? && GuardRebinding.may_rebind?(body, after)
|
|
1618
|
+
|
|
1619
|
+
after.forget_guard_narrowings
|
|
1620
|
+
end
|
|
1621
|
+
|
|
1622
|
+
def when_lambda_body(condition)
|
|
1623
|
+
condition = condition.body.body.first while condition.is_a?(Prism::ParenthesesNode) &&
|
|
1624
|
+
condition.body.is_a?(Prism::StatementsNode) &&
|
|
1625
|
+
condition.body.body.size == 1
|
|
1626
|
+
condition.body if condition.is_a?(Prism::LambdaNode)
|
|
1627
|
+
end
|
|
1628
|
+
|
|
1629
|
+
# Whether the `when` test `condition`, which took `before` to `after`, may have rebound the variable `subject`
|
|
1630
|
+
# reads: its binding moved, the test writes it, or — for an instance variable — a call in the test may write
|
|
1631
|
+
# it ({InterveningIvarReset.may_write?}), or — for a global or constant — the test may run code that rebinds
|
|
1632
|
+
# one ({GuardRebinding.may_rebind?}).
|
|
1633
|
+
#
|
|
1634
|
+
# A subject that writes a local or instance variable (`case (@x = v)`) is read as that variable, as
|
|
1635
|
+
# {Narrowing.case_when_scopes} narrows it (issue #2211). For such an instance variable the binding test above
|
|
1636
|
+
# answers first (the test's call widens it in {#run_when_test}), so its `may_write?` arm is a guard.
|
|
1637
|
+
def when_test_rebinds_subject?(subject, condition, before, after)
|
|
1638
|
+
subject = case_subject_variable(subject)
|
|
1639
|
+
name = case_subject_name(subject)
|
|
1640
|
+
return false if name.nil?
|
|
1641
|
+
return true unless case_subject_binding(subject, before) == case_subject_binding(subject, after)
|
|
1642
|
+
return true if OperandEffects.written_variables(condition).include?(name)
|
|
1643
|
+
|
|
1644
|
+
case subject
|
|
1645
|
+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode
|
|
1646
|
+
InterveningIvarReset.may_write?(condition, name, before, run_lambdas: true)
|
|
1647
|
+
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode
|
|
1648
|
+
false
|
|
1649
|
+
else
|
|
1650
|
+
body = when_lambda_body(condition)
|
|
1651
|
+
GuardRebinding.may_rebind?(condition, before) || (!body.nil? && GuardRebinding.may_rebind?(body, before))
|
|
1652
|
+
end
|
|
1653
|
+
end
|
|
1654
|
+
|
|
1655
|
+
# Issue #2211 — a global subject from a `when` test that may rebind it on ({#when_test_rebinds_subject?}): a
|
|
1656
|
+
# method may assign a global, and the statement walk keeps a global's binding past a call, so the clause
|
|
1657
|
+
# bodies and the later clauses read it as `Dynamic[top]`, as the scope index's walk of a value-position `case`
|
|
1658
|
+
# reads it
|
|
1659
|
+
# (`ScopeIndexer#forget_called_subject`). `$g = nil; case $g when String then 1 when reset! then 2 else $g + 1
|
|
1660
|
+
# end`, with `reset!` assigning `$g`, is `6` in Ruby. An instance variable a test's call may write is widened by
|
|
1661
|
+
# {#run_when_test} instead; any other subject is left as is.
|
|
1662
|
+
def forget_called_global_subject(subject, scope)
|
|
1663
|
+
return scope unless subject.is_a?(Prism::GlobalVariableReadNode)
|
|
1664
|
+
|
|
1665
|
+
scope.with_global(subject.name, Type::Combinator.untyped)
|
|
1666
|
+
end
|
|
1667
|
+
|
|
1668
|
+
# The subject node the rebinding test reads: a parenthesised one unwrapped, and a local or instance variable
|
|
1669
|
+
# write kept as the variable it writes.
|
|
1670
|
+
def case_subject_variable(subject)
|
|
1671
|
+
Narrowing.unwrap_case_subject(subject)
|
|
1672
|
+
end
|
|
1673
|
+
|
|
1674
|
+
def case_subject_name(subject)
|
|
1675
|
+
case subject
|
|
1676
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode,
|
|
1677
|
+
Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
|
|
1678
|
+
subject.name
|
|
1679
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
1680
|
+
Narrowing.constant_key(subject)
|
|
1681
|
+
end
|
|
1682
|
+
end
|
|
1683
|
+
|
|
1684
|
+
def case_subject_binding(subject, scope)
|
|
1685
|
+
case subject
|
|
1686
|
+
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode then scope.local(subject.name)
|
|
1687
|
+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode then scope.ivar(subject.name)
|
|
1688
|
+
when Prism::GlobalVariableReadNode then scope.global(subject.name)
|
|
1689
|
+
else scope.constant_narrowing(Narrowing.constant_key(subject))
|
|
1690
|
+
end
|
|
1691
|
+
end
|
|
1692
|
+
|
|
1402
1693
|
# Issue #1495 — a `case` without a subject tests each `when` condition for truthiness, in order, and stops at the
|
|
1403
1694
|
# first truthy one, so a clause is the `||` of its conditions in an `if` / `elsif` chain. Each condition is
|
|
1404
1695
|
# evaluated under the falsey edges of the conditions before it, in this clause and every earlier one; the body
|
|
@@ -1517,9 +1808,32 @@ module Rigor
|
|
|
1517
1808
|
|
|
1518
1809
|
# The scope past `node`'s `ensure` clause, entered with `exit_scope`, the `next` and `break` scopes recorded past
|
|
1519
1810
|
# `jump_marks` carried through it.
|
|
1811
|
+
#
|
|
1812
|
+
# Issue #1839 — the code after the `begin` runs only when the clause finished from a `begin` that did, so the
|
|
1813
|
+
# scope it continues from is the clause's under `exit_scope`. A `return`, `next` or `break` in the clause can
|
|
1814
|
+
# also leave from one that raised, after any prefix of the body: those jumps are taken from the pass under the
|
|
1815
|
+
# widened entry ({#widen_clause_entry}), when a jump sink is installed to collect them ({#jump_sinks_installed?}).
|
|
1520
1816
|
def eval_begin_ensure(node, exit_scope, jump_marks)
|
|
1521
|
-
|
|
1522
|
-
|
|
1817
|
+
clause = node.ensure_clause
|
|
1818
|
+
carry_jumps_through_ensure(clause, jump_marks)
|
|
1819
|
+
jumps = jump_sinks_installed? && clause_jumps?(clause.statements)
|
|
1820
|
+
record_widened = widening_recorded?
|
|
1821
|
+
widened = clause.statements && (jumps || record_widened) &&
|
|
1822
|
+
widen_clause_entry(exit_scope, ensure_writes(node), clause.statements)
|
|
1823
|
+
return sub_eval(clause, exit_scope).last unless widened
|
|
1824
|
+
|
|
1825
|
+
record_operand_clause(clause.statements, widened) if record_widened
|
|
1826
|
+
|
|
1827
|
+
widened_options = record_widened ? {} : UNRECORDED
|
|
1828
|
+
if jumps
|
|
1829
|
+
sub_eval(clause, widened, **widened_options)
|
|
1830
|
+
else
|
|
1831
|
+
without_jump_sinks { sub_eval(clause, widened, next_scope_sink: nil, **widened_options) }
|
|
1832
|
+
end
|
|
1833
|
+
plain_options = record_widened ? UNRECORDED : {}
|
|
1834
|
+
return sub_eval(clause, exit_scope, **plain_options).last unless jumps
|
|
1835
|
+
|
|
1836
|
+
without_jump_sinks { sub_eval(clause, exit_scope, next_scope_sink: nil, **plain_options) }.last
|
|
1523
1837
|
end
|
|
1524
1838
|
|
|
1525
1839
|
# Issue #1231 — a rescue clause runs after the body raised from any point in it, and an `ensure` clause after the
|
|
@@ -1527,22 +1841,27 @@ module Rigor
|
|
|
1527
1841
|
# instance variable those wrote at a value the scope it enters with does not hold: `conn = nil; begin; conn =
|
|
1528
1842
|
# open_conn; Integer(s); rescue; conn.size; end` reads the connection. The engine does not track where the body
|
|
1529
1843
|
# raises (the precise recorder of #1397 put new false positives on correct code); it widens instead, and
|
|
1530
|
-
# deliberately imprecisely. Each name the clause
|
|
1531
|
-
#
|
|
1532
|
-
# join would read `x` as `1?` in `x
|
|
1533
|
-
#
|
|
1534
|
-
#
|
|
1535
|
-
# a raise, is covered by the same
|
|
1844
|
+
# deliberately imprecisely. Each name that the clause's entry binds and those clauses write, outside a nested
|
|
1845
|
+
# `def` or class body, and that the clause mentions or they write more than once ({ClauseWidening}),
|
|
1846
|
+
# reads as `Dynamic[top]`, which reports nothing. A plain join would read `x` as `1?` in `x
|
|
1847
|
+
# = 1; begin; foo; x = nil; rescue; x.succ; end`, where it cannot be nil, and a `Dynamic` that keeps the join as
|
|
1848
|
+
# its facet still narrows: `x.nil?` on `Dynamic[1 | 2]` leaves a `nil` member for the next call to report. A name
|
|
1849
|
+
# written inside a nested `ensure` clause, which also runs on the way out of a raise, is covered by the same
|
|
1850
|
+
# floor.
|
|
1851
|
+
#
|
|
1852
|
+
# Issue #1839 — the widening is not for the clause's own reads only: a rescue clause's value, the scope it falls
|
|
1853
|
+
# through with and a `return`, `next` or `break` it takes all come from its widened entry, on every path (an
|
|
1854
|
+
# inferred return included), so `got = begin; conn = make; work; rescue; conn; end` leaves `got` untyped rather
|
|
1855
|
+
# than the entry's `nil`, and a name the clause never mentions but the body writes twice joins the code after the
|
|
1856
|
+
# `begin` untyped rather than at the entry's binding, which misses every write the body made before the last.
|
|
1536
1857
|
#
|
|
1537
1858
|
# The names the clauses of `node` read from: the primary body's for a rescue clause, none under a retry `edge`.
|
|
1538
1859
|
def rescue_writes(node, edge)
|
|
1539
|
-
clause_writes([node.statements]) if node.rescue_clause && edge.nil?
|
|
1860
|
+
clause_writes([node.statements]) if node.rescue_clause && edge.nil?
|
|
1540
1861
|
end
|
|
1541
1862
|
|
|
1542
1863
|
# For the `ensure` clause: the writes of the body, its `else` and every rescue clause.
|
|
1543
1864
|
def ensure_writes(node)
|
|
1544
|
-
return nil unless widening_recorded?
|
|
1545
|
-
|
|
1546
1865
|
sources = [node.statements, node.else_clause]
|
|
1547
1866
|
current = node.rescue_clause
|
|
1548
1867
|
while current
|
|
@@ -1552,83 +1871,68 @@ module Rigor
|
|
|
1552
1871
|
clause_writes(sources)
|
|
1553
1872
|
end
|
|
1554
1873
|
|
|
1555
|
-
|
|
1556
|
-
# or nil for none. A `def` or class body writes nothing here.
|
|
1557
|
-
def clause_writes(sources)
|
|
1558
|
-
writes = nil
|
|
1559
|
-
sources.each { |source| writes = collect_clause_names(source, CLAUSE_WRITE_NODES, nil, writes) if source }
|
|
1560
|
-
writes
|
|
1561
|
-
end
|
|
1874
|
+
def clause_writes(sources) = ClauseWidening.writes(sources)
|
|
1562
1875
|
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
Prism::InstanceVariableWriteNode,
|
|
1566
|
-
Prism::InstanceVariableOperatorWriteNode,
|
|
1567
|
-
Prism::InstanceVariableOrWriteNode,
|
|
1568
|
-
Prism::InstanceVariableAndWriteNode,
|
|
1569
|
-
Prism::InstanceVariableTargetNode
|
|
1570
|
-
].freeze
|
|
1571
|
-
private_constant :CLAUSE_WRITE_NODES
|
|
1876
|
+
CLAUSE_JUMP_NODES = Set[Prism::ReturnNode, Prism::NextNode, Prism::BreakNode].freeze
|
|
1877
|
+
private_constant :CLAUSE_JUMP_NODES
|
|
1572
1878
|
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1879
|
+
# Whether a `return`, `next` or `break` reached here is collected: a pass {#without_jump_sinks} runs collects
|
|
1880
|
+
# none, so an `ensure` clause inside it, nested at any depth, is evaluated once rather than once per pass of
|
|
1881
|
+
# every enclosing clause holding a jump.
|
|
1882
|
+
def jump_sinks_installed?
|
|
1883
|
+
!@next_scope_sink.nil? || JUMP_SINK_KEYS.any? { |key| Thread.current[key] }
|
|
1884
|
+
end
|
|
1577
1885
|
|
|
1578
|
-
#
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
return
|
|
1886
|
+
# Whether `node` holds a `return`, `next` or `break` outside a nested `def`.
|
|
1887
|
+
def clause_jumps?(node)
|
|
1888
|
+
return false if node.nil? || node.is_a?(Prism::DefNode)
|
|
1889
|
+
return true if CLAUSE_JUMP_NODES.include?(node.class)
|
|
1582
1890
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
end
|
|
1586
|
-
node.rigor_each_child { |child| found = collect_clause_names(child, node_classes, among, found) }
|
|
1587
|
-
found
|
|
1891
|
+
node.rigor_each_child { |child| return true if clause_jumps?(child) }
|
|
1892
|
+
false
|
|
1588
1893
|
end
|
|
1589
1894
|
|
|
1590
|
-
# `clause_scope` with
|
|
1591
|
-
# `Dynamic[top]`, or nil when none changes.
|
|
1592
|
-
|
|
1593
|
-
|
|
1895
|
+
# `clause_scope` entering `clause` with the names of `writes` {ClauseWidening.widen} picks rebound to
|
|
1896
|
+
# `Dynamic[top]`, or nil when none changes. An empty rescue clause is widened too: the scope it falls through
|
|
1897
|
+
# with is its entry.
|
|
1898
|
+
def widen_clause_entry(clause_scope, writes, clause) = ClauseWidening.widen(clause_scope, writes, clause)
|
|
1594
1899
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1900
|
+
# Issue #1839 — an operand evaluator ({#thread_operand}) records only the later operands its walk takes, so the
|
|
1901
|
+
# index's walk hands a clause it evaluated the scope of the `begin`'s nearest recorded ancestor, which binds
|
|
1902
|
+
# each name the body writes as the `begin` found it: `puts(begin; conn = make; work; rescue; conn.size; end)`
|
|
1903
|
+
# read `conn` as `nil`. On the check path it records the clause's `statements` under the `widened` entry, with
|
|
1904
|
+
# `$!`, `$@` and `$?` unbound as the walk leaves them for a clause the evaluator did not enter; the
|
|
1905
|
+
# walk fills the clause's nodes from there. The clause's later operands are recorded by its own pass. An
|
|
1906
|
+
# evaluator that records its nodes itself skips this: its pass over the clause records `statements` right
|
|
1907
|
+
# after, and the index keeps the last visit, so the `@on_enter` test is a guard no example can fail.
|
|
1908
|
+
def record_operand_clause(statements, widened)
|
|
1909
|
+
return unless statements && widened && @on_enter.nil? && @operand_recorder && widening_recorded?
|
|
1597
1910
|
|
|
1598
|
-
|
|
1599
|
-
widened = names.reduce(clause_scope) do |acc, name|
|
|
1600
|
-
kind = name.start_with?("@") ? :ivar : :local
|
|
1601
|
-
current = clause_scope.public_send(VAR_KIND_GETTERS.fetch(kind), name)
|
|
1602
|
-
current.nil? || current == untyped ? acc : rebind_variable(acc, kind, name, untyped)
|
|
1603
|
-
end
|
|
1604
|
-
widened.equal?(clause_scope) ? nil : widened
|
|
1911
|
+
@operand_recorder.call(statements, widened.forget_error_info.forget_last_status)
|
|
1605
1912
|
end
|
|
1606
1913
|
|
|
1607
|
-
# Whether
|
|
1608
|
-
# `widen_clauses`)
|
|
1609
|
-
# evaluated once.
|
|
1914
|
+
# Whether this pass records the per-node index under a clause's widened entry: only the check path asks for that
|
|
1915
|
+
# ({#initialize}'s `widen_clauses`). Every other recorder keeps the clause's plain entry ({#recording?}).
|
|
1610
1916
|
def widening_recorded?
|
|
1611
|
-
@widen_clauses &&
|
|
1917
|
+
@widen_clauses && recording?
|
|
1612
1918
|
end
|
|
1613
1919
|
|
|
1614
|
-
#
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
# `begin` continues from. The widened pass collects no `return`, `next`, `break` or retry-edge scope, so the
|
|
1618
|
-
# widening reaches neither an inferred return nor a loop: it can only take diagnostics away inside the clause.
|
|
1619
|
-
def eval_widened_clause(node, statements, clause_scope, writes)
|
|
1620
|
-
widened = widen_clause_entry(clause_scope, statements, writes)
|
|
1621
|
-
return sub_eval(node, clause_scope) unless widened
|
|
1622
|
-
|
|
1623
|
-
eval_widened_pass(node, widened)
|
|
1624
|
-
sub_eval(node, clause_scope, **UNRECORDED)
|
|
1920
|
+
# Whether this pass records the per-node index at all.
|
|
1921
|
+
def recording?
|
|
1922
|
+
!(@on_enter.nil? && @operand_recorder.nil?)
|
|
1625
1923
|
end
|
|
1626
1924
|
|
|
1627
|
-
#
|
|
1628
|
-
|
|
1629
|
-
|
|
1925
|
+
# `[type, scope]` of a rescue clause's `statements` entered with `widened` ({#widen_clause_entry}), the value,
|
|
1926
|
+
# the scope and the jumps the code after the `begin` sees. A recorder that keeps the clause's plain entry
|
|
1927
|
+
# ({#widening_recorded?}) records a second pass under `plain` that reaches no jump sink, so nothing records twice.
|
|
1928
|
+
# The widened pass then records nothing, and a clause nested inside it is evaluated once, so `d` nested clauses
|
|
1929
|
+
# cost `d * (d + 1) / 2` evaluations; every other pass evaluates the clause once.
|
|
1930
|
+
def eval_widened_clause(statements, plain, widened, on_enter:)
|
|
1931
|
+
return [Type::Combinator.constant_of(nil), widened] if statements.nil?
|
|
1932
|
+
return sub_eval(statements, widened, on_enter: on_enter) if widening_recorded? || !recording?
|
|
1630
1933
|
|
|
1631
|
-
without_jump_sinks { sub_eval(
|
|
1934
|
+
without_jump_sinks { sub_eval(statements, plain, next_scope_sink: nil, on_enter: on_enter) }
|
|
1935
|
+
sub_eval(statements, widened, **UNRECORDED)
|
|
1632
1936
|
end
|
|
1633
1937
|
|
|
1634
1938
|
# Runs `block` with no `return`, `next`, `break` or retry-frame sink installed.
|
|
@@ -1739,6 +2043,10 @@ module Rigor
|
|
|
1739
2043
|
CLAUSE_SINK_KEYS = [RETURN_SINK_KEY, NEXT_SINK_KEY, BREAK_SINK_KEY, BREAK_VALUE_SINK_KEY, RETRY_FRAMES_KEY].freeze
|
|
1740
2044
|
private_constant :CLAUSE_SINK_KEYS
|
|
1741
2045
|
|
|
2046
|
+
# The thread-local sinks a `return`, `next` or `break` is collected into.
|
|
2047
|
+
JUMP_SINK_KEYS = (CLAUSE_SINK_KEYS - [RETRY_FRAMES_KEY]).freeze
|
|
2048
|
+
private_constant :JUMP_SINK_KEYS
|
|
2049
|
+
|
|
1742
2050
|
# The retry edge of `node`, or nil when no `retry` in its rescue chain targets it. Allocation-free for a `begin`
|
|
1743
2051
|
# no `retry` targets.
|
|
1744
2052
|
def retry_edge_for(node)
|
|
@@ -2016,7 +2324,7 @@ module Rigor
|
|
|
2016
2324
|
# runs on the way out (`begin; retry; ensure; tries += 1; end`), or one made before a `retry` the evaluator only
|
|
2017
2325
|
# types (`log(tries < 5 ? retry : :gave_up)`).
|
|
2018
2326
|
#
|
|
2019
|
-
# Issue #1231 — `writes` widens the scope each clause enters with
|
|
2327
|
+
# Issue #1231 — `writes` widens the scope each clause enters with ({#widen_clause_entry}) before its
|
|
2020
2328
|
# reference is bound, so `rescue => e` still binds `e` to the exception. It is nil under a retry edge, which
|
|
2021
2329
|
# already carries every scope the primary body raises from ({#widen_entry_for_retry}).
|
|
2022
2330
|
def collect_rescue_chain_results(rescue_node, entry_scope, edge: nil, writes: nil)
|
|
@@ -2024,11 +2332,12 @@ module Rigor
|
|
|
2024
2332
|
results = []
|
|
2025
2333
|
current = rescue_node
|
|
2026
2334
|
while current
|
|
2027
|
-
widened = widen_clause_entry(entry_scope, current.statements
|
|
2028
|
-
|
|
2335
|
+
widened = widen_clause_entry(entry_scope, writes, current.statements)
|
|
2336
|
+
record_operand_clause(current.statements, widened)
|
|
2029
2337
|
rescue_scope = bind_rescue_reference(current, entry_scope)
|
|
2030
2338
|
arm = if widened
|
|
2031
|
-
|
|
2339
|
+
eval_widened_clause(current.statements, rescue_scope, bind_rescue_reference(current, widened),
|
|
2340
|
+
on_enter: on_enter)
|
|
2032
2341
|
else
|
|
2033
2342
|
eval_branch_or_nil(current.statements, rescue_scope, on_enter: on_enter)
|
|
2034
2343
|
end
|
|
@@ -2069,24 +2378,50 @@ module Rigor
|
|
|
2069
2378
|
after.global(:$_) == Type::Combinator.untyped && !LastLine.may_set?(node.statements, scope)
|
|
2070
2379
|
end
|
|
2071
2380
|
|
|
2072
|
-
# Issue #1889 — {#
|
|
2073
|
-
#
|
|
2074
|
-
|
|
2075
|
-
|
|
2381
|
+
# Issue #1889 — {#eval_for} with a literal-name `instance_variable_set` in the body joining its value into the
|
|
2382
|
+
# ivar's binding rather than replacing it ({ReflectiveIvarWrite.in_repeating_body}): a `for` body has no rebind
|
|
2383
|
+
# fixpoint. A `while` / `until` body needs no such join since issue #1930: its fixpoint widens the ivar
|
|
2384
|
+
# ({#loop_body_ivar_writes}).
|
|
2076
2385
|
def eval_repeating_for(node) = ReflectiveIvarWrite.in_repeating_body { eval_for(node) }
|
|
2077
2386
|
|
|
2387
|
+
# A loop's evaluation, with its value recorded for `ExpressionTyper#type_of_loop` ({LoopValueRecord}).
|
|
2388
|
+
# A conditional's evaluation, recorded when it evaluated a loop ({LoopValueRecord.conditional}): the typer
|
|
2389
|
+
# types a conditional that holds a loop again from the scope it was evaluated from, and the loop in its branch
|
|
2390
|
+
# is evaluated from a branch scope no record matches.
|
|
2391
|
+
def eval_recorded_if(node) = record_conditional_value(node) { eval_if(node) }
|
|
2392
|
+
|
|
2393
|
+
def eval_recorded_unless(node) = record_conditional_value(node) { eval_unless(node) }
|
|
2394
|
+
|
|
2395
|
+
# The "evaluated a loop" gate is a memory-only guard: recording every conditional would give the same answers, at
|
|
2396
|
+
# the cost of a retained scope per `if` node, so no behavioural example pins it.
|
|
2397
|
+
def record_conditional_value(node)
|
|
2398
|
+
before = LoopValueRecord.loops_evaluated
|
|
2399
|
+
result = yield
|
|
2400
|
+
LoopValueRecord.conditional(node, scope, result.first) unless LoopValueRecord.loops_evaluated == before
|
|
2401
|
+
result
|
|
2402
|
+
end
|
|
2403
|
+
|
|
2404
|
+
def eval_recorded_loop(node) = record_loop_value(node, eval_loop(node))
|
|
2405
|
+
|
|
2406
|
+
def eval_recorded_for(node) = record_loop_value(node, eval_repeating_for(node))
|
|
2407
|
+
|
|
2408
|
+
def record_loop_value(node, result)
|
|
2409
|
+
LoopValueRecord.record(node, scope, result.first)
|
|
2410
|
+
result
|
|
2411
|
+
end
|
|
2412
|
+
|
|
2078
2413
|
# `while pred; body; end` / `until pred; body; end`. The body might run zero or more times, so half-bound names
|
|
2079
|
-
# degrade to `T | nil` in the post-loop scope. The loop expression
|
|
2080
|
-
#
|
|
2414
|
+
# degrade to `T | nil` in the post-loop scope. The loop expression is `nil` joined with the value of every `break`
|
|
2415
|
+
# that leaves this loop ({#loop_value}, issue #2031).
|
|
2081
2416
|
def eval_loop(node)
|
|
2082
|
-
|
|
2083
|
-
post_pred = widen_predicate_pins(node, post_pred)
|
|
2417
|
+
post_pred = loop_predicate_scope(node)
|
|
2084
2418
|
return [Type::Combinator.constant_of(nil), narrow_loop_exit_edge(node, post_pred)] if node.statements.nil?
|
|
2085
2419
|
|
|
2086
2420
|
# The historical single body pass joined with the pre-loop scope. This continues to carry everything the
|
|
2087
2421
|
# fixpoint does NOT track: receiver-mutation widening of non-rebound locals (`buf.push(i)` widens `buf`'s
|
|
2088
|
-
# Tuple), body-introduced locals' nil-injection, an instance variable
|
|
2089
|
-
# fixpoint then OVERLAYS only the rebound
|
|
2422
|
+
# Tuple), body-introduced locals' nil-injection, a rebind of an instance variable the fixpoint does not track
|
|
2423
|
+
# ({#loop_body_ivar_writes}), and the loop value itself. The fixpoint then OVERLAYS only the rebound bindings it
|
|
2424
|
+
# corrects.
|
|
2090
2425
|
#
|
|
2091
2426
|
# Like every fixpoint pass, it enters the body on the predicate's loop-entry edge ({#loop_pass_entry}): the
|
|
2092
2427
|
# body of `while (line = gets)` reads `line` as `String`, and the body of `while gets` reads `$_` as one
|
|
@@ -2096,28 +2431,60 @@ module Rigor
|
|
|
2096
2431
|
# The pass ends with its `next` exits as well as its fall-through ({#loop_iteration}). Its `break` arms are
|
|
2097
2432
|
# superseded by the fixpoint's converged pass ({#loop_break_arms}) except in a `begin … end while` loop, below.
|
|
2098
2433
|
jumps = loop_jumps(node.statements)
|
|
2099
|
-
body_scope, first_breaks = single_pass(node, post_pred, jumps)
|
|
2434
|
+
body_scope, first_breaks, first_values = single_pass(node, post_pred, jumps)
|
|
2100
2435
|
base_scope = join_with_nil_injection(post_pred, body_scope)
|
|
2101
2436
|
|
|
2102
|
-
|
|
2103
|
-
names = rebound + body_first
|
|
2437
|
+
names, body_first = loop_rebound_names(node.statements, post_pred)
|
|
2104
2438
|
|
|
2105
2439
|
# Fast path: a loop whose body rebinds no local skips the rebind fixpoint, but still needs the slice-C content
|
|
2106
2440
|
# writeback (a loop may content-mutate a collection without rebinding any local — `acc << x`), so apply it to
|
|
2107
2441
|
# the single-pass join before returning.
|
|
2108
2442
|
if names.empty?
|
|
2109
2443
|
fast = loop_content_writeback(node.statements, base_scope, pre_body: post_pred)
|
|
2110
|
-
return [
|
|
2444
|
+
return [loop_value(node, first_values), loop_exit(node, post_pred, fast)]
|
|
2111
2445
|
end
|
|
2112
2446
|
|
|
2113
|
-
post_loop = converged_loop_scope(node, post_pred, base_scope, names, body_first, jumps)
|
|
2447
|
+
post_loop, values = converged_loop_scope(node, post_pred, base_scope, names, body_first, jumps)
|
|
2114
2448
|
# A `begin … end while` / `until` body runs once before the predicate is first tested, so that iteration's
|
|
2115
2449
|
# entry lies outside every fixpoint pass's predicate-narrowed one, and a `break` only it can take (`if state ==
|
|
2116
2450
|
# :idle` under `end while state != :idle`) is dead in every converged pass. The single pass runs from the
|
|
2117
2451
|
# un-narrowed post-predicate scope, so its arms stand in for that first iteration.
|
|
2118
2452
|
post_loop = join_break_scopes(post_loop, first_breaks, names) if node.begin_modifier?
|
|
2119
2453
|
post_loop = loop_exit(node, post_pred, post_loop)
|
|
2120
|
-
[
|
|
2454
|
+
[loop_value(node, first_values + values), post_loop]
|
|
2455
|
+
end
|
|
2456
|
+
|
|
2457
|
+
# The scope a loop's predicate leaves, which the body and the exit edge start from. Issue #1930 — the predicate
|
|
2458
|
+
# runs before every iteration but is walked once, so a literal-name `instance_variable_set` in it joins its value
|
|
2459
|
+
# and `Dynamic[top]` ({ReflectiveIvarWrite.in_repeating_body}): the rebind fixpoint tracks the body's writes only.
|
|
2460
|
+
def loop_predicate_scope(node)
|
|
2461
|
+
entry = loop_entry_scope(node)
|
|
2462
|
+
_pred_type, post_pred = ReflectiveIvarWrite.in_repeating_body { sub_eval(node.predicate, entry) }
|
|
2463
|
+
widen_predicate_pins(node, post_pred)
|
|
2464
|
+
end
|
|
2465
|
+
|
|
2466
|
+
# Issue #2031 — a `while` / `until` expression is `nil` when its predicate ends it and the value of the `break`
|
|
2467
|
+
# that ends it otherwise: `x = while i < k; break @r if i == 4; end` is `"four"` or `nil`. `break_values` holds
|
|
2468
|
+
# what each `break` targeting this loop carried, from the single body pass (the first iteration) and the pass
|
|
2469
|
+
# from the converged bindings ({#loop_break_arms}), which together cover the entry of every iteration. A literal
|
|
2470
|
+
# `while true` / `until false` / `until nil` is never ended by its predicate, so only its `break`s give it a
|
|
2471
|
+
# value; any other predicate is assumed to end it, since its first evaluation says nothing about later ones. A
|
|
2472
|
+
# loop with no reachable `break` keeps its `nil`.
|
|
2473
|
+
def loop_value(node, break_values)
|
|
2474
|
+
nil_type = Type::Combinator.constant_of(nil)
|
|
2475
|
+
return nil_type if break_values.empty?
|
|
2476
|
+
return Type::Combinator.union(*break_values) if endless_loop_predicate?(node)
|
|
2477
|
+
|
|
2478
|
+
Type::Combinator.union(nil_type, *break_values)
|
|
2479
|
+
end
|
|
2480
|
+
|
|
2481
|
+
def endless_loop_predicate?(node)
|
|
2482
|
+
predicate = node.predicate
|
|
2483
|
+
if node.is_a?(Prism::UntilNode)
|
|
2484
|
+
predicate.is_a?(Prism::FalseNode) || predicate.is_a?(Prism::NilNode)
|
|
2485
|
+
else
|
|
2486
|
+
predicate.is_a?(Prism::TrueNode)
|
|
2487
|
+
end
|
|
2121
2488
|
end
|
|
2122
2489
|
|
|
2123
2490
|
# Issue #1359 — the scope a loop's predicate first runs from: a body that may set `$_` runs again after it ran,
|
|
@@ -2179,7 +2546,8 @@ module Rigor
|
|
|
2179
2546
|
|
|
2180
2547
|
# The continuation scope for a loop whose body rebinds locals: the ADR-56 slice-B rebind fixpoint overlaid on
|
|
2181
2548
|
# `base_scope`, then the slice-C receiver-content writeback, then the `break`-path bindings the fall-through
|
|
2182
|
-
# dropped (`flag = true; break` -> `flag` is `false | true`, not the stale `false`).
|
|
2549
|
+
# dropped (`flag = true; break` -> `flag` is `false | true`, not the stale `false`). Returns `[scope, values]`,
|
|
2550
|
+
# `values` being what the converged pass's `break`s carry ({#loop_value}).
|
|
2183
2551
|
def converged_loop_scope(node, post_pred, base_scope, names, body_first, jumps)
|
|
2184
2552
|
# ADR-56 slice B — loop-body fixpoint. The body runs 0..N times and may compound (`d *= 2`), so the historical
|
|
2185
2553
|
# single body pass joined with the pre-loop scope kept stale folded constants (`d = 1; while …; d *= 2; end` →
|
|
@@ -2187,20 +2555,20 @@ module Rigor
|
|
|
2187
2555
|
# fixpoint slice A uses for non-escaping block captures. Seed: a pre-existing local seeds with its
|
|
2188
2556
|
# post-predicate binding; a local FIRST assigned inside the body seeds with `nil` so the 0-iteration path
|
|
2189
2557
|
# degrades it to `T | nil`, matching the nil-injection treatment.
|
|
2190
|
-
break_pass =
|
|
2558
|
+
break_pass = { entry: nil, arms: [], values: [] } if jumps.breaks || later_pass_returns?(node.statements)
|
|
2191
2559
|
result = loop_rebind_fixpoint(node, post_pred, names, body_first, jumps, break_pass)
|
|
2192
2560
|
# Display-path re-record: the fixpoint's body re-evaluations fire `on_enter` with the cap-N INTERMEDIATE
|
|
2193
2561
|
# assumptions, so the last-visit-wins scope index would annotate loop-body lines with stale pre-convergence
|
|
2194
2562
|
# constants. One extra pass from the converged bindings (result discarded) re-records the body's entry scopes.
|
|
2195
2563
|
record_converged_loop_body(node, post_pred, result, names, body_first, jumps, break_pass)
|
|
2196
|
-
post_loop = result.reduce(base_scope) { |acc, (name, type)| acc
|
|
2564
|
+
post_loop = result.reduce(base_scope) { |acc, (name, type)| bind_capture(acc, name, type) }
|
|
2197
2565
|
# ADR-56 slice C — loop-body receiver-content element-type join. A loop that content-mutates a collection (`acc
|
|
2198
2566
|
# << n`) keeps only the seed's element types after the single-pass widen; join the appended/stored types into
|
|
2199
2567
|
# the continuation collection. Pre-state comes from `post_pred` for a name the loop only content-mutates and
|
|
2200
2568
|
# from `post_loop` for one it also rebinds, so composition still works — see {#loop_content_writeback}.
|
|
2201
2569
|
post_loop = loop_content_writeback(node.statements, post_loop, pre_body: post_pred, rebound: names)
|
|
2202
|
-
arms = loop_break_arms(node, post_pred, result, body_first, jumps, break_pass)
|
|
2203
|
-
join_break_scopes(post_loop, arms, names)
|
|
2570
|
+
arms, values = loop_break_arms(node, post_pred, result, body_first, jumps, break_pass)
|
|
2571
|
+
[join_break_scopes(post_loop, arms, names), values]
|
|
2204
2572
|
end
|
|
2205
2573
|
|
|
2206
2574
|
# A loop's continuation: a local its body and predicate only narrowed from `Dynamic[top]` re-joins to it
|
|
@@ -2261,6 +2629,13 @@ module Rigor
|
|
|
2261
2629
|
NO_LOOP_NAMES = [].freeze
|
|
2262
2630
|
private_constant :NO_LOOP_BINDINGS, :NO_LOOP_NAMES
|
|
2263
2631
|
|
|
2632
|
+
# The instance-variable write forms {#loop_body_ivar_writes} counts.
|
|
2633
|
+
IVAR_WRITE_NODES = Set[
|
|
2634
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
2635
|
+
Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableTargetNode
|
|
2636
|
+
].freeze
|
|
2637
|
+
private_constant :IVAR_WRITE_NODES
|
|
2638
|
+
|
|
2264
2639
|
# A body with no targeting jump pays two allocation-free scans.
|
|
2265
2640
|
def loop_jumps(statements)
|
|
2266
2641
|
kinds = JumpTargets.kinds(statements, LOOP_JUMP_CLASSES)
|
|
@@ -2286,8 +2661,11 @@ module Rigor
|
|
|
2286
2661
|
[sink, result]
|
|
2287
2662
|
end
|
|
2288
2663
|
|
|
2289
|
-
# One evaluation of a loop body from `entry`. Returns `[exit, breaks]`: the scope the iteration ends with,
|
|
2290
|
-
# scopes at the `break`s that target the loop ({LoopJumps})
|
|
2664
|
+
# One evaluation of a loop body from `entry`. Returns `[exit, breaks, values]`: the scope the iteration ends with,
|
|
2665
|
+
# the scopes at the `break`s that target the loop ({LoopJumps}), and the values those `break`s carry, read from a
|
|
2666
|
+
# `break`-value sink of the loop's own ({#loop_value}); filtering it by the loop's `break`s is defensive, since a
|
|
2667
|
+
# nested loop installs its own sink and a nested block's call evaluates its body under another, so no example
|
|
2668
|
+
# tells the filter apart today. Every reader of a loop body goes through here — the
|
|
2291
2669
|
# single pass `eval_loop` joins with the pre-loop scope, each pass of its rebind fixpoint, and `eval_for`'s only
|
|
2292
2670
|
# pass.
|
|
2293
2671
|
#
|
|
@@ -2305,18 +2683,21 @@ module Rigor
|
|
|
2305
2683
|
recording = recorded ? {} : UNRECORDED
|
|
2306
2684
|
evaluate = -> { sub_eval(statements, entry, next_scope_sink: next_sink, **recording).last }
|
|
2307
2685
|
if jumps.breaks
|
|
2308
|
-
break_sink, fall_through = collect_break_scopes
|
|
2686
|
+
break_sink, (fall_through, value_sink) = collect_break_scopes do
|
|
2687
|
+
StatementEvaluator.with_break_value_sink(&evaluate)
|
|
2688
|
+
end
|
|
2309
2689
|
breaks = targeted_scopes(break_sink, jumps.breaks)
|
|
2690
|
+
values = targeted_scopes(value_sink, jumps.breaks)
|
|
2310
2691
|
else
|
|
2311
2692
|
fall_through = evaluate.call
|
|
2312
|
-
breaks = NO_BREAK_ARMS
|
|
2693
|
+
breaks = values = NO_BREAK_ARMS
|
|
2313
2694
|
end
|
|
2314
|
-
return [fall_through, breaks] if next_sink.nil?
|
|
2695
|
+
return [fall_through, breaks, values] if next_sink.nil?
|
|
2315
2696
|
|
|
2316
2697
|
exit_scope = targeted_scopes(next_sink, jumps.nexts).reduce(fall_through) do |acc, next_scope|
|
|
2317
2698
|
join_with_nil_injection(acc, next_scope)
|
|
2318
2699
|
end
|
|
2319
|
-
[exit_scope, breaks]
|
|
2700
|
+
[exit_scope, breaks, values]
|
|
2320
2701
|
end
|
|
2321
2702
|
|
|
2322
2703
|
# The `break` scopes the continuation joins. A `break` leaves the loop, so its binding starts no further iteration
|
|
@@ -2336,12 +2717,24 @@ module Rigor
|
|
|
2336
2717
|
# The unrecorded pass is otherwise an ordinary evaluation: a `return` it reaches joins the enclosing method's
|
|
2337
2718
|
# inferred return type, as one any other pass reaches does. That only widens the return, toward values a capped
|
|
2338
2719
|
# fixpoint's own passes never evaluated.
|
|
2720
|
+
#
|
|
2721
|
+
# Returns `[arms, values]`, `values` being what the same pass's `break`s carry ({#loop_value}).
|
|
2339
2722
|
def loop_break_arms(node, post_pred, converged, body_first, jumps, break_pass)
|
|
2340
|
-
return NO_BREAK_ARMS if break_pass.nil?
|
|
2341
|
-
return break_pass
|
|
2723
|
+
return [NO_BREAK_ARMS, NO_BREAK_ARMS] if break_pass.nil?
|
|
2724
|
+
return break_pass.values_at(:arms, :values) if break_pass[:entry] == converged.except(*body_first)
|
|
2342
2725
|
|
|
2343
2726
|
entry = loop_pass_entry(node, post_pred, converged, body_first, jumps)
|
|
2344
|
-
loop_iteration(node.statements, entry, jumps, recorded: false).
|
|
2727
|
+
loop_iteration(node.statements, entry, jumps, recorded: false).drop(1)
|
|
2728
|
+
end
|
|
2729
|
+
|
|
2730
|
+
# Issue #2128 — whether a pass from the converged bindings must run for the `return`s in a loop body: a method's
|
|
2731
|
+
# return sink is active and the body holds a `return` that exits the method ({ReturnBarrier.exits_method?}). A
|
|
2732
|
+
# `return` whose condition reads a loop-carried local (`return false if seen > 3` after `seen += 1`) is dead in
|
|
2733
|
+
# every capped fixpoint pass, which runs from a value-pinned assumption (`seen` at most `3`), and only the pass
|
|
2734
|
+
# from the widened binding reaches it ({#loop_break_arms}); without it the method inferred `true` and a
|
|
2735
|
+
# caller's test on it folded always-truthy on correct code.
|
|
2736
|
+
def later_pass_returns?(statements)
|
|
2737
|
+
!Thread.current[RETURN_SINK_KEY].nil? && ReturnBarrier.exits_method?(statements)
|
|
2345
2738
|
end
|
|
2346
2739
|
|
|
2347
2740
|
# Joins each `break` arm's body-written local bindings into the loop continuation, so a `break`-path binding the
|
|
@@ -2356,11 +2749,11 @@ module Rigor
|
|
|
2356
2749
|
floor = Type::Combinator.untyped
|
|
2357
2750
|
breaks.reduce(continuation) do |cont, break_scope|
|
|
2358
2751
|
names.reduce(cont) do |acc, name|
|
|
2359
|
-
break_value =
|
|
2360
|
-
current =
|
|
2752
|
+
break_value = CapturedLocals.bound_type(break_scope, name)
|
|
2753
|
+
current = CapturedLocals.bound_type(acc, name)
|
|
2361
2754
|
next acc if break_value.nil? || current == floor
|
|
2362
2755
|
|
|
2363
|
-
acc
|
|
2756
|
+
bind_capture(acc, name, current ? Type::Combinator.union(current, break_value) : break_value)
|
|
2364
2757
|
end
|
|
2365
2758
|
end
|
|
2366
2759
|
end
|
|
@@ -2433,7 +2826,7 @@ module Rigor
|
|
|
2433
2826
|
# treatment. `break_pass` rides along so every pass leaves its `break` arms for {#loop_break_arms}.
|
|
2434
2827
|
def loop_rebind_fixpoint(node, post_pred, names, body_first, jumps, break_pass)
|
|
2435
2828
|
nil_const = Type::Combinator.constant_of(nil)
|
|
2436
|
-
seed = names.to_h { |name| [name,
|
|
2829
|
+
seed = names.to_h { |name| [name, CapturedLocals.bound_type(post_pred, name) || nil_const] }
|
|
2437
2830
|
evaluate_body = lambda do |bindings|
|
|
2438
2831
|
loop_body_exit_bindings(node, post_pred, bindings, names, body_first, jumps, break_pass)
|
|
2439
2832
|
end
|
|
@@ -2465,6 +2858,34 @@ module Rigor
|
|
|
2465
2858
|
[pre_existing.uniq, body_first.uniq - pre_existing.uniq]
|
|
2466
2859
|
end
|
|
2467
2860
|
|
|
2861
|
+
# Every name {#eval_loop}'s rebind fixpoint tracks, and the body-first locals among them: the locals the body
|
|
2862
|
+
# rebinds ({#loop_body_local_writes}) and the instance variables ({#loop_body_ivar_writes}).
|
|
2863
|
+
def loop_rebound_names(statements, base_scope)
|
|
2864
|
+
rebound, body_first = loop_body_local_writes(statements, base_scope)
|
|
2865
|
+
[rebound + loop_body_ivar_writes(statements, base_scope) + body_first, body_first]
|
|
2866
|
+
end
|
|
2867
|
+
|
|
2868
|
+
# Issue #1930 — the instance variables the loop body can rebind, which the rebind fixpoint widens as it widens a
|
|
2869
|
+
# local: a write node (`@n = i + 1`, any depth, a nested block's included) or a literal-name
|
|
2870
|
+
# `instance_variable_set` on `self` ({ReflectiveIvarWrite.literal_name}). Only an ivar `base_scope` binds off its
|
|
2871
|
+
# ADR-58 declaration seed counts ({CapturedLocals.rebindable_ivar?}), as for a block's write-back: one the method
|
|
2872
|
+
# has not bound reads `Dynamic[top]` before the loop, and the join drops it after; one on its declaration seed
|
|
2873
|
+
# already holds the union of every write in the class. A call that writes the ivar inside the callee
|
|
2874
|
+
# (`self.bump`) is not a write here; the intervening-call reset handles it.
|
|
2875
|
+
def loop_body_ivar_writes(statements, base_scope)
|
|
2876
|
+
return NO_LOOP_NAMES if base_scope.ivars.empty?
|
|
2877
|
+
|
|
2878
|
+
names = nil
|
|
2879
|
+
Source::NodeWalker.each(statements) do |descendant|
|
|
2880
|
+
name = ReflectiveIvarWrite.literal_name(descendant) ||
|
|
2881
|
+
(descendant.name if IVAR_WRITE_NODES.include?(descendant.class))
|
|
2882
|
+
next if name.nil? || names&.include?(name) || !CapturedLocals.rebindable_ivar?(base_scope, name)
|
|
2883
|
+
|
|
2884
|
+
(names ||= []) << name
|
|
2885
|
+
end
|
|
2886
|
+
names || NO_LOOP_NAMES
|
|
2887
|
+
end
|
|
2888
|
+
|
|
2468
2889
|
# Evaluates the loop body once with each fixpoint-tracked local bound to the supplied running assumption and
|
|
2469
2890
|
# returns the per-name exit binding. Used as the {BodyFixpoint} body-evaluator for `eval_loop`.
|
|
2470
2891
|
#
|
|
@@ -2488,19 +2909,20 @@ module Rigor
|
|
|
2488
2909
|
# it.
|
|
2489
2910
|
def loop_body_exit_bindings(node, post_pred, bindings, names, body_first, jumps, break_pass = nil)
|
|
2490
2911
|
entry = loop_pass_entry(node, post_pred, bindings, body_first, jumps)
|
|
2491
|
-
exit_scope, breaks = loop_iteration(node.statements, entry, jumps)
|
|
2912
|
+
exit_scope, breaks, values = loop_iteration(node.statements, entry, jumps)
|
|
2492
2913
|
if break_pass
|
|
2493
2914
|
break_pass[:entry] = bindings.except(*body_first)
|
|
2494
2915
|
break_pass[:arms] = breaks
|
|
2916
|
+
break_pass[:values] = values
|
|
2495
2917
|
end
|
|
2496
|
-
names.to_h { |name| [name,
|
|
2918
|
+
names.to_h { |name| [name, CapturedLocals.bound_type(exit_scope, name)] }
|
|
2497
2919
|
end
|
|
2498
2920
|
|
|
2499
2921
|
# The scope one fixpoint pass enters the body with: `post_pred` overlaid with the pre-existing names' running
|
|
2500
2922
|
# assumption, then narrowed by the predicate's loop-entry edge ({#loop_body_exit_bindings} carries the why).
|
|
2501
2923
|
def loop_pass_entry(node, post_pred, bindings, body_first, jumps)
|
|
2502
2924
|
overlaid = bindings.except(*body_first)
|
|
2503
|
-
entry = overlaid.reduce(post_pred) { |acc, (name, type)| acc
|
|
2925
|
+
entry = overlaid.reduce(post_pred) { |acc, (name, type)| bind_capture(acc, name, type) }
|
|
2504
2926
|
entry = loop_content_entry(node.statements, post_pred, entry)
|
|
2505
2927
|
truthy_scope, falsey_scope = Narrowing.predicate_scopes(node.predicate, entry)
|
|
2506
2928
|
edge = node.is_a?(Prism::UntilNode) ? falsey_scope : truthy_scope
|
|
@@ -2550,29 +2972,157 @@ module Rigor
|
|
|
2550
2972
|
# index variable AND every local written in the body leak to the surrounding scope. The collection is evaluated
|
|
2551
2973
|
# once; the body runs zero or more times, so the post-loop scope is the join of the no-iteration scope (just
|
|
2552
2974
|
# `post_collection`) and the body scope, with half-bound names degraded to `T | nil` via nil-injection. The loop
|
|
2553
|
-
# expression
|
|
2554
|
-
# `for`, whose value in Ruby is the collection it iterated (issue #1216).
|
|
2975
|
+
# expression is the collection's `each` value joined with every `break` value ({#for_value}, issue #1216).
|
|
2555
2976
|
def eval_for(node)
|
|
2556
2977
|
coll_type, post_coll = sub_eval(node.collection, scope)
|
|
2557
|
-
|
|
2558
|
-
# Issue #1359 — a body that may set `$_` runs again after it ran, as a `while` body does ({#eval_loop}).
|
|
2559
|
-
body_entry = LastLine.forget_if_set(bind_for_index(node.index, element_type, post_coll), node.statements)
|
|
2560
|
-
body_entry = forget_guard_for_body(node, post_coll, body_entry)
|
|
2561
|
-
body_entry = loop_content_entry(node.statements, post_coll, body_entry)
|
|
2978
|
+
body_entry = for_body_entry(node, for_iteration_element_type(coll_type), post_coll)
|
|
2562
2979
|
|
|
2563
2980
|
if node.statements.nil?
|
|
2564
|
-
return [
|
|
2981
|
+
return [for_value(node, coll_type, post_coll, NO_BREAK_ARMS),
|
|
2982
|
+
join_with_nil_injection(post_coll, body_entry)]
|
|
2565
2983
|
end
|
|
2566
2984
|
|
|
2567
2985
|
# The body pass ends with its `next` exits as well as its fall-through, and its `break` arms are recovered into
|
|
2568
2986
|
# the continuation (the `for` sibling of `eval_loop`'s break join; `for` has no fixpoint, so the single pass is
|
|
2569
2987
|
# the only continuation and the only source of `break` arms).
|
|
2570
2988
|
jumps = loop_jumps(node.statements)
|
|
2571
|
-
body_scope, breaks = loop_iteration(node.statements, body_entry, jumps)
|
|
2989
|
+
body_scope, breaks, values = loop_iteration(node.statements, body_entry, jumps)
|
|
2990
|
+
values += later_for_pass_values(node.statements, body_entry, body_scope, jumps)
|
|
2572
2991
|
continuation = join_with_nil_injection(post_coll, body_scope)
|
|
2573
2992
|
pre_existing, body_first = loop_body_local_writes(node.statements, post_coll)
|
|
2574
|
-
continuation = join_break_scopes(continuation, breaks, pre_existing + body_first)
|
|
2575
|
-
|
|
2993
|
+
continuation = for_exit(node, post_coll, join_break_scopes(continuation, breaks, pre_existing + body_first))
|
|
2994
|
+
continuation = widen_for_elements(node, continuation)
|
|
2995
|
+
[for_value(node, coll_type, continuation, values), continuation]
|
|
2996
|
+
end
|
|
2997
|
+
|
|
2998
|
+
# Issue #2262 — the variables the collection reads, past a body that mutates an element in place: `for x in a;
|
|
2999
|
+
# x << 2; end` leaves `a[0]` holding what the literal's shape did not.
|
|
3000
|
+
def widen_for_elements(node, scope)
|
|
3001
|
+
DeepErasure.widen_element_aliases(
|
|
3002
|
+
node.collection, node.statements, DeepErasure.parameter_names(node.index), scope
|
|
3003
|
+
)
|
|
3004
|
+
end
|
|
3005
|
+
|
|
3006
|
+
# Issue #2128 — a `for` body has no rebind fixpoint, so its only pass enters with every loop-carried local at its
|
|
3007
|
+
# pre-loop binding, and a `return` or `break` whose condition needs a later iteration (`n = 0; for x in xs;
|
|
3008
|
+
# return :f if (n += 1) > 2; end`) was never reached. When the body holds such a jump (a `break`, or a `return`
|
|
3009
|
+
# that exits the method while a return sink is active, {#later_pass_returns?}), one more unrecorded pass runs
|
|
3010
|
+
# with each local and instance variable the body rebinds at its value-pinned widening
|
|
3011
|
+
# (`Type::Combinator.widen_value_pinned`) of its entry binding joined with the first pass's exit, a binding that
|
|
3012
|
+
# covers what any iteration enters with, as the `while` fixpoint's widened binding does. The `return`s it
|
|
3013
|
+
# reaches join the method's return type as any pass's do, and the `break` values it collects are returned for
|
|
3014
|
+
# the loop's value ({#for_value}). The pass records no scope, and the continuation keeps the first pass's
|
|
3015
|
+
# answer.
|
|
3016
|
+
def later_for_pass_values(statements, body_entry, body_scope, jumps)
|
|
3017
|
+
return NO_BREAK_ARMS unless jumps.breaks || later_pass_returns?(statements)
|
|
3018
|
+
|
|
3019
|
+
names, body_first = loop_rebound_names(statements, body_entry)
|
|
3020
|
+
bindings = (names - body_first).filter_map do |name|
|
|
3021
|
+
joined = [body_entry, body_scope].filter_map { |s| CapturedLocals.bound_type(s, name) }
|
|
3022
|
+
[name, Type::Combinator.widen_value_pinned(Type::Combinator.union(*joined))] unless joined.empty?
|
|
3023
|
+
end
|
|
3024
|
+
return NO_BREAK_ARMS if bindings.empty?
|
|
3025
|
+
|
|
3026
|
+
entry = bindings.reduce(body_entry) { |acc, (name, type)| bind_capture(acc, name, type) }
|
|
3027
|
+
loop_iteration(statements, entry, jumps, recorded: false)[2]
|
|
3028
|
+
end
|
|
3029
|
+
|
|
3030
|
+
# The core collections whose `each` with a block answers its receiver.
|
|
3031
|
+
FOR_SELF_EACH_CLASSES = Set["Array", "Hash", "Range"].freeze
|
|
3032
|
+
private_constant :FOR_SELF_EACH_CLASSES
|
|
3033
|
+
|
|
3034
|
+
# The nodes through which a `for` body or index may change the iterated collection's contents: a call (an
|
|
3035
|
+
# in-place mutator on the collection or on an alias of it, or a method that reaches it), `yield` / `super`, an
|
|
3036
|
+
# index store, and an operator write, which calls its operator on the variable's value (`b <<= 9` appends to
|
|
3037
|
+
# the array `b` holds).
|
|
3038
|
+
FOR_MUTATING_NODES = Set[
|
|
3039
|
+
Prism::CallNode, Prism::YieldNode, Prism::SuperNode, Prism::ForwardingSuperNode, Prism::IndexTargetNode,
|
|
3040
|
+
Prism::IndexOperatorWriteNode, Prism::IndexOrWriteNode, Prism::IndexAndWriteNode,
|
|
3041
|
+
Prism::CallOperatorWriteNode, Prism::CallOrWriteNode, Prism::CallAndWriteNode,
|
|
3042
|
+
Prism::LocalVariableOperatorWriteNode, Prism::InstanceVariableOperatorWriteNode,
|
|
3043
|
+
Prism::ClassVariableOperatorWriteNode, Prism::GlobalVariableOperatorWriteNode
|
|
3044
|
+
].freeze
|
|
3045
|
+
FOR_VARIABLE_READS = Set[Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode].freeze
|
|
3046
|
+
private_constant :FOR_MUTATING_NODES, :FOR_VARIABLE_READS
|
|
3047
|
+
|
|
3048
|
+
# Issue #1216 — `for x in coll` is `coll.each { |x| … }`, so the loop's value is what that `each` answers, or
|
|
3049
|
+
# the value of the `break` that ends it. An Array, a Hash and a Range answer themselves, including when
|
|
3050
|
+
# they are empty and the body never runs, so the value is the collection ({#for_collection_after}). Any other
|
|
3051
|
+
# collection (`Dynamic`, a project class, an `Enumerator`, a union with `nil`) answers `Dynamic[top]`: its
|
|
3052
|
+
# `each` is not known to answer its receiver, and a guessed type would be a claim the analysis cannot back.
|
|
3053
|
+
def for_value(node, coll_type, continuation, break_values)
|
|
3054
|
+
each_value = each_answers_self?(coll_type) ? for_collection_after(node, coll_type, continuation) : Type::Combinator.untyped
|
|
3055
|
+
break_values.empty? ? each_value : Type::Combinator.union(each_value, *break_values)
|
|
3056
|
+
end
|
|
3057
|
+
|
|
3058
|
+
# The collection as the loop leaves it. `each` answers the object it iterated, after the body ran, so a body that
|
|
3059
|
+
# may change its contents ({FOR_MUTATING_NODES}: `a.pop`, `h.delete(k)`, `b << 3` on an alias `b`) leaves
|
|
3060
|
+
# neither its shape nor its size: `a = [1, 2]; for i in a; a.pop; end` is `[1]`. The value is then the
|
|
3061
|
+
# collection's type with every shape and refinement dropped to its nominal ({#shape_erased_collection}), joined,
|
|
3062
|
+
# when the collection is a local or instance variable the loop does not rebind, with that variable's binding
|
|
3063
|
+
# after the loop, which holds the contents the body's own mutator calls add. A body that runs no such code
|
|
3064
|
+
# leaves the collection as it was.
|
|
3065
|
+
def for_collection_after(node, coll_type, continuation)
|
|
3066
|
+
return coll_type unless [node.index, node.statements].any? { |part| for_may_mutate?(part) }
|
|
3067
|
+
|
|
3068
|
+
types = [coll_type]
|
|
3069
|
+
name = for_collection_variable(node)
|
|
3070
|
+
types << CapturedLocals.bound_type(continuation, name) if name
|
|
3071
|
+
deep = DeepErasure.element_mutation?(node.statements, DeepErasure.parameter_names(node.index))
|
|
3072
|
+
Type::Combinator.union(*types.compact.map { |type| shape_erased_collection(type, deep: deep) })
|
|
3073
|
+
end
|
|
3074
|
+
|
|
3075
|
+
def for_may_mutate?(part)
|
|
3076
|
+
return false if part.nil?
|
|
3077
|
+
|
|
3078
|
+
found = false
|
|
3079
|
+
Source::NodeWalker.each(part) { |descendant| found ||= FOR_MUTATING_NODES.include?(descendant.class) }
|
|
3080
|
+
found || FOR_MUTATING_NODES.include?(part.class)
|
|
3081
|
+
end
|
|
3082
|
+
|
|
3083
|
+
# The local or instance variable `for` iterates, when neither its index nor its body rebinds it.
|
|
3084
|
+
def for_collection_variable(node)
|
|
3085
|
+
collection = node.collection
|
|
3086
|
+
return nil unless FOR_VARIABLE_READS.include?(collection.class)
|
|
3087
|
+
|
|
3088
|
+
written = OperandEffects.written_variables(node.index) + OperandEffects.written_variables(node.statements)
|
|
3089
|
+
written.include?(collection.name) ? nil : collection.name
|
|
3090
|
+
end
|
|
3091
|
+
|
|
3092
|
+
# `type` with each `Tuple` / `HashShape` / literal Array or Hash dropped to its nominal, each refinement to its
|
|
3093
|
+
# base. A Range is immutable and keeps its value.
|
|
3094
|
+
def shape_erased_collection(type, deep: false)
|
|
3095
|
+
# A body that mutates an element in place changes the shapes one level down too, which `each` answers
|
|
3096
|
+
# as the elements' own objects ({DeepErasure.widen}).
|
|
3097
|
+
return DeepErasure.widen(type) if deep && !(type.is_a?(Type::Constant) && type.value.is_a?(Range))
|
|
3098
|
+
|
|
3099
|
+
case type
|
|
3100
|
+
when Type::Tuple then MutationWidening.widen_tuple(type)
|
|
3101
|
+
when Type::HashShape then MutationWidening.widen_hash_shape(type)
|
|
3102
|
+
when Type::Constant then type.value.is_a?(Range) ? type : Type::Combinator.widen_value_pinned(type)
|
|
3103
|
+
when Type::Refined, Type::Difference then shape_erased_collection(type.base)
|
|
3104
|
+
when Type::Union then Type::Combinator.union(*type.members.map { |member| shape_erased_collection(member) })
|
|
3105
|
+
else type
|
|
3106
|
+
end
|
|
3107
|
+
end
|
|
3108
|
+
|
|
3109
|
+
def each_answers_self?(type)
|
|
3110
|
+
case type
|
|
3111
|
+
when Type::Tuple, Type::HashShape then true
|
|
3112
|
+
when Type::Nominal then FOR_SELF_EACH_CLASSES.include?(type.class_name)
|
|
3113
|
+
when Type::Constant then type.value.is_a?(Array) || type.value.is_a?(Hash) || type.value.is_a?(Range)
|
|
3114
|
+
when Type::Refined, Type::Difference then each_answers_self?(type.base)
|
|
3115
|
+
when Type::Union then type.members.all? { |member| each_answers_self?(member) }
|
|
3116
|
+
else false
|
|
3117
|
+
end
|
|
3118
|
+
end
|
|
3119
|
+
|
|
3120
|
+
# The scope a `for` body enters with: the index bound to the element. Issue #1359 — a body that may set `$_` runs
|
|
3121
|
+
# again after it ran, as a `while` body does ({#eval_loop}).
|
|
3122
|
+
def for_body_entry(node, element_type, post_coll)
|
|
3123
|
+
body_entry = LastLine.forget_if_set(bind_for_index(node.index, element_type, post_coll), node.statements)
|
|
3124
|
+
body_entry = forget_guard_for_body(node, post_coll, body_entry)
|
|
3125
|
+
loop_content_entry(node.statements, post_coll, body_entry)
|
|
2576
3126
|
end
|
|
2577
3127
|
|
|
2578
3128
|
# Issue #1429 — a `for` body runs after the collection's `each`, which may be a project method, and after itself.
|
|
@@ -2866,15 +3416,47 @@ module Rigor
|
|
|
2866
3416
|
# u.strip`, where the raise almost always comes from `Float` after it. Neither the arm nor anything in it
|
|
2867
3417
|
# is recorded or typed from there, which keeps it where it was before #1256: an ADR-5 trade of the rare
|
|
2868
3418
|
# raise-before-write path for no false positive on the common one.
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
3419
|
+
#
|
|
3420
|
+
# Issue #1839 — the arm enters with each name `expr` writes that it mentions, or that `expr` writes more
|
|
3421
|
+
# than once, read as `Dynamic[top]` ({#widen_clause_entry}), as a `begin`'s rescue clause does: the raise
|
|
3422
|
+
# may come after any of those writes. The scope past the modifier joins that entry through the arm.
|
|
3423
|
+
fallback = node.rescue_expression
|
|
3424
|
+
writes = clause_writes([node.expression])
|
|
3425
|
+
record_modifier_fallback(fallback, writes, walk)
|
|
3426
|
+
joined = join_with_nil_injection(scope, after_expression)
|
|
3427
|
+
widened = widen_clause_entry(joined, writes, fallback)
|
|
3428
|
+
arm_entry = ErrorInfo.modifier_entry(widened || joined, fallback)
|
|
3429
|
+
after_rescue = thread_operand(fallback, arm_entry, OperandWalk.new(nil), arm_entry)
|
|
2872
3430
|
after = if branch_unconditionally_exits?(node.rescue_expression)
|
|
2873
3431
|
after_expression
|
|
2874
3432
|
else
|
|
2875
3433
|
join_with_nil_injection(after_expression, after_rescue)
|
|
2876
3434
|
end
|
|
2877
|
-
[
|
|
3435
|
+
[rescue_modifier_type(node, walk, widened && arm_entry), after]
|
|
3436
|
+
end
|
|
3437
|
+
|
|
3438
|
+
# Issue #1839 — the walk records nothing in the fallback, so the index's walk hands it the modifier's own entry,
|
|
3439
|
+
# which binds each name `expr` writes as the modifier found it: `conn = nil; (conn = make; work) rescue
|
|
3440
|
+
# conn.size` read `conn` as `nil`. The fallback is recorded under that entry with the names `writes`
|
|
3441
|
+
# {ClauseWidening.widen} picks read as `Dynamic[top]`, and `$!`, `$@` and `$?` unbound as the walk leaves them,
|
|
3442
|
+
# when the widening changes it and the check path asks for it ({#widening_recorded?}). A name `expr` first
|
|
3443
|
+
# binds keeps the entry's reading, for the trade above.
|
|
3444
|
+
def record_modifier_fallback(fallback, writes, walk)
|
|
3445
|
+
return unless walk.recorder && @widen_clauses
|
|
3446
|
+
|
|
3447
|
+
widened = widen_clause_entry(scope, writes, fallback)
|
|
3448
|
+
walk.recorder.call(fallback, widened.forget_error_info.forget_last_status) if widened
|
|
3449
|
+
end
|
|
3450
|
+
|
|
3451
|
+
# Issue #1839 — the value of `expr rescue alt`: the fallback is typed from `widened_entry`, its entry with each
|
|
3452
|
+
# name `expr` writes read as `Dynamic[top]` ({#widen_clause_entry}), as a `begin`'s rescue clause reads it, not
|
|
3453
|
+
# at the entry's binding: `got = (conn = make; work) rescue conn` leaves `got` untyped, not `nil`. Nil when the
|
|
3454
|
+
# widening changes nothing, and the modifier is typed whole.
|
|
3455
|
+
def rescue_modifier_type(node, walk, widened_entry)
|
|
3456
|
+
return OperandWalk.type_of(scope, node, tracer, walk.types(tracer)) unless widened_entry
|
|
3457
|
+
|
|
3458
|
+
Type::Combinator.union(OperandWalk.type_of(scope, node.expression, tracer, walk.types(tracer)),
|
|
3459
|
+
widened_entry.type_of(node.rescue_expression, tracer: tracer))
|
|
2878
3460
|
end
|
|
2879
3461
|
|
|
2880
3462
|
# `class Foo; body; end` and `module Foo; body; end`. The class body runs in a fresh scope (Ruby's class scope
|
|
@@ -3130,7 +3712,7 @@ module Rigor
|
|
|
3130
3712
|
# map …) that rebinds an outer local must not leave that local's pre-call binding unmodified in the continuation
|
|
3131
3713
|
# scope; the spec MUST in § "Fact stability and mutation" names captured locals a first-class invalidation
|
|
3132
3714
|
# category. (The escaping / unknown path already widened to Dynamic[top] via `record_closure_escape_if_any`.)
|
|
3133
|
-
post_scope =
|
|
3715
|
+
post_scope = block_capture_continuation(node, post_scope)
|
|
3134
3716
|
statement_call = !call_type.nil? && !@in_operand
|
|
3135
3717
|
post_scope = apply_post_return_narrowing(node, post_scope) if statement_call
|
|
3136
3718
|
# Flow-folding G1 / G2 — widen a local- or instance-variable binding when the call is an in-place mutator on it
|
|
@@ -3337,30 +3919,21 @@ module Rigor
|
|
|
3337
3919
|
# is one that could plausibly mutate ivars on the enclosing class (implicit-self or explicit `self.foo`).
|
|
3338
3920
|
# External- receiver calls (`obj.method`) cannot reach the caller's ivars; they pass through unchanged.
|
|
3339
3921
|
# Issue #1596 — where the call is proven to write only some ivars ({#self_call_ivar_writes}), only those widen.
|
|
3922
|
+
#
|
|
3923
|
+
# Issue #1402 — a proc's call (`blk.call`) is a candidate too, and so is a call on another receiver whose literal
|
|
3924
|
+
# block makes one ({InterveningIvarReset.after_block}: `[1].each { reset }`). Both widen as a condition's
|
|
3925
|
+
# calls do, marking a binding whose narrowing had ruled `nil` out, since the code they run is far more often
|
|
3926
|
+
# code that leaves the variable alone. A `yield` or proc call in the call's receiver or arguments
|
|
3927
|
+
# (`@items << yield`, `puts(yield)`) widens every narrowed ivar ({InterveningIvarReset.after_operands}).
|
|
3340
3928
|
def invalidate_ivars_for_intervening_call(call_node, current_scope)
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
widened = current_scope
|
|
3350
|
-
seed.each do |ivar_name, seed_type|
|
|
3351
|
-
local_type = current_scope.ivar(ivar_name)
|
|
3352
|
-
next if local_type.nil? || local_type == seed_type
|
|
3353
|
-
|
|
3354
|
-
writes = self_call_ivar_writes(call_node)
|
|
3355
|
-
next if writes && !writes.include?(ivar_name)
|
|
3356
|
-
|
|
3357
|
-
joined = Type::Combinator.union(local_type, seed_type)
|
|
3358
|
-
widened = widened.with_ivar(ivar_name, joined)
|
|
3359
|
-
# Issue #1889 — an ivar a reflective write widened keeps its declaration mark, so the reset must too.
|
|
3360
|
-
widened = widened.seed_declaration_sourced_ivar(ivar_name, joined) if
|
|
3361
|
-
current_scope.declaration_sourced?(:ivar, ivar_name)
|
|
3362
|
-
end
|
|
3363
|
-
widened
|
|
3929
|
+
widened =
|
|
3930
|
+
if intervening_call_candidate?(call_node)
|
|
3931
|
+
mark = !InterveningIvarReset.self_call?(call_node)
|
|
3932
|
+
InterveningIvarReset.widen(current_scope, mark: mark) { self_call_ivar_writes(call_node) }
|
|
3933
|
+
else
|
|
3934
|
+
InterveningIvarReset.after_block(call_node, current_scope)
|
|
3935
|
+
end
|
|
3936
|
+
InterveningIvarReset.after_operands(call_node, widened)
|
|
3364
3937
|
end
|
|
3365
3938
|
|
|
3366
3939
|
# Issue #1596 — the ivars the implicit-self / `self.` call `call_node` may write, or nil when that is not proven
|
|
@@ -3373,10 +3946,7 @@ module Rigor
|
|
|
3373
3946
|
end
|
|
3374
3947
|
|
|
3375
3948
|
def intervening_call_candidate?(call_node)
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
receiver = call_node.receiver
|
|
3379
|
-
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
3949
|
+
InterveningIvarReset.candidate?(call_node)
|
|
3380
3950
|
end
|
|
3381
3951
|
|
|
3382
3952
|
def enclosing_class_name_for(self_type)
|
|
@@ -3876,7 +4446,7 @@ module Rigor
|
|
|
3876
4446
|
classification = classify_closure_escape(node)
|
|
3877
4447
|
return post_scope if classification == :non_escaping
|
|
3878
4448
|
|
|
3879
|
-
post_scope = escaping_closure_captures(node.block, post_scope)
|
|
4449
|
+
post_scope = escaping_closure_captures(node.block, post_scope, foreign_self: foreign_self_block?(node))
|
|
3880
4450
|
post_scope.with_fact(
|
|
3881
4451
|
Analysis::FactStore::Fact.new(
|
|
3882
4452
|
bucket: :dynamic_origin,
|
|
@@ -3918,13 +4488,163 @@ module Rigor
|
|
|
3918
4488
|
end
|
|
3919
4489
|
end
|
|
3920
4490
|
|
|
3921
|
-
# The `{ name =>
|
|
3922
|
-
#
|
|
3923
|
-
#
|
|
3924
|
-
# call, so
|
|
4491
|
+
# The `{ name => slot }` parameters whose content the callee mutates, a slot being a positional index or a
|
|
4492
|
+
# keyword symbol: those it mutates itself ({#own_content_parameters}) and, issue #2104, those it passes on to a
|
|
4493
|
+
# self-call whose callee mutates them, through any number of such hand-offs ({CalleeMutationChain}). Each leaves
|
|
4494
|
+
# the caller's argument binding stale after the call, so each floors it. Memoised per def node (the closure is
|
|
4495
|
+
# otherwise recomputed at every call site), and across evaluators by {#resolved_census}.
|
|
3925
4496
|
def callee_content_mutated_parameters(def_node)
|
|
3926
4497
|
cache = (@callee_mutated_param_cache ||= {}.compare_by_identity)
|
|
3927
|
-
cache[def_node] ||=
|
|
4498
|
+
cache[def_node] ||= resolved_census(:close, def_node) do
|
|
4499
|
+
CalleeMutationChain.close(def_node, own: method(:own_content_parameters), edges: method(:forwarding_edges))
|
|
4500
|
+
end
|
|
4501
|
+
end
|
|
4502
|
+
|
|
4503
|
+
# Issue #2104 — the per-thread memo of a census that also resolves self-calls ({#resolve_self_callee_def}), so it
|
|
4504
|
+
# reads, besides `def_node`'s AST, the class self-calls resolve in and the discovery index whose def table they
|
|
4505
|
+
# resolve through. Those two key it: the index by identity, as it is immutable (one slot, as a file's analysis
|
|
4506
|
+
# reads one index; a scope indexed for another file replaces it), the class by name. A run builds hundreds of
|
|
4507
|
+
# thousands of evaluators, and a per-evaluator memo alone recomputed each closure and each def's edges about
|
|
4508
|
+
# thirty times over (#2295's cost on the release gate).
|
|
4509
|
+
#
|
|
4510
|
+
# While a dependency recorder is active the memo is bypassed: a resolution records an ADR-46 read in the file
|
|
4511
|
+
# and in each capture window open at the time, and a hit would leave that read out of them.
|
|
4512
|
+
def resolved_census(kind, def_node)
|
|
4513
|
+
return yield if Analysis::DependencyRecorder.active?
|
|
4514
|
+
|
|
4515
|
+
discovery = scope.discovery
|
|
4516
|
+
slot = Thread.current[RESOLVED_CENSUS_KEY]
|
|
4517
|
+
slot = Thread.current[RESOLVED_CENSUS_KEY] = [discovery, {}] unless slot&.first.equal?(discovery)
|
|
4518
|
+
by_class = (slot.last[kind] ||= {})
|
|
4519
|
+
class_name = enclosing_class_name_for(scope.self_type)
|
|
4520
|
+
by_def = (by_class[class_name] ||= {}.compare_by_identity)
|
|
4521
|
+
by_def.fetch(def_node) { by_def[def_node] = yield }
|
|
4522
|
+
end
|
|
4523
|
+
|
|
4524
|
+
RESOLVED_CENSUS_KEY = :rigor_callee_resolved_census
|
|
4525
|
+
private_constant :RESOLVED_CENSUS_KEY
|
|
4526
|
+
|
|
4527
|
+
# The parameters `def_node` mutates itself, from either channel: those escape-mutated inside a block (which may
|
|
4528
|
+
# run later / repeatedly) AND those mutated directly in the method body during the call itself
|
|
4529
|
+
# (`declaration[:prefix] = v`). A property of the def's AST alone, so it is memoised per def node for the thread
|
|
4530
|
+
# ({#def_census}) rather than per evaluator: a chain reads its callees' census from every evaluator that floors a
|
|
4531
|
+
# call into it.
|
|
4532
|
+
def own_content_parameters(def_node)
|
|
4533
|
+
def_census(:own, def_node) { escaped_content_parameters(def_node).merge(direct_content_parameters(def_node)) }
|
|
4534
|
+
end
|
|
4535
|
+
|
|
4536
|
+
# Issue #2104 — the per-thread memo of a census that reads nothing but `def_node`'s AST, under `kind`.
|
|
4537
|
+
def def_census(kind, def_node)
|
|
4538
|
+
table = (Thread.current[DEF_CENSUS_KEY] ||= ObjectSpace::WeakKeyMap.new)
|
|
4539
|
+
entry = (table[def_node] ||= {})
|
|
4540
|
+
entry.fetch(kind) { entry[kind] = yield }
|
|
4541
|
+
end
|
|
4542
|
+
|
|
4543
|
+
DEF_CENSUS_KEY = :rigor_callee_def_census
|
|
4544
|
+
private_constant :DEF_CENSUS_KEY
|
|
4545
|
+
|
|
4546
|
+
# Issue #2104 — the {CalleeMutationChain::Edge}s along which `def_node` hands one of its parameters, unchanged, to
|
|
4547
|
+
# a self-call resolving to a user def: a positional argument or a keyword value that reads the parameter itself
|
|
4548
|
+
# (not a block-local shadowing it), in the method body or a block in it. The callee resolves in this evaluator's
|
|
4549
|
+
# class, the receiver's class at every hop of a self-call chain. Memoised per def node, and across evaluators by
|
|
4550
|
+
# {#resolved_census}.
|
|
4551
|
+
def forwarding_edges(def_node)
|
|
4552
|
+
cache = (@forwarding_edge_cache ||= {}.compare_by_identity)
|
|
4553
|
+
cache[def_node] ||= resolved_census(:edges, def_node) { compute_forwarding_edges(def_node) }
|
|
4554
|
+
end
|
|
4555
|
+
|
|
4556
|
+
def compute_forwarding_edges(def_node)
|
|
4557
|
+
slots = content_parameter_slots(def_node)
|
|
4558
|
+
edges = []
|
|
4559
|
+
each_forwarding_call(def_node, slots) do |call_node, forwarded|
|
|
4560
|
+
callee = resolve_self_callee_def(call_node)
|
|
4561
|
+
next if callee.nil?
|
|
4562
|
+
|
|
4563
|
+
callee_slots = content_parameter_slots(callee)
|
|
4564
|
+
forwarded.each do |name, callee_slot|
|
|
4565
|
+
names = bound_parameter_names(callee_slots, callee_slot)
|
|
4566
|
+
next if names.empty?
|
|
4567
|
+
|
|
4568
|
+
edges << CalleeMutationChain::Edge.new(name: name, slot: slots[name], callee: callee, callee_names: names)
|
|
4569
|
+
end
|
|
4570
|
+
end
|
|
4571
|
+
edges.empty? ? NO_FORWARDS : edges
|
|
4572
|
+
end
|
|
4573
|
+
|
|
4574
|
+
# Yields each self-call in `def_node`'s body that hands on one of `slots`' parameters, with
|
|
4575
|
+
# {#forwarded_parameters}' answer for it. The calls are an AST property, memoised per def node ({#def_census}).
|
|
4576
|
+
def each_forwarding_call(def_node, slots, &)
|
|
4577
|
+
return if slots.empty?
|
|
4578
|
+
|
|
4579
|
+
def_census(:forwards, def_node) { forwarding_calls(def_node, slots) }.each(&)
|
|
4580
|
+
end
|
|
4581
|
+
|
|
4582
|
+
def forwarding_calls(def_node, slots)
|
|
4583
|
+
calls = []
|
|
4584
|
+
each_node_in_method_scope(def_node.body, 0) do |descendant, depth|
|
|
4585
|
+
next unless self_dispatch_call?(descendant)
|
|
4586
|
+
|
|
4587
|
+
forwarded = forwarded_parameters(descendant, slots, depth)
|
|
4588
|
+
calls << [descendant, forwarded] unless forwarded.empty?
|
|
4589
|
+
end
|
|
4590
|
+
calls.empty? ? NO_FORWARDS : calls
|
|
4591
|
+
end
|
|
4592
|
+
|
|
4593
|
+
NO_FORWARDS = [].freeze
|
|
4594
|
+
private_constant :NO_FORWARDS
|
|
4595
|
+
|
|
4596
|
+
# `[[parameter_name, callee_slot], ...]` for the arguments of `call_node` that read one of `slots`' parameters at
|
|
4597
|
+
# `depth`, the block nesting the call sits at: a read of the method's own parameter there has that depth. An
|
|
4598
|
+
# argument after a splat or `...` has an index that says nothing about the parameter it binds, so it binds
|
|
4599
|
+
# {ANY_SLOT}, any positional parameter of the callee; one before the first splat keeps its index.
|
|
4600
|
+
def forwarded_parameters(call_node, slots, depth)
|
|
4601
|
+
args = call_node.arguments
|
|
4602
|
+
return NO_FORWARDS unless args.is_a?(Prism::ArgumentsNode)
|
|
4603
|
+
|
|
4604
|
+
spread = false
|
|
4605
|
+
forwarded = []
|
|
4606
|
+
args.arguments.each_with_index do |argument, index|
|
|
4607
|
+
if argument.is_a?(Prism::KeywordHashNode)
|
|
4608
|
+
forwarded.concat(forwarded_keywords(argument, slots, depth))
|
|
4609
|
+
elsif argument.is_a?(Prism::SplatNode) || argument.is_a?(Prism::ForwardingArgumentsNode)
|
|
4610
|
+
spread = true
|
|
4611
|
+
elsif (name = parameter_read_name(argument, slots, depth))
|
|
4612
|
+
forwarded << [name, spread ? ANY_SLOT : index]
|
|
4613
|
+
end
|
|
4614
|
+
end
|
|
4615
|
+
forwarded
|
|
4616
|
+
end
|
|
4617
|
+
|
|
4618
|
+
# The `callee_slot` of an argument whose parameter the call does not show (one after a splat).
|
|
4619
|
+
ANY_SLOT = :__any_positional__
|
|
4620
|
+
private_constant :ANY_SLOT
|
|
4621
|
+
|
|
4622
|
+
# The names of the callee's parameters (`callee_slots`, {#content_parameter_slots}) an argument bound to `slot`
|
|
4623
|
+
# may reach: the one at that slot, or every positional one for {ANY_SLOT}.
|
|
4624
|
+
def bound_parameter_names(callee_slots, slot)
|
|
4625
|
+
if slot == ANY_SLOT
|
|
4626
|
+
callee_slots.filter_map { |name, at| name if at.is_a?(Integer) }
|
|
4627
|
+
else
|
|
4628
|
+
callee_slots.filter_map { |name, at| name if at == slot }
|
|
4629
|
+
end
|
|
4630
|
+
end
|
|
4631
|
+
|
|
4632
|
+
def forwarded_keywords(hash_node, slots, depth)
|
|
4633
|
+
hash_node.elements.filter_map do |pair|
|
|
4634
|
+
next unless pair.is_a?(Prism::AssocNode) && pair.key.is_a?(Prism::SymbolNode)
|
|
4635
|
+
|
|
4636
|
+
name = parameter_read_name(pair.value, slots, depth)
|
|
4637
|
+
[name, pair.key.unescaped.to_sym] if name
|
|
4638
|
+
end
|
|
4639
|
+
end
|
|
4640
|
+
|
|
4641
|
+
# The parameter `node` reads when it is a read of one of `slots`' names at `depth` (a keyword shorthand
|
|
4642
|
+
# `poke(acc:)` wraps the read in an `ImplicitNode`), else nil.
|
|
4643
|
+
def parameter_read_name(node, slots, depth)
|
|
4644
|
+
node = node.value if node.is_a?(Prism::ImplicitNode)
|
|
4645
|
+
return unless node.is_a?(Prism::LocalVariableReadNode) && node.depth == depth
|
|
4646
|
+
|
|
4647
|
+
node.name if slots.key?(node.name)
|
|
3928
4648
|
end
|
|
3929
4649
|
|
|
3930
4650
|
# The user def a self-dispatch `node` resolves to in the enclosing class, or nil. Reuses the discovery index
|
|
@@ -3943,7 +4663,7 @@ module Rigor
|
|
|
3943
4663
|
node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
3944
4664
|
end
|
|
3945
4665
|
|
|
3946
|
-
# The set of `[name,
|
|
4666
|
+
# The set of `[name, slot]` parameters of `def_node` whose content a block in the body escape-mutates.
|
|
3947
4667
|
# Memoised per def node (the body walk is otherwise repeated at every call site). A parameter is "escape- mutated"
|
|
3948
4668
|
# when a `param[k] = v` / `param << x` mutation on it appears inside a block whose receiving call is not proven
|
|
3949
4669
|
# non-escaping.
|
|
@@ -3953,7 +4673,7 @@ module Rigor
|
|
|
3953
4673
|
end
|
|
3954
4674
|
|
|
3955
4675
|
def compute_escaped_content_parameters(def_node)
|
|
3956
|
-
positions =
|
|
4676
|
+
positions = content_parameter_slots(def_node)
|
|
3957
4677
|
return {} if positions.empty?
|
|
3958
4678
|
|
|
3959
4679
|
mutated = Set.new
|
|
@@ -3968,18 +4688,22 @@ module Rigor
|
|
|
3968
4688
|
positions.slice(*mutated)
|
|
3969
4689
|
end
|
|
3970
4690
|
|
|
3971
|
-
# The `{ name =>
|
|
3972
|
-
#
|
|
3973
|
-
#
|
|
4691
|
+
# The `{ name => slot }` parameters whose CONTENT the callee mutates directly in its method body — a top-level
|
|
4692
|
+
# `param[k] = v` / `param << x`, outside a nested block. Memoised per def node. The block-nested case is
|
|
4693
|
+
# `escaped_content_parameters`'s job; walking only outside nested blocks / defs / lambdas here means a matching
|
|
3974
4694
|
# parameter-name read is genuinely at method scope (depth 0) rather than a block-local of the same name that
|
|
3975
4695
|
# merely shadows the parameter — so we never floor a caller argument the callee did not actually mutate.
|
|
4696
|
+
#
|
|
4697
|
+
# A mutation inside a NON-escaping block (`n.times { |i| acc[i] = v }`) is not counted: the bare-collection floor
|
|
4698
|
+
# it would apply turned haml's `AttributeCompiler#compile` (a `temple` array a helper fills in a `times` block,
|
|
4699
|
+
# returned against a declared tuple) into a `def.return-type-mismatch` (af3efef33, and again under #2104).
|
|
3976
4700
|
def direct_content_parameters(def_node)
|
|
3977
4701
|
cache = (@direct_param_cache ||= {}.compare_by_identity)
|
|
3978
4702
|
cache[def_node] ||= compute_direct_content_parameters(def_node)
|
|
3979
4703
|
end
|
|
3980
4704
|
|
|
3981
4705
|
def compute_direct_content_parameters(def_node)
|
|
3982
|
-
positions =
|
|
4706
|
+
positions = content_parameter_slots(def_node)
|
|
3983
4707
|
return {} if positions.empty?
|
|
3984
4708
|
|
|
3985
4709
|
mutated = Set.new
|
|
@@ -4004,6 +4728,43 @@ module Rigor
|
|
|
4004
4728
|
end
|
|
4005
4729
|
end
|
|
4006
4730
|
|
|
4731
|
+
# Yields every node reachable from `node` with the number of blocks it sits in: a read of the method's own local
|
|
4732
|
+
# there has that depth. A nested def or lambda is not entered: a def has no access to the method's locals, and a
|
|
4733
|
+
# lambda body is not run by the call. Nor is the block of a {#self_rebinding_block_call?}: a self-call there
|
|
4734
|
+
# either runs on another `self`, or runs later as a method body, so the walk leaves it out.
|
|
4735
|
+
def each_node_in_method_scope(node, depth, &)
|
|
4736
|
+
return if node.nil?
|
|
4737
|
+
|
|
4738
|
+
yield node, depth
|
|
4739
|
+
rebinding = self_rebinding_block_call?(node)
|
|
4740
|
+
node.rigor_each_child do |child|
|
|
4741
|
+
next if child.is_a?(Prism::DefNode) || child.is_a?(Prism::LambdaNode)
|
|
4742
|
+
|
|
4743
|
+
if child.is_a?(Prism::BlockNode)
|
|
4744
|
+
each_node_in_method_scope(child, depth + 1, &) unless rebinding
|
|
4745
|
+
else
|
|
4746
|
+
each_node_in_method_scope(child, depth, &)
|
|
4747
|
+
end
|
|
4748
|
+
end
|
|
4749
|
+
end
|
|
4750
|
+
|
|
4751
|
+
# The calls whose block the hand-off walk leaves out. In an `instance_eval` / `class_exec` / … block, and in a
|
|
4752
|
+
# `Class.new` / `Module.new` / `Struct.new` / `Data.define` block ({AnonymousMetaClass.block_form_receiver}),
|
|
4753
|
+
# `self` is another object, so a self-call there is not a call on the method's receiver. A `define_method` or
|
|
4754
|
+
# `define_singleton_method` block keeps an instance of the class as `self`, but it is a method body that runs
|
|
4755
|
+
# later, if ever, so leaving it out is the conservative reading: a hand-off it holds is not one the call makes.
|
|
4756
|
+
SELF_REBINDING_BLOCK_CALLS = Set[
|
|
4757
|
+
:instance_eval, :instance_exec, :class_eval, :class_exec, :module_eval, :module_exec, :define_method,
|
|
4758
|
+
:define_singleton_method
|
|
4759
|
+
].freeze
|
|
4760
|
+
private_constant :SELF_REBINDING_BLOCK_CALLS
|
|
4761
|
+
|
|
4762
|
+
def self_rebinding_block_call?(node)
|
|
4763
|
+
return false unless node.is_a?(Prism::CallNode) && node.block.is_a?(Prism::BlockNode)
|
|
4764
|
+
|
|
4765
|
+
SELF_REBINDING_BLOCK_CALLS.include?(node.name) || !AnonymousMetaClass.block_form_receiver(node).nil?
|
|
4766
|
+
end
|
|
4767
|
+
|
|
4007
4768
|
# A receiver-independent over-approximation of `ClosureEscapeAnalyzer`'s non-escaping verdict, used when scanning
|
|
4008
4769
|
# a callee body where the block- owning call's receiver TYPE is not available. A call whose method name is a known
|
|
4009
4770
|
# structural iterator (`each` / `map` / `tap` / …) runs its block synchronously and does not retain it, so its
|
|
@@ -4024,18 +4785,20 @@ module Rigor
|
|
|
4024
4785
|
SYNTACTIC_NON_ESCAPING_BLOCK_METHODS.include?(call_node.name)
|
|
4025
4786
|
end
|
|
4026
4787
|
|
|
4027
|
-
# `{ name =>
|
|
4028
|
-
#
|
|
4029
|
-
|
|
4788
|
+
# `{ name => slot }` for the parameters a caller's argument binds by name or position: the required / optional
|
|
4789
|
+
# positional ones (slot: their index) and, issue #2104, the keyword ones (slot: the keyword symbol). Rest, post
|
|
4790
|
+
# and block parameters are skipped.
|
|
4791
|
+
def content_parameter_slots(def_node)
|
|
4030
4792
|
params = def_node.parameters
|
|
4031
4793
|
return {} if params.nil?
|
|
4032
4794
|
|
|
4033
4795
|
ordered = (params.requireds || []) + (params.optionals || [])
|
|
4034
|
-
|
|
4796
|
+
slots = {}
|
|
4035
4797
|
ordered.each_with_index do |param, index|
|
|
4036
|
-
|
|
4798
|
+
slots[param.name] = index if param.respond_to?(:name)
|
|
4037
4799
|
end
|
|
4038
|
-
|
|
4800
|
+
(params.keywords || []).each { |param| slots[param.name] = param.name }
|
|
4801
|
+
slots
|
|
4039
4802
|
end
|
|
4040
4803
|
|
|
4041
4804
|
# True when at least one argument of `node` is a bare local-variable read (positional or keyword value) bound in
|
|
@@ -4051,9 +4814,10 @@ module Rigor
|
|
|
4051
4814
|
scope.locals.key?(arg.name)
|
|
4052
4815
|
when Prism::KeywordHashNode
|
|
4053
4816
|
arg.elements.any? do |pair|
|
|
4054
|
-
pair.is_a?(Prism::AssocNode)
|
|
4055
|
-
|
|
4056
|
-
|
|
4817
|
+
next false unless pair.is_a?(Prism::AssocNode)
|
|
4818
|
+
|
|
4819
|
+
value = pair.value.is_a?(Prism::ImplicitNode) ? pair.value.value : pair.value
|
|
4820
|
+
value.is_a?(Prism::LocalVariableReadNode) && scope.locals.key?(value.name)
|
|
4057
4821
|
end
|
|
4058
4822
|
else
|
|
4059
4823
|
false
|
|
@@ -4180,13 +4944,80 @@ module Rigor
|
|
|
4180
4944
|
# callback that runs later writes the very ivar the continuation reads (`@clicked = false; button.on_click {
|
|
4181
4945
|
# @clicked = true }` left `if @clicked` folding always-falsey). One still on its ADR-58 declaration seed is
|
|
4182
4946
|
# left alone ({CapturedLocals.writes}): that seed already holds whatever the callback stores.
|
|
4183
|
-
def drop_captured_narrowing(block_node, base_scope)
|
|
4184
|
-
names = CapturedLocals.writes(block_node, base_scope, ivars: true)
|
|
4947
|
+
def drop_captured_narrowing(block_node, base_scope, foreign_self: false)
|
|
4948
|
+
names = CapturedLocals.writes(block_node, base_scope, ivars: true, foreign_self: foreign_self)
|
|
4185
4949
|
return base_scope if names.empty?
|
|
4186
4950
|
|
|
4187
4951
|
names.reduce(base_scope) { |acc, name| bind_capture(acc, name, Type::Combinator.untyped) }
|
|
4188
4952
|
end
|
|
4189
4953
|
|
|
4954
|
+
# The continuation {#write_back_block_captures} binds, after which the block's later-run `return`s are collected
|
|
4955
|
+
# ({#collect_repeated_block_returns}).
|
|
4956
|
+
def block_capture_continuation(call_node, post_scope)
|
|
4957
|
+
post_scope = write_back_block_captures(call_node, post_scope)
|
|
4958
|
+
collect_repeated_block_returns(call_node, post_scope)
|
|
4959
|
+
post_scope
|
|
4960
|
+
end
|
|
4961
|
+
|
|
4962
|
+
# Issue #2123 — the `return`s a repeating block reaches only on a later run. Every statement pass over a block
|
|
4963
|
+
# enters with a captured local or ivar the body rebinds at a binding some run can hold, but not at every such
|
|
4964
|
+
# binding: the statement pass enters at the call-site binding, and the ADR-56 write-back's capped fixpoint widens
|
|
4965
|
+
# its last assumption without running a pass from it. So
|
|
4966
|
+
# `seen = 0; items.each { return false if (seen += 1) > 3 }; true` read `seen` as at most `3` on every pass, the
|
|
4967
|
+
# `return` was never reached, the method inferred `true`, and a caller's `if` on it folded always-falsey on
|
|
4968
|
+
# correct code.
|
|
4969
|
+
#
|
|
4970
|
+
# While a method's return sink is active, a block that may repeat ({#repeating_block_class}), rebinds a capture,
|
|
4971
|
+
# and holds a `return` that exits the method ({ReturnBarrier.exits_method?}) gets one more unrecorded pass, with
|
|
4972
|
+
# each rebound name bound to its pre-call binding joined with its continuation `post_scope` holds. The
|
|
4973
|
+
# continuation covers every run count, so it covers the binding any run enters with. The pass records no
|
|
4974
|
+
# scope, and the method's fall-through and earlier `return`s are kept, so the inferred return type only widens.
|
|
4975
|
+
# The fresh `next`, `break`-value and `break`-scope sinks are defensive: every consumer of those sinks filters by
|
|
4976
|
+
# jump-node identity, so no example tells them apart from the outer sinks today. So is the `rescue`: a raise here
|
|
4977
|
+
# keeps the arms the earlier passes collected rather than taking out the method.
|
|
4978
|
+
#
|
|
4979
|
+
# Issue #2127 — a block passed to a project method whose body may run it more than once
|
|
4980
|
+
# ({ProjectYielder.may_run_block_repeatedly?}) gets the same pass. No write-back runs for such a call, so its
|
|
4981
|
+
# continuation holds each rebound capture at `Dynamic[top]` ({#drop_captured_narrowing}), and the pass reads it
|
|
4982
|
+
# so: `seen = 0; yielder { return :ret if (seen += 1) > 1 }` reaches the `return`.
|
|
4983
|
+
def collect_repeated_block_returns(call_node, post_scope)
|
|
4984
|
+
return if Thread.current[RETURN_SINK_KEY].nil?
|
|
4985
|
+
|
|
4986
|
+
block = call_node.block
|
|
4987
|
+
return unless repeated_block_return_candidate?(call_node, block)
|
|
4988
|
+
|
|
4989
|
+
names = CapturedLocals.writes(block, scope, ivars: true, foreign_self: foreign_self_block?(call_node))
|
|
4990
|
+
return if names.empty?
|
|
4991
|
+
|
|
4992
|
+
bindings = names.to_h do |name|
|
|
4993
|
+
[name, Type::Combinator.union(*[scope, post_scope].filter_map { |s| CapturedLocals.bound_type(s, name) })]
|
|
4994
|
+
end
|
|
4995
|
+
entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, bindings))
|
|
4996
|
+
StatementEvaluator.with_next_sink do
|
|
4997
|
+
StatementEvaluator.with_break_value_sink do
|
|
4998
|
+
collect_break_scopes { sub_eval(block, entry, **UNRECORDED, next_scope_sink: nil) }
|
|
4999
|
+
end
|
|
5000
|
+
end
|
|
5001
|
+
rescue StandardError
|
|
5002
|
+
nil
|
|
5003
|
+
end
|
|
5004
|
+
|
|
5005
|
+
# Whether `block` is an ordinary block (not a return barrier or a class-body block) whose body may `return` from
|
|
5006
|
+
# the method and which `call_node` may run more than once: {#repeating_block_class}'s rule, `Kernel#loop`, which
|
|
5007
|
+
# that rule leaves out and which runs its block until it breaks or raises, or a project method whose body may
|
|
5008
|
+
# run it more than once ({ProjectYielder.may_run_block_repeatedly?}, issue #2127). The barrier, refinement and
|
|
5009
|
+
# anonymous-class checks are defensive guards: no such call repeats under that rule today.
|
|
5010
|
+
def repeated_block_return_candidate?(call_node, block)
|
|
5011
|
+
return false unless block.is_a?(Prism::BlockNode)
|
|
5012
|
+
return false unless ReturnBarrier.exits_method?(block.body)
|
|
5013
|
+
return false if return_barrier_block?(call_node) || refined_class_context(call_node)
|
|
5014
|
+
return false if AnonymousMetaClass.name_for(call_node, scope.source_path)
|
|
5015
|
+
return true if BlockCallTiming.kernel_loop?(call_node) || !repeating_block_class(call_node).nil?
|
|
5016
|
+
|
|
5017
|
+
receiver_type = block_call_receiver_type(call_node)
|
|
5018
|
+
!receiver_type.nil? && ProjectYielder.may_run_block_repeatedly?(call_node, receiver_type, scope)
|
|
5019
|
+
end
|
|
5020
|
+
|
|
4190
5021
|
# ADR-56 slice A. For a `:non_escaping` block, fold the continuation binding of every outer local the body can
|
|
4191
5022
|
# rebind back into `post_scope`. The binding is a capped fixpoint (cap 3) over the block body re-evaluated under
|
|
4192
5023
|
# the running per-name assumption, joined with the pre-call binding (kept as a constituent so the 0-iteration path
|
|
@@ -4211,13 +5042,27 @@ module Rigor
|
|
|
4211
5042
|
return post_scope unless block.is_a?(Prism::BlockNode)
|
|
4212
5043
|
return post_scope unless classify_closure_escape(call_node) == :non_escaping
|
|
4213
5044
|
|
|
4214
|
-
names = CapturedLocals.writes(block, scope, ivars: true)
|
|
5045
|
+
names = CapturedLocals.writes(block, scope, ivars: true, foreign_self: foreign_self_block?(call_node))
|
|
4215
5046
|
return post_scope if names.empty?
|
|
4216
5047
|
|
|
4217
5048
|
break_pass = block_break_pass(block)
|
|
4218
|
-
|
|
5049
|
+
marks = {}
|
|
5050
|
+
result = with_capture_exit_marks(marks) { converge_captures_by_kind(call_node, block, names, break_pass) }
|
|
4219
5051
|
result = join_block_break_bindings(call_node, block, result, break_pass)
|
|
4220
|
-
result.reduce(post_scope) { |acc, (name, type)| bind_capture(acc, name, type) }
|
|
5052
|
+
result.reduce(post_scope) { |acc, (name, type)| bind_capture(acc, name, type, optimistic: marks[name]) }
|
|
5053
|
+
end
|
|
5054
|
+
|
|
5055
|
+
# Issue #1402 — runs the write-back's passes with `marks` collecting the ADR-101 optimistic mark a pass's exit
|
|
5056
|
+
# holds for each rebound name ({#block_exit_bindings}), as the per-element fold collects them
|
|
5057
|
+
# (`ExpressionTyper#captured_exit_bindings`). A later pass enters on that mark, and the continuation binds it:
|
|
5058
|
+
# `x = false; pairs.each { x = pairs.first&.empty? }` stored the miss's `nil` at run time, and without the mark
|
|
5059
|
+
# `x.nil?` folded to `false` where the inline `pairs.first&.empty?.nil?` answers `bool`.
|
|
5060
|
+
def with_capture_exit_marks(marks)
|
|
5061
|
+
outer = @capture_exit_marks
|
|
5062
|
+
@capture_exit_marks = marks
|
|
5063
|
+
yield
|
|
5064
|
+
ensure
|
|
5065
|
+
@capture_exit_marks = outer
|
|
4221
5066
|
end
|
|
4222
5067
|
|
|
4223
5068
|
# The {BodyFixpoint} continuation of `names`, seeded from their pre-call bindings.
|
|
@@ -4276,7 +5121,12 @@ module Rigor
|
|
|
4276
5121
|
# Binds a name from `CapturedLocals.writes`. An ivar goes through {CapturedLocals.bind}, which keeps its issue
|
|
4277
5122
|
# #286 optimistic mark. A local keeps the plain `with_local` these seams have always used, so a block that
|
|
4278
5123
|
# rebinds no ivar leaves the locals exactly as before.
|
|
4279
|
-
|
|
5124
|
+
#
|
|
5125
|
+
# Issue #1402 — `optimistic` is a mark a write-back pass's exit held ({#with_capture_exit_marks}); a name that has
|
|
5126
|
+
# one binds through {CapturedLocals.bind} whatever its kind, which adds it.
|
|
5127
|
+
def bind_capture(scope, name, type, optimistic: nil)
|
|
5128
|
+
return CapturedLocals.bind(scope, name, type, optimistic: optimistic) if optimistic
|
|
5129
|
+
|
|
4280
5130
|
CapturedLocals.ivar_name?(name) ? CapturedLocals.bind(scope, name, type) : scope.with_local(name, type)
|
|
4281
5131
|
end
|
|
4282
5132
|
|
|
@@ -5140,9 +5990,22 @@ module Rigor
|
|
|
5140
5990
|
def block_exit_bindings(call_node, block, bindings, names)
|
|
5141
5991
|
entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, bindings))
|
|
5142
5992
|
_type, exit_scope = sub_eval(block, entry)
|
|
5993
|
+
record_capture_exit_marks(exit_scope, names)
|
|
5143
5994
|
names.to_h { |name| [name, CapturedLocals.bound_type(exit_scope, name)] }
|
|
5144
5995
|
end
|
|
5145
5996
|
|
|
5997
|
+
# Issue #1402 — records into {#with_capture_exit_marks}' table the mark `exit_scope` holds for each of `names`;
|
|
5998
|
+
# the first mark a name takes stays.
|
|
5999
|
+
def record_capture_exit_marks(exit_scope, names)
|
|
6000
|
+
marks = @capture_exit_marks
|
|
6001
|
+
return if marks.nil?
|
|
6002
|
+
|
|
6003
|
+
names.each do |name|
|
|
6004
|
+
mark = CapturedLocals.optimistic_mark(exit_scope, name)
|
|
6005
|
+
marks[name] ||= mark if mark
|
|
6006
|
+
end
|
|
6007
|
+
end
|
|
6008
|
+
|
|
5146
6009
|
# The entry scope of one write-back pass: the block's entry with each written outer local or ivar bound to
|
|
5147
6010
|
# `bindings`, and each captured local the body only mutates in place at its widening ({#capture_pass_bindings}).
|
|
5148
6011
|
# That widening describes the same object, so it keeps the marks a source-level write drops (issue #1287,
|
|
@@ -5151,7 +6014,9 @@ module Rigor
|
|
|
5151
6014
|
def block_pass_entry(call_node, block, bindings)
|
|
5152
6015
|
entry = build_block_entry_scope(call_node, block)
|
|
5153
6016
|
capture_pass_bindings(block, bindings).reduce(entry) do |acc, (name, type)|
|
|
5154
|
-
|
|
6017
|
+
next acc.with_mutated_local(name, type) unless bindings.key?(name)
|
|
6018
|
+
|
|
6019
|
+
bind_capture(acc, name, type, optimistic: @capture_exit_marks&.[](name))
|
|
5155
6020
|
end
|
|
5156
6021
|
end
|
|
5157
6022
|
|
|
@@ -5224,12 +6089,7 @@ module Rigor
|
|
|
5224
6089
|
# receiver is the one the block-return pass asks about: the explicit receiver, or the implicit `self`
|
|
5225
6090
|
# (`Object` at the top level).
|
|
5226
6091
|
def repeating_block_class(call_node)
|
|
5227
|
-
receiver_type =
|
|
5228
|
-
if call_node.receiver
|
|
5229
|
-
explicit_receiver_type(call_node)
|
|
5230
|
-
else
|
|
5231
|
-
operand_scope.self_type || operand_scope.environment.nominal_for_name("Object")
|
|
5232
|
-
end
|
|
6092
|
+
receiver_type = block_call_receiver_type(call_node)
|
|
5233
6093
|
return nil if receiver_type.nil?
|
|
5234
6094
|
|
|
5235
6095
|
method_name = call_node.name
|
|
@@ -5243,6 +6103,13 @@ module Rigor
|
|
|
5243
6103
|
nil
|
|
5244
6104
|
end
|
|
5245
6105
|
|
|
6106
|
+
# The receiver a block-carrying call dispatches on: its explicit receiver's type, else `self`, else `Object`.
|
|
6107
|
+
def block_call_receiver_type(call_node)
|
|
6108
|
+
return explicit_receiver_type(call_node) if call_node.receiver
|
|
6109
|
+
|
|
6110
|
+
operand_scope.self_type || operand_scope.environment.nominal_for_name("Object")
|
|
6111
|
+
end
|
|
6112
|
+
|
|
5246
6113
|
# The binding each outer local or ivar the body rebinds ({CapturedLocals.writes}) from a sentinel seed — `nil` or
|
|
5247
6114
|
# `false`, alone or as a union of the two — enters a repeating call's body with when the write-back will not run
|
|
5248
6115
|
# its passes for it ({#write_back_block_captures} reaches only an explicit receiver classified
|
|
@@ -5255,7 +6122,7 @@ module Rigor
|
|
|
5255
6122
|
def unproven_rebind_bindings(call_node, block, classification)
|
|
5256
6123
|
return NO_CAPTURE_BINDINGS if call_node.receiver && classification == :non_escaping
|
|
5257
6124
|
|
|
5258
|
-
names = CapturedLocals.writes(block, scope, ivars: true)
|
|
6125
|
+
names = CapturedLocals.writes(block, scope, ivars: true, foreign_self: foreign_self_block?(call_node))
|
|
5259
6126
|
return NO_CAPTURE_BINDINGS if names.empty?
|
|
5260
6127
|
|
|
5261
6128
|
untyped = Type::Combinator.untyped
|
|
@@ -5370,8 +6237,16 @@ module Rigor
|
|
|
5370
6237
|
# The continuation effects of an escaping closure body: the outer locals it can rebind drop their narrowing, and
|
|
5371
6238
|
# the ones it content-mutates are floored. Shared by the two spellings — a block on an escaping call
|
|
5372
6239
|
# ({#record_closure_escape_if_any}) and a lambda literal ({#eval_lambda}) — so they cannot drift.
|
|
5373
|
-
def escaping_closure_captures(closure_node, post_scope)
|
|
5374
|
-
widen_escaping_content_captures(closure_node,
|
|
6240
|
+
def escaping_closure_captures(closure_node, post_scope, foreign_self: false)
|
|
6241
|
+
widen_escaping_content_captures(closure_node,
|
|
6242
|
+
drop_captured_narrowing(closure_node, post_scope, foreign_self: foreign_self))
|
|
6243
|
+
end
|
|
6244
|
+
|
|
6245
|
+
# Issue #1402 — whether `call_node`'s block runs on an object that is provably not `self`
|
|
6246
|
+
# ({InterveningIvarReset.foreign_self_block?}, its receiver typed where the operands were), so that no instance
|
|
6247
|
+
# variable it writes is this scope's ({CapturedLocals.writes}' `foreign_self:`).
|
|
6248
|
+
def foreign_self_block?(call_node)
|
|
6249
|
+
InterveningIvarReset.foreign_self_block?(call_node, operand_scope)
|
|
5375
6250
|
end
|
|
5376
6251
|
|
|
5377
6252
|
# Builds the entry scope for a block body. The block sees the outer scope's locals (Ruby's lexical scoping rule)
|
|
@@ -5822,7 +6697,7 @@ module Rigor
|
|
|
5822
6697
|
# inferred.
|
|
5823
6698
|
def eval_return(node)
|
|
5824
6699
|
sink = Thread.current[RETURN_SINK_KEY]
|
|
5825
|
-
sink
|
|
6700
|
+
sink&.record(node, jump_value_type(node))
|
|
5826
6701
|
[Type::Combinator.bot, scope]
|
|
5827
6702
|
end
|
|
5828
6703
|
|