rigortype 0.4.3 → 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/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- 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 +60 -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 +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- 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/mcp_command.rb +1 -1
- 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 +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- 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 +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- 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 +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- 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 +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- 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 +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -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 +56 -8
- 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 +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- 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 +50 -35
- 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-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- 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 +16 -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"
|
|
@@ -27,7 +28,10 @@ require_relative "content_join"
|
|
|
27
28
|
require_relative "define_method_block_self"
|
|
28
29
|
require_relative "macro_block_self_type"
|
|
29
30
|
require_relative "guard_rebinding"
|
|
31
|
+
require_relative "self_call_ivar_writes"
|
|
32
|
+
require_relative "intervening_ivar_reset"
|
|
30
33
|
require_relative "match_rebinding"
|
|
34
|
+
require_relative "clause_widening"
|
|
31
35
|
require_relative "element_read_widening"
|
|
32
36
|
require_relative "hash_lookup_mutation"
|
|
33
37
|
require_relative "indexed_narrowing"
|
|
@@ -125,7 +129,7 @@ module Rigor
|
|
|
125
129
|
Prism::EnsureNode => :eval_ensure,
|
|
126
130
|
Prism::WhileNode => :eval_loop,
|
|
127
131
|
Prism::UntilNode => :eval_loop,
|
|
128
|
-
Prism::ForNode => :
|
|
132
|
+
Prism::ForNode => :eval_repeating_for,
|
|
129
133
|
Prism::AndNode => :eval_and_or,
|
|
130
134
|
Prism::OrNode => :eval_and_or,
|
|
131
135
|
Prism::ParenthesesNode => :eval_parentheses,
|
|
@@ -364,7 +368,18 @@ module Rigor
|
|
|
364
368
|
# Default: the node is treated as a pure expression. Type it through the existing expression typer (which
|
|
365
369
|
# observes the current scope's locals) and leave the scope unchanged, but for the match globals a call in it
|
|
366
370
|
# may rebind (`super(line.sub(re, ""))`, issue #1365).
|
|
367
|
-
|
|
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)
|
|
368
383
|
end
|
|
369
384
|
|
|
370
385
|
# Issue #1429 — a compound write or a `for` loop calls a method its syntax does not spell (`r += r` calls `r.+`,
|
|
@@ -525,10 +540,13 @@ module Rigor
|
|
|
525
540
|
[result_type, current]
|
|
526
541
|
end
|
|
527
542
|
|
|
543
|
+
# The top level is a straight-line body: an index `||=` there whose slot an earlier store may have filled is
|
|
544
|
+
# marked ({RepeatedOrWrites.straight_line_sites}).
|
|
528
545
|
def eval_program(node)
|
|
529
546
|
return [Type::Combinator.constant_of(nil), scope] if node.statements.nil?
|
|
530
547
|
|
|
531
|
-
|
|
548
|
+
marks = RepeatedOrWrites.straight_line_sites(node.statements, scope)
|
|
549
|
+
sub_eval(node.statements, scope.with_repeated_or_writes(marks))
|
|
532
550
|
end
|
|
533
551
|
|
|
534
552
|
# `name = rvalue` evaluates the rvalue under the entry scope (so earlier assignments in a chained `a = b = expr`
|
|
@@ -1345,6 +1363,9 @@ module Rigor
|
|
|
1345
1363
|
def eval_case_when_branches(subject_type, subject, conditions, entry_scope)
|
|
1346
1364
|
results = []
|
|
1347
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
|
|
1348
1369
|
conditions.each do |branch|
|
|
1349
1370
|
# Issue #1359 — a clause's conditions, or its pattern's pins and guard, run before its body and before
|
|
1350
1371
|
# every later clause, and the walk types them without evaluating them, so a reader there forgets `$_` here.
|
|
@@ -1355,7 +1376,11 @@ module Rigor
|
|
|
1355
1376
|
# recursion) so no condition sub-expression is newly typed; `propagate` preserves the entry because it already
|
|
1356
1377
|
# keys the node.
|
|
1357
1378
|
record_clause_entry_scope(branch, falsey_scope)
|
|
1358
|
-
|
|
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
|
|
1359
1384
|
results << sub_eval(branch, body_scope)
|
|
1360
1385
|
end
|
|
1361
1386
|
[results, falsey_scope]
|
|
@@ -1380,46 +1405,154 @@ module Rigor
|
|
|
1380
1405
|
end
|
|
1381
1406
|
|
|
1382
1407
|
# Returns `[body_scope, updated_falsey_scope]` for a single branch. `WhenNode` branches narrow through
|
|
1383
|
-
# `Narrowing.case_when_scopes
|
|
1384
|
-
#
|
|
1385
|
-
#
|
|
1408
|
+
# `Narrowing.case_when_scopes`, or through {#subjectless_when_scopes} in a `case` without a subject. `InNode`
|
|
1409
|
+
# branches narrow soundly only for a bare class pattern (`in C` / `in C => x`, pure `is_a?`); every other pattern
|
|
1410
|
+
# keeps the conservative "body = entry + bindings, falsey unchanged" shape. `subject_type` is the predicate's
|
|
1411
|
+
# type, which an `in` branch's pattern decomposes to type the names it binds.
|
|
1386
1412
|
def branch_body_and_falsey_scopes(subject_type, subject, branch, falsey_scope)
|
|
1387
1413
|
if branch.is_a?(Prism::InNode)
|
|
1388
1414
|
in_branch_body_and_falsey_scopes(subject_type, subject, branch, falsey_scope)
|
|
1415
|
+
elsif subject.nil?
|
|
1416
|
+
subjectless_when_scopes(branch.conditions, falsey_scope)
|
|
1389
1417
|
else
|
|
1390
1418
|
when_conditions = branch.respond_to?(:conditions) ? branch.conditions : []
|
|
1391
1419
|
Narrowing.case_when_scopes(subject, when_conditions, falsey_scope)
|
|
1392
1420
|
end
|
|
1393
1421
|
end
|
|
1394
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
|
+
|
|
1531
|
+
# Issue #1495 — a `case` without a subject tests each `when` condition for truthiness, in order, and stops at the
|
|
1532
|
+
# first truthy one, so a clause is the `||` of its conditions in an `if` / `elsif` chain. Each condition is
|
|
1533
|
+
# evaluated under the falsey edges of the conditions before it, in this clause and every earlier one; the body
|
|
1534
|
+
# joins the conditions' truthy edges, and the next clause starts from the last falsey edge. A condition
|
|
1535
|
+
# `Narrowing.predicate_scopes` does not recognise narrows nothing: a splat (`when *conds`), and a regexp literal,
|
|
1536
|
+
# which is tested for its truthiness and matches nothing, so it binds no match global (#1486).
|
|
1537
|
+
def subjectless_when_scopes(conditions, entry)
|
|
1538
|
+
falsey = entry
|
|
1539
|
+
truthy_edges = conditions.map do |condition|
|
|
1540
|
+
_type, _post, truthy, falsey = eval_with_edges(condition, falsey)
|
|
1541
|
+
truthy
|
|
1542
|
+
end
|
|
1543
|
+
[reduce_scopes_with_nil_injection(truthy_edges), falsey]
|
|
1544
|
+
end
|
|
1545
|
+
|
|
1395
1546
|
# ADR-47 WD3a — a bare class pattern matches on `C === subject`, i.e. exactly `subject.is_a?(C)` with no
|
|
1396
1547
|
# deconstruction, so it narrows like `when C`: the body sees the subject narrowed to `C` and the next clause's
|
|
1397
1548
|
# falsey scope has `C` removed. Other patterns can fail to match even when a class test would pass (deconstruction
|
|
1398
1549
|
# arity, hash keys, ...), so removing anything from the falsey scope would be unsound — they keep the conservative
|
|
1399
1550
|
# shape.
|
|
1400
1551
|
def in_branch_body_and_falsey_scopes(subject_type, subject, branch, falsey_scope)
|
|
1401
|
-
|
|
1402
|
-
unless class_node
|
|
1403
|
-
bound = apply_in_pattern_bindings(subject_type, subject, branch.pattern, falsey_scope)
|
|
1404
|
-
return [bound, falsey_scope]
|
|
1405
|
-
end
|
|
1406
|
-
|
|
1407
|
-
truthy_scope, narrowed_falsey = Narrowing.case_when_scopes(subject, [class_node], falsey_scope)
|
|
1552
|
+
truthy_scope, narrowed_falsey = Narrowing.case_clause_scopes(subject, branch, falsey_scope)
|
|
1408
1553
|
[apply_in_pattern_bindings(subject_type, subject, branch.pattern, truthy_scope), narrowed_falsey]
|
|
1409
1554
|
end
|
|
1410
1555
|
|
|
1411
|
-
# The class-constant node of a `in C` / `in C => x` pattern (the only `in` shapes whose match is pure `is_a?`), or
|
|
1412
|
-
# nil for any pattern that deconstructs, binds, or matches a value.
|
|
1413
|
-
def bare_class_pattern_node(pattern)
|
|
1414
|
-
case pattern
|
|
1415
|
-
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
1416
|
-
pattern
|
|
1417
|
-
when Prism::CapturePatternNode
|
|
1418
|
-
value = pattern.value
|
|
1419
|
-
value if value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
|
|
1420
|
-
end
|
|
1421
|
-
end
|
|
1422
|
-
|
|
1423
1556
|
def eval_case_else(else_clause, falsey_scope)
|
|
1424
1557
|
return sub_eval(else_clause, falsey_scope) if else_clause
|
|
1425
1558
|
|
|
@@ -1513,9 +1646,32 @@ module Rigor
|
|
|
1513
1646
|
|
|
1514
1647
|
# The scope past `node`'s `ensure` clause, entered with `exit_scope`, the `next` and `break` scopes recorded past
|
|
1515
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?}).
|
|
1516
1654
|
def eval_begin_ensure(node, exit_scope, jump_marks)
|
|
1517
|
-
|
|
1518
|
-
|
|
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
|
|
1519
1675
|
end
|
|
1520
1676
|
|
|
1521
1677
|
# Issue #1231 — a rescue clause runs after the body raised from any point in it, and an `ensure` clause after the
|
|
@@ -1523,22 +1679,27 @@ module Rigor
|
|
|
1523
1679
|
# instance variable those wrote at a value the scope it enters with does not hold: `conn = nil; begin; conn =
|
|
1524
1680
|
# open_conn; Integer(s); rescue; conn.size; end` reads the connection. The engine does not track where the body
|
|
1525
1681
|
# raises (the precise recorder of #1397 put new false positives on correct code); it widens instead, and
|
|
1526
|
-
# deliberately imprecisely. Each name the clause
|
|
1527
|
-
#
|
|
1528
|
-
# join would read `x` as `1?` in `x
|
|
1529
|
-
#
|
|
1530
|
-
#
|
|
1531
|
-
# 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.
|
|
1532
1695
|
#
|
|
1533
1696
|
# The names the clauses of `node` read from: the primary body's for a rescue clause, none under a retry `edge`.
|
|
1534
1697
|
def rescue_writes(node, edge)
|
|
1535
|
-
clause_writes([node.statements]) if node.rescue_clause && edge.nil?
|
|
1698
|
+
clause_writes([node.statements]) if node.rescue_clause && edge.nil?
|
|
1536
1699
|
end
|
|
1537
1700
|
|
|
1538
1701
|
# For the `ensure` clause: the writes of the body, its `else` and every rescue clause.
|
|
1539
1702
|
def ensure_writes(node)
|
|
1540
|
-
return nil unless widening_recorded?
|
|
1541
|
-
|
|
1542
1703
|
sources = [node.statements, node.else_clause]
|
|
1543
1704
|
current = node.rescue_clause
|
|
1544
1705
|
while current
|
|
@@ -1548,83 +1709,68 @@ module Rigor
|
|
|
1548
1709
|
clause_writes(sources)
|
|
1549
1710
|
end
|
|
1550
1711
|
|
|
1551
|
-
|
|
1552
|
-
# or nil for none. A `def` or class body writes nothing here.
|
|
1553
|
-
def clause_writes(sources)
|
|
1554
|
-
writes = nil
|
|
1555
|
-
sources.each { |source| writes = collect_clause_names(source, CLAUSE_WRITE_NODES, nil, writes) if source }
|
|
1556
|
-
writes
|
|
1557
|
-
end
|
|
1712
|
+
def clause_writes(sources) = ClauseWidening.writes(sources)
|
|
1558
1713
|
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
Prism::InstanceVariableWriteNode,
|
|
1562
|
-
Prism::InstanceVariableOperatorWriteNode,
|
|
1563
|
-
Prism::InstanceVariableOrWriteNode,
|
|
1564
|
-
Prism::InstanceVariableAndWriteNode,
|
|
1565
|
-
Prism::InstanceVariableTargetNode
|
|
1566
|
-
].freeze
|
|
1567
|
-
private_constant :CLAUSE_WRITE_NODES
|
|
1714
|
+
CLAUSE_JUMP_NODES = Set[Prism::ReturnNode, Prism::NextNode, Prism::BreakNode].freeze
|
|
1715
|
+
private_constant :CLAUSE_JUMP_NODES
|
|
1568
1716
|
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
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
|
|
1573
1723
|
|
|
1574
|
-
#
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
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)
|
|
1578
1728
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
end
|
|
1582
|
-
node.rigor_each_child { |child| found = collect_clause_names(child, node_classes, among, found) }
|
|
1583
|
-
found
|
|
1729
|
+
node.rigor_each_child { |child| return true if clause_jumps?(child) }
|
|
1730
|
+
false
|
|
1584
1731
|
end
|
|
1585
1732
|
|
|
1586
|
-
# `clause_scope` with
|
|
1587
|
-
# `Dynamic[top]`, or nil when none changes.
|
|
1588
|
-
|
|
1589
|
-
|
|
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)
|
|
1590
1737
|
|
|
1591
|
-
|
|
1592
|
-
|
|
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?
|
|
1593
1748
|
|
|
1594
|
-
|
|
1595
|
-
widened = names.reduce(clause_scope) do |acc, name|
|
|
1596
|
-
kind = name.start_with?("@") ? :ivar : :local
|
|
1597
|
-
current = clause_scope.public_send(VAR_KIND_GETTERS.fetch(kind), name)
|
|
1598
|
-
current.nil? || current == untyped ? acc : rebind_variable(acc, kind, name, untyped)
|
|
1599
|
-
end
|
|
1600
|
-
widened.equal?(clause_scope) ? nil : widened
|
|
1749
|
+
@operand_recorder.call(statements, widened.forget_error_info.forget_last_status)
|
|
1601
1750
|
end
|
|
1602
1751
|
|
|
1603
|
-
# Whether
|
|
1604
|
-
# `widen_clauses`)
|
|
1605
|
-
# 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?}).
|
|
1606
1754
|
def widening_recorded?
|
|
1607
|
-
@widen_clauses &&
|
|
1755
|
+
@widen_clauses && recording?
|
|
1608
1756
|
end
|
|
1609
1757
|
|
|
1610
|
-
#
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
# `begin` continues from. The widened pass collects no `return`, `next`, `break` or retry-edge scope, so the
|
|
1614
|
-
# widening reaches neither an inferred return nor a loop: it can only take diagnostics away inside the clause.
|
|
1615
|
-
def eval_widened_clause(node, statements, clause_scope, writes)
|
|
1616
|
-
widened = widen_clause_entry(clause_scope, statements, writes)
|
|
1617
|
-
return sub_eval(node, clause_scope) unless widened
|
|
1618
|
-
|
|
1619
|
-
eval_widened_pass(node, widened)
|
|
1620
|
-
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?)
|
|
1621
1761
|
end
|
|
1622
1762
|
|
|
1623
|
-
#
|
|
1624
|
-
|
|
1625
|
-
|
|
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?
|
|
1626
1771
|
|
|
1627
|
-
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)
|
|
1628
1774
|
end
|
|
1629
1775
|
|
|
1630
1776
|
# Runs `block` with no `return`, `next`, `break` or retry-frame sink installed.
|
|
@@ -1735,6 +1881,10 @@ module Rigor
|
|
|
1735
1881
|
CLAUSE_SINK_KEYS = [RETURN_SINK_KEY, NEXT_SINK_KEY, BREAK_SINK_KEY, BREAK_VALUE_SINK_KEY, RETRY_FRAMES_KEY].freeze
|
|
1736
1882
|
private_constant :CLAUSE_SINK_KEYS
|
|
1737
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
|
+
|
|
1738
1888
|
# The retry edge of `node`, or nil when no `retry` in its rescue chain targets it. Allocation-free for a `begin`
|
|
1739
1889
|
# no `retry` targets.
|
|
1740
1890
|
def retry_edge_for(node)
|
|
@@ -2012,7 +2162,7 @@ module Rigor
|
|
|
2012
2162
|
# runs on the way out (`begin; retry; ensure; tries += 1; end`), or one made before a `retry` the evaluator only
|
|
2013
2163
|
# types (`log(tries < 5 ? retry : :gave_up)`).
|
|
2014
2164
|
#
|
|
2015
|
-
# 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
|
|
2016
2166
|
# reference is bound, so `rescue => e` still binds `e` to the exception. It is nil under a retry edge, which
|
|
2017
2167
|
# already carries every scope the primary body raises from ({#widen_entry_for_retry}).
|
|
2018
2168
|
def collect_rescue_chain_results(rescue_node, entry_scope, edge: nil, writes: nil)
|
|
@@ -2020,11 +2170,12 @@ module Rigor
|
|
|
2020
2170
|
results = []
|
|
2021
2171
|
current = rescue_node
|
|
2022
2172
|
while current
|
|
2023
|
-
widened = widen_clause_entry(entry_scope, current.statements
|
|
2024
|
-
|
|
2173
|
+
widened = widen_clause_entry(entry_scope, writes, current.statements)
|
|
2174
|
+
record_operand_clause(current.statements, widened)
|
|
2025
2175
|
rescue_scope = bind_rescue_reference(current, entry_scope)
|
|
2026
2176
|
arm = if widened
|
|
2027
|
-
|
|
2177
|
+
eval_widened_clause(current.statements, rescue_scope, bind_rescue_reference(current, widened),
|
|
2178
|
+
on_enter: on_enter)
|
|
2028
2179
|
else
|
|
2029
2180
|
eval_branch_or_nil(current.statements, rescue_scope, on_enter: on_enter)
|
|
2030
2181
|
end
|
|
@@ -2065,18 +2216,29 @@ module Rigor
|
|
|
2065
2216
|
after.global(:$_) == Type::Combinator.untyped && !LastLine.may_set?(node.statements, scope)
|
|
2066
2217
|
end
|
|
2067
2218
|
|
|
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}).
|
|
2223
|
+
def eval_repeating_for(node) = ReflectiveIvarWrite.in_repeating_body { eval_for(node) }
|
|
2224
|
+
|
|
2068
2225
|
# `while pred; body; end` / `until pred; body; end`. The body might run zero or more times, so half-bound names
|
|
2069
2226
|
# degrade to `T | nil` in the post-loop scope. The loop expression itself types as `Constant[nil]` (Slice 3 phase
|
|
2070
2227
|
# 1), reflecting the common case where no `break VALUE` is observed.
|
|
2071
2228
|
def eval_loop(node)
|
|
2072
|
-
|
|
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) }
|
|
2073
2234
|
post_pred = widen_predicate_pins(node, post_pred)
|
|
2074
2235
|
return [Type::Combinator.constant_of(nil), narrow_loop_exit_edge(node, post_pred)] if node.statements.nil?
|
|
2075
2236
|
|
|
2076
2237
|
# The historical single body pass joined with the pre-loop scope. This continues to carry everything the
|
|
2077
2238
|
# fixpoint does NOT track: receiver-mutation widening of non-rebound locals (`buf.push(i)` widens `buf`'s
|
|
2078
|
-
# Tuple), body-introduced locals' nil-injection, an instance variable
|
|
2079
|
-
# 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.
|
|
2080
2242
|
#
|
|
2081
2243
|
# Like every fixpoint pass, it enters the body on the predicate's loop-entry edge ({#loop_pass_entry}): the
|
|
2082
2244
|
# body of `while (line = gets)` reads `line` as `String`, and the body of `while gets` reads `$_` as one
|
|
@@ -2089,8 +2251,7 @@ module Rigor
|
|
|
2089
2251
|
body_scope, first_breaks = single_pass(node, post_pred, jumps)
|
|
2090
2252
|
base_scope = join_with_nil_injection(post_pred, body_scope)
|
|
2091
2253
|
|
|
2092
|
-
|
|
2093
|
-
names = rebound + body_first
|
|
2254
|
+
names, body_first = loop_rebound_names(node.statements, post_pred)
|
|
2094
2255
|
|
|
2095
2256
|
# Fast path: a loop whose body rebinds no local skips the rebind fixpoint, but still needs the slice-C content
|
|
2096
2257
|
# writeback (a loop may content-mutate a collection without rebinding any local — `acc << x`), so apply it to
|
|
@@ -2183,7 +2344,7 @@ module Rigor
|
|
|
2183
2344
|
# assumptions, so the last-visit-wins scope index would annotate loop-body lines with stale pre-convergence
|
|
2184
2345
|
# constants. One extra pass from the converged bindings (result discarded) re-records the body's entry scopes.
|
|
2185
2346
|
record_converged_loop_body(node, post_pred, result, names, body_first, jumps, break_pass)
|
|
2186
|
-
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) }
|
|
2187
2348
|
# ADR-56 slice C — loop-body receiver-content element-type join. A loop that content-mutates a collection (`acc
|
|
2188
2349
|
# << n`) keeps only the seed's element types after the single-pass widen; join the appended/stored types into
|
|
2189
2350
|
# the continuation collection. Pre-state comes from `post_pred` for a name the loop only content-mutates and
|
|
@@ -2251,6 +2412,13 @@ module Rigor
|
|
|
2251
2412
|
NO_LOOP_NAMES = [].freeze
|
|
2252
2413
|
private_constant :NO_LOOP_BINDINGS, :NO_LOOP_NAMES
|
|
2253
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
|
+
|
|
2254
2422
|
# A body with no targeting jump pays two allocation-free scans.
|
|
2255
2423
|
def loop_jumps(statements)
|
|
2256
2424
|
kinds = JumpTargets.kinds(statements, LOOP_JUMP_CLASSES)
|
|
@@ -2346,11 +2514,11 @@ module Rigor
|
|
|
2346
2514
|
floor = Type::Combinator.untyped
|
|
2347
2515
|
breaks.reduce(continuation) do |cont, break_scope|
|
|
2348
2516
|
names.reduce(cont) do |acc, name|
|
|
2349
|
-
break_value =
|
|
2350
|
-
current =
|
|
2517
|
+
break_value = CapturedLocals.bound_type(break_scope, name)
|
|
2518
|
+
current = CapturedLocals.bound_type(acc, name)
|
|
2351
2519
|
next acc if break_value.nil? || current == floor
|
|
2352
2520
|
|
|
2353
|
-
acc
|
|
2521
|
+
bind_capture(acc, name, current ? Type::Combinator.union(current, break_value) : break_value)
|
|
2354
2522
|
end
|
|
2355
2523
|
end
|
|
2356
2524
|
end
|
|
@@ -2423,7 +2591,7 @@ module Rigor
|
|
|
2423
2591
|
# treatment. `break_pass` rides along so every pass leaves its `break` arms for {#loop_break_arms}.
|
|
2424
2592
|
def loop_rebind_fixpoint(node, post_pred, names, body_first, jumps, break_pass)
|
|
2425
2593
|
nil_const = Type::Combinator.constant_of(nil)
|
|
2426
|
-
seed = names.to_h { |name| [name,
|
|
2594
|
+
seed = names.to_h { |name| [name, CapturedLocals.bound_type(post_pred, name) || nil_const] }
|
|
2427
2595
|
evaluate_body = lambda do |bindings|
|
|
2428
2596
|
loop_body_exit_bindings(node, post_pred, bindings, names, body_first, jumps, break_pass)
|
|
2429
2597
|
end
|
|
@@ -2455,6 +2623,34 @@ module Rigor
|
|
|
2455
2623
|
[pre_existing.uniq, body_first.uniq - pre_existing.uniq]
|
|
2456
2624
|
end
|
|
2457
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
|
+
|
|
2458
2654
|
# Evaluates the loop body once with each fixpoint-tracked local bound to the supplied running assumption and
|
|
2459
2655
|
# returns the per-name exit binding. Used as the {BodyFixpoint} body-evaluator for `eval_loop`.
|
|
2460
2656
|
#
|
|
@@ -2483,14 +2679,14 @@ module Rigor
|
|
|
2483
2679
|
break_pass[:entry] = bindings.except(*body_first)
|
|
2484
2680
|
break_pass[:arms] = breaks
|
|
2485
2681
|
end
|
|
2486
|
-
names.to_h { |name| [name,
|
|
2682
|
+
names.to_h { |name| [name, CapturedLocals.bound_type(exit_scope, name)] }
|
|
2487
2683
|
end
|
|
2488
2684
|
|
|
2489
2685
|
# The scope one fixpoint pass enters the body with: `post_pred` overlaid with the pre-existing names' running
|
|
2490
2686
|
# assumption, then narrowed by the predicate's loop-entry edge ({#loop_body_exit_bindings} carries the why).
|
|
2491
2687
|
def loop_pass_entry(node, post_pred, bindings, body_first, jumps)
|
|
2492
2688
|
overlaid = bindings.except(*body_first)
|
|
2493
|
-
entry = overlaid.reduce(post_pred) { |acc, (name, type)| acc
|
|
2689
|
+
entry = overlaid.reduce(post_pred) { |acc, (name, type)| bind_capture(acc, name, type) }
|
|
2494
2690
|
entry = loop_content_entry(node.statements, post_pred, entry)
|
|
2495
2691
|
truthy_scope, falsey_scope = Narrowing.predicate_scopes(node.predicate, entry)
|
|
2496
2692
|
edge = node.is_a?(Prism::UntilNode) ? falsey_scope : truthy_scope
|
|
@@ -2856,15 +3052,47 @@ module Rigor
|
|
|
2856
3052
|
# u.strip`, where the raise almost always comes from `Float` after it. Neither the arm nor anything in it
|
|
2857
3053
|
# is recorded or typed from there, which keeps it where it was before #1256: an ADR-5 trade of the rare
|
|
2858
3054
|
# raise-before-write path for no false positive on the common one.
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
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)
|
|
2862
3066
|
after = if branch_unconditionally_exits?(node.rescue_expression)
|
|
2863
3067
|
after_expression
|
|
2864
3068
|
else
|
|
2865
3069
|
join_with_nil_injection(after_expression, after_rescue)
|
|
2866
3070
|
end
|
|
2867
|
-
[
|
|
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))
|
|
2868
3096
|
end
|
|
2869
3097
|
|
|
2870
3098
|
# `class Foo; body; end` and `module Foo; body; end`. The class body runs in a fresh scope (Ruby's class scope
|
|
@@ -2929,6 +3157,7 @@ module Rigor
|
|
|
2929
3157
|
# evaluates `def` to the method's name as a Symbol, so the produced type is `Constant[:name]`.
|
|
2930
3158
|
def eval_def(node)
|
|
2931
3159
|
body_scope = build_method_entry_scope(node)
|
|
3160
|
+
body_scope = body_scope.with_repeated_or_writes(RepeatedOrWrites.straight_line_sites(node.body, body_scope))
|
|
2932
3161
|
# Parameter default value expressions (e.g. `self.x` in `def copy(x: self.x)`) execute when the method is
|
|
2933
3162
|
# *invoked*, not when the `def` is read; their `self` is the instance receiver, not the surrounding class body.
|
|
2934
3163
|
# Walk the parameters subtree under `body_scope` so the scope-index records the instance `self_type` for every
|
|
@@ -3119,7 +3348,7 @@ module Rigor
|
|
|
3119
3348
|
# map …) that rebinds an outer local must not leave that local's pre-call binding unmodified in the continuation
|
|
3120
3349
|
# scope; the spec MUST in § "Fact stability and mutation" names captured locals a first-class invalidation
|
|
3121
3350
|
# category. (The escaping / unknown path already widened to Dynamic[top] via `record_closure_escape_if_any`.)
|
|
3122
|
-
post_scope =
|
|
3351
|
+
post_scope = block_capture_continuation(node, post_scope)
|
|
3123
3352
|
statement_call = !call_type.nil? && !@in_operand
|
|
3124
3353
|
post_scope = apply_post_return_narrowing(node, post_scope) if statement_call
|
|
3125
3354
|
# Flow-folding G1 / G2 — widen a local- or instance-variable binding when the call is an in-place mutator on it
|
|
@@ -3170,6 +3399,9 @@ module Rigor
|
|
|
3170
3399
|
# observes the seed's union (not the pre-call narrowed value). Always-safe (only widens; no new facts). See
|
|
3171
3400
|
# [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md) § "Flow-folding" — G2 intervening-call case.
|
|
3172
3401
|
post_scope = invalidate_ivars_for_intervening_call(node, post_scope) if statement_call
|
|
3402
|
+
# Issue #1889 — `instance_variable_set` on `self` writes an ivar no write node shows. It runs after the reset
|
|
3403
|
+
# above, which would join a literal-name write back into the class seed.
|
|
3404
|
+
post_scope = ReflectiveIvarWrite.apply(node, post_scope) { type_operand(ReflectiveIvarWrite.value_node(node)) }
|
|
3173
3405
|
# C1 — regex match-data globals (`$~`, `$1..$N`, `$&`, …) are narrowed to non-nil on a successful-match edge; a
|
|
3174
3406
|
# later call that itself runs a regex match rebinds them, so the narrowed facts must be dropped. We forget them
|
|
3175
3407
|
# only when the call may run a match in this frame ({#rebinds_match_globals?}). A call provably match-free on a
|
|
@@ -3191,7 +3423,13 @@ module Rigor
|
|
|
3191
3423
|
post_scope = post_scope.forget_match_globals if rebinds_match_globals?(node, post_scope)
|
|
3192
3424
|
post_scope = post_scope.forget_last_line if rebinds_last_line?(node, post_scope)
|
|
3193
3425
|
if post_scope.guard_narrowed? && GuardRebinding.call_may_rebind?(node, operand_scope)
|
|
3194
|
-
|
|
3426
|
+
# Issue #1596 — a self-call proven to write only some instance variables restores only theirs.
|
|
3427
|
+
writes = intervening_call_candidate?(node) ? self_call_ivar_writes(node) : nil
|
|
3428
|
+
post_scope = if writes
|
|
3429
|
+
writes.reduce(post_scope) { |acc, name| acc.forget_guard_narrowing_of(:ivar, name) }
|
|
3430
|
+
else
|
|
3431
|
+
post_scope.forget_guard_narrowings
|
|
3432
|
+
end
|
|
3195
3433
|
end
|
|
3196
3434
|
forget_rescued_status(LastStatus.after(node, post_scope, scope), node)
|
|
3197
3435
|
end
|
|
@@ -3316,30 +3554,35 @@ module Rigor
|
|
|
3316
3554
|
# Returns a scope with each ivar's narrowed local binding widened back to its class-ivar seed value when the call
|
|
3317
3555
|
# is one that could plausibly mutate ivars on the enclosing class (implicit-self or explicit `self.foo`).
|
|
3318
3556
|
# External- receiver calls (`obj.method`) cannot reach the caller's ivars; they pass through unchanged.
|
|
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}).
|
|
3319
3564
|
def invalidate_ivars_for_intervening_call(call_node, current_scope)
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
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)
|
|
3573
|
+
end
|
|
3327
3574
|
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3575
|
+
# Issue #1596 — the ivars the implicit-self / `self.` call `call_node` may write, or nil when that is not proven
|
|
3576
|
+
# ({SelfCallIvarWrites.of}). Memoised per call node: the reset and the guard restore ask it of the same call.
|
|
3577
|
+
def self_call_ivar_writes(call_node)
|
|
3578
|
+
memo = (@self_call_ivar_writes ||= {}.compare_by_identity)
|
|
3579
|
+
return memo[call_node] if memo.key?(call_node)
|
|
3332
3580
|
|
|
3333
|
-
|
|
3334
|
-
end
|
|
3335
|
-
widened
|
|
3581
|
+
memo[call_node] = SelfCallIvarWrites.of(call_node, operand_scope)
|
|
3336
3582
|
end
|
|
3337
3583
|
|
|
3338
3584
|
def intervening_call_candidate?(call_node)
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
receiver = call_node.receiver
|
|
3342
|
-
receiver.nil? || receiver.is_a?(Prism::SelfNode)
|
|
3585
|
+
InterveningIvarReset.candidate?(call_node)
|
|
3343
3586
|
end
|
|
3344
3587
|
|
|
3345
3588
|
def enclosing_class_name_for(self_type)
|
|
@@ -3839,7 +4082,7 @@ module Rigor
|
|
|
3839
4082
|
classification = classify_closure_escape(node)
|
|
3840
4083
|
return post_scope if classification == :non_escaping
|
|
3841
4084
|
|
|
3842
|
-
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))
|
|
3843
4086
|
post_scope.with_fact(
|
|
3844
4087
|
Analysis::FactStore::Fact.new(
|
|
3845
4088
|
bucket: :dynamic_origin,
|
|
@@ -4143,13 +4386,72 @@ module Rigor
|
|
|
4143
4386
|
# callback that runs later writes the very ivar the continuation reads (`@clicked = false; button.on_click {
|
|
4144
4387
|
# @clicked = true }` left `if @clicked` folding always-falsey). One still on its ADR-58 declaration seed is
|
|
4145
4388
|
# left alone ({CapturedLocals.writes}): that seed already holds whatever the callback stores.
|
|
4146
|
-
def drop_captured_narrowing(block_node, base_scope)
|
|
4147
|
-
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)
|
|
4148
4391
|
return base_scope if names.empty?
|
|
4149
4392
|
|
|
4150
4393
|
names.reduce(base_scope) { |acc, name| bind_capture(acc, name, Type::Combinator.untyped) }
|
|
4151
4394
|
end
|
|
4152
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
|
+
|
|
4153
4455
|
# ADR-56 slice A. For a `:non_escaping` block, fold the continuation binding of every outer local the body can
|
|
4154
4456
|
# rebind back into `post_scope`. The binding is a capped fixpoint (cap 3) over the block body re-evaluated under
|
|
4155
4457
|
# the running per-name assumption, joined with the pre-call binding (kept as a constituent so the 0-iteration path
|
|
@@ -4174,13 +4476,27 @@ module Rigor
|
|
|
4174
4476
|
return post_scope unless block.is_a?(Prism::BlockNode)
|
|
4175
4477
|
return post_scope unless classify_closure_escape(call_node) == :non_escaping
|
|
4176
4478
|
|
|
4177
|
-
names = CapturedLocals.writes(block, scope, ivars: true)
|
|
4479
|
+
names = CapturedLocals.writes(block, scope, ivars: true, foreign_self: foreign_self_block?(call_node))
|
|
4178
4480
|
return post_scope if names.empty?
|
|
4179
4481
|
|
|
4180
4482
|
break_pass = block_break_pass(block)
|
|
4181
|
-
|
|
4483
|
+
marks = {}
|
|
4484
|
+
result = with_capture_exit_marks(marks) { converge_captures_by_kind(call_node, block, names, break_pass) }
|
|
4182
4485
|
result = join_block_break_bindings(call_node, block, result, break_pass)
|
|
4183
|
-
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
|
|
4184
4500
|
end
|
|
4185
4501
|
|
|
4186
4502
|
# The {BodyFixpoint} continuation of `names`, seeded from their pre-call bindings.
|
|
@@ -4239,7 +4555,12 @@ module Rigor
|
|
|
4239
4555
|
# Binds a name from `CapturedLocals.writes`. An ivar goes through {CapturedLocals.bind}, which keeps its issue
|
|
4240
4556
|
# #286 optimistic mark. A local keeps the plain `with_local` these seams have always used, so a block that
|
|
4241
4557
|
# rebinds no ivar leaves the locals exactly as before.
|
|
4242
|
-
|
|
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
|
+
|
|
4243
4564
|
CapturedLocals.ivar_name?(name) ? CapturedLocals.bind(scope, name, type) : scope.with_local(name, type)
|
|
4244
4565
|
end
|
|
4245
4566
|
|
|
@@ -5103,9 +5424,22 @@ module Rigor
|
|
|
5103
5424
|
def block_exit_bindings(call_node, block, bindings, names)
|
|
5104
5425
|
entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, bindings))
|
|
5105
5426
|
_type, exit_scope = sub_eval(block, entry)
|
|
5427
|
+
record_capture_exit_marks(exit_scope, names)
|
|
5106
5428
|
names.to_h { |name| [name, CapturedLocals.bound_type(exit_scope, name)] }
|
|
5107
5429
|
end
|
|
5108
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
|
+
|
|
5109
5443
|
# The entry scope of one write-back pass: the block's entry with each written outer local or ivar bound to
|
|
5110
5444
|
# `bindings`, and each captured local the body only mutates in place at its widening ({#capture_pass_bindings}).
|
|
5111
5445
|
# That widening describes the same object, so it keeps the marks a source-level write drops (issue #1287,
|
|
@@ -5114,7 +5448,9 @@ module Rigor
|
|
|
5114
5448
|
def block_pass_entry(call_node, block, bindings)
|
|
5115
5449
|
entry = build_block_entry_scope(call_node, block)
|
|
5116
5450
|
capture_pass_bindings(block, bindings).reduce(entry) do |acc, (name, type)|
|
|
5117
|
-
|
|
5451
|
+
next acc.with_mutated_local(name, type) unless bindings.key?(name)
|
|
5452
|
+
|
|
5453
|
+
bind_capture(acc, name, type, optimistic: @capture_exit_marks&.[](name))
|
|
5118
5454
|
end
|
|
5119
5455
|
end
|
|
5120
5456
|
|
|
@@ -5218,7 +5554,7 @@ module Rigor
|
|
|
5218
5554
|
def unproven_rebind_bindings(call_node, block, classification)
|
|
5219
5555
|
return NO_CAPTURE_BINDINGS if call_node.receiver && classification == :non_escaping
|
|
5220
5556
|
|
|
5221
|
-
names = CapturedLocals.writes(block, scope, ivars: true)
|
|
5557
|
+
names = CapturedLocals.writes(block, scope, ivars: true, foreign_self: foreign_self_block?(call_node))
|
|
5222
5558
|
return NO_CAPTURE_BINDINGS if names.empty?
|
|
5223
5559
|
|
|
5224
5560
|
untyped = Type::Combinator.untyped
|
|
@@ -5333,8 +5669,16 @@ module Rigor
|
|
|
5333
5669
|
# The continuation effects of an escaping closure body: the outer locals it can rebind drop their narrowing, and
|
|
5334
5670
|
# the ones it content-mutates are floored. Shared by the two spellings — a block on an escaping call
|
|
5335
5671
|
# ({#record_closure_escape_if_any}) and a lambda literal ({#eval_lambda}) — so they cannot drift.
|
|
5336
|
-
def escaping_closure_captures(closure_node, post_scope)
|
|
5337
|
-
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)
|
|
5338
5682
|
end
|
|
5339
5683
|
|
|
5340
5684
|
# Builds the entry scope for a block body. The block sees the outer scope's locals (Ruby's lexical scoping rule)
|
|
@@ -5412,7 +5756,7 @@ module Rigor
|
|
|
5412
5756
|
return [] if receiver_type.nil?
|
|
5413
5757
|
|
|
5414
5758
|
arg_types = call_arg_types_for(call_node)
|
|
5415
|
-
MethodDispatcher.expected_block_param_types(
|
|
5759
|
+
expected = MethodDispatcher.expected_block_param_types(
|
|
5416
5760
|
receiver_type: receiver_type,
|
|
5417
5761
|
method_name: call_node.name,
|
|
5418
5762
|
arg_types: arg_types,
|
|
@@ -5420,10 +5764,28 @@ module Rigor
|
|
|
5420
5764
|
scope: scope,
|
|
5421
5765
|
call_node: call_node
|
|
5422
5766
|
)
|
|
5767
|
+
with_inject_accumulator(call_node, receiver_type, arg_types, expected)
|
|
5423
5768
|
rescue StandardError
|
|
5424
5769
|
[]
|
|
5425
5770
|
end
|
|
5426
5771
|
|
|
5772
|
+
# Issue #1889 — a block-form `inject` / `reduce` call's accumulator parameter at the type it holds across
|
|
5773
|
+
# every run ({ExpressionTyper#inject_accumulator_type}) rather than the seed the signature binds it to, once
|
|
5774
|
+
# per call: every write-back pass builds the entry again.
|
|
5775
|
+
def with_inject_accumulator(call_node, receiver_type, arg_types, expected)
|
|
5776
|
+
memo = @inject_accumulator
|
|
5777
|
+
accumulator =
|
|
5778
|
+
if memo && memo[0].equal?(call_node)
|
|
5779
|
+
memo[1]
|
|
5780
|
+
else
|
|
5781
|
+
typed = ExpressionTyper.new(scope: scope, tracer: tracer)
|
|
5782
|
+
.inject_accumulator_type(call_node, receiver_type, arg_types)
|
|
5783
|
+
@inject_accumulator = [call_node, typed]
|
|
5784
|
+
typed
|
|
5785
|
+
end
|
|
5786
|
+
accumulator ? [accumulator, *expected.drop(1)] : expected
|
|
5787
|
+
end
|
|
5788
|
+
|
|
5427
5789
|
def call_arg_types_for(call_node)
|
|
5428
5790
|
arguments = call_node.arguments
|
|
5429
5791
|
return [] if arguments.nil?
|