rigortype 0.4.4 → 0.4.5
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 +90 -17
- data/docs/manual/02-cli-reference.md +39 -9
- data/docs/manual/03-configuration.md +10 -8
- data/docs/manual/04-diagnostics.md +16 -5
- 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 +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -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 +13 -4
- data/lib/rigor/analysis/check_rules.rb +228 -15
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +49 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +166 -24
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/rule_catalog.rb +17 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
- data/lib/rigor/analysis/runner.rb +69 -65
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +12 -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 +26 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -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/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +60 -15
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- 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 +21 -1
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +87 -20
- data/lib/rigor/inference/expression_typer.rb +35 -5
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +137 -42
- data/lib/rigor/inference/narrowing.rb +54 -10
- 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 +120 -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.rb +675 -105
- data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
- 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 +446 -139
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- 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 +2 -2
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -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 +45 -2
- 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 +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- 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/constant.rb +10 -1
- 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 +1 -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-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-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-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- 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-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- 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/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +13 -1
|
@@ -10,6 +10,7 @@ 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"
|
|
@@ -28,7 +29,9 @@ require_relative "define_method_block_self"
|
|
|
28
29
|
require_relative "macro_block_self_type"
|
|
29
30
|
require_relative "guard_rebinding"
|
|
30
31
|
require_relative "self_call_ivar_writes"
|
|
32
|
+
require_relative "intervening_ivar_reset"
|
|
31
33
|
require_relative "match_rebinding"
|
|
34
|
+
require_relative "clause_widening"
|
|
32
35
|
require_relative "element_read_widening"
|
|
33
36
|
require_relative "hash_lookup_mutation"
|
|
34
37
|
require_relative "indexed_narrowing"
|
|
@@ -124,8 +127,8 @@ module Rigor
|
|
|
124
127
|
Prism::BeginNode => :eval_begin_node,
|
|
125
128
|
Prism::RescueNode => :eval_rescue,
|
|
126
129
|
Prism::EnsureNode => :eval_ensure,
|
|
127
|
-
Prism::WhileNode => :
|
|
128
|
-
Prism::UntilNode => :
|
|
130
|
+
Prism::WhileNode => :eval_loop,
|
|
131
|
+
Prism::UntilNode => :eval_loop,
|
|
129
132
|
Prism::ForNode => :eval_repeating_for,
|
|
130
133
|
Prism::AndNode => :eval_and_or,
|
|
131
134
|
Prism::OrNode => :eval_and_or,
|
|
@@ -365,7 +368,18 @@ module Rigor
|
|
|
365
368
|
# Default: the node is treated as a pure expression. Type it through the existing expression typer (which
|
|
366
369
|
# observes the current scope's locals) and leave the scope unchanged, but for the match globals a call in it
|
|
367
370
|
# may rebind (`super(line.sub(re, ""))`, issue #1365).
|
|
368
|
-
|
|
371
|
+
after = reset_ivars_past_unknown_code(node, forget_rebound_specials(@scope, node))
|
|
372
|
+
[@scope.type_of(node, tracer: @tracer), after]
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Issue #1402 — `after`, past a statement `yield` or `super`, with each narrowed instance variable widened over
|
|
376
|
+
# its class-ivar seed ({InterveningIvarReset.after_operand}): the block `yield` runs, and the superclass method
|
|
377
|
+
# `super` runs on `self`, may write any of them, as an implicit-self call may. Inside an operand the statement
|
|
378
|
+
# that holds it answers instead.
|
|
379
|
+
def reset_ivars_past_unknown_code(node, after)
|
|
380
|
+
return after if @in_operand || !InterveningIvarReset.candidate?(node)
|
|
381
|
+
|
|
382
|
+
InterveningIvarReset.after_operand(node, after)
|
|
369
383
|
end
|
|
370
384
|
|
|
371
385
|
# Issue #1429 — a compound write or a `for` loop calls a method its syntax does not spell (`r += r` calls `r.+`,
|
|
@@ -1349,6 +1363,9 @@ module Rigor
|
|
|
1349
1363
|
def eval_case_when_branches(subject_type, subject, conditions, entry_scope)
|
|
1350
1364
|
results = []
|
|
1351
1365
|
falsey_scope = entry_scope
|
|
1366
|
+
# Issue #1963 — set once a `when` test may have rebound the subject variable, which from there on no longer
|
|
1367
|
+
# holds the value the later tests match ({#subject_when_scopes}).
|
|
1368
|
+
rebound = false
|
|
1352
1369
|
conditions.each do |branch|
|
|
1353
1370
|
# Issue #1359 — a clause's conditions, or its pattern's pins and guard, run before its body and before
|
|
1354
1371
|
# every later clause, and the walk types them without evaluating them, so a reader there forgets `$_` here.
|
|
@@ -1359,7 +1376,11 @@ module Rigor
|
|
|
1359
1376
|
# recursion) so no condition sub-expression is newly typed; `propagate` preserves the entry because it already
|
|
1360
1377
|
# keys the node.
|
|
1361
1378
|
record_clause_entry_scope(branch, falsey_scope)
|
|
1362
|
-
|
|
1379
|
+
if subject && branch.is_a?(Prism::WhenNode)
|
|
1380
|
+
body_scope, falsey_scope, rebound = subject_when_scopes(subject, branch.conditions, falsey_scope, rebound)
|
|
1381
|
+
else
|
|
1382
|
+
body_scope, falsey_scope = branch_body_and_falsey_scopes(subject_type, subject, branch, falsey_scope)
|
|
1383
|
+
end
|
|
1363
1384
|
results << sub_eval(branch, body_scope)
|
|
1364
1385
|
end
|
|
1365
1386
|
[results, falsey_scope]
|
|
@@ -1399,6 +1420,114 @@ module Rigor
|
|
|
1399
1420
|
end
|
|
1400
1421
|
end
|
|
1401
1422
|
|
|
1423
|
+
# Issue #1963 — a `when` clause of a `case` with a subject. Ruby runs each test in order (`test === subject`)
|
|
1424
|
+
# until one matches, so a test that runs code — a call, a write, a lambda literal, whose `===` runs its body —
|
|
1425
|
+
# runs before the clause's body and every later clause. Such a test is evaluated ({#run_when_test}), so a call
|
|
1426
|
+
# in it resets the instance variables and the guard narrowings it may rebind, as a statement call does, and a
|
|
1427
|
+
# write in it binds. The body is reached after any prefix of the tests ran, so it enters from the join of the
|
|
1428
|
+
# scopes each test leaves, the next clause from the scope the last one leaves.
|
|
1429
|
+
#
|
|
1430
|
+
# The subject is evaluated once, before every test, so a test that may rebind the subject variable
|
|
1431
|
+
# ({#when_test_rebinds_subject?}) leaves the variable holding something other than the value the later tests
|
|
1432
|
+
# match: from that test on, no clause narrows it (`case @x when String then 1 when reset! then @x.upcase`, with
|
|
1433
|
+
# `reset!` assigning `@x`, reads `@x` as `reset!` may leave it, not as `nil`). `rebound` carries that across
|
|
1434
|
+
# the clauses; the answer is `[body_scope, falsey_scope, rebound]`. A clause whose tests run no code, before
|
|
1435
|
+
# any test rebound the subject, keeps {Narrowing.case_when_scopes}' narrowing unchanged.
|
|
1436
|
+
def subject_when_scopes(subject, conditions, entry, rebound)
|
|
1437
|
+
unless rebound || conditions.any? { |condition| when_test_runs_code?(condition) }
|
|
1438
|
+
return [*Narrowing.case_when_scopes(subject, conditions, entry), false]
|
|
1439
|
+
end
|
|
1440
|
+
|
|
1441
|
+
after = entry
|
|
1442
|
+
test_exits = conditions.map do |condition|
|
|
1443
|
+
before = after
|
|
1444
|
+
after = run_when_test(condition, before)
|
|
1445
|
+
rebound ||= when_test_rebinds_subject?(subject, condition, before, after)
|
|
1446
|
+
after
|
|
1447
|
+
end
|
|
1448
|
+
body_entry = reduce_scopes_with_nil_injection(test_exits)
|
|
1449
|
+
return [Narrowing.apply_when_regex_globals(conditions, body_entry), after, true] if rebound
|
|
1450
|
+
|
|
1451
|
+
[Narrowing.case_when_scopes(subject, conditions, body_entry).first,
|
|
1452
|
+
Narrowing.case_when_scopes(subject, conditions, after).last, false]
|
|
1453
|
+
end
|
|
1454
|
+
|
|
1455
|
+
# Whether a `when` test runs code beyond reading a value: a call, `super`, `yield`, a lambda literal or a write.
|
|
1456
|
+
WHEN_TEST_CODE_NODES = [
|
|
1457
|
+
Prism::CallNode, Prism::SuperNode, Prism::ForwardingSuperNode, Prism::YieldNode, Prism::LambdaNode
|
|
1458
|
+
].freeze
|
|
1459
|
+
private_constant :WHEN_TEST_CODE_NODES
|
|
1460
|
+
|
|
1461
|
+
def when_test_runs_code?(condition)
|
|
1462
|
+
return true if OperandEffects.any?(condition)
|
|
1463
|
+
|
|
1464
|
+
found = false
|
|
1465
|
+
Source::NodeWalker.each(condition) do |descendant|
|
|
1466
|
+
found ||= WHEN_TEST_CODE_NODES.any? { |klass| descendant.is_a?(klass) }
|
|
1467
|
+
end
|
|
1468
|
+
found
|
|
1469
|
+
end
|
|
1470
|
+
|
|
1471
|
+
# The scope a `when` test leaves. The instance variables its implicit-self calls may write are widened first,
|
|
1472
|
+
# a lambda literal's body included, since its `===` runs it ({InterveningIvarReset.after_operand}, which
|
|
1473
|
+
# marks a binding whose narrowing had ruled `nil` out). The test is then evaluated, which binds its writes
|
|
1474
|
+
# and drops the narrowing of a variable a lambda literal captures and writes ({#eval_lambda}). A guard's
|
|
1475
|
+
# narrowing that a lambda body may rebind is restored as a call's would be, since its `===` runs the body.
|
|
1476
|
+
def run_when_test(condition, before)
|
|
1477
|
+
before = InterveningIvarReset.after_operand(condition, before, run_lambdas: true)
|
|
1478
|
+
_type, after = sub_eval(condition, before)
|
|
1479
|
+
body = when_lambda_body(condition)
|
|
1480
|
+
return after unless body && after.guard_narrowed? && GuardRebinding.may_rebind?(body, after)
|
|
1481
|
+
|
|
1482
|
+
after.forget_guard_narrowings
|
|
1483
|
+
end
|
|
1484
|
+
|
|
1485
|
+
def when_lambda_body(condition)
|
|
1486
|
+
condition = condition.body.body.first while condition.is_a?(Prism::ParenthesesNode) &&
|
|
1487
|
+
condition.body.is_a?(Prism::StatementsNode) &&
|
|
1488
|
+
condition.body.body.size == 1
|
|
1489
|
+
condition.body if condition.is_a?(Prism::LambdaNode)
|
|
1490
|
+
end
|
|
1491
|
+
|
|
1492
|
+
# Whether the `when` test `condition`, which took `before` to `after`, may have rebound the variable `subject`
|
|
1493
|
+
# reads: its binding moved, the test writes it, or — for an instance variable — a call in the test may write
|
|
1494
|
+
# it ({InterveningIvarReset.may_write?}), or — for a global or constant — the test may run code that rebinds
|
|
1495
|
+
# one ({GuardRebinding.may_rebind?}).
|
|
1496
|
+
def when_test_rebinds_subject?(subject, condition, before, after)
|
|
1497
|
+
name = case_subject_name(subject)
|
|
1498
|
+
return false if name.nil?
|
|
1499
|
+
return true unless case_subject_binding(subject, before) == case_subject_binding(subject, after)
|
|
1500
|
+
return true if OperandEffects.written_variables(condition).include?(name)
|
|
1501
|
+
|
|
1502
|
+
case subject
|
|
1503
|
+
when Prism::InstanceVariableReadNode
|
|
1504
|
+
InterveningIvarReset.may_write?(condition, name, before, run_lambdas: true)
|
|
1505
|
+
when Prism::LocalVariableReadNode
|
|
1506
|
+
false
|
|
1507
|
+
else
|
|
1508
|
+
body = when_lambda_body(condition)
|
|
1509
|
+
GuardRebinding.may_rebind?(condition, before) || (!body.nil? && GuardRebinding.may_rebind?(body, before))
|
|
1510
|
+
end
|
|
1511
|
+
end
|
|
1512
|
+
|
|
1513
|
+
def case_subject_name(subject)
|
|
1514
|
+
case subject
|
|
1515
|
+
when Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, Prism::GlobalVariableReadNode
|
|
1516
|
+
subject.name
|
|
1517
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
1518
|
+
Narrowing.constant_key(subject)
|
|
1519
|
+
end
|
|
1520
|
+
end
|
|
1521
|
+
|
|
1522
|
+
def case_subject_binding(subject, scope)
|
|
1523
|
+
case subject
|
|
1524
|
+
when Prism::LocalVariableReadNode then scope.local(subject.name)
|
|
1525
|
+
when Prism::InstanceVariableReadNode then scope.ivar(subject.name)
|
|
1526
|
+
when Prism::GlobalVariableReadNode then scope.global(subject.name)
|
|
1527
|
+
else scope.constant_narrowing(Narrowing.constant_key(subject))
|
|
1528
|
+
end
|
|
1529
|
+
end
|
|
1530
|
+
|
|
1402
1531
|
# Issue #1495 — a `case` without a subject tests each `when` condition for truthiness, in order, and stops at the
|
|
1403
1532
|
# first truthy one, so a clause is the `||` of its conditions in an `if` / `elsif` chain. Each condition is
|
|
1404
1533
|
# evaluated under the falsey edges of the conditions before it, in this clause and every earlier one; the body
|
|
@@ -1517,9 +1646,32 @@ module Rigor
|
|
|
1517
1646
|
|
|
1518
1647
|
# The scope past `node`'s `ensure` clause, entered with `exit_scope`, the `next` and `break` scopes recorded past
|
|
1519
1648
|
# `jump_marks` carried through it.
|
|
1649
|
+
#
|
|
1650
|
+
# Issue #1839 — the code after the `begin` runs only when the clause finished from a `begin` that did, so the
|
|
1651
|
+
# scope it continues from is the clause's under `exit_scope`. A `return`, `next` or `break` in the clause can
|
|
1652
|
+
# also leave from one that raised, after any prefix of the body: those jumps are taken from the pass under the
|
|
1653
|
+
# widened entry ({#widen_clause_entry}), when a jump sink is installed to collect them ({#jump_sinks_installed?}).
|
|
1520
1654
|
def eval_begin_ensure(node, exit_scope, jump_marks)
|
|
1521
|
-
|
|
1522
|
-
|
|
1655
|
+
clause = node.ensure_clause
|
|
1656
|
+
carry_jumps_through_ensure(clause, jump_marks)
|
|
1657
|
+
jumps = jump_sinks_installed? && clause_jumps?(clause.statements)
|
|
1658
|
+
record_widened = widening_recorded?
|
|
1659
|
+
widened = clause.statements && (jumps || record_widened) &&
|
|
1660
|
+
widen_clause_entry(exit_scope, ensure_writes(node), clause.statements)
|
|
1661
|
+
return sub_eval(clause, exit_scope).last unless widened
|
|
1662
|
+
|
|
1663
|
+
record_operand_clause(clause.statements, widened) if record_widened
|
|
1664
|
+
|
|
1665
|
+
widened_options = record_widened ? {} : UNRECORDED
|
|
1666
|
+
if jumps
|
|
1667
|
+
sub_eval(clause, widened, **widened_options)
|
|
1668
|
+
else
|
|
1669
|
+
without_jump_sinks { sub_eval(clause, widened, next_scope_sink: nil, **widened_options) }
|
|
1670
|
+
end
|
|
1671
|
+
plain_options = record_widened ? UNRECORDED : {}
|
|
1672
|
+
return sub_eval(clause, exit_scope, **plain_options).last unless jumps
|
|
1673
|
+
|
|
1674
|
+
without_jump_sinks { sub_eval(clause, exit_scope, next_scope_sink: nil, **plain_options) }.last
|
|
1523
1675
|
end
|
|
1524
1676
|
|
|
1525
1677
|
# Issue #1231 — a rescue clause runs after the body raised from any point in it, and an `ensure` clause after the
|
|
@@ -1527,22 +1679,27 @@ module Rigor
|
|
|
1527
1679
|
# instance variable those wrote at a value the scope it enters with does not hold: `conn = nil; begin; conn =
|
|
1528
1680
|
# open_conn; Integer(s); rescue; conn.size; end` reads the connection. The engine does not track where the body
|
|
1529
1681
|
# 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
|
|
1682
|
+
# deliberately imprecisely. Each name that the clause's entry binds and those clauses write, outside a nested
|
|
1683
|
+
# `def` or class body, and that the clause mentions or they write more than once ({ClauseWidening}),
|
|
1684
|
+
# reads as `Dynamic[top]`, which reports nothing. A plain join would read `x` as `1?` in `x
|
|
1685
|
+
# = 1; begin; foo; x = nil; rescue; x.succ; end`, where it cannot be nil, and a `Dynamic` that keeps the join as
|
|
1686
|
+
# its facet still narrows: `x.nil?` on `Dynamic[1 | 2]` leaves a `nil` member for the next call to report. A name
|
|
1687
|
+
# written inside a nested `ensure` clause, which also runs on the way out of a raise, is covered by the same
|
|
1688
|
+
# floor.
|
|
1689
|
+
#
|
|
1690
|
+
# Issue #1839 — the widening is not for the clause's own reads only: a rescue clause's value, the scope it falls
|
|
1691
|
+
# through with and a `return`, `next` or `break` it takes all come from its widened entry, on every path (an
|
|
1692
|
+
# inferred return included), so `got = begin; conn = make; work; rescue; conn; end` leaves `got` untyped rather
|
|
1693
|
+
# than the entry's `nil`, and a name the clause never mentions but the body writes twice joins the code after the
|
|
1694
|
+
# `begin` untyped rather than at the entry's binding, which misses every write the body made before the last.
|
|
1536
1695
|
#
|
|
1537
1696
|
# The names the clauses of `node` read from: the primary body's for a rescue clause, none under a retry `edge`.
|
|
1538
1697
|
def rescue_writes(node, edge)
|
|
1539
|
-
clause_writes([node.statements]) if node.rescue_clause && edge.nil?
|
|
1698
|
+
clause_writes([node.statements]) if node.rescue_clause && edge.nil?
|
|
1540
1699
|
end
|
|
1541
1700
|
|
|
1542
1701
|
# For the `ensure` clause: the writes of the body, its `else` and every rescue clause.
|
|
1543
1702
|
def ensure_writes(node)
|
|
1544
|
-
return nil unless widening_recorded?
|
|
1545
|
-
|
|
1546
1703
|
sources = [node.statements, node.else_clause]
|
|
1547
1704
|
current = node.rescue_clause
|
|
1548
1705
|
while current
|
|
@@ -1552,83 +1709,68 @@ module Rigor
|
|
|
1552
1709
|
clause_writes(sources)
|
|
1553
1710
|
end
|
|
1554
1711
|
|
|
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
|
|
1712
|
+
def clause_writes(sources) = ClauseWidening.writes(sources)
|
|
1562
1713
|
|
|
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
|
|
1714
|
+
CLAUSE_JUMP_NODES = Set[Prism::ReturnNode, Prism::NextNode, Prism::BreakNode].freeze
|
|
1715
|
+
private_constant :CLAUSE_JUMP_NODES
|
|
1572
1716
|
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1717
|
+
# Whether a `return`, `next` or `break` reached here is collected: a pass {#without_jump_sinks} runs collects
|
|
1718
|
+
# none, so an `ensure` clause inside it, nested at any depth, is evaluated once rather than once per pass of
|
|
1719
|
+
# every enclosing clause holding a jump.
|
|
1720
|
+
def jump_sinks_installed?
|
|
1721
|
+
!@next_scope_sink.nil? || JUMP_SINK_KEYS.any? { |key| Thread.current[key] }
|
|
1722
|
+
end
|
|
1577
1723
|
|
|
1578
|
-
#
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
return
|
|
1724
|
+
# Whether `node` holds a `return`, `next` or `break` outside a nested `def`.
|
|
1725
|
+
def clause_jumps?(node)
|
|
1726
|
+
return false if node.nil? || node.is_a?(Prism::DefNode)
|
|
1727
|
+
return true if CLAUSE_JUMP_NODES.include?(node.class)
|
|
1582
1728
|
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
end
|
|
1586
|
-
node.rigor_each_child { |child| found = collect_clause_names(child, node_classes, among, found) }
|
|
1587
|
-
found
|
|
1729
|
+
node.rigor_each_child { |child| return true if clause_jumps?(child) }
|
|
1730
|
+
false
|
|
1588
1731
|
end
|
|
1589
1732
|
|
|
1590
|
-
# `clause_scope` with
|
|
1591
|
-
# `Dynamic[top]`, or nil when none changes.
|
|
1592
|
-
|
|
1593
|
-
|
|
1733
|
+
# `clause_scope` entering `clause` with the names of `writes` {ClauseWidening.widen} picks rebound to
|
|
1734
|
+
# `Dynamic[top]`, or nil when none changes. An empty rescue clause is widened too: the scope it falls through
|
|
1735
|
+
# with is its entry.
|
|
1736
|
+
def widen_clause_entry(clause_scope, writes, clause) = ClauseWidening.widen(clause_scope, writes, clause)
|
|
1594
1737
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1738
|
+
# Issue #1839 — an operand evaluator ({#thread_operand}) records only the later operands its walk takes, so the
|
|
1739
|
+
# index's walk hands a clause it evaluated the scope of the `begin`'s nearest recorded ancestor, which binds
|
|
1740
|
+
# each name the body writes as the `begin` found it: `puts(begin; conn = make; work; rescue; conn.size; end)`
|
|
1741
|
+
# read `conn` as `nil`. On the check path it records the clause's `statements` under the `widened` entry, with
|
|
1742
|
+
# `$!`, `$@` and `$?` unbound as the walk leaves them for a clause the evaluator did not enter; the
|
|
1743
|
+
# walk fills the clause's nodes from there. The clause's later operands are recorded by its own pass. An
|
|
1744
|
+
# evaluator that records its nodes itself skips this: its pass over the clause records `statements` right
|
|
1745
|
+
# after, and the index keeps the last visit, so the `@on_enter` test is a guard no example can fail.
|
|
1746
|
+
def record_operand_clause(statements, widened)
|
|
1747
|
+
return unless statements && widened && @on_enter.nil? && @operand_recorder && widening_recorded?
|
|
1597
1748
|
|
|
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
|
|
1749
|
+
@operand_recorder.call(statements, widened.forget_error_info.forget_last_status)
|
|
1605
1750
|
end
|
|
1606
1751
|
|
|
1607
|
-
# Whether
|
|
1608
|
-
# `widen_clauses`)
|
|
1609
|
-
# evaluated once.
|
|
1752
|
+
# Whether this pass records the per-node index under a clause's widened entry: only the check path asks for that
|
|
1753
|
+
# ({#initialize}'s `widen_clauses`). Every other recorder keeps the clause's plain entry ({#recording?}).
|
|
1610
1754
|
def widening_recorded?
|
|
1611
|
-
@widen_clauses &&
|
|
1755
|
+
@widen_clauses && recording?
|
|
1612
1756
|
end
|
|
1613
1757
|
|
|
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)
|
|
1758
|
+
# Whether this pass records the per-node index at all.
|
|
1759
|
+
def recording?
|
|
1760
|
+
!(@on_enter.nil? && @operand_recorder.nil?)
|
|
1625
1761
|
end
|
|
1626
1762
|
|
|
1627
|
-
#
|
|
1628
|
-
|
|
1629
|
-
|
|
1763
|
+
# `[type, scope]` of a rescue clause's `statements` entered with `widened` ({#widen_clause_entry}), the value,
|
|
1764
|
+
# the scope and the jumps the code after the `begin` sees. A recorder that keeps the clause's plain entry
|
|
1765
|
+
# ({#widening_recorded?}) records a second pass under `plain` that reaches no jump sink, so nothing records twice.
|
|
1766
|
+
# The widened pass then records nothing, and a clause nested inside it is evaluated once, so `d` nested clauses
|
|
1767
|
+
# cost `d * (d + 1) / 2` evaluations; every other pass evaluates the clause once.
|
|
1768
|
+
def eval_widened_clause(statements, plain, widened, on_enter:)
|
|
1769
|
+
return [Type::Combinator.constant_of(nil), widened] if statements.nil?
|
|
1770
|
+
return sub_eval(statements, widened, on_enter: on_enter) if widening_recorded? || !recording?
|
|
1630
1771
|
|
|
1631
|
-
without_jump_sinks { sub_eval(
|
|
1772
|
+
without_jump_sinks { sub_eval(statements, plain, next_scope_sink: nil, on_enter: on_enter) }
|
|
1773
|
+
sub_eval(statements, widened, **UNRECORDED)
|
|
1632
1774
|
end
|
|
1633
1775
|
|
|
1634
1776
|
# Runs `block` with no `return`, `next`, `break` or retry-frame sink installed.
|
|
@@ -1739,6 +1881,10 @@ module Rigor
|
|
|
1739
1881
|
CLAUSE_SINK_KEYS = [RETURN_SINK_KEY, NEXT_SINK_KEY, BREAK_SINK_KEY, BREAK_VALUE_SINK_KEY, RETRY_FRAMES_KEY].freeze
|
|
1740
1882
|
private_constant :CLAUSE_SINK_KEYS
|
|
1741
1883
|
|
|
1884
|
+
# The thread-local sinks a `return`, `next` or `break` is collected into.
|
|
1885
|
+
JUMP_SINK_KEYS = (CLAUSE_SINK_KEYS - [RETRY_FRAMES_KEY]).freeze
|
|
1886
|
+
private_constant :JUMP_SINK_KEYS
|
|
1887
|
+
|
|
1742
1888
|
# The retry edge of `node`, or nil when no `retry` in its rescue chain targets it. Allocation-free for a `begin`
|
|
1743
1889
|
# no `retry` targets.
|
|
1744
1890
|
def retry_edge_for(node)
|
|
@@ -2016,7 +2162,7 @@ module Rigor
|
|
|
2016
2162
|
# runs on the way out (`begin; retry; ensure; tries += 1; end`), or one made before a `retry` the evaluator only
|
|
2017
2163
|
# types (`log(tries < 5 ? retry : :gave_up)`).
|
|
2018
2164
|
#
|
|
2019
|
-
# Issue #1231 — `writes` widens the scope each clause enters with
|
|
2165
|
+
# Issue #1231 — `writes` widens the scope each clause enters with ({#widen_clause_entry}) before its
|
|
2020
2166
|
# reference is bound, so `rescue => e` still binds `e` to the exception. It is nil under a retry edge, which
|
|
2021
2167
|
# already carries every scope the primary body raises from ({#widen_entry_for_retry}).
|
|
2022
2168
|
def collect_rescue_chain_results(rescue_node, entry_scope, edge: nil, writes: nil)
|
|
@@ -2024,11 +2170,12 @@ module Rigor
|
|
|
2024
2170
|
results = []
|
|
2025
2171
|
current = rescue_node
|
|
2026
2172
|
while current
|
|
2027
|
-
widened = widen_clause_entry(entry_scope, current.statements
|
|
2028
|
-
|
|
2173
|
+
widened = widen_clause_entry(entry_scope, writes, current.statements)
|
|
2174
|
+
record_operand_clause(current.statements, widened)
|
|
2029
2175
|
rescue_scope = bind_rescue_reference(current, entry_scope)
|
|
2030
2176
|
arm = if widened
|
|
2031
|
-
|
|
2177
|
+
eval_widened_clause(current.statements, rescue_scope, bind_rescue_reference(current, widened),
|
|
2178
|
+
on_enter: on_enter)
|
|
2032
2179
|
else
|
|
2033
2180
|
eval_branch_or_nil(current.statements, rescue_scope, on_enter: on_enter)
|
|
2034
2181
|
end
|
|
@@ -2069,24 +2216,29 @@ module Rigor
|
|
|
2069
2216
|
after.global(:$_) == Type::Combinator.untyped && !LastLine.may_set?(node.statements, scope)
|
|
2070
2217
|
end
|
|
2071
2218
|
|
|
2072
|
-
# Issue #1889 — {#
|
|
2073
|
-
#
|
|
2074
|
-
|
|
2075
|
-
|
|
2219
|
+
# Issue #1889 — {#eval_for} with a literal-name `instance_variable_set` in the body joining its value into the
|
|
2220
|
+
# ivar's binding rather than replacing it ({ReflectiveIvarWrite.in_repeating_body}): a `for` body has no rebind
|
|
2221
|
+
# fixpoint. A `while` / `until` body needs no such join since issue #1930: its fixpoint widens the ivar
|
|
2222
|
+
# ({#loop_body_ivar_writes}).
|
|
2076
2223
|
def eval_repeating_for(node) = ReflectiveIvarWrite.in_repeating_body { eval_for(node) }
|
|
2077
2224
|
|
|
2078
2225
|
# `while pred; body; end` / `until pred; body; end`. The body might run zero or more times, so half-bound names
|
|
2079
2226
|
# degrade to `T | nil` in the post-loop scope. The loop expression itself types as `Constant[nil]` (Slice 3 phase
|
|
2080
2227
|
# 1), reflecting the common case where no `break VALUE` is observed.
|
|
2081
2228
|
def eval_loop(node)
|
|
2082
|
-
|
|
2229
|
+
# Issue #1930 — the predicate runs before every iteration but is walked once, so a literal-name
|
|
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) }
|
|
2083
2234
|
post_pred = widen_predicate_pins(node, post_pred)
|
|
2084
2235
|
return [Type::Combinator.constant_of(nil), narrow_loop_exit_edge(node, post_pred)] if node.statements.nil?
|
|
2085
2236
|
|
|
2086
2237
|
# The historical single body pass joined with the pre-loop scope. This continues to carry everything the
|
|
2087
2238
|
# 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
|
|
2239
|
+
# Tuple), body-introduced locals' nil-injection, a rebind of an instance variable the fixpoint does not track
|
|
2240
|
+
# ({#loop_body_ivar_writes}), and the loop value itself. The fixpoint then OVERLAYS only the rebound bindings it
|
|
2241
|
+
# corrects.
|
|
2090
2242
|
#
|
|
2091
2243
|
# Like every fixpoint pass, it enters the body on the predicate's loop-entry edge ({#loop_pass_entry}): the
|
|
2092
2244
|
# body of `while (line = gets)` reads `line` as `String`, and the body of `while gets` reads `$_` as one
|
|
@@ -2099,8 +2251,7 @@ module Rigor
|
|
|
2099
2251
|
body_scope, first_breaks = single_pass(node, post_pred, jumps)
|
|
2100
2252
|
base_scope = join_with_nil_injection(post_pred, body_scope)
|
|
2101
2253
|
|
|
2102
|
-
|
|
2103
|
-
names = rebound + body_first
|
|
2254
|
+
names, body_first = loop_rebound_names(node.statements, post_pred)
|
|
2104
2255
|
|
|
2105
2256
|
# Fast path: a loop whose body rebinds no local skips the rebind fixpoint, but still needs the slice-C content
|
|
2106
2257
|
# writeback (a loop may content-mutate a collection without rebinding any local — `acc << x`), so apply it to
|
|
@@ -2193,7 +2344,7 @@ module Rigor
|
|
|
2193
2344
|
# assumptions, so the last-visit-wins scope index would annotate loop-body lines with stale pre-convergence
|
|
2194
2345
|
# constants. One extra pass from the converged bindings (result discarded) re-records the body's entry scopes.
|
|
2195
2346
|
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
|
|
2347
|
+
post_loop = result.reduce(base_scope) { |acc, (name, type)| bind_capture(acc, name, type) }
|
|
2197
2348
|
# ADR-56 slice C — loop-body receiver-content element-type join. A loop that content-mutates a collection (`acc
|
|
2198
2349
|
# << n`) keeps only the seed's element types after the single-pass widen; join the appended/stored types into
|
|
2199
2350
|
# the continuation collection. Pre-state comes from `post_pred` for a name the loop only content-mutates and
|
|
@@ -2261,6 +2412,13 @@ module Rigor
|
|
|
2261
2412
|
NO_LOOP_NAMES = [].freeze
|
|
2262
2413
|
private_constant :NO_LOOP_BINDINGS, :NO_LOOP_NAMES
|
|
2263
2414
|
|
|
2415
|
+
# The instance-variable write forms {#loop_body_ivar_writes} counts.
|
|
2416
|
+
IVAR_WRITE_NODES = Set[
|
|
2417
|
+
Prism::InstanceVariableWriteNode, Prism::InstanceVariableOperatorWriteNode, Prism::InstanceVariableOrWriteNode,
|
|
2418
|
+
Prism::InstanceVariableAndWriteNode, Prism::InstanceVariableTargetNode
|
|
2419
|
+
].freeze
|
|
2420
|
+
private_constant :IVAR_WRITE_NODES
|
|
2421
|
+
|
|
2264
2422
|
# A body with no targeting jump pays two allocation-free scans.
|
|
2265
2423
|
def loop_jumps(statements)
|
|
2266
2424
|
kinds = JumpTargets.kinds(statements, LOOP_JUMP_CLASSES)
|
|
@@ -2356,11 +2514,11 @@ module Rigor
|
|
|
2356
2514
|
floor = Type::Combinator.untyped
|
|
2357
2515
|
breaks.reduce(continuation) do |cont, break_scope|
|
|
2358
2516
|
names.reduce(cont) do |acc, name|
|
|
2359
|
-
break_value =
|
|
2360
|
-
current =
|
|
2517
|
+
break_value = CapturedLocals.bound_type(break_scope, name)
|
|
2518
|
+
current = CapturedLocals.bound_type(acc, name)
|
|
2361
2519
|
next acc if break_value.nil? || current == floor
|
|
2362
2520
|
|
|
2363
|
-
acc
|
|
2521
|
+
bind_capture(acc, name, current ? Type::Combinator.union(current, break_value) : break_value)
|
|
2364
2522
|
end
|
|
2365
2523
|
end
|
|
2366
2524
|
end
|
|
@@ -2433,7 +2591,7 @@ module Rigor
|
|
|
2433
2591
|
# treatment. `break_pass` rides along so every pass leaves its `break` arms for {#loop_break_arms}.
|
|
2434
2592
|
def loop_rebind_fixpoint(node, post_pred, names, body_first, jumps, break_pass)
|
|
2435
2593
|
nil_const = Type::Combinator.constant_of(nil)
|
|
2436
|
-
seed = names.to_h { |name| [name,
|
|
2594
|
+
seed = names.to_h { |name| [name, CapturedLocals.bound_type(post_pred, name) || nil_const] }
|
|
2437
2595
|
evaluate_body = lambda do |bindings|
|
|
2438
2596
|
loop_body_exit_bindings(node, post_pred, bindings, names, body_first, jumps, break_pass)
|
|
2439
2597
|
end
|
|
@@ -2465,6 +2623,34 @@ module Rigor
|
|
|
2465
2623
|
[pre_existing.uniq, body_first.uniq - pre_existing.uniq]
|
|
2466
2624
|
end
|
|
2467
2625
|
|
|
2626
|
+
# Every name {#eval_loop}'s rebind fixpoint tracks, and the body-first locals among them: the locals the body
|
|
2627
|
+
# rebinds ({#loop_body_local_writes}) and the instance variables ({#loop_body_ivar_writes}).
|
|
2628
|
+
def loop_rebound_names(statements, base_scope)
|
|
2629
|
+
rebound, body_first = loop_body_local_writes(statements, base_scope)
|
|
2630
|
+
[rebound + loop_body_ivar_writes(statements, base_scope) + body_first, body_first]
|
|
2631
|
+
end
|
|
2632
|
+
|
|
2633
|
+
# Issue #1930 — the instance variables the loop body can rebind, which the rebind fixpoint widens as it widens a
|
|
2634
|
+
# local: a write node (`@n = i + 1`, any depth, a nested block's included) or a literal-name
|
|
2635
|
+
# `instance_variable_set` on `self` ({ReflectiveIvarWrite.literal_name}). Only an ivar `base_scope` binds off its
|
|
2636
|
+
# ADR-58 declaration seed counts ({CapturedLocals.rebindable_ivar?}), as for a block's write-back: one the method
|
|
2637
|
+
# has not bound reads `Dynamic[top]` before the loop, and the join drops it after; one on its declaration seed
|
|
2638
|
+
# already holds the union of every write in the class. A call that writes the ivar inside the callee
|
|
2639
|
+
# (`self.bump`) is not a write here; the intervening-call reset handles it.
|
|
2640
|
+
def loop_body_ivar_writes(statements, base_scope)
|
|
2641
|
+
return NO_LOOP_NAMES if base_scope.ivars.empty?
|
|
2642
|
+
|
|
2643
|
+
names = nil
|
|
2644
|
+
Source::NodeWalker.each(statements) do |descendant|
|
|
2645
|
+
name = ReflectiveIvarWrite.literal_name(descendant) ||
|
|
2646
|
+
(descendant.name if IVAR_WRITE_NODES.include?(descendant.class))
|
|
2647
|
+
next if name.nil? || names&.include?(name) || !CapturedLocals.rebindable_ivar?(base_scope, name)
|
|
2648
|
+
|
|
2649
|
+
(names ||= []) << name
|
|
2650
|
+
end
|
|
2651
|
+
names || NO_LOOP_NAMES
|
|
2652
|
+
end
|
|
2653
|
+
|
|
2468
2654
|
# Evaluates the loop body once with each fixpoint-tracked local bound to the supplied running assumption and
|
|
2469
2655
|
# returns the per-name exit binding. Used as the {BodyFixpoint} body-evaluator for `eval_loop`.
|
|
2470
2656
|
#
|
|
@@ -2493,14 +2679,14 @@ module Rigor
|
|
|
2493
2679
|
break_pass[:entry] = bindings.except(*body_first)
|
|
2494
2680
|
break_pass[:arms] = breaks
|
|
2495
2681
|
end
|
|
2496
|
-
names.to_h { |name| [name,
|
|
2682
|
+
names.to_h { |name| [name, CapturedLocals.bound_type(exit_scope, name)] }
|
|
2497
2683
|
end
|
|
2498
2684
|
|
|
2499
2685
|
# The scope one fixpoint pass enters the body with: `post_pred` overlaid with the pre-existing names' running
|
|
2500
2686
|
# assumption, then narrowed by the predicate's loop-entry edge ({#loop_body_exit_bindings} carries the why).
|
|
2501
2687
|
def loop_pass_entry(node, post_pred, bindings, body_first, jumps)
|
|
2502
2688
|
overlaid = bindings.except(*body_first)
|
|
2503
|
-
entry = overlaid.reduce(post_pred) { |acc, (name, type)| acc
|
|
2689
|
+
entry = overlaid.reduce(post_pred) { |acc, (name, type)| bind_capture(acc, name, type) }
|
|
2504
2690
|
entry = loop_content_entry(node.statements, post_pred, entry)
|
|
2505
2691
|
truthy_scope, falsey_scope = Narrowing.predicate_scopes(node.predicate, entry)
|
|
2506
2692
|
edge = node.is_a?(Prism::UntilNode) ? falsey_scope : truthy_scope
|
|
@@ -2866,15 +3052,47 @@ module Rigor
|
|
|
2866
3052
|
# u.strip`, where the raise almost always comes from `Float` after it. Neither the arm nor anything in it
|
|
2867
3053
|
# is recorded or typed from there, which keeps it where it was before #1256: an ADR-5 trade of the rare
|
|
2868
3054
|
# raise-before-write path for no false positive on the common one.
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
3055
|
+
#
|
|
3056
|
+
# Issue #1839 — the arm enters with each name `expr` writes that it mentions, or that `expr` writes more
|
|
3057
|
+
# than once, read as `Dynamic[top]` ({#widen_clause_entry}), as a `begin`'s rescue clause does: the raise
|
|
3058
|
+
# may come after any of those writes. The scope past the modifier joins that entry through the arm.
|
|
3059
|
+
fallback = node.rescue_expression
|
|
3060
|
+
writes = clause_writes([node.expression])
|
|
3061
|
+
record_modifier_fallback(fallback, writes, walk)
|
|
3062
|
+
joined = join_with_nil_injection(scope, after_expression)
|
|
3063
|
+
widened = widen_clause_entry(joined, writes, fallback)
|
|
3064
|
+
arm_entry = ErrorInfo.modifier_entry(widened || joined, fallback)
|
|
3065
|
+
after_rescue = thread_operand(fallback, arm_entry, OperandWalk.new(nil), arm_entry)
|
|
2872
3066
|
after = if branch_unconditionally_exits?(node.rescue_expression)
|
|
2873
3067
|
after_expression
|
|
2874
3068
|
else
|
|
2875
3069
|
join_with_nil_injection(after_expression, after_rescue)
|
|
2876
3070
|
end
|
|
2877
|
-
[
|
|
3071
|
+
[rescue_modifier_type(node, walk, widened && arm_entry), after]
|
|
3072
|
+
end
|
|
3073
|
+
|
|
3074
|
+
# Issue #1839 — the walk records nothing in the fallback, so the index's walk hands it the modifier's own entry,
|
|
3075
|
+
# which binds each name `expr` writes as the modifier found it: `conn = nil; (conn = make; work) rescue
|
|
3076
|
+
# conn.size` read `conn` as `nil`. The fallback is recorded under that entry with the names `writes`
|
|
3077
|
+
# {ClauseWidening.widen} picks read as `Dynamic[top]`, and `$!`, `$@` and `$?` unbound as the walk leaves them,
|
|
3078
|
+
# when the widening changes it and the check path asks for it ({#widening_recorded?}). A name `expr` first
|
|
3079
|
+
# binds keeps the entry's reading, for the trade above.
|
|
3080
|
+
def record_modifier_fallback(fallback, writes, walk)
|
|
3081
|
+
return unless walk.recorder && @widen_clauses
|
|
3082
|
+
|
|
3083
|
+
widened = widen_clause_entry(scope, writes, fallback)
|
|
3084
|
+
walk.recorder.call(fallback, widened.forget_error_info.forget_last_status) if widened
|
|
3085
|
+
end
|
|
3086
|
+
|
|
3087
|
+
# Issue #1839 — the value of `expr rescue alt`: the fallback is typed from `widened_entry`, its entry with each
|
|
3088
|
+
# name `expr` writes read as `Dynamic[top]` ({#widen_clause_entry}), as a `begin`'s rescue clause reads it, not
|
|
3089
|
+
# at the entry's binding: `got = (conn = make; work) rescue conn` leaves `got` untyped, not `nil`. Nil when the
|
|
3090
|
+
# widening changes nothing, and the modifier is typed whole.
|
|
3091
|
+
def rescue_modifier_type(node, walk, widened_entry)
|
|
3092
|
+
return OperandWalk.type_of(scope, node, tracer, walk.types(tracer)) unless widened_entry
|
|
3093
|
+
|
|
3094
|
+
Type::Combinator.union(OperandWalk.type_of(scope, node.expression, tracer, walk.types(tracer)),
|
|
3095
|
+
widened_entry.type_of(node.rescue_expression, tracer: tracer))
|
|
2878
3096
|
end
|
|
2879
3097
|
|
|
2880
3098
|
# `class Foo; body; end` and `module Foo; body; end`. The class body runs in a fresh scope (Ruby's class scope
|
|
@@ -3130,7 +3348,7 @@ module Rigor
|
|
|
3130
3348
|
# map …) that rebinds an outer local must not leave that local's pre-call binding unmodified in the continuation
|
|
3131
3349
|
# scope; the spec MUST in § "Fact stability and mutation" names captured locals a first-class invalidation
|
|
3132
3350
|
# category. (The escaping / unknown path already widened to Dynamic[top] via `record_closure_escape_if_any`.)
|
|
3133
|
-
post_scope =
|
|
3351
|
+
post_scope = block_capture_continuation(node, post_scope)
|
|
3134
3352
|
statement_call = !call_type.nil? && !@in_operand
|
|
3135
3353
|
post_scope = apply_post_return_narrowing(node, post_scope) if statement_call
|
|
3136
3354
|
# Flow-folding G1 / G2 — widen a local- or instance-variable binding when the call is an in-place mutator on it
|
|
@@ -3337,30 +3555,21 @@ module Rigor
|
|
|
3337
3555
|
# is one that could plausibly mutate ivars on the enclosing class (implicit-self or explicit `self.foo`).
|
|
3338
3556
|
# External- receiver calls (`obj.method`) cannot reach the caller's ivars; they pass through unchanged.
|
|
3339
3557
|
# Issue #1596 — where the call is proven to write only some ivars ({#self_call_ivar_writes}), only those widen.
|
|
3558
|
+
#
|
|
3559
|
+
# Issue #1402 — a proc's call (`blk.call`) is a candidate too, and so is a call on another receiver whose literal
|
|
3560
|
+
# block makes one ({InterveningIvarReset.after_block}: `[1].each { reset }`). Both widen as a condition's
|
|
3561
|
+
# calls do, marking a binding whose narrowing had ruled `nil` out, since the code they run is far more often
|
|
3562
|
+
# code that leaves the variable alone. A `yield` or proc call in the call's receiver or arguments
|
|
3563
|
+
# (`@items << yield`, `puts(yield)`) widens every narrowed ivar ({InterveningIvarReset.after_operands}).
|
|
3340
3564
|
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
|
|
3565
|
+
widened =
|
|
3566
|
+
if intervening_call_candidate?(call_node)
|
|
3567
|
+
mark = !InterveningIvarReset.self_call?(call_node)
|
|
3568
|
+
InterveningIvarReset.widen(current_scope, mark: mark) { self_call_ivar_writes(call_node) }
|
|
3569
|
+
else
|
|
3570
|
+
InterveningIvarReset.after_block(call_node, current_scope)
|
|
3571
|
+
end
|
|
3572
|
+
InterveningIvarReset.after_operands(call_node, widened)
|
|
3364
3573
|
end
|
|
3365
3574
|
|
|
3366
3575
|
# Issue #1596 — the ivars the implicit-self / `self.` call `call_node` may write, or nil when that is not proven
|
|
@@ -3373,10 +3582,7 @@ module Rigor
|
|
|
3373
3582
|
end
|
|
3374
3583
|
|
|
3375
3584
|
def intervening_call_candidate?(call_node)
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
receiver = call_node.receiver
|
|
3379
|
-
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
3585
|
+
InterveningIvarReset.candidate?(call_node)
|
|
3380
3586
|
end
|
|
3381
3587
|
|
|
3382
3588
|
def enclosing_class_name_for(self_type)
|
|
@@ -3876,7 +4082,7 @@ module Rigor
|
|
|
3876
4082
|
classification = classify_closure_escape(node)
|
|
3877
4083
|
return post_scope if classification == :non_escaping
|
|
3878
4084
|
|
|
3879
|
-
post_scope = escaping_closure_captures(node.block, post_scope)
|
|
4085
|
+
post_scope = escaping_closure_captures(node.block, post_scope, foreign_self: foreign_self_block?(node))
|
|
3880
4086
|
post_scope.with_fact(
|
|
3881
4087
|
Analysis::FactStore::Fact.new(
|
|
3882
4088
|
bucket: :dynamic_origin,
|
|
@@ -4180,13 +4386,72 @@ module Rigor
|
|
|
4180
4386
|
# callback that runs later writes the very ivar the continuation reads (`@clicked = false; button.on_click {
|
|
4181
4387
|
# @clicked = true }` left `if @clicked` folding always-falsey). One still on its ADR-58 declaration seed is
|
|
4182
4388
|
# 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)
|
|
4389
|
+
def drop_captured_narrowing(block_node, base_scope, foreign_self: false)
|
|
4390
|
+
names = CapturedLocals.writes(block_node, base_scope, ivars: true, foreign_self: foreign_self)
|
|
4185
4391
|
return base_scope if names.empty?
|
|
4186
4392
|
|
|
4187
4393
|
names.reduce(base_scope) { |acc, name| bind_capture(acc, name, Type::Combinator.untyped) }
|
|
4188
4394
|
end
|
|
4189
4395
|
|
|
4396
|
+
# The continuation {#write_back_block_captures} binds, after which the block's later-run `return`s are collected
|
|
4397
|
+
# ({#collect_repeated_block_returns}).
|
|
4398
|
+
def block_capture_continuation(call_node, post_scope)
|
|
4399
|
+
post_scope = write_back_block_captures(call_node, post_scope)
|
|
4400
|
+
collect_repeated_block_returns(call_node, post_scope)
|
|
4401
|
+
post_scope
|
|
4402
|
+
end
|
|
4403
|
+
|
|
4404
|
+
# Issue #2123 — the `return`s a repeating block reaches only on a later run. Every statement pass over a block
|
|
4405
|
+
# enters with a captured local or ivar the body rebinds at a binding some run can hold, but not at every such
|
|
4406
|
+
# binding: the statement pass enters at the call-site binding, and the ADR-56 write-back's capped fixpoint widens
|
|
4407
|
+
# its last assumption without running a pass from it. So
|
|
4408
|
+
# `seen = 0; items.each { return false if (seen += 1) > 3 }; true` read `seen` as at most `3` on every pass, the
|
|
4409
|
+
# `return` was never reached, the method inferred `true`, and a caller's `if` on it folded always-falsey on
|
|
4410
|
+
# correct code.
|
|
4411
|
+
#
|
|
4412
|
+
# While a method's return sink is active, a block that may repeat ({#repeating_block_class}), rebinds a capture,
|
|
4413
|
+
# and holds a `return` that exits the method ({ReturnBarrier.exits_method?}) gets one more unrecorded pass, with
|
|
4414
|
+
# each rebound name bound to its pre-call binding joined with its continuation `post_scope` holds. The
|
|
4415
|
+
# continuation covers every run count, so it covers the binding any run enters with. The pass records no
|
|
4416
|
+
# scope, and the method's fall-through and earlier `return`s are kept, so the inferred return type only widens.
|
|
4417
|
+
# The fresh `next`, `break`-value and `break`-scope sinks are defensive: every consumer of those sinks filters by
|
|
4418
|
+
# jump-node identity, so no example tells them apart from the outer sinks today. So is the `rescue`: a raise here
|
|
4419
|
+
# keeps the arms the earlier passes collected rather than taking out the method.
|
|
4420
|
+
def collect_repeated_block_returns(call_node, post_scope)
|
|
4421
|
+
return if Thread.current[RETURN_SINK_KEY].nil?
|
|
4422
|
+
|
|
4423
|
+
block = call_node.block
|
|
4424
|
+
return unless repeated_block_return_candidate?(call_node, block)
|
|
4425
|
+
|
|
4426
|
+
names = CapturedLocals.writes(block, scope, ivars: true, foreign_self: foreign_self_block?(call_node))
|
|
4427
|
+
return if names.empty?
|
|
4428
|
+
|
|
4429
|
+
bindings = names.to_h do |name|
|
|
4430
|
+
[name, Type::Combinator.union(*[scope, post_scope].filter_map { |s| CapturedLocals.bound_type(s, name) })]
|
|
4431
|
+
end
|
|
4432
|
+
entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, bindings))
|
|
4433
|
+
StatementEvaluator.with_next_sink do
|
|
4434
|
+
StatementEvaluator.with_break_value_sink do
|
|
4435
|
+
collect_break_scopes { sub_eval(block, entry, **UNRECORDED, next_scope_sink: nil) }
|
|
4436
|
+
end
|
|
4437
|
+
end
|
|
4438
|
+
rescue StandardError
|
|
4439
|
+
nil
|
|
4440
|
+
end
|
|
4441
|
+
|
|
4442
|
+
# 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, or `Kernel#loop`,
|
|
4444
|
+
# which that rule leaves out and which runs its block until it breaks or raises. The barrier, refinement and
|
|
4445
|
+
# anonymous-class checks are defensive guards: no such call repeats under that rule today.
|
|
4446
|
+
def repeated_block_return_candidate?(call_node, block)
|
|
4447
|
+
return false unless block.is_a?(Prism::BlockNode)
|
|
4448
|
+
return false unless ReturnBarrier.exits_method?(block.body)
|
|
4449
|
+
return false if return_barrier_block?(call_node) || refined_class_context(call_node)
|
|
4450
|
+
return false if AnonymousMetaClass.name_for(call_node, scope.source_path)
|
|
4451
|
+
|
|
4452
|
+
BlockCallTiming.kernel_loop?(call_node) || !repeating_block_class(call_node).nil?
|
|
4453
|
+
end
|
|
4454
|
+
|
|
4190
4455
|
# ADR-56 slice A. For a `:non_escaping` block, fold the continuation binding of every outer local the body can
|
|
4191
4456
|
# rebind back into `post_scope`. The binding is a capped fixpoint (cap 3) over the block body re-evaluated under
|
|
4192
4457
|
# the running per-name assumption, joined with the pre-call binding (kept as a constituent so the 0-iteration path
|
|
@@ -4211,13 +4476,27 @@ module Rigor
|
|
|
4211
4476
|
return post_scope unless block.is_a?(Prism::BlockNode)
|
|
4212
4477
|
return post_scope unless classify_closure_escape(call_node) == :non_escaping
|
|
4213
4478
|
|
|
4214
|
-
names = CapturedLocals.writes(block, scope, ivars: true)
|
|
4479
|
+
names = CapturedLocals.writes(block, scope, ivars: true, foreign_self: foreign_self_block?(call_node))
|
|
4215
4480
|
return post_scope if names.empty?
|
|
4216
4481
|
|
|
4217
4482
|
break_pass = block_break_pass(block)
|
|
4218
|
-
|
|
4483
|
+
marks = {}
|
|
4484
|
+
result = with_capture_exit_marks(marks) { converge_captures_by_kind(call_node, block, names, break_pass) }
|
|
4219
4485
|
result = join_block_break_bindings(call_node, block, result, break_pass)
|
|
4220
|
-
result.reduce(post_scope) { |acc, (name, type)| bind_capture(acc, name, type) }
|
|
4486
|
+
result.reduce(post_scope) { |acc, (name, type)| bind_capture(acc, name, type, optimistic: marks[name]) }
|
|
4487
|
+
end
|
|
4488
|
+
|
|
4489
|
+
# Issue #1402 — runs the write-back's passes with `marks` collecting the ADR-101 optimistic mark a pass's exit
|
|
4490
|
+
# holds for each rebound name ({#block_exit_bindings}), as the per-element fold collects them
|
|
4491
|
+
# (`ExpressionTyper#captured_exit_bindings`). A later pass enters on that mark, and the continuation binds it:
|
|
4492
|
+
# `x = false; pairs.each { x = pairs.first&.empty? }` stored the miss's `nil` at run time, and without the mark
|
|
4493
|
+
# `x.nil?` folded to `false` where the inline `pairs.first&.empty?.nil?` answers `bool`.
|
|
4494
|
+
def with_capture_exit_marks(marks)
|
|
4495
|
+
outer = @capture_exit_marks
|
|
4496
|
+
@capture_exit_marks = marks
|
|
4497
|
+
yield
|
|
4498
|
+
ensure
|
|
4499
|
+
@capture_exit_marks = outer
|
|
4221
4500
|
end
|
|
4222
4501
|
|
|
4223
4502
|
# The {BodyFixpoint} continuation of `names`, seeded from their pre-call bindings.
|
|
@@ -4276,7 +4555,12 @@ module Rigor
|
|
|
4276
4555
|
# Binds a name from `CapturedLocals.writes`. An ivar goes through {CapturedLocals.bind}, which keeps its issue
|
|
4277
4556
|
# #286 optimistic mark. A local keeps the plain `with_local` these seams have always used, so a block that
|
|
4278
4557
|
# rebinds no ivar leaves the locals exactly as before.
|
|
4279
|
-
|
|
4558
|
+
#
|
|
4559
|
+
# Issue #1402 — `optimistic` is a mark a write-back pass's exit held ({#with_capture_exit_marks}); a name that has
|
|
4560
|
+
# one binds through {CapturedLocals.bind} whatever its kind, which adds it.
|
|
4561
|
+
def bind_capture(scope, name, type, optimistic: nil)
|
|
4562
|
+
return CapturedLocals.bind(scope, name, type, optimistic: optimistic) if optimistic
|
|
4563
|
+
|
|
4280
4564
|
CapturedLocals.ivar_name?(name) ? CapturedLocals.bind(scope, name, type) : scope.with_local(name, type)
|
|
4281
4565
|
end
|
|
4282
4566
|
|
|
@@ -5140,9 +5424,22 @@ module Rigor
|
|
|
5140
5424
|
def block_exit_bindings(call_node, block, bindings, names)
|
|
5141
5425
|
entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, bindings))
|
|
5142
5426
|
_type, exit_scope = sub_eval(block, entry)
|
|
5427
|
+
record_capture_exit_marks(exit_scope, names)
|
|
5143
5428
|
names.to_h { |name| [name, CapturedLocals.bound_type(exit_scope, name)] }
|
|
5144
5429
|
end
|
|
5145
5430
|
|
|
5431
|
+
# Issue #1402 — records into {#with_capture_exit_marks}' table the mark `exit_scope` holds for each of `names`;
|
|
5432
|
+
# the first mark a name takes stays.
|
|
5433
|
+
def record_capture_exit_marks(exit_scope, names)
|
|
5434
|
+
marks = @capture_exit_marks
|
|
5435
|
+
return if marks.nil?
|
|
5436
|
+
|
|
5437
|
+
names.each do |name|
|
|
5438
|
+
mark = CapturedLocals.optimistic_mark(exit_scope, name)
|
|
5439
|
+
marks[name] ||= mark if mark
|
|
5440
|
+
end
|
|
5441
|
+
end
|
|
5442
|
+
|
|
5146
5443
|
# The entry scope of one write-back pass: the block's entry with each written outer local or ivar bound to
|
|
5147
5444
|
# `bindings`, and each captured local the body only mutates in place at its widening ({#capture_pass_bindings}).
|
|
5148
5445
|
# That widening describes the same object, so it keeps the marks a source-level write drops (issue #1287,
|
|
@@ -5151,7 +5448,9 @@ module Rigor
|
|
|
5151
5448
|
def block_pass_entry(call_node, block, bindings)
|
|
5152
5449
|
entry = build_block_entry_scope(call_node, block)
|
|
5153
5450
|
capture_pass_bindings(block, bindings).reduce(entry) do |acc, (name, type)|
|
|
5154
|
-
|
|
5451
|
+
next acc.with_mutated_local(name, type) unless bindings.key?(name)
|
|
5452
|
+
|
|
5453
|
+
bind_capture(acc, name, type, optimistic: @capture_exit_marks&.[](name))
|
|
5155
5454
|
end
|
|
5156
5455
|
end
|
|
5157
5456
|
|
|
@@ -5255,7 +5554,7 @@ module Rigor
|
|
|
5255
5554
|
def unproven_rebind_bindings(call_node, block, classification)
|
|
5256
5555
|
return NO_CAPTURE_BINDINGS if call_node.receiver && classification == :non_escaping
|
|
5257
5556
|
|
|
5258
|
-
names = CapturedLocals.writes(block, scope, ivars: true)
|
|
5557
|
+
names = CapturedLocals.writes(block, scope, ivars: true, foreign_self: foreign_self_block?(call_node))
|
|
5259
5558
|
return NO_CAPTURE_BINDINGS if names.empty?
|
|
5260
5559
|
|
|
5261
5560
|
untyped = Type::Combinator.untyped
|
|
@@ -5370,8 +5669,16 @@ module Rigor
|
|
|
5370
5669
|
# The continuation effects of an escaping closure body: the outer locals it can rebind drop their narrowing, and
|
|
5371
5670
|
# the ones it content-mutates are floored. Shared by the two spellings — a block on an escaping call
|
|
5372
5671
|
# ({#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,
|
|
5672
|
+
def escaping_closure_captures(closure_node, post_scope, foreign_self: false)
|
|
5673
|
+
widen_escaping_content_captures(closure_node,
|
|
5674
|
+
drop_captured_narrowing(closure_node, post_scope, foreign_self: foreign_self))
|
|
5675
|
+
end
|
|
5676
|
+
|
|
5677
|
+
# Issue #1402 — whether `call_node`'s block runs on an object that is provably not `self`
|
|
5678
|
+
# ({InterveningIvarReset.foreign_self_block?}, its receiver typed where the operands were), so that no instance
|
|
5679
|
+
# variable it writes is this scope's ({CapturedLocals.writes}' `foreign_self:`).
|
|
5680
|
+
def foreign_self_block?(call_node)
|
|
5681
|
+
InterveningIvarReset.foreign_self_block?(call_node, operand_scope)
|
|
5375
5682
|
end
|
|
5376
5683
|
|
|
5377
5684
|
# Builds the entry scope for a block body. The block sees the outer scope's locals (Ruby's lexical scoping rule)
|