rigortype 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +26 -9
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- 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-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +21 -1
|
@@ -27,6 +27,7 @@ require_relative "content_join"
|
|
|
27
27
|
require_relative "define_method_block_self"
|
|
28
28
|
require_relative "macro_block_self_type"
|
|
29
29
|
require_relative "guard_rebinding"
|
|
30
|
+
require_relative "self_call_ivar_writes"
|
|
30
31
|
require_relative "match_rebinding"
|
|
31
32
|
require_relative "element_read_widening"
|
|
32
33
|
require_relative "hash_lookup_mutation"
|
|
@@ -42,8 +43,10 @@ require_relative "operand_effects"
|
|
|
42
43
|
require_relative "operand_walk"
|
|
43
44
|
require_relative "optimistic_origin"
|
|
44
45
|
require_relative "return_barrier"
|
|
46
|
+
require_relative "repeated_or_writes"
|
|
45
47
|
require_relative "rewrite_mutation"
|
|
46
48
|
require_relative "unknown_store_widening"
|
|
49
|
+
require_relative "untyped_rejoin"
|
|
47
50
|
require_relative "version_guard"
|
|
48
51
|
|
|
49
52
|
module Rigor
|
|
@@ -121,9 +124,9 @@ module Rigor
|
|
|
121
124
|
Prism::BeginNode => :eval_begin_node,
|
|
122
125
|
Prism::RescueNode => :eval_rescue,
|
|
123
126
|
Prism::EnsureNode => :eval_ensure,
|
|
124
|
-
Prism::WhileNode => :
|
|
125
|
-
Prism::UntilNode => :
|
|
126
|
-
Prism::ForNode => :
|
|
127
|
+
Prism::WhileNode => :eval_repeating_loop,
|
|
128
|
+
Prism::UntilNode => :eval_repeating_loop,
|
|
129
|
+
Prism::ForNode => :eval_repeating_for,
|
|
127
130
|
Prism::AndNode => :eval_and_or,
|
|
128
131
|
Prism::OrNode => :eval_and_or,
|
|
129
132
|
Prism::ParenthesesNode => :eval_parentheses,
|
|
@@ -278,11 +281,15 @@ module Rigor
|
|
|
278
281
|
# a declined `class self::X`, a parse-error header), where `class_context` is empty without the code
|
|
279
282
|
# being at the top level. A `def` (or `class << self`) there runs under an untyped `self` instead of
|
|
280
283
|
# the top level's nil one, so its implicit-self calls answer `Dynamic` and no top-level rule fires.
|
|
284
|
+
# @param widen_clauses — issue #1231: when true (and a recorder is installed), a rescue or `ensure` clause
|
|
285
|
+
# records its per-node entry scopes under {#widen_clause_entry}'s widening. The check path sets it; every
|
|
286
|
+
# other reader of the index (parameter inference, sig-gen, the scanners) keeps the clause's plain entry.
|
|
281
287
|
def initialize(scope:, tracer: nil, on_enter: nil, class_context: [].freeze, # rubocop:disable Metrics/ParameterLists
|
|
282
288
|
lexical_nesting: EMPTY_NESTING, converged_loop_recording: false, next_scope_sink: nil,
|
|
283
289
|
operand_scope: nil, in_operand: false, operand_recorder: nil, operand_types: nil,
|
|
284
|
-
opaque_class: false)
|
|
290
|
+
opaque_class: false, widen_clauses: false)
|
|
285
291
|
@scope = scope
|
|
292
|
+
@widen_clauses = widen_clauses
|
|
286
293
|
@opaque_class = opaque_class
|
|
287
294
|
@tracer = tracer
|
|
288
295
|
@on_enter = on_enter
|
|
@@ -484,9 +491,10 @@ module Rigor
|
|
|
484
491
|
# `opts[k] ||= raise KeyError` is a guard, never `bot`, and `@flags[n] ||= false` is `true` after an
|
|
485
492
|
# `@flags[n] = true` elsewhere, never provably `false`.
|
|
486
493
|
#
|
|
487
|
-
# Nor is a site
|
|
488
|
-
#
|
|
489
|
-
# stored rather than an
|
|
494
|
+
# Nor is a marked site (`Scope#repeated_or_write?`, {RepeatedOrWrites}): a block-return pass, and this
|
|
495
|
+
# evaluator's statement and write-back passes ({#with_repeated_or_write_marks}), type every run of a repeating
|
|
496
|
+
# body from one entry scope, so its slot's gradual type may be what an EARLIER run stored rather than an
|
|
497
|
+
# absence of evidence.
|
|
490
498
|
def index_compound_write_value(node)
|
|
491
499
|
return index_write_stored_type(node, scope) unless node.is_a?(Prism::IndexOrWriteNode)
|
|
492
500
|
|
|
@@ -518,10 +526,13 @@ module Rigor
|
|
|
518
526
|
[result_type, current]
|
|
519
527
|
end
|
|
520
528
|
|
|
529
|
+
# The top level is a straight-line body: an index `||=` there whose slot an earlier store may have filled is
|
|
530
|
+
# marked ({RepeatedOrWrites.straight_line_sites}).
|
|
521
531
|
def eval_program(node)
|
|
522
532
|
return [Type::Combinator.constant_of(nil), scope] if node.statements.nil?
|
|
523
533
|
|
|
524
|
-
|
|
534
|
+
marks = RepeatedOrWrites.straight_line_sites(node.statements, scope)
|
|
535
|
+
sub_eval(node.statements, scope.with_repeated_or_writes(marks))
|
|
525
536
|
end
|
|
526
537
|
|
|
527
538
|
# `name = rvalue` evaluates the rvalue under the entry scope (so earlier assignments in a chained `a = b = expr`
|
|
@@ -1180,7 +1191,8 @@ module Rigor
|
|
|
1180
1191
|
|
|
1181
1192
|
[
|
|
1182
1193
|
Type::Combinator.union(then_type, else_type),
|
|
1183
|
-
join_with_nil_injection(then_scope, else_scope)
|
|
1194
|
+
UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred,
|
|
1195
|
+
[node.statements, node.subsequent])
|
|
1184
1196
|
]
|
|
1185
1197
|
end
|
|
1186
1198
|
|
|
@@ -1215,7 +1227,8 @@ module Rigor
|
|
|
1215
1227
|
|
|
1216
1228
|
[
|
|
1217
1229
|
Type::Combinator.union(then_type, else_type),
|
|
1218
|
-
join_with_nil_injection(then_scope, else_scope)
|
|
1230
|
+
UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred,
|
|
1231
|
+
[node.statements, node.else_clause])
|
|
1219
1232
|
]
|
|
1220
1233
|
end
|
|
1221
1234
|
|
|
@@ -1286,7 +1299,7 @@ module Rigor
|
|
|
1286
1299
|
subject_type, post_pred = node.predicate ? sub_eval(node.predicate, scope) : [nil, scope]
|
|
1287
1300
|
branch_results, falsey_scope = eval_case_when_branches(subject_type, node.predicate, node.conditions, post_pred)
|
|
1288
1301
|
if pattern_case_matches_every_path?(node, branch_results)
|
|
1289
|
-
return unmatched_pattern_result(branch_results, node.conditions)
|
|
1302
|
+
return unmatched_pattern_result(branch_results, node.conditions, post_pred)
|
|
1290
1303
|
end
|
|
1291
1304
|
|
|
1292
1305
|
else_result = eval_case_else(node.else_clause, falsey_scope)
|
|
@@ -1295,7 +1308,7 @@ module Rigor
|
|
|
1295
1308
|
branch_nodes = [*node.conditions, node.else_clause]
|
|
1296
1309
|
[
|
|
1297
1310
|
Type::Combinator.union(*all_results.map(&:first)),
|
|
1298
|
-
join_case_branch_scopes(all_results, branch_nodes)
|
|
1311
|
+
join_case_branch_scopes(all_results, branch_nodes, post_pred)
|
|
1299
1312
|
]
|
|
1300
1313
|
end
|
|
1301
1314
|
|
|
@@ -1310,8 +1323,9 @@ module Rigor
|
|
|
1310
1323
|
node.is_a?(Prism::CaseMatchNode) && node.else_clause.nil? && !branch_results.empty?
|
|
1311
1324
|
end
|
|
1312
1325
|
|
|
1313
|
-
def unmatched_pattern_result(branch_results, branch_nodes)
|
|
1314
|
-
[Type::Combinator.union(*branch_results.map(&:first)),
|
|
1326
|
+
def unmatched_pattern_result(branch_results, branch_nodes, entry)
|
|
1327
|
+
[Type::Combinator.union(*branch_results.map(&:first)),
|
|
1328
|
+
join_case_branch_scopes(branch_results, branch_nodes, entry)]
|
|
1315
1329
|
end
|
|
1316
1330
|
|
|
1317
1331
|
# Joins the post-scopes of every `when`/`in`/`else` branch, dropping the scope of any branch that terminates
|
|
@@ -1320,15 +1334,16 @@ module Rigor
|
|
|
1320
1334
|
# `branch_terminates?` rule `eval_if`/`eval_unless` already apply to the if/else merge: e.g. `case x; when 1 then
|
|
1321
1335
|
# v="a"; when 2 then v="b"; else raise; end` keeps `v: "a" | "b"` instead of `... | nil`. When every branch
|
|
1322
1336
|
# terminates the merge is itself unreachable; fall back to the full join so the continuation scope stays
|
|
1323
|
-
# well-formed.
|
|
1324
|
-
|
|
1337
|
+
# well-formed. A local the clauses only narrowed from `Dynamic[top]` re-joins to it ({UntypedRejoin}, issue
|
|
1338
|
+
# #1840), read against the post-predicate `entry`.
|
|
1339
|
+
def join_case_branch_scopes(results, nodes, entry)
|
|
1325
1340
|
live = []
|
|
1326
1341
|
results.each_with_index do |(type, branch_scope), i|
|
|
1327
1342
|
live << branch_scope unless branch_terminates?(nodes[i], type)
|
|
1328
1343
|
end
|
|
1329
1344
|
|
|
1330
1345
|
live = results.map(&:last) if live.empty?
|
|
1331
|
-
reduce_scopes_with_nil_injection(live)
|
|
1346
|
+
UntypedRejoin.apply(reduce_scopes_with_nil_injection(live), entry, nodes)
|
|
1332
1347
|
end
|
|
1333
1348
|
|
|
1334
1349
|
def eval_case_when_branches(subject_type, subject, conditions, entry_scope)
|
|
@@ -1369,46 +1384,46 @@ module Rigor
|
|
|
1369
1384
|
end
|
|
1370
1385
|
|
|
1371
1386
|
# Returns `[body_scope, updated_falsey_scope]` for a single branch. `WhenNode` branches narrow through
|
|
1372
|
-
# `Narrowing.case_when_scopes
|
|
1373
|
-
#
|
|
1374
|
-
#
|
|
1387
|
+
# `Narrowing.case_when_scopes`, or through {#subjectless_when_scopes} in a `case` without a subject. `InNode`
|
|
1388
|
+
# branches narrow soundly only for a bare class pattern (`in C` / `in C => x`, pure `is_a?`); every other pattern
|
|
1389
|
+
# keeps the conservative "body = entry + bindings, falsey unchanged" shape. `subject_type` is the predicate's
|
|
1390
|
+
# type, which an `in` branch's pattern decomposes to type the names it binds.
|
|
1375
1391
|
def branch_body_and_falsey_scopes(subject_type, subject, branch, falsey_scope)
|
|
1376
1392
|
if branch.is_a?(Prism::InNode)
|
|
1377
1393
|
in_branch_body_and_falsey_scopes(subject_type, subject, branch, falsey_scope)
|
|
1394
|
+
elsif subject.nil?
|
|
1395
|
+
subjectless_when_scopes(branch.conditions, falsey_scope)
|
|
1378
1396
|
else
|
|
1379
1397
|
when_conditions = branch.respond_to?(:conditions) ? branch.conditions : []
|
|
1380
1398
|
Narrowing.case_when_scopes(subject, when_conditions, falsey_scope)
|
|
1381
1399
|
end
|
|
1382
1400
|
end
|
|
1383
1401
|
|
|
1402
|
+
# Issue #1495 — a `case` without a subject tests each `when` condition for truthiness, in order, and stops at the
|
|
1403
|
+
# first truthy one, so a clause is the `||` of its conditions in an `if` / `elsif` chain. Each condition is
|
|
1404
|
+
# evaluated under the falsey edges of the conditions before it, in this clause and every earlier one; the body
|
|
1405
|
+
# joins the conditions' truthy edges, and the next clause starts from the last falsey edge. A condition
|
|
1406
|
+
# `Narrowing.predicate_scopes` does not recognise narrows nothing: a splat (`when *conds`), and a regexp literal,
|
|
1407
|
+
# which is tested for its truthiness and matches nothing, so it binds no match global (#1486).
|
|
1408
|
+
def subjectless_when_scopes(conditions, entry)
|
|
1409
|
+
falsey = entry
|
|
1410
|
+
truthy_edges = conditions.map do |condition|
|
|
1411
|
+
_type, _post, truthy, falsey = eval_with_edges(condition, falsey)
|
|
1412
|
+
truthy
|
|
1413
|
+
end
|
|
1414
|
+
[reduce_scopes_with_nil_injection(truthy_edges), falsey]
|
|
1415
|
+
end
|
|
1416
|
+
|
|
1384
1417
|
# ADR-47 WD3a — a bare class pattern matches on `C === subject`, i.e. exactly `subject.is_a?(C)` with no
|
|
1385
1418
|
# deconstruction, so it narrows like `when C`: the body sees the subject narrowed to `C` and the next clause's
|
|
1386
1419
|
# falsey scope has `C` removed. Other patterns can fail to match even when a class test would pass (deconstruction
|
|
1387
1420
|
# arity, hash keys, ...), so removing anything from the falsey scope would be unsound — they keep the conservative
|
|
1388
1421
|
# shape.
|
|
1389
1422
|
def in_branch_body_and_falsey_scopes(subject_type, subject, branch, falsey_scope)
|
|
1390
|
-
|
|
1391
|
-
unless class_node
|
|
1392
|
-
bound = apply_in_pattern_bindings(subject_type, subject, branch.pattern, falsey_scope)
|
|
1393
|
-
return [bound, falsey_scope]
|
|
1394
|
-
end
|
|
1395
|
-
|
|
1396
|
-
truthy_scope, narrowed_falsey = Narrowing.case_when_scopes(subject, [class_node], falsey_scope)
|
|
1423
|
+
truthy_scope, narrowed_falsey = Narrowing.case_clause_scopes(subject, branch, falsey_scope)
|
|
1397
1424
|
[apply_in_pattern_bindings(subject_type, subject, branch.pattern, truthy_scope), narrowed_falsey]
|
|
1398
1425
|
end
|
|
1399
1426
|
|
|
1400
|
-
# The class-constant node of a `in C` / `in C => x` pattern (the only `in` shapes whose match is pure `is_a?`), or
|
|
1401
|
-
# nil for any pattern that deconstructs, binds, or matches a value.
|
|
1402
|
-
def bare_class_pattern_node(pattern)
|
|
1403
|
-
case pattern
|
|
1404
|
-
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
1405
|
-
pattern
|
|
1406
|
-
when Prism::CapturePatternNode
|
|
1407
|
-
value = pattern.value
|
|
1408
|
-
value if value.is_a?(Prism::ConstantReadNode) || value.is_a?(Prism::ConstantPathNode)
|
|
1409
|
-
end
|
|
1410
|
-
end
|
|
1411
|
-
|
|
1412
1427
|
def eval_case_else(else_clause, falsey_scope)
|
|
1413
1428
|
return sub_eval(else_clause, falsey_scope) if else_clause
|
|
1414
1429
|
|
|
@@ -1477,7 +1492,8 @@ module Rigor
|
|
|
1477
1492
|
# prefix of the body, and so reads it forgotten whenever the body may set it.
|
|
1478
1493
|
entry = edge ? forget_guard_if_rebinds(LastLine.forget_if_set(scope, node), node) : scope
|
|
1479
1494
|
primary_type, primary_scope = eval_begin_primary_under(node, entry, edge: edge)
|
|
1480
|
-
rescue_chain = collect_rescue_chain_results(node.rescue_clause, rescue_entry_scope(node, entry),
|
|
1495
|
+
rescue_chain = collect_rescue_chain_results(node.rescue_clause, rescue_entry_scope(node, entry),
|
|
1496
|
+
edge: edge, writes: rescue_writes(node, edge))
|
|
1481
1497
|
|
|
1482
1498
|
# B2.1 — retry-edge widening. When a `retry` in the rescue chain targets this `begin`, control re-enters the
|
|
1483
1499
|
# primary body carrying every rebind made before the retry: the arm's (`rescue; tries += 1; retry; end`), and
|
|
@@ -1496,13 +1512,132 @@ module Rigor
|
|
|
1496
1512
|
exit_scope = reduce_scopes_with_nil_injection([primary_scope, *live_rescues.map(&:last)])
|
|
1497
1513
|
end
|
|
1498
1514
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1515
|
+
[exit_type, node.ensure_clause ? eval_begin_ensure(node, exit_scope, jump_marks) : exit_scope]
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1518
|
+
# The scope past `node`'s `ensure` clause, entered with `exit_scope`, the `next` and `break` scopes recorded past
|
|
1519
|
+
# `jump_marks` carried through it.
|
|
1520
|
+
def eval_begin_ensure(node, exit_scope, jump_marks)
|
|
1521
|
+
carry_jumps_through_ensure(node.ensure_clause, jump_marks)
|
|
1522
|
+
eval_widened_clause(node.ensure_clause, node.ensure_clause.statements, exit_scope, ensure_writes(node)).last
|
|
1523
|
+
end
|
|
1524
|
+
|
|
1525
|
+
# Issue #1231 — a rescue clause runs after the body raised from any point in it, and an `ensure` clause after the
|
|
1526
|
+
# body, its `else` or a rescue clause raised as well as after they finished, so a clause reads each local or
|
|
1527
|
+
# instance variable those wrote at a value the scope it enters with does not hold: `conn = nil; begin; conn =
|
|
1528
|
+
# open_conn; Integer(s); rescue; conn.size; end` reads the connection. The engine does not track where the body
|
|
1529
|
+
# raises (the precise recorder of #1397 put new false positives on correct code); it widens instead, and
|
|
1530
|
+
# deliberately imprecisely. Each name the clause mentions ({#clause_mentions}) that its entry binds and those
|
|
1531
|
+
# clauses write, outside a nested `def` or class body, reads as `Dynamic[top]`, which reports nothing. A plain
|
|
1532
|
+
# join would read `x` as `1?` in `x = 1; begin; foo; x = nil; rescue; x.succ; end`, where it cannot be nil, and a
|
|
1533
|
+
# `Dynamic` that keeps the join as its facet still narrows: `x.nil?` on `Dynamic[1 | 2]` leaves a `nil` member
|
|
1534
|
+
# for the next call to report. A name written inside a nested `ensure` clause, which also runs on the way out of
|
|
1535
|
+
# a raise, is covered by the same floor.
|
|
1536
|
+
#
|
|
1537
|
+
# The names the clauses of `node` read from: the primary body's for a rescue clause, none under a retry `edge`.
|
|
1538
|
+
def rescue_writes(node, edge)
|
|
1539
|
+
clause_writes([node.statements]) if node.rescue_clause && edge.nil? && widening_recorded?
|
|
1540
|
+
end
|
|
1541
|
+
|
|
1542
|
+
# For the `ensure` clause: the writes of the body, its `else` and every rescue clause.
|
|
1543
|
+
def ensure_writes(node)
|
|
1544
|
+
return nil unless widening_recorded?
|
|
1545
|
+
|
|
1546
|
+
sources = [node.statements, node.else_clause]
|
|
1547
|
+
current = node.rescue_clause
|
|
1548
|
+
while current
|
|
1549
|
+
sources << current
|
|
1550
|
+
current = current.subsequent
|
|
1551
|
+
end
|
|
1552
|
+
clause_writes(sources)
|
|
1553
|
+
end
|
|
1554
|
+
|
|
1555
|
+
# The local and instance variable names `sources` write, a nested block's included (it may write an outer local),
|
|
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
|
|
1562
|
+
|
|
1563
|
+
CLAUSE_WRITE_NODES = Set[
|
|
1564
|
+
*CapturedLocals::LOCAL_WRITE_NODES,
|
|
1565
|
+
Prism::InstanceVariableWriteNode,
|
|
1566
|
+
Prism::InstanceVariableOperatorWriteNode,
|
|
1567
|
+
Prism::InstanceVariableOrWriteNode,
|
|
1568
|
+
Prism::InstanceVariableAndWriteNode,
|
|
1569
|
+
Prism::InstanceVariableTargetNode
|
|
1570
|
+
].freeze
|
|
1571
|
+
private_constant :CLAUSE_WRITE_NODES
|
|
1572
|
+
|
|
1573
|
+
CLAUSE_MENTION_NODES = Set[
|
|
1574
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, *CLAUSE_WRITE_NODES
|
|
1575
|
+
].freeze
|
|
1576
|
+
private_constant :CLAUSE_MENTION_NODES
|
|
1577
|
+
|
|
1578
|
+
# The names of the `node_classes` nodes under `node` (restricted to `among` when given) added to `found`, outside
|
|
1579
|
+
# a nested `def` or class body.
|
|
1580
|
+
def collect_clause_names(node, node_classes, among, found)
|
|
1581
|
+
return found if SCOPE_BODY_NODES.any? { |klass| node.is_a?(klass) }
|
|
1582
|
+
|
|
1583
|
+
if node_classes.include?(node.class) && (among.nil? || among.include?(node.name))
|
|
1584
|
+
(found ||= Set.new) << node.name
|
|
1503
1585
|
end
|
|
1586
|
+
node.rigor_each_child { |child| found = collect_clause_names(child, node_classes, among, found) }
|
|
1587
|
+
found
|
|
1588
|
+
end
|
|
1504
1589
|
|
|
1505
|
-
|
|
1590
|
+
# `clause_scope` with each name of `writes` that `statements` mentions, and `clause_scope` binds, rebound to
|
|
1591
|
+
# `Dynamic[top]`, or nil when none changes.
|
|
1592
|
+
def widen_clause_entry(clause_scope, statements, writes)
|
|
1593
|
+
return nil if writes.nil? || statements.nil? || !widening_recorded?
|
|
1594
|
+
|
|
1595
|
+
names = collect_clause_names(statements, CLAUSE_MENTION_NODES, writes, nil)
|
|
1596
|
+
return nil if names.nil?
|
|
1597
|
+
|
|
1598
|
+
untyped = Type::Combinator.untyped
|
|
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
|
|
1605
|
+
end
|
|
1606
|
+
|
|
1607
|
+
# Whether a widened pass would record anything: only the check path asks for one ({#initialize}'s
|
|
1608
|
+
# `widen_clauses`), and a pass nothing records is skipped, so a nested clause inside an unrecorded pass is
|
|
1609
|
+
# evaluated once.
|
|
1610
|
+
def widening_recorded?
|
|
1611
|
+
@widen_clauses && !(@on_enter.nil? && @operand_recorder.nil?)
|
|
1612
|
+
end
|
|
1613
|
+
|
|
1614
|
+
# `[type, scope]` of a rescue or `ensure` clause entered with `clause_scope`. When {#widen_clause_entry} widens
|
|
1615
|
+
# it, the clause is evaluated twice: under the widened entry for the per-node index, which is what the check
|
|
1616
|
+
# rules read, and under `clause_scope` without recording, for the value and the scope the code after the
|
|
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)
|
|
1625
|
+
end
|
|
1626
|
+
|
|
1627
|
+
# The recorded pass over a widened clause: it reaches no `return`, `next`, `break` or retry-frame sink.
|
|
1628
|
+
def eval_widened_pass(node, widened)
|
|
1629
|
+
return if node.nil?
|
|
1630
|
+
|
|
1631
|
+
without_jump_sinks { sub_eval(node, widened, next_scope_sink: nil) }
|
|
1632
|
+
end
|
|
1633
|
+
|
|
1634
|
+
# Runs `block` with no `return`, `next`, `break` or retry-frame sink installed.
|
|
1635
|
+
def without_jump_sinks
|
|
1636
|
+
previous = CLAUSE_SINK_KEYS.map { |key| Thread.current[key] }
|
|
1637
|
+
CLAUSE_SINK_KEYS.each { |key| Thread.current[key] = nil }
|
|
1638
|
+
yield
|
|
1639
|
+
ensure
|
|
1640
|
+
CLAUSE_SINK_KEYS.zip(previous).each { |key, value| Thread.current[key] = value } if previous
|
|
1506
1641
|
end
|
|
1507
1642
|
|
|
1508
1643
|
# The scope a rescue clause of `node` enters with: it runs after any prefix of the body, so neither a `$_`
|
|
@@ -1600,6 +1735,10 @@ module Rigor
|
|
|
1600
1735
|
RETRY_FRAMES_KEY = :rigor_retry_frames
|
|
1601
1736
|
private_constant :RETRY_FRAMES_KEY
|
|
1602
1737
|
|
|
1738
|
+
# The thread-local sinks a widened clause pass ({#without_jump_sinks}) leaves uninstalled.
|
|
1739
|
+
CLAUSE_SINK_KEYS = [RETURN_SINK_KEY, NEXT_SINK_KEY, BREAK_SINK_KEY, BREAK_VALUE_SINK_KEY, RETRY_FRAMES_KEY].freeze
|
|
1740
|
+
private_constant :CLAUSE_SINK_KEYS
|
|
1741
|
+
|
|
1603
1742
|
# The retry edge of `node`, or nil when no `retry` in its rescue chain targets it. Allocation-free for a `begin`
|
|
1604
1743
|
# no `retry` targets.
|
|
1605
1744
|
def retry_edge_for(node)
|
|
@@ -1876,13 +2015,23 @@ module Rigor
|
|
|
1876
2015
|
# each arm holding one. The latter still carries a rebind the `retry` itself does not see: a write an `ensure`
|
|
1877
2016
|
# runs on the way out (`begin; retry; ensure; tries += 1; end`), or one made before a `retry` the evaluator only
|
|
1878
2017
|
# types (`log(tries < 5 ? retry : :gave_up)`).
|
|
1879
|
-
|
|
2018
|
+
#
|
|
2019
|
+
# Issue #1231 — `writes` widens the scope each clause enters with, as {#eval_widened_clause} does, before its
|
|
2020
|
+
# reference is bound, so `rescue => e` still binds `e` to the exception. It is nil under a retry edge, which
|
|
2021
|
+
# already carries every scope the primary body raises from ({#widen_entry_for_retry}).
|
|
2022
|
+
def collect_rescue_chain_results(rescue_node, entry_scope, edge: nil, writes: nil)
|
|
1880
2023
|
on_enter = edge ? retry_scope_recorder(edge) : @on_enter
|
|
1881
2024
|
results = []
|
|
1882
2025
|
current = rescue_node
|
|
1883
2026
|
while current
|
|
2027
|
+
widened = widen_clause_entry(entry_scope, current.statements, writes)
|
|
2028
|
+
eval_widened_pass(current.statements, bind_rescue_reference(current, widened)) if widened
|
|
1884
2029
|
rescue_scope = bind_rescue_reference(current, entry_scope)
|
|
1885
|
-
arm =
|
|
2030
|
+
arm = if widened
|
|
2031
|
+
sub_eval(current.statements, rescue_scope, **UNRECORDED)
|
|
2032
|
+
else
|
|
2033
|
+
eval_branch_or_nil(current.statements, rescue_scope, on_enter: on_enter)
|
|
2034
|
+
end
|
|
1886
2035
|
edge.retry_scopes << arm.last if edge&.retrying_arms&.include?(current)
|
|
1887
2036
|
results << [arm, current]
|
|
1888
2037
|
current = current.subsequent
|
|
@@ -1920,6 +2069,12 @@ module Rigor
|
|
|
1920
2069
|
after.global(:$_) == Type::Combinator.untyped && !LastLine.may_set?(node.statements, scope)
|
|
1921
2070
|
end
|
|
1922
2071
|
|
|
2072
|
+
# Issue #1889 — {#eval_loop} / {#eval_for} with a literal-name `instance_variable_set` in the body joining its
|
|
2073
|
+
# value into the ivar's binding rather than replacing it ({ReflectiveIvarWrite.in_repeating_body}).
|
|
2074
|
+
def eval_repeating_loop(node) = ReflectiveIvarWrite.in_repeating_body { eval_loop(node) }
|
|
2075
|
+
|
|
2076
|
+
def eval_repeating_for(node) = ReflectiveIvarWrite.in_repeating_body { eval_for(node) }
|
|
2077
|
+
|
|
1923
2078
|
# `while pred; body; end` / `until pred; body; end`. The body might run zero or more times, so half-bound names
|
|
1924
2079
|
# degrade to `T | nil` in the post-loop scope. The loop expression itself types as `Constant[nil]` (Slice 3 phase
|
|
1925
2080
|
# 1), reflecting the common case where no `break VALUE` is observed.
|
|
@@ -1952,7 +2107,7 @@ module Rigor
|
|
|
1952
2107
|
# the single-pass join before returning.
|
|
1953
2108
|
if names.empty?
|
|
1954
2109
|
fast = loop_content_writeback(node.statements, base_scope, pre_body: post_pred)
|
|
1955
|
-
return [Type::Combinator.constant_of(nil),
|
|
2110
|
+
return [Type::Combinator.constant_of(nil), loop_exit(node, post_pred, fast)]
|
|
1956
2111
|
end
|
|
1957
2112
|
|
|
1958
2113
|
post_loop = converged_loop_scope(node, post_pred, base_scope, names, body_first, jumps)
|
|
@@ -1961,7 +2116,7 @@ module Rigor
|
|
|
1961
2116
|
# :idle` under `end while state != :idle`) is dead in every converged pass. The single pass runs from the
|
|
1962
2117
|
# un-narrowed post-predicate scope, so its arms stand in for that first iteration.
|
|
1963
2118
|
post_loop = join_break_scopes(post_loop, first_breaks, names) if node.begin_modifier?
|
|
1964
|
-
post_loop =
|
|
2119
|
+
post_loop = loop_exit(node, post_pred, post_loop)
|
|
1965
2120
|
[Type::Combinator.constant_of(nil), post_loop]
|
|
1966
2121
|
end
|
|
1967
2122
|
|
|
@@ -2048,6 +2203,12 @@ module Rigor
|
|
|
2048
2203
|
join_break_scopes(post_loop, arms, names)
|
|
2049
2204
|
end
|
|
2050
2205
|
|
|
2206
|
+
# A loop's continuation: a local its body and predicate only narrowed from `Dynamic[top]` re-joins to it
|
|
2207
|
+
# ({UntypedRejoin}, issue #1840), then the exit edge narrows.
|
|
2208
|
+
def loop_exit(node, post_pred, post_loop)
|
|
2209
|
+
narrow_loop_exit_edge(node, UntypedRejoin.apply(post_loop, post_pred, [node.predicate, node.statements]))
|
|
2210
|
+
end
|
|
2211
|
+
|
|
2051
2212
|
# Item 4 — loop-exit predicate narrowing. A `while pred` / `until pred` loop exits PRECISELY on the predicate's
|
|
2052
2213
|
# exit edge: `while` exits when `pred` is falsey, `until` when `pred` is truthy. So after the loop the
|
|
2053
2214
|
# predicate-assignment target carries the exit polarity — `until line = io.gets; …; end; line.foo` reads `line`
|
|
@@ -2379,6 +2540,12 @@ module Rigor
|
|
|
2379
2540
|
stores
|
|
2380
2541
|
end
|
|
2381
2542
|
|
|
2543
|
+
# A `for` loop's continuation, with a local the body only narrowed from `Dynamic[top]` re-joined to it
|
|
2544
|
+
# ({UntypedRejoin}, issue #1840). The index is written on every iteration.
|
|
2545
|
+
def for_exit(node, post_coll, continuation)
|
|
2546
|
+
UntypedRejoin.apply(continuation, post_coll, [node.index, node.statements])
|
|
2547
|
+
end
|
|
2548
|
+
|
|
2382
2549
|
# `for index in collection; body; end`. Unlike `each {}` blocks, `for` does NOT create a new variable scope: the
|
|
2383
2550
|
# index variable AND every local written in the body leak to the surrounding scope. The collection is evaluated
|
|
2384
2551
|
# once; the body runs zero or more times, so the post-loop scope is the join of the no-iteration scope (just
|
|
@@ -2405,7 +2572,7 @@ module Rigor
|
|
|
2405
2572
|
continuation = join_with_nil_injection(post_coll, body_scope)
|
|
2406
2573
|
pre_existing, body_first = loop_body_local_writes(node.statements, post_coll)
|
|
2407
2574
|
continuation = join_break_scopes(continuation, breaks, pre_existing + body_first)
|
|
2408
|
-
[Type::Combinator.constant_of(nil), continuation]
|
|
2575
|
+
[Type::Combinator.constant_of(nil), for_exit(node, post_coll, continuation)]
|
|
2409
2576
|
end
|
|
2410
2577
|
|
|
2411
2578
|
# Issue #1429 — a `for` body runs after the collection's `each`, which may be a project method, and after itself.
|
|
@@ -2565,7 +2732,7 @@ module Rigor
|
|
|
2565
2732
|
|
|
2566
2733
|
# A dead RHS is still evaluated and its scope still joins, so a write inside it nil-injects exactly as
|
|
2567
2734
|
# before; only its value is dropped, because it cannot be the value of the expression.
|
|
2568
|
-
joined_scope = join_with_nil_injection(left_scope, right_scope)
|
|
2735
|
+
joined_scope = UntypedRejoin.apply(join_with_nil_injection(left_scope, right_scope), left_scope, [node.right])
|
|
2569
2736
|
type = skipped_type
|
|
2570
2737
|
type = Type::Combinator.union(skipped_type, right_type) unless right_operand_dead?(node, left_type, left_scope)
|
|
2571
2738
|
return [type, joined_scope] unless edges
|
|
@@ -2772,6 +2939,7 @@ module Rigor
|
|
|
2772
2939
|
# evaluates `def` to the method's name as a Symbol, so the produced type is `Constant[:name]`.
|
|
2773
2940
|
def eval_def(node)
|
|
2774
2941
|
body_scope = build_method_entry_scope(node)
|
|
2942
|
+
body_scope = body_scope.with_repeated_or_writes(RepeatedOrWrites.straight_line_sites(node.body, body_scope))
|
|
2775
2943
|
# Parameter default value expressions (e.g. `self.x` in `def copy(x: self.x)`) execute when the method is
|
|
2776
2944
|
# *invoked*, not when the `def` is read; their `self` is the instance receiver, not the surrounding class body.
|
|
2777
2945
|
# Walk the parameters subtree under `body_scope` so the scope-index records the instance `self_type` for every
|
|
@@ -3013,7 +3181,10 @@ module Rigor
|
|
|
3013
3181
|
# observes the seed's union (not the pre-call narrowed value). Always-safe (only widens; no new facts). See
|
|
3014
3182
|
# [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md) § "Flow-folding" — G2 intervening-call case.
|
|
3015
3183
|
post_scope = invalidate_ivars_for_intervening_call(node, post_scope) if statement_call
|
|
3016
|
-
#
|
|
3184
|
+
# Issue #1889 — `instance_variable_set` on `self` writes an ivar no write node shows. It runs after the reset
|
|
3185
|
+
# above, which would join a literal-name write back into the class seed.
|
|
3186
|
+
post_scope = ReflectiveIvarWrite.apply(node, post_scope) { type_operand(ReflectiveIvarWrite.value_node(node)) }
|
|
3187
|
+
# C1 — regex match-data globals (`$~`, `$1..$N`, `$&`, …) are narrowed to non-nil on a successful-match edge; a
|
|
3017
3188
|
# later call that itself runs a regex match rebinds them, so the narrowed facts must be dropped. We forget them
|
|
3018
3189
|
# only when the call may run a match in this frame ({#rebinds_match_globals?}). A call provably match-free on a
|
|
3019
3190
|
# known receiver — `$3.to_i`, `year < 50` — does NOT clobber, so the multi-statement `m = /…/ =~ s; …; use($2)`
|
|
@@ -3034,7 +3205,13 @@ module Rigor
|
|
|
3034
3205
|
post_scope = post_scope.forget_match_globals if rebinds_match_globals?(node, post_scope)
|
|
3035
3206
|
post_scope = post_scope.forget_last_line if rebinds_last_line?(node, post_scope)
|
|
3036
3207
|
if post_scope.guard_narrowed? && GuardRebinding.call_may_rebind?(node, operand_scope)
|
|
3037
|
-
|
|
3208
|
+
# Issue #1596 — a self-call proven to write only some instance variables restores only theirs.
|
|
3209
|
+
writes = intervening_call_candidate?(node) ? self_call_ivar_writes(node) : nil
|
|
3210
|
+
post_scope = if writes
|
|
3211
|
+
writes.reduce(post_scope) { |acc, name| acc.forget_guard_narrowing_of(:ivar, name) }
|
|
3212
|
+
else
|
|
3213
|
+
post_scope.forget_guard_narrowings
|
|
3214
|
+
end
|
|
3038
3215
|
end
|
|
3039
3216
|
forget_rescued_status(LastStatus.after(node, post_scope, scope), node)
|
|
3040
3217
|
end
|
|
@@ -3159,6 +3336,7 @@ module Rigor
|
|
|
3159
3336
|
# Returns a scope with each ivar's narrowed local binding widened back to its class-ivar seed value when the call
|
|
3160
3337
|
# is one that could plausibly mutate ivars on the enclosing class (implicit-self or explicit `self.foo`).
|
|
3161
3338
|
# External- receiver calls (`obj.method`) cannot reach the caller's ivars; they pass through unchanged.
|
|
3339
|
+
# Issue #1596 — where the call is proven to write only some ivars ({#self_call_ivar_writes}), only those widen.
|
|
3162
3340
|
def invalidate_ivars_for_intervening_call(call_node, current_scope)
|
|
3163
3341
|
return current_scope unless intervening_call_candidate?(call_node)
|
|
3164
3342
|
|
|
@@ -3173,11 +3351,27 @@ module Rigor
|
|
|
3173
3351
|
local_type = current_scope.ivar(ivar_name)
|
|
3174
3352
|
next if local_type.nil? || local_type == seed_type
|
|
3175
3353
|
|
|
3176
|
-
|
|
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)
|
|
3177
3362
|
end
|
|
3178
3363
|
widened
|
|
3179
3364
|
end
|
|
3180
3365
|
|
|
3366
|
+
# Issue #1596 — the ivars the implicit-self / `self.` call `call_node` may write, or nil when that is not proven
|
|
3367
|
+
# ({SelfCallIvarWrites.of}). Memoised per call node: the reset and the guard restore ask it of the same call.
|
|
3368
|
+
def self_call_ivar_writes(call_node)
|
|
3369
|
+
memo = (@self_call_ivar_writes ||= {}.compare_by_identity)
|
|
3370
|
+
return memo[call_node] if memo.key?(call_node)
|
|
3371
|
+
|
|
3372
|
+
memo[call_node] = SelfCallIvarWrites.of(call_node, operand_scope)
|
|
3373
|
+
end
|
|
3374
|
+
|
|
3181
3375
|
def intervening_call_candidate?(call_node)
|
|
3182
3376
|
return false unless call_node.is_a?(Prism::CallNode)
|
|
3183
3377
|
|
|
@@ -3542,7 +3736,9 @@ module Rigor
|
|
|
3542
3736
|
end
|
|
3543
3737
|
|
|
3544
3738
|
def enter_call_block(node, block)
|
|
3545
|
-
block_entry = narrow_define_method_block_self(
|
|
3739
|
+
block_entry = narrow_define_method_block_self(
|
|
3740
|
+
node, with_repeated_or_write_marks(node, block, repeating_block_entry(node, block))
|
|
3741
|
+
)
|
|
3546
3742
|
# #319 — `Class.new do ... end` and friends evaluate their block as a CLASS BODY (`class_eval`
|
|
3547
3743
|
# semantics): `self` is the freshly created class, so a `def` inside defines an instance method on it
|
|
3548
3744
|
# and `attr_reader` runs as a class-level macro. Enter the block under the same `self_type` /
|
|
@@ -4142,7 +4338,7 @@ module Rigor
|
|
|
4142
4338
|
# The `break` scopes of one unrecorded pass from `converged` — for a capped fixpoint, whose widened binding no
|
|
4143
4339
|
# pass has run from.
|
|
4144
4340
|
def converged_break_arms(call_node, block, converged, targets)
|
|
4145
|
-
entry = block_pass_entry(call_node, block, converged)
|
|
4341
|
+
entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, converged))
|
|
4146
4342
|
sink, = collect_break_scopes { sub_eval(block, entry, **UNRECORDED) }
|
|
4147
4343
|
targeted_scopes(sink, targets)
|
|
4148
4344
|
end
|
|
@@ -4942,7 +5138,8 @@ module Rigor
|
|
|
4942
5138
|
# params / `;`-locals re-bound as usual) and returns the per-name exit binding for `names`. Used as the
|
|
4943
5139
|
# `BodyFixpoint` body-evaluator.
|
|
4944
5140
|
def block_exit_bindings(call_node, block, bindings, names)
|
|
4945
|
-
|
|
5141
|
+
entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, bindings))
|
|
5142
|
+
_type, exit_scope = sub_eval(block, entry)
|
|
4946
5143
|
names.to_h { |name| [name, CapturedLocals.bound_type(exit_scope, name)] }
|
|
4947
5144
|
end
|
|
4948
5145
|
|
|
@@ -4958,6 +5155,38 @@ module Rigor
|
|
|
4958
5155
|
end
|
|
4959
5156
|
end
|
|
4960
5157
|
|
|
5158
|
+
# Issue #1336 — `entry` with the index `||=` sites of a repeating block marked as the generic block-return pass
|
|
5159
|
+
# marks them ({RepeatedOrWrites}, `Scope#with_repeated_or_writes`). The statement pass and every ADR-56
|
|
5160
|
+
# write-back pass type the body from one entry scope just as that pass does, so a slot an earlier run stored
|
|
5161
|
+
# into reads as a lone `Dynamic` there too, and the memoizing reading ({#index_compound_write_value}) answered
|
|
5162
|
+
# one site's rvalue: `last = (cache[:k] ||= :f).itself` beside a `cache[:k] ||= :e` wrote `:f` alone back
|
|
5163
|
+
# into `last`, and `last == :e` folded always-falsey. A call that runs its block at most once marks nothing.
|
|
5164
|
+
def with_repeated_or_write_marks(call_node, block, entry)
|
|
5165
|
+
sites = repeated_or_write_sites(call_node, block)
|
|
5166
|
+
sites.empty? ? entry : entry.with_repeated_or_writes(sites)
|
|
5167
|
+
end
|
|
5168
|
+
|
|
5169
|
+
# The sites {#with_repeated_or_write_marks} lays, once per block: a body that spells no `||=` — nearly every
|
|
5170
|
+
# one — answers none without the repetition question.
|
|
5171
|
+
def repeated_or_write_sites(call_node, block)
|
|
5172
|
+
memo = (@repeated_or_write_sites ||= {}.compare_by_identity)
|
|
5173
|
+
return memo[block] if memo.key?(block)
|
|
5174
|
+
|
|
5175
|
+
memo[block] = compute_repeated_or_write_sites(call_node, block)
|
|
5176
|
+
end
|
|
5177
|
+
|
|
5178
|
+
def compute_repeated_or_write_sites(call_node, block)
|
|
5179
|
+
body = block.body
|
|
5180
|
+
return NO_REPEATED_OR_WRITES if body.nil? || !RepeatedOrWrites.may_hold_or_write?(body)
|
|
5181
|
+
return NO_REPEATED_OR_WRITES if repeating_block_class(call_node).nil?
|
|
5182
|
+
|
|
5183
|
+
stores = CapturedLocals.content_mutations(block, scope, non_locals: true)
|
|
5184
|
+
RepeatedOrWrites.sites(block, stores, scope).at(nil)
|
|
5185
|
+
end
|
|
5186
|
+
|
|
5187
|
+
NO_REPEATED_OR_WRITES = [].freeze
|
|
5188
|
+
private_constant :NO_REPEATED_OR_WRITES
|
|
5189
|
+
|
|
4961
5190
|
NO_CAPTURE_BINDINGS = {}.freeze
|
|
4962
5191
|
private_constant :NO_CAPTURE_BINDINGS
|
|
4963
5192
|
|
|
@@ -5220,7 +5449,7 @@ module Rigor
|
|
|
5220
5449
|
return [] if receiver_type.nil?
|
|
5221
5450
|
|
|
5222
5451
|
arg_types = call_arg_types_for(call_node)
|
|
5223
|
-
MethodDispatcher.expected_block_param_types(
|
|
5452
|
+
expected = MethodDispatcher.expected_block_param_types(
|
|
5224
5453
|
receiver_type: receiver_type,
|
|
5225
5454
|
method_name: call_node.name,
|
|
5226
5455
|
arg_types: arg_types,
|
|
@@ -5228,10 +5457,28 @@ module Rigor
|
|
|
5228
5457
|
scope: scope,
|
|
5229
5458
|
call_node: call_node
|
|
5230
5459
|
)
|
|
5460
|
+
with_inject_accumulator(call_node, receiver_type, arg_types, expected)
|
|
5231
5461
|
rescue StandardError
|
|
5232
5462
|
[]
|
|
5233
5463
|
end
|
|
5234
5464
|
|
|
5465
|
+
# Issue #1889 — a block-form `inject` / `reduce` call's accumulator parameter at the type it holds across
|
|
5466
|
+
# every run ({ExpressionTyper#inject_accumulator_type}) rather than the seed the signature binds it to, once
|
|
5467
|
+
# per call: every write-back pass builds the entry again.
|
|
5468
|
+
def with_inject_accumulator(call_node, receiver_type, arg_types, expected)
|
|
5469
|
+
memo = @inject_accumulator
|
|
5470
|
+
accumulator =
|
|
5471
|
+
if memo && memo[0].equal?(call_node)
|
|
5472
|
+
memo[1]
|
|
5473
|
+
else
|
|
5474
|
+
typed = ExpressionTyper.new(scope: scope, tracer: tracer)
|
|
5475
|
+
.inject_accumulator_type(call_node, receiver_type, arg_types)
|
|
5476
|
+
@inject_accumulator = [call_node, typed]
|
|
5477
|
+
typed
|
|
5478
|
+
end
|
|
5479
|
+
accumulator ? [accumulator, *expected.drop(1)] : expected
|
|
5480
|
+
end
|
|
5481
|
+
|
|
5235
5482
|
def call_arg_types_for(call_node)
|
|
5236
5483
|
arguments = call_node.arguments
|
|
5237
5484
|
return [] if arguments.nil?
|
|
@@ -5662,6 +5909,7 @@ module Rigor
|
|
|
5662
5909
|
class_context: class_context,
|
|
5663
5910
|
lexical_nesting: lexical_nesting,
|
|
5664
5911
|
converged_loop_recording: @converged_loop_recording,
|
|
5912
|
+
widen_clauses: @widen_clauses,
|
|
5665
5913
|
next_scope_sink: next_scope_sink,
|
|
5666
5914
|
operand_scope: operand_scope,
|
|
5667
5915
|
in_operand: in_operand,
|
|
@@ -5956,8 +6204,7 @@ module Rigor
|
|
|
5956
6204
|
# it is frozen, and the dispatcher already projects it (see `DataFolding`).
|
|
5957
6205
|
def struct_instance_projection(subject_type, method_name, scope, subject_node)
|
|
5958
6206
|
return nil unless subject_type.is_a?(Type::StructInstance)
|
|
5959
|
-
return nil unless MethodDispatcher::StructMaterialization.materialization_call?(subject_node,
|
|
5960
|
-
scope)
|
|
6207
|
+
return nil unless MethodDispatcher::StructMaterialization.materialization_call?(subject_node, scope)
|
|
5961
6208
|
|
|
5962
6209
|
case method_name
|
|
5963
6210
|
when :deconstruct then Type::Combinator.tuple_of(*subject_type.members.values)
|