rigortype 0.4.5 → 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 +1 -1
- data/docs/handbook/11-sig-gen.md +11 -2
- data/docs/manual/02-cli-reference.md +46 -7
- data/docs/manual/03-configuration.md +2 -1
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/10-mcp-server.md +9 -1
- data/docs/manual/12-caching.md +8 -1
- 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 +2 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -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 +4 -0
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- 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/unreachable_clause_collector.rb +51 -0
- data/lib/rigor/analysis/check_rules.rb +93 -8
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_session.rb +175 -27
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- 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 +17 -4
- 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 +156 -49
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/cache/incremental_snapshot.rb +30 -7
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/diff_command.rb +2 -1
- 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/plugins_command.rb +5 -0
- 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 +46 -7
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/config_audit.rb +16 -5
- data/lib/rigor/configuration.rb +14 -4
- data/lib/rigor/environment/rbs_loader.rb +115 -6
- 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/callee_mutation_chain.rb +115 -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 +17 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +62 -13
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +46 -16
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +12 -0
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +66 -5
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +128 -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 +480 -103
- data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
- data/lib/rigor/inference/statement_evaluator.rb +658 -90
- 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/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/mcp/server.rb +9 -1
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +57 -2
- data/lib/rigor/rooted_glob.rb +10 -1
- data/lib/rigor/scope.rb +14 -5
- data/lib/rigor/sig_gen/generator.rb +308 -37
- data/lib/rigor/sig_gen/observation_collector.rb +4 -1
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +6 -3
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +45 -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-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -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 +4 -4
- 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/scope.rbs +31 -50
- data/sig/rigor/type.rbs +0 -6
- metadata +20 -1
|
@@ -14,6 +14,7 @@ require_relative "block_call_timing"
|
|
|
14
14
|
require_relative "block_repetition"
|
|
15
15
|
require_relative "block_parameter_binder"
|
|
16
16
|
require_relative "body_fixpoint"
|
|
17
|
+
require_relative "callee_mutation_chain"
|
|
17
18
|
require_relative "captured_locals"
|
|
18
19
|
require_relative "dynamic_origin"
|
|
19
20
|
require_relative "error_info"
|
|
@@ -45,6 +46,8 @@ require_relative "narrowing"
|
|
|
45
46
|
require_relative "operand_effects"
|
|
46
47
|
require_relative "operand_walk"
|
|
47
48
|
require_relative "optimistic_origin"
|
|
49
|
+
require_relative "loop_value_record"
|
|
50
|
+
require_relative "project_yielder"
|
|
48
51
|
require_relative "return_barrier"
|
|
49
52
|
require_relative "repeated_or_writes"
|
|
50
53
|
require_relative "rewrite_mutation"
|
|
@@ -117,8 +120,8 @@ module Rigor
|
|
|
117
120
|
# the default expression pair, so routing the or-writes here adds the body entry and nothing else.
|
|
118
121
|
Prism::ConstantOrWriteNode => :eval_constant_write,
|
|
119
122
|
Prism::ConstantPathOrWriteNode => :eval_constant_write,
|
|
120
|
-
Prism::IfNode => :
|
|
121
|
-
Prism::UnlessNode => :
|
|
123
|
+
Prism::IfNode => :eval_recorded_if,
|
|
124
|
+
Prism::UnlessNode => :eval_recorded_unless,
|
|
122
125
|
Prism::ElseNode => :eval_else,
|
|
123
126
|
Prism::CaseNode => :eval_case,
|
|
124
127
|
Prism::CaseMatchNode => :eval_case,
|
|
@@ -127,9 +130,9 @@ module Rigor
|
|
|
127
130
|
Prism::BeginNode => :eval_begin_node,
|
|
128
131
|
Prism::RescueNode => :eval_rescue,
|
|
129
132
|
Prism::EnsureNode => :eval_ensure,
|
|
130
|
-
Prism::WhileNode => :
|
|
131
|
-
Prism::UntilNode => :
|
|
132
|
-
Prism::ForNode => :
|
|
133
|
+
Prism::WhileNode => :eval_recorded_loop,
|
|
134
|
+
Prism::UntilNode => :eval_recorded_loop,
|
|
135
|
+
Prism::ForNode => :eval_recorded_for,
|
|
133
136
|
Prism::AndNode => :eval_and_or,
|
|
134
137
|
Prism::OrNode => :eval_and_or,
|
|
135
138
|
Prism::ParenthesesNode => :eval_parentheses,
|
|
@@ -186,6 +189,40 @@ module Rigor
|
|
|
186
189
|
RETURN_SINK_KEY = :rigor_return_sink
|
|
187
190
|
private_constant :RETURN_SINK_KEY
|
|
188
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
|
+
|
|
189
226
|
# Thread-local sink (an Array of `[NextNode, Type]`) collecting the value each `next` carries out of the block it
|
|
190
227
|
# leaves, so `ExpressionTyper#type_block_body` can join those arms into the block's value type. Issue #841: the
|
|
191
228
|
# block-return pass modelled only the fall-through tail, so `ops.all? { |o| next false unless o; true }` read as
|
|
@@ -313,14 +350,31 @@ module Rigor
|
|
|
313
350
|
# method-return type.
|
|
314
351
|
def self.with_return_sink
|
|
315
352
|
previous = Thread.current[RETURN_SINK_KEY]
|
|
316
|
-
|
|
353
|
+
previous_provisional = Thread.current[PROVISIONAL_RETURNS_KEY]
|
|
354
|
+
sink = ReturnSink.new
|
|
317
355
|
Thread.current[RETURN_SINK_KEY] = sink
|
|
356
|
+
Thread.current[PROVISIONAL_RETURNS_KEY] = nil
|
|
318
357
|
begin
|
|
319
358
|
result = yield
|
|
320
359
|
ensure
|
|
321
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
|
|
322
377
|
end
|
|
323
|
-
[result, sink]
|
|
324
378
|
end
|
|
325
379
|
|
|
326
380
|
# Runs `block` with a fresh `next` sink installed, then yields the collected `[NextNode, Type]` pairs to the
|
|
@@ -452,19 +506,28 @@ module Rigor
|
|
|
452
506
|
[type, fall_through, join_jump_scopes(fall_through, sink, JumpTargets.of(body, Prism::NextNode))]
|
|
453
507
|
end
|
|
454
508
|
|
|
455
|
-
# ADR-89 WD2 — the sorted
|
|
509
|
+
# ADR-89 WD2 — the sorted slots of the parameters (positional, then keyword) whose CONTENT `def_node` mutates
|
|
456
510
|
# (`callee_content_mutated_parameters`, the ADR-56 arg-flooring surface a caller consumes). A per-def
|
|
457
511
|
# static property of the AST — this is a pure exposure of the existing private computation (it never
|
|
458
512
|
# reads the scope), so the incremental session can compute it on any `StatementEvaluator` (an empty
|
|
459
513
|
# scope suffices), persist it in a return summary, and re-check a callee's symbol dependents when it
|
|
460
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.
|
|
461
520
|
def content_mutated_parameter_positions(def_node)
|
|
462
|
-
|
|
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
|
|
463
526
|
end
|
|
464
527
|
|
|
465
|
-
# The local-variable reads among `call_node`'s positional
|
|
466
|
-
# content-mutates, when `call_node` is a self-dispatch call resolving to a user def in this evaluator's
|
|
467
|
-
# (`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
|
|
468
531
|
# callee floor ({#widen_callee_escaped_argument_captures}) floors after the call, and the ones
|
|
469
532
|
# {CapturedLocals.content_mutations} reports as a block's callee-mutated captures.
|
|
470
533
|
def content_mutated_arguments(call_node)
|
|
@@ -480,12 +543,25 @@ module Rigor
|
|
|
480
543
|
return NO_ARGUMENT_READS if mutated.empty?
|
|
481
544
|
|
|
482
545
|
argument_nodes = call_node.arguments.arguments
|
|
483
|
-
mutated.values.uniq.filter_map do |
|
|
484
|
-
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)
|
|
485
549
|
argument if argument.is_a?(Prism::LocalVariableReadNode)
|
|
486
550
|
end
|
|
487
551
|
end
|
|
488
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
|
+
|
|
489
565
|
NO_ARGUMENT_READS = [].freeze
|
|
490
566
|
private_constant :NO_ARGUMENT_READS
|
|
491
567
|
|
|
@@ -678,7 +754,19 @@ module Rigor
|
|
|
678
754
|
# stamped by {#seed_instance_ivars}.
|
|
679
755
|
bound = bound.with_published_constant_mark(:ivar, node.name) if
|
|
680
756
|
published_constant_copy?(node.value, rhs_type, post_rhs)
|
|
681
|
-
[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)
|
|
682
770
|
end
|
|
683
771
|
|
|
684
772
|
def eval_cvar_write(node)
|
|
@@ -832,11 +920,24 @@ module Rigor
|
|
|
832
920
|
|
|
833
921
|
def compound_eval(node, kind:, op:) # rubocop:disable Naming/MethodParameterName
|
|
834
922
|
current_type = current_type_for(kind, node.name)
|
|
835
|
-
rhs_type, post_rhs = sub_eval(node.value,
|
|
923
|
+
rhs_type, post_rhs = sub_eval(node.value, compound_value_scope(kind, node.name, op))
|
|
836
924
|
result_type = compound_result_type(current_type, rhs_type, op)
|
|
837
925
|
[result_type, rebind_variable(post_rhs, kind, node.name, result_type)]
|
|
838
926
|
end
|
|
839
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
|
+
|
|
840
941
|
VAR_KIND_GETTERS = {
|
|
841
942
|
local: :local, ivar: :ivar, cvar: :cvar, global: :global
|
|
842
943
|
}.freeze
|
|
@@ -906,7 +1007,40 @@ module Rigor
|
|
|
906
1007
|
arg_types: index_write_arg_types(node, result_type))
|
|
907
1008
|
post = post.with_indexed_narrowing(*address, result_type) if address
|
|
908
1009
|
|
|
909
|
-
|
|
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
|
|
910
1044
|
end
|
|
911
1045
|
|
|
912
1046
|
# `h[k] &&= v` / `h[k] += v`. Neither had a handler, so both fell to `evaluate`'s default — typed as a pure
|
|
@@ -1446,7 +1580,10 @@ module Rigor
|
|
|
1446
1580
|
after
|
|
1447
1581
|
end
|
|
1448
1582
|
body_entry = reduce_scopes_with_nil_injection(test_exits)
|
|
1449
|
-
|
|
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
|
|
1450
1587
|
|
|
1451
1588
|
[Narrowing.case_when_scopes(subject, conditions, body_entry).first,
|
|
1452
1589
|
Narrowing.case_when_scopes(subject, conditions, after).last, false]
|
|
@@ -1493,16 +1630,21 @@ module Rigor
|
|
|
1493
1630
|
# reads: its binding moved, the test writes it, or — for an instance variable — a call in the test may write
|
|
1494
1631
|
# it ({InterveningIvarReset.may_write?}), or — for a global or constant — the test may run code that rebinds
|
|
1495
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.
|
|
1496
1637
|
def when_test_rebinds_subject?(subject, condition, before, after)
|
|
1638
|
+
subject = case_subject_variable(subject)
|
|
1497
1639
|
name = case_subject_name(subject)
|
|
1498
1640
|
return false if name.nil?
|
|
1499
1641
|
return true unless case_subject_binding(subject, before) == case_subject_binding(subject, after)
|
|
1500
1642
|
return true if OperandEffects.written_variables(condition).include?(name)
|
|
1501
1643
|
|
|
1502
1644
|
case subject
|
|
1503
|
-
when Prism::InstanceVariableReadNode
|
|
1645
|
+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode
|
|
1504
1646
|
InterveningIvarReset.may_write?(condition, name, before, run_lambdas: true)
|
|
1505
|
-
when Prism::LocalVariableReadNode
|
|
1647
|
+
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode
|
|
1506
1648
|
false
|
|
1507
1649
|
else
|
|
1508
1650
|
body = when_lambda_body(condition)
|
|
@@ -1510,9 +1652,29 @@ module Rigor
|
|
|
1510
1652
|
end
|
|
1511
1653
|
end
|
|
1512
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
|
+
|
|
1513
1674
|
def case_subject_name(subject)
|
|
1514
1675
|
case subject
|
|
1515
|
-
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode
|
|
1676
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode,
|
|
1677
|
+
Prism::LocalVariableWriteNode, Prism::InstanceVariableWriteNode
|
|
1516
1678
|
subject.name
|
|
1517
1679
|
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
1518
1680
|
Narrowing.constant_key(subject)
|
|
@@ -1521,8 +1683,8 @@ module Rigor
|
|
|
1521
1683
|
|
|
1522
1684
|
def case_subject_binding(subject, scope)
|
|
1523
1685
|
case subject
|
|
1524
|
-
when Prism::LocalVariableReadNode then scope.local(subject.name)
|
|
1525
|
-
when Prism::InstanceVariableReadNode then scope.ivar(subject.name)
|
|
1686
|
+
when Prism::LocalVariableReadNode, Prism::LocalVariableWriteNode then scope.local(subject.name)
|
|
1687
|
+
when Prism::InstanceVariableReadNode, Prism::InstanceVariableWriteNode then scope.ivar(subject.name)
|
|
1526
1688
|
when Prism::GlobalVariableReadNode then scope.global(subject.name)
|
|
1527
1689
|
else scope.constant_narrowing(Narrowing.constant_key(subject))
|
|
1528
1690
|
end
|
|
@@ -2222,16 +2384,37 @@ module Rigor
|
|
|
2222
2384
|
# ({#loop_body_ivar_writes}).
|
|
2223
2385
|
def eval_repeating_for(node) = ReflectiveIvarWrite.in_repeating_body { eval_for(node) }
|
|
2224
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
|
+
|
|
2225
2413
|
# `while pred; body; end` / `until pred; body; end`. The body might run zero or more times, so half-bound names
|
|
2226
|
-
# degrade to `T | nil` in the post-loop scope. The loop expression
|
|
2227
|
-
#
|
|
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).
|
|
2228
2416
|
def eval_loop(node)
|
|
2229
|
-
|
|
2230
|
-
# `instance_variable_set` in it joins its value and `Dynamic[top]` ({ReflectiveIvarWrite.in_repeating_body}):
|
|
2231
|
-
# the rebind fixpoint tracks the body's writes only.
|
|
2232
|
-
entry = loop_entry_scope(node)
|
|
2233
|
-
_pred_type, post_pred = ReflectiveIvarWrite.in_repeating_body { sub_eval(node.predicate, entry) }
|
|
2234
|
-
post_pred = widen_predicate_pins(node, post_pred)
|
|
2417
|
+
post_pred = loop_predicate_scope(node)
|
|
2235
2418
|
return [Type::Combinator.constant_of(nil), narrow_loop_exit_edge(node, post_pred)] if node.statements.nil?
|
|
2236
2419
|
|
|
2237
2420
|
# The historical single body pass joined with the pre-loop scope. This continues to carry everything the
|
|
@@ -2248,7 +2431,7 @@ module Rigor
|
|
|
2248
2431
|
# The pass ends with its `next` exits as well as its fall-through ({#loop_iteration}). Its `break` arms are
|
|
2249
2432
|
# superseded by the fixpoint's converged pass ({#loop_break_arms}) except in a `begin … end while` loop, below.
|
|
2250
2433
|
jumps = loop_jumps(node.statements)
|
|
2251
|
-
body_scope, first_breaks = single_pass(node, post_pred, jumps)
|
|
2434
|
+
body_scope, first_breaks, first_values = single_pass(node, post_pred, jumps)
|
|
2252
2435
|
base_scope = join_with_nil_injection(post_pred, body_scope)
|
|
2253
2436
|
|
|
2254
2437
|
names, body_first = loop_rebound_names(node.statements, post_pred)
|
|
@@ -2258,17 +2441,50 @@ module Rigor
|
|
|
2258
2441
|
# the single-pass join before returning.
|
|
2259
2442
|
if names.empty?
|
|
2260
2443
|
fast = loop_content_writeback(node.statements, base_scope, pre_body: post_pred)
|
|
2261
|
-
return [
|
|
2444
|
+
return [loop_value(node, first_values), loop_exit(node, post_pred, fast)]
|
|
2262
2445
|
end
|
|
2263
2446
|
|
|
2264
|
-
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)
|
|
2265
2448
|
# A `begin … end while` / `until` body runs once before the predicate is first tested, so that iteration's
|
|
2266
2449
|
# entry lies outside every fixpoint pass's predicate-narrowed one, and a `break` only it can take (`if state ==
|
|
2267
2450
|
# :idle` under `end while state != :idle`) is dead in every converged pass. The single pass runs from the
|
|
2268
2451
|
# un-narrowed post-predicate scope, so its arms stand in for that first iteration.
|
|
2269
2452
|
post_loop = join_break_scopes(post_loop, first_breaks, names) if node.begin_modifier?
|
|
2270
2453
|
post_loop = loop_exit(node, post_pred, post_loop)
|
|
2271
|
-
[
|
|
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
|
|
2272
2488
|
end
|
|
2273
2489
|
|
|
2274
2490
|
# Issue #1359 — the scope a loop's predicate first runs from: a body that may set `$_` runs again after it ran,
|
|
@@ -2330,7 +2546,8 @@ module Rigor
|
|
|
2330
2546
|
|
|
2331
2547
|
# The continuation scope for a loop whose body rebinds locals: the ADR-56 slice-B rebind fixpoint overlaid on
|
|
2332
2548
|
# `base_scope`, then the slice-C receiver-content writeback, then the `break`-path bindings the fall-through
|
|
2333
|
-
# 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}).
|
|
2334
2551
|
def converged_loop_scope(node, post_pred, base_scope, names, body_first, jumps)
|
|
2335
2552
|
# ADR-56 slice B — loop-body fixpoint. The body runs 0..N times and may compound (`d *= 2`), so the historical
|
|
2336
2553
|
# single body pass joined with the pre-loop scope kept stale folded constants (`d = 1; while …; d *= 2; end` →
|
|
@@ -2338,7 +2555,7 @@ module Rigor
|
|
|
2338
2555
|
# fixpoint slice A uses for non-escaping block captures. Seed: a pre-existing local seeds with its
|
|
2339
2556
|
# post-predicate binding; a local FIRST assigned inside the body seeds with `nil` so the 0-iteration path
|
|
2340
2557
|
# degrades it to `T | nil`, matching the nil-injection treatment.
|
|
2341
|
-
break_pass =
|
|
2558
|
+
break_pass = { entry: nil, arms: [], values: [] } if jumps.breaks || later_pass_returns?(node.statements)
|
|
2342
2559
|
result = loop_rebind_fixpoint(node, post_pred, names, body_first, jumps, break_pass)
|
|
2343
2560
|
# Display-path re-record: the fixpoint's body re-evaluations fire `on_enter` with the cap-N INTERMEDIATE
|
|
2344
2561
|
# assumptions, so the last-visit-wins scope index would annotate loop-body lines with stale pre-convergence
|
|
@@ -2350,8 +2567,8 @@ module Rigor
|
|
|
2350
2567
|
# the continuation collection. Pre-state comes from `post_pred` for a name the loop only content-mutates and
|
|
2351
2568
|
# from `post_loop` for one it also rebinds, so composition still works — see {#loop_content_writeback}.
|
|
2352
2569
|
post_loop = loop_content_writeback(node.statements, post_loop, pre_body: post_pred, rebound: names)
|
|
2353
|
-
arms = loop_break_arms(node, post_pred, result, body_first, jumps, break_pass)
|
|
2354
|
-
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]
|
|
2355
2572
|
end
|
|
2356
2573
|
|
|
2357
2574
|
# A loop's continuation: a local its body and predicate only narrowed from `Dynamic[top]` re-joins to it
|
|
@@ -2444,8 +2661,11 @@ module Rigor
|
|
|
2444
2661
|
[sink, result]
|
|
2445
2662
|
end
|
|
2446
2663
|
|
|
2447
|
-
# One evaluation of a loop body from `entry`. Returns `[exit, breaks]`: the scope the iteration ends with,
|
|
2448
|
-
# 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
|
|
2449
2669
|
# single pass `eval_loop` joins with the pre-loop scope, each pass of its rebind fixpoint, and `eval_for`'s only
|
|
2450
2670
|
# pass.
|
|
2451
2671
|
#
|
|
@@ -2463,18 +2683,21 @@ module Rigor
|
|
|
2463
2683
|
recording = recorded ? {} : UNRECORDED
|
|
2464
2684
|
evaluate = -> { sub_eval(statements, entry, next_scope_sink: next_sink, **recording).last }
|
|
2465
2685
|
if jumps.breaks
|
|
2466
|
-
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
|
|
2467
2689
|
breaks = targeted_scopes(break_sink, jumps.breaks)
|
|
2690
|
+
values = targeted_scopes(value_sink, jumps.breaks)
|
|
2468
2691
|
else
|
|
2469
2692
|
fall_through = evaluate.call
|
|
2470
|
-
breaks = NO_BREAK_ARMS
|
|
2693
|
+
breaks = values = NO_BREAK_ARMS
|
|
2471
2694
|
end
|
|
2472
|
-
return [fall_through, breaks] if next_sink.nil?
|
|
2695
|
+
return [fall_through, breaks, values] if next_sink.nil?
|
|
2473
2696
|
|
|
2474
2697
|
exit_scope = targeted_scopes(next_sink, jumps.nexts).reduce(fall_through) do |acc, next_scope|
|
|
2475
2698
|
join_with_nil_injection(acc, next_scope)
|
|
2476
2699
|
end
|
|
2477
|
-
[exit_scope, breaks]
|
|
2700
|
+
[exit_scope, breaks, values]
|
|
2478
2701
|
end
|
|
2479
2702
|
|
|
2480
2703
|
# The `break` scopes the continuation joins. A `break` leaves the loop, so its binding starts no further iteration
|
|
@@ -2494,12 +2717,24 @@ module Rigor
|
|
|
2494
2717
|
# The unrecorded pass is otherwise an ordinary evaluation: a `return` it reaches joins the enclosing method's
|
|
2495
2718
|
# inferred return type, as one any other pass reaches does. That only widens the return, toward values a capped
|
|
2496
2719
|
# fixpoint's own passes never evaluated.
|
|
2720
|
+
#
|
|
2721
|
+
# Returns `[arms, values]`, `values` being what the same pass's `break`s carry ({#loop_value}).
|
|
2497
2722
|
def loop_break_arms(node, post_pred, converged, body_first, jumps, break_pass)
|
|
2498
|
-
return NO_BREAK_ARMS if break_pass.nil?
|
|
2499
|
-
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)
|
|
2500
2725
|
|
|
2501
2726
|
entry = loop_pass_entry(node, post_pred, converged, body_first, jumps)
|
|
2502
|
-
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)
|
|
2503
2738
|
end
|
|
2504
2739
|
|
|
2505
2740
|
# Joins each `break` arm's body-written local bindings into the loop continuation, so a `break`-path binding the
|
|
@@ -2674,10 +2909,11 @@ module Rigor
|
|
|
2674
2909
|
# it.
|
|
2675
2910
|
def loop_body_exit_bindings(node, post_pred, bindings, names, body_first, jumps, break_pass = nil)
|
|
2676
2911
|
entry = loop_pass_entry(node, post_pred, bindings, body_first, jumps)
|
|
2677
|
-
exit_scope, breaks = loop_iteration(node.statements, entry, jumps)
|
|
2912
|
+
exit_scope, breaks, values = loop_iteration(node.statements, entry, jumps)
|
|
2678
2913
|
if break_pass
|
|
2679
2914
|
break_pass[:entry] = bindings.except(*body_first)
|
|
2680
2915
|
break_pass[:arms] = breaks
|
|
2916
|
+
break_pass[:values] = values
|
|
2681
2917
|
end
|
|
2682
2918
|
names.to_h { |name| [name, CapturedLocals.bound_type(exit_scope, name)] }
|
|
2683
2919
|
end
|
|
@@ -2736,29 +2972,157 @@ module Rigor
|
|
|
2736
2972
|
# index variable AND every local written in the body leak to the surrounding scope. The collection is evaluated
|
|
2737
2973
|
# once; the body runs zero or more times, so the post-loop scope is the join of the no-iteration scope (just
|
|
2738
2974
|
# `post_collection`) and the body scope, with half-bound names degraded to `T | nil` via nil-injection. The loop
|
|
2739
|
-
# expression
|
|
2740
|
-
# `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).
|
|
2741
2976
|
def eval_for(node)
|
|
2742
2977
|
coll_type, post_coll = sub_eval(node.collection, scope)
|
|
2743
|
-
|
|
2744
|
-
# Issue #1359 — a body that may set `$_` runs again after it ran, as a `while` body does ({#eval_loop}).
|
|
2745
|
-
body_entry = LastLine.forget_if_set(bind_for_index(node.index, element_type, post_coll), node.statements)
|
|
2746
|
-
body_entry = forget_guard_for_body(node, post_coll, body_entry)
|
|
2747
|
-
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)
|
|
2748
2979
|
|
|
2749
2980
|
if node.statements.nil?
|
|
2750
|
-
return [
|
|
2981
|
+
return [for_value(node, coll_type, post_coll, NO_BREAK_ARMS),
|
|
2982
|
+
join_with_nil_injection(post_coll, body_entry)]
|
|
2751
2983
|
end
|
|
2752
2984
|
|
|
2753
2985
|
# The body pass ends with its `next` exits as well as its fall-through, and its `break` arms are recovered into
|
|
2754
2986
|
# the continuation (the `for` sibling of `eval_loop`'s break join; `for` has no fixpoint, so the single pass is
|
|
2755
2987
|
# the only continuation and the only source of `break` arms).
|
|
2756
2988
|
jumps = loop_jumps(node.statements)
|
|
2757
|
-
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)
|
|
2758
2991
|
continuation = join_with_nil_injection(post_coll, body_scope)
|
|
2759
2992
|
pre_existing, body_first = loop_body_local_writes(node.statements, post_coll)
|
|
2760
|
-
continuation = join_break_scopes(continuation, breaks, pre_existing + body_first)
|
|
2761
|
-
|
|
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)
|
|
2762
3126
|
end
|
|
2763
3127
|
|
|
2764
3128
|
# Issue #1429 — a `for` body runs after the collection's `each`, which may be a project method, and after itself.
|
|
@@ -4124,13 +4488,163 @@ module Rigor
|
|
|
4124
4488
|
end
|
|
4125
4489
|
end
|
|
4126
4490
|
|
|
4127
|
-
# The `{ name =>
|
|
4128
|
-
#
|
|
4129
|
-
#
|
|
4130
|
-
# 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}.
|
|
4131
4496
|
def callee_content_mutated_parameters(def_node)
|
|
4132
4497
|
cache = (@callee_mutated_param_cache ||= {}.compare_by_identity)
|
|
4133
|
-
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)
|
|
4134
4648
|
end
|
|
4135
4649
|
|
|
4136
4650
|
# The user def a self-dispatch `node` resolves to in the enclosing class, or nil. Reuses the discovery index
|
|
@@ -4149,7 +4663,7 @@ module Rigor
|
|
|
4149
4663
|
node.receiver.nil? || node.receiver.is_a?(Prism::SelfNode)
|
|
4150
4664
|
end
|
|
4151
4665
|
|
|
4152
|
-
# The set of `[name,
|
|
4666
|
+
# The set of `[name, slot]` parameters of `def_node` whose content a block in the body escape-mutates.
|
|
4153
4667
|
# Memoised per def node (the body walk is otherwise repeated at every call site). A parameter is "escape- mutated"
|
|
4154
4668
|
# when a `param[k] = v` / `param << x` mutation on it appears inside a block whose receiving call is not proven
|
|
4155
4669
|
# non-escaping.
|
|
@@ -4159,7 +4673,7 @@ module Rigor
|
|
|
4159
4673
|
end
|
|
4160
4674
|
|
|
4161
4675
|
def compute_escaped_content_parameters(def_node)
|
|
4162
|
-
positions =
|
|
4676
|
+
positions = content_parameter_slots(def_node)
|
|
4163
4677
|
return {} if positions.empty?
|
|
4164
4678
|
|
|
4165
4679
|
mutated = Set.new
|
|
@@ -4174,18 +4688,22 @@ module Rigor
|
|
|
4174
4688
|
positions.slice(*mutated)
|
|
4175
4689
|
end
|
|
4176
4690
|
|
|
4177
|
-
# The `{ name =>
|
|
4178
|
-
#
|
|
4179
|
-
#
|
|
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
|
|
4180
4694
|
# parameter-name read is genuinely at method scope (depth 0) rather than a block-local of the same name that
|
|
4181
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).
|
|
4182
4700
|
def direct_content_parameters(def_node)
|
|
4183
4701
|
cache = (@direct_param_cache ||= {}.compare_by_identity)
|
|
4184
4702
|
cache[def_node] ||= compute_direct_content_parameters(def_node)
|
|
4185
4703
|
end
|
|
4186
4704
|
|
|
4187
4705
|
def compute_direct_content_parameters(def_node)
|
|
4188
|
-
positions =
|
|
4706
|
+
positions = content_parameter_slots(def_node)
|
|
4189
4707
|
return {} if positions.empty?
|
|
4190
4708
|
|
|
4191
4709
|
mutated = Set.new
|
|
@@ -4210,6 +4728,43 @@ module Rigor
|
|
|
4210
4728
|
end
|
|
4211
4729
|
end
|
|
4212
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
|
+
|
|
4213
4768
|
# A receiver-independent over-approximation of `ClosureEscapeAnalyzer`'s non-escaping verdict, used when scanning
|
|
4214
4769
|
# a callee body where the block- owning call's receiver TYPE is not available. A call whose method name is a known
|
|
4215
4770
|
# structural iterator (`each` / `map` / `tap` / …) runs its block synchronously and does not retain it, so its
|
|
@@ -4230,18 +4785,20 @@ module Rigor
|
|
|
4230
4785
|
SYNTACTIC_NON_ESCAPING_BLOCK_METHODS.include?(call_node.name)
|
|
4231
4786
|
end
|
|
4232
4787
|
|
|
4233
|
-
# `{ name =>
|
|
4234
|
-
#
|
|
4235
|
-
|
|
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)
|
|
4236
4792
|
params = def_node.parameters
|
|
4237
4793
|
return {} if params.nil?
|
|
4238
4794
|
|
|
4239
4795
|
ordered = (params.requireds || []) + (params.optionals || [])
|
|
4240
|
-
|
|
4796
|
+
slots = {}
|
|
4241
4797
|
ordered.each_with_index do |param, index|
|
|
4242
|
-
|
|
4798
|
+
slots[param.name] = index if param.respond_to?(:name)
|
|
4243
4799
|
end
|
|
4244
|
-
|
|
4800
|
+
(params.keywords || []).each { |param| slots[param.name] = param.name }
|
|
4801
|
+
slots
|
|
4245
4802
|
end
|
|
4246
4803
|
|
|
4247
4804
|
# True when at least one argument of `node` is a bare local-variable read (positional or keyword value) bound in
|
|
@@ -4257,9 +4814,10 @@ module Rigor
|
|
|
4257
4814
|
scope.locals.key?(arg.name)
|
|
4258
4815
|
when Prism::KeywordHashNode
|
|
4259
4816
|
arg.elements.any? do |pair|
|
|
4260
|
-
pair.is_a?(Prism::AssocNode)
|
|
4261
|
-
|
|
4262
|
-
|
|
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)
|
|
4263
4821
|
end
|
|
4264
4822
|
else
|
|
4265
4823
|
false
|
|
@@ -4417,6 +4975,11 @@ module Rigor
|
|
|
4417
4975
|
# The fresh `next`, `break`-value and `break`-scope sinks are defensive: every consumer of those sinks filters by
|
|
4418
4976
|
# jump-node identity, so no example tells them apart from the outer sinks today. So is the `rescue`: a raise here
|
|
4419
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`.
|
|
4420
4983
|
def collect_repeated_block_returns(call_node, post_scope)
|
|
4421
4984
|
return if Thread.current[RETURN_SINK_KEY].nil?
|
|
4422
4985
|
|
|
@@ -4440,16 +5003,19 @@ module Rigor
|
|
|
4440
5003
|
end
|
|
4441
5004
|
|
|
4442
5005
|
# Whether `block` is an ordinary block (not a return barrier or a class-body block) whose body may `return` from
|
|
4443
|
-
# the method and which `call_node` may run more than once: {#repeating_block_class}'s rule,
|
|
4444
|
-
#
|
|
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
|
|
4445
5009
|
# anonymous-class checks are defensive guards: no such call repeats under that rule today.
|
|
4446
5010
|
def repeated_block_return_candidate?(call_node, block)
|
|
4447
5011
|
return false unless block.is_a?(Prism::BlockNode)
|
|
4448
5012
|
return false unless ReturnBarrier.exits_method?(block.body)
|
|
4449
5013
|
return false if return_barrier_block?(call_node) || refined_class_context(call_node)
|
|
4450
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?
|
|
4451
5016
|
|
|
4452
|
-
|
|
5017
|
+
receiver_type = block_call_receiver_type(call_node)
|
|
5018
|
+
!receiver_type.nil? && ProjectYielder.may_run_block_repeatedly?(call_node, receiver_type, scope)
|
|
4453
5019
|
end
|
|
4454
5020
|
|
|
4455
5021
|
# ADR-56 slice A. For a `:non_escaping` block, fold the continuation binding of every outer local the body can
|
|
@@ -5523,12 +6089,7 @@ module Rigor
|
|
|
5523
6089
|
# receiver is the one the block-return pass asks about: the explicit receiver, or the implicit `self`
|
|
5524
6090
|
# (`Object` at the top level).
|
|
5525
6091
|
def repeating_block_class(call_node)
|
|
5526
|
-
receiver_type =
|
|
5527
|
-
if call_node.receiver
|
|
5528
|
-
explicit_receiver_type(call_node)
|
|
5529
|
-
else
|
|
5530
|
-
operand_scope.self_type || operand_scope.environment.nominal_for_name("Object")
|
|
5531
|
-
end
|
|
6092
|
+
receiver_type = block_call_receiver_type(call_node)
|
|
5532
6093
|
return nil if receiver_type.nil?
|
|
5533
6094
|
|
|
5534
6095
|
method_name = call_node.name
|
|
@@ -5542,6 +6103,13 @@ module Rigor
|
|
|
5542
6103
|
nil
|
|
5543
6104
|
end
|
|
5544
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
|
+
|
|
5545
6113
|
# The binding each outer local or ivar the body rebinds ({CapturedLocals.writes}) from a sentinel seed — `nil` or
|
|
5546
6114
|
# `false`, alone or as a union of the two — enters a repeating call's body with when the write-back will not run
|
|
5547
6115
|
# its passes for it ({#write_back_block_captures} reaches only an explicit receiver classified
|
|
@@ -6129,7 +6697,7 @@ module Rigor
|
|
|
6129
6697
|
# inferred.
|
|
6130
6698
|
def eval_return(node)
|
|
6131
6699
|
sink = Thread.current[RETURN_SINK_KEY]
|
|
6132
|
-
sink
|
|
6700
|
+
sink&.record(node, jump_value_type(node))
|
|
6133
6701
|
[Type::Combinator.bot, scope]
|
|
6134
6702
|
end
|
|
6135
6703
|
|