rigortype 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/docs/handbook/11-sig-gen.md +12 -5
- data/docs/manual/02-cli-reference.md +19 -1
- data/docs/manual/03-configuration.md +6 -4
- data/docs/manual/05-inspecting-types.md +50 -0
- 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 +16 -9
- data/lib/rigor/analysis/check_rules/rule_ids.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +23 -22
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +89 -22
- 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 +119 -15
- data/lib/rigor/analysis/reachability/scan.rb +332 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- data/lib/rigor/analysis/run_cache_probe.rb +1 -1
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +26 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +28 -9
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -1
- data/lib/rigor/analysis/runner.rb +29 -11
- data/lib/rigor/analysis/worker_session.rb +1 -1
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +36 -8
- 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/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/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 +33 -0
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +43 -3
- data/lib/rigor/inference/expression_typer.rb +58 -25
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- 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/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/repeated_or_writes.rb +2 -0
- data/lib/rigor/inference/scope_indexer.rb +140 -5
- data/lib/rigor/inference/statement_evaluator.rb +223 -31
- 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 +118 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +433 -0
- data/lib/rigor/lens/members.rb +153 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +280 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +40 -0
- data/lib/rigor/project_environment.rb +20 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +22 -11
- data/lib/rigor/sig_gen/generator.rb +45 -5
- 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/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +18 -1
|
@@ -42,8 +42,10 @@ require_relative "operand_effects"
|
|
|
42
42
|
require_relative "operand_walk"
|
|
43
43
|
require_relative "optimistic_origin"
|
|
44
44
|
require_relative "return_barrier"
|
|
45
|
+
require_relative "repeated_or_writes"
|
|
45
46
|
require_relative "rewrite_mutation"
|
|
46
47
|
require_relative "unknown_store_widening"
|
|
48
|
+
require_relative "untyped_rejoin"
|
|
47
49
|
require_relative "version_guard"
|
|
48
50
|
|
|
49
51
|
module Rigor
|
|
@@ -278,11 +280,15 @@ module Rigor
|
|
|
278
280
|
# a declined `class self::X`, a parse-error header), where `class_context` is empty without the code
|
|
279
281
|
# being at the top level. A `def` (or `class << self`) there runs under an untyped `self` instead of
|
|
280
282
|
# the top level's nil one, so its implicit-self calls answer `Dynamic` and no top-level rule fires.
|
|
283
|
+
# @param widen_clauses — issue #1231: when true (and a recorder is installed), a rescue or `ensure` clause
|
|
284
|
+
# records its per-node entry scopes under {#widen_clause_entry}'s widening. The check path sets it; every
|
|
285
|
+
# other reader of the index (parameter inference, sig-gen, the scanners) keeps the clause's plain entry.
|
|
281
286
|
def initialize(scope:, tracer: nil, on_enter: nil, class_context: [].freeze, # rubocop:disable Metrics/ParameterLists
|
|
282
287
|
lexical_nesting: EMPTY_NESTING, converged_loop_recording: false, next_scope_sink: nil,
|
|
283
288
|
operand_scope: nil, in_operand: false, operand_recorder: nil, operand_types: nil,
|
|
284
|
-
opaque_class: false)
|
|
289
|
+
opaque_class: false, widen_clauses: false)
|
|
285
290
|
@scope = scope
|
|
291
|
+
@widen_clauses = widen_clauses
|
|
286
292
|
@opaque_class = opaque_class
|
|
287
293
|
@tracer = tracer
|
|
288
294
|
@on_enter = on_enter
|
|
@@ -484,9 +490,10 @@ module Rigor
|
|
|
484
490
|
# `opts[k] ||= raise KeyError` is a guard, never `bot`, and `@flags[n] ||= false` is `true` after an
|
|
485
491
|
# `@flags[n] = true` elsewhere, never provably `false`.
|
|
486
492
|
#
|
|
487
|
-
# Nor is a site
|
|
488
|
-
#
|
|
489
|
-
# stored rather than an
|
|
493
|
+
# Nor is a marked site (`Scope#repeated_or_write?`, {RepeatedOrWrites}): a block-return pass, and this
|
|
494
|
+
# evaluator's statement and write-back passes ({#with_repeated_or_write_marks}), type every run of a repeating
|
|
495
|
+
# body from one entry scope, so its slot's gradual type may be what an EARLIER run stored rather than an
|
|
496
|
+
# absence of evidence.
|
|
490
497
|
def index_compound_write_value(node)
|
|
491
498
|
return index_write_stored_type(node, scope) unless node.is_a?(Prism::IndexOrWriteNode)
|
|
492
499
|
|
|
@@ -1180,7 +1187,8 @@ module Rigor
|
|
|
1180
1187
|
|
|
1181
1188
|
[
|
|
1182
1189
|
Type::Combinator.union(then_type, else_type),
|
|
1183
|
-
join_with_nil_injection(then_scope, else_scope)
|
|
1190
|
+
UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred,
|
|
1191
|
+
[node.statements, node.subsequent])
|
|
1184
1192
|
]
|
|
1185
1193
|
end
|
|
1186
1194
|
|
|
@@ -1215,7 +1223,8 @@ module Rigor
|
|
|
1215
1223
|
|
|
1216
1224
|
[
|
|
1217
1225
|
Type::Combinator.union(then_type, else_type),
|
|
1218
|
-
join_with_nil_injection(then_scope, else_scope)
|
|
1226
|
+
UntypedRejoin.apply(join_with_nil_injection(then_scope, else_scope), post_pred,
|
|
1227
|
+
[node.statements, node.else_clause])
|
|
1219
1228
|
]
|
|
1220
1229
|
end
|
|
1221
1230
|
|
|
@@ -1286,7 +1295,7 @@ module Rigor
|
|
|
1286
1295
|
subject_type, post_pred = node.predicate ? sub_eval(node.predicate, scope) : [nil, scope]
|
|
1287
1296
|
branch_results, falsey_scope = eval_case_when_branches(subject_type, node.predicate, node.conditions, post_pred)
|
|
1288
1297
|
if pattern_case_matches_every_path?(node, branch_results)
|
|
1289
|
-
return unmatched_pattern_result(branch_results, node.conditions)
|
|
1298
|
+
return unmatched_pattern_result(branch_results, node.conditions, post_pred)
|
|
1290
1299
|
end
|
|
1291
1300
|
|
|
1292
1301
|
else_result = eval_case_else(node.else_clause, falsey_scope)
|
|
@@ -1295,7 +1304,7 @@ module Rigor
|
|
|
1295
1304
|
branch_nodes = [*node.conditions, node.else_clause]
|
|
1296
1305
|
[
|
|
1297
1306
|
Type::Combinator.union(*all_results.map(&:first)),
|
|
1298
|
-
join_case_branch_scopes(all_results, branch_nodes)
|
|
1307
|
+
join_case_branch_scopes(all_results, branch_nodes, post_pred)
|
|
1299
1308
|
]
|
|
1300
1309
|
end
|
|
1301
1310
|
|
|
@@ -1310,8 +1319,9 @@ module Rigor
|
|
|
1310
1319
|
node.is_a?(Prism::CaseMatchNode) && node.else_clause.nil? && !branch_results.empty?
|
|
1311
1320
|
end
|
|
1312
1321
|
|
|
1313
|
-
def unmatched_pattern_result(branch_results, branch_nodes)
|
|
1314
|
-
[Type::Combinator.union(*branch_results.map(&:first)),
|
|
1322
|
+
def unmatched_pattern_result(branch_results, branch_nodes, entry)
|
|
1323
|
+
[Type::Combinator.union(*branch_results.map(&:first)),
|
|
1324
|
+
join_case_branch_scopes(branch_results, branch_nodes, entry)]
|
|
1315
1325
|
end
|
|
1316
1326
|
|
|
1317
1327
|
# Joins the post-scopes of every `when`/`in`/`else` branch, dropping the scope of any branch that terminates
|
|
@@ -1320,15 +1330,16 @@ module Rigor
|
|
|
1320
1330
|
# `branch_terminates?` rule `eval_if`/`eval_unless` already apply to the if/else merge: e.g. `case x; when 1 then
|
|
1321
1331
|
# v="a"; when 2 then v="b"; else raise; end` keeps `v: "a" | "b"` instead of `... | nil`. When every branch
|
|
1322
1332
|
# terminates the merge is itself unreachable; fall back to the full join so the continuation scope stays
|
|
1323
|
-
# well-formed.
|
|
1324
|
-
|
|
1333
|
+
# well-formed. A local the clauses only narrowed from `Dynamic[top]` re-joins to it ({UntypedRejoin}, issue
|
|
1334
|
+
# #1840), read against the post-predicate `entry`.
|
|
1335
|
+
def join_case_branch_scopes(results, nodes, entry)
|
|
1325
1336
|
live = []
|
|
1326
1337
|
results.each_with_index do |(type, branch_scope), i|
|
|
1327
1338
|
live << branch_scope unless branch_terminates?(nodes[i], type)
|
|
1328
1339
|
end
|
|
1329
1340
|
|
|
1330
1341
|
live = results.map(&:last) if live.empty?
|
|
1331
|
-
reduce_scopes_with_nil_injection(live)
|
|
1342
|
+
UntypedRejoin.apply(reduce_scopes_with_nil_injection(live), entry, nodes)
|
|
1332
1343
|
end
|
|
1333
1344
|
|
|
1334
1345
|
def eval_case_when_branches(subject_type, subject, conditions, entry_scope)
|
|
@@ -1477,7 +1488,8 @@ module Rigor
|
|
|
1477
1488
|
# prefix of the body, and so reads it forgotten whenever the body may set it.
|
|
1478
1489
|
entry = edge ? forget_guard_if_rebinds(LastLine.forget_if_set(scope, node), node) : scope
|
|
1479
1490
|
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),
|
|
1491
|
+
rescue_chain = collect_rescue_chain_results(node.rescue_clause, rescue_entry_scope(node, entry),
|
|
1492
|
+
edge: edge, writes: rescue_writes(node, edge))
|
|
1481
1493
|
|
|
1482
1494
|
# B2.1 — retry-edge widening. When a `retry` in the rescue chain targets this `begin`, control re-enters the
|
|
1483
1495
|
# primary body carrying every rebind made before the retry: the arm's (`rescue; tries += 1; retry; end`), and
|
|
@@ -1496,13 +1508,132 @@ module Rigor
|
|
|
1496
1508
|
exit_scope = reduce_scopes_with_nil_injection([primary_scope, *live_rescues.map(&:last)])
|
|
1497
1509
|
end
|
|
1498
1510
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1511
|
+
[exit_type, node.ensure_clause ? eval_begin_ensure(node, exit_scope, jump_marks) : exit_scope]
|
|
1512
|
+
end
|
|
1513
|
+
|
|
1514
|
+
# The scope past `node`'s `ensure` clause, entered with `exit_scope`, the `next` and `break` scopes recorded past
|
|
1515
|
+
# `jump_marks` carried through it.
|
|
1516
|
+
def eval_begin_ensure(node, exit_scope, jump_marks)
|
|
1517
|
+
carry_jumps_through_ensure(node.ensure_clause, jump_marks)
|
|
1518
|
+
eval_widened_clause(node.ensure_clause, node.ensure_clause.statements, exit_scope, ensure_writes(node)).last
|
|
1519
|
+
end
|
|
1520
|
+
|
|
1521
|
+
# Issue #1231 — a rescue clause runs after the body raised from any point in it, and an `ensure` clause after the
|
|
1522
|
+
# body, its `else` or a rescue clause raised as well as after they finished, so a clause reads each local or
|
|
1523
|
+
# instance variable those wrote at a value the scope it enters with does not hold: `conn = nil; begin; conn =
|
|
1524
|
+
# open_conn; Integer(s); rescue; conn.size; end` reads the connection. The engine does not track where the body
|
|
1525
|
+
# raises (the precise recorder of #1397 put new false positives on correct code); it widens instead, and
|
|
1526
|
+
# deliberately imprecisely. Each name the clause mentions ({#clause_mentions}) that its entry binds and those
|
|
1527
|
+
# clauses write, outside a nested `def` or class body, reads as `Dynamic[top]`, which reports nothing. A plain
|
|
1528
|
+
# join would read `x` as `1?` in `x = 1; begin; foo; x = nil; rescue; x.succ; end`, where it cannot be nil, and a
|
|
1529
|
+
# `Dynamic` that keeps the join as its facet still narrows: `x.nil?` on `Dynamic[1 | 2]` leaves a `nil` member
|
|
1530
|
+
# for the next call to report. A name written inside a nested `ensure` clause, which also runs on the way out of
|
|
1531
|
+
# a raise, is covered by the same floor.
|
|
1532
|
+
#
|
|
1533
|
+
# The names the clauses of `node` read from: the primary body's for a rescue clause, none under a retry `edge`.
|
|
1534
|
+
def rescue_writes(node, edge)
|
|
1535
|
+
clause_writes([node.statements]) if node.rescue_clause && edge.nil? && widening_recorded?
|
|
1536
|
+
end
|
|
1537
|
+
|
|
1538
|
+
# For the `ensure` clause: the writes of the body, its `else` and every rescue clause.
|
|
1539
|
+
def ensure_writes(node)
|
|
1540
|
+
return nil unless widening_recorded?
|
|
1541
|
+
|
|
1542
|
+
sources = [node.statements, node.else_clause]
|
|
1543
|
+
current = node.rescue_clause
|
|
1544
|
+
while current
|
|
1545
|
+
sources << current
|
|
1546
|
+
current = current.subsequent
|
|
1503
1547
|
end
|
|
1548
|
+
clause_writes(sources)
|
|
1549
|
+
end
|
|
1550
|
+
|
|
1551
|
+
# The local and instance variable names `sources` write, a nested block's included (it may write an outer local),
|
|
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
|
|
1558
|
+
|
|
1559
|
+
CLAUSE_WRITE_NODES = Set[
|
|
1560
|
+
*CapturedLocals::LOCAL_WRITE_NODES,
|
|
1561
|
+
Prism::InstanceVariableWriteNode,
|
|
1562
|
+
Prism::InstanceVariableOperatorWriteNode,
|
|
1563
|
+
Prism::InstanceVariableOrWriteNode,
|
|
1564
|
+
Prism::InstanceVariableAndWriteNode,
|
|
1565
|
+
Prism::InstanceVariableTargetNode
|
|
1566
|
+
].freeze
|
|
1567
|
+
private_constant :CLAUSE_WRITE_NODES
|
|
1568
|
+
|
|
1569
|
+
CLAUSE_MENTION_NODES = Set[
|
|
1570
|
+
Prism::LocalVariableReadNode, Prism::InstanceVariableReadNode, *CLAUSE_WRITE_NODES
|
|
1571
|
+
].freeze
|
|
1572
|
+
private_constant :CLAUSE_MENTION_NODES
|
|
1573
|
+
|
|
1574
|
+
# The names of the `node_classes` nodes under `node` (restricted to `among` when given) added to `found`, outside
|
|
1575
|
+
# a nested `def` or class body.
|
|
1576
|
+
def collect_clause_names(node, node_classes, among, found)
|
|
1577
|
+
return found if SCOPE_BODY_NODES.any? { |klass| node.is_a?(klass) }
|
|
1578
|
+
|
|
1579
|
+
if node_classes.include?(node.class) && (among.nil? || among.include?(node.name))
|
|
1580
|
+
(found ||= Set.new) << node.name
|
|
1581
|
+
end
|
|
1582
|
+
node.rigor_each_child { |child| found = collect_clause_names(child, node_classes, among, found) }
|
|
1583
|
+
found
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
# `clause_scope` with each name of `writes` that `statements` mentions, and `clause_scope` binds, rebound to
|
|
1587
|
+
# `Dynamic[top]`, or nil when none changes.
|
|
1588
|
+
def widen_clause_entry(clause_scope, statements, writes)
|
|
1589
|
+
return nil if writes.nil? || statements.nil? || !widening_recorded?
|
|
1590
|
+
|
|
1591
|
+
names = collect_clause_names(statements, CLAUSE_MENTION_NODES, writes, nil)
|
|
1592
|
+
return nil if names.nil?
|
|
1593
|
+
|
|
1594
|
+
untyped = Type::Combinator.untyped
|
|
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
|
|
1601
|
+
end
|
|
1504
1602
|
|
|
1505
|
-
|
|
1603
|
+
# Whether a widened pass would record anything: only the check path asks for one ({#initialize}'s
|
|
1604
|
+
# `widen_clauses`), and a pass nothing records is skipped, so a nested clause inside an unrecorded pass is
|
|
1605
|
+
# evaluated once.
|
|
1606
|
+
def widening_recorded?
|
|
1607
|
+
@widen_clauses && !(@on_enter.nil? && @operand_recorder.nil?)
|
|
1608
|
+
end
|
|
1609
|
+
|
|
1610
|
+
# `[type, scope]` of a rescue or `ensure` clause entered with `clause_scope`. When {#widen_clause_entry} widens
|
|
1611
|
+
# it, the clause is evaluated twice: under the widened entry for the per-node index, which is what the check
|
|
1612
|
+
# rules read, and under `clause_scope` without recording, for the value and the scope the code after the
|
|
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)
|
|
1621
|
+
end
|
|
1622
|
+
|
|
1623
|
+
# The recorded pass over a widened clause: it reaches no `return`, `next`, `break` or retry-frame sink.
|
|
1624
|
+
def eval_widened_pass(node, widened)
|
|
1625
|
+
return if node.nil?
|
|
1626
|
+
|
|
1627
|
+
without_jump_sinks { sub_eval(node, widened, next_scope_sink: nil) }
|
|
1628
|
+
end
|
|
1629
|
+
|
|
1630
|
+
# Runs `block` with no `return`, `next`, `break` or retry-frame sink installed.
|
|
1631
|
+
def without_jump_sinks
|
|
1632
|
+
previous = CLAUSE_SINK_KEYS.map { |key| Thread.current[key] }
|
|
1633
|
+
CLAUSE_SINK_KEYS.each { |key| Thread.current[key] = nil }
|
|
1634
|
+
yield
|
|
1635
|
+
ensure
|
|
1636
|
+
CLAUSE_SINK_KEYS.zip(previous).each { |key, value| Thread.current[key] = value } if previous
|
|
1506
1637
|
end
|
|
1507
1638
|
|
|
1508
1639
|
# The scope a rescue clause of `node` enters with: it runs after any prefix of the body, so neither a `$_`
|
|
@@ -1600,6 +1731,10 @@ module Rigor
|
|
|
1600
1731
|
RETRY_FRAMES_KEY = :rigor_retry_frames
|
|
1601
1732
|
private_constant :RETRY_FRAMES_KEY
|
|
1602
1733
|
|
|
1734
|
+
# The thread-local sinks a widened clause pass ({#without_jump_sinks}) leaves uninstalled.
|
|
1735
|
+
CLAUSE_SINK_KEYS = [RETURN_SINK_KEY, NEXT_SINK_KEY, BREAK_SINK_KEY, BREAK_VALUE_SINK_KEY, RETRY_FRAMES_KEY].freeze
|
|
1736
|
+
private_constant :CLAUSE_SINK_KEYS
|
|
1737
|
+
|
|
1603
1738
|
# The retry edge of `node`, or nil when no `retry` in its rescue chain targets it. Allocation-free for a `begin`
|
|
1604
1739
|
# no `retry` targets.
|
|
1605
1740
|
def retry_edge_for(node)
|
|
@@ -1876,13 +2011,23 @@ module Rigor
|
|
|
1876
2011
|
# each arm holding one. The latter still carries a rebind the `retry` itself does not see: a write an `ensure`
|
|
1877
2012
|
# runs on the way out (`begin; retry; ensure; tries += 1; end`), or one made before a `retry` the evaluator only
|
|
1878
2013
|
# types (`log(tries < 5 ? retry : :gave_up)`).
|
|
1879
|
-
|
|
2014
|
+
#
|
|
2015
|
+
# Issue #1231 — `writes` widens the scope each clause enters with, as {#eval_widened_clause} does, before its
|
|
2016
|
+
# reference is bound, so `rescue => e` still binds `e` to the exception. It is nil under a retry edge, which
|
|
2017
|
+
# already carries every scope the primary body raises from ({#widen_entry_for_retry}).
|
|
2018
|
+
def collect_rescue_chain_results(rescue_node, entry_scope, edge: nil, writes: nil)
|
|
1880
2019
|
on_enter = edge ? retry_scope_recorder(edge) : @on_enter
|
|
1881
2020
|
results = []
|
|
1882
2021
|
current = rescue_node
|
|
1883
2022
|
while current
|
|
2023
|
+
widened = widen_clause_entry(entry_scope, current.statements, writes)
|
|
2024
|
+
eval_widened_pass(current.statements, bind_rescue_reference(current, widened)) if widened
|
|
1884
2025
|
rescue_scope = bind_rescue_reference(current, entry_scope)
|
|
1885
|
-
arm =
|
|
2026
|
+
arm = if widened
|
|
2027
|
+
sub_eval(current.statements, rescue_scope, **UNRECORDED)
|
|
2028
|
+
else
|
|
2029
|
+
eval_branch_or_nil(current.statements, rescue_scope, on_enter: on_enter)
|
|
2030
|
+
end
|
|
1886
2031
|
edge.retry_scopes << arm.last if edge&.retrying_arms&.include?(current)
|
|
1887
2032
|
results << [arm, current]
|
|
1888
2033
|
current = current.subsequent
|
|
@@ -1952,7 +2097,7 @@ module Rigor
|
|
|
1952
2097
|
# the single-pass join before returning.
|
|
1953
2098
|
if names.empty?
|
|
1954
2099
|
fast = loop_content_writeback(node.statements, base_scope, pre_body: post_pred)
|
|
1955
|
-
return [Type::Combinator.constant_of(nil),
|
|
2100
|
+
return [Type::Combinator.constant_of(nil), loop_exit(node, post_pred, fast)]
|
|
1956
2101
|
end
|
|
1957
2102
|
|
|
1958
2103
|
post_loop = converged_loop_scope(node, post_pred, base_scope, names, body_first, jumps)
|
|
@@ -1961,7 +2106,7 @@ module Rigor
|
|
|
1961
2106
|
# :idle` under `end while state != :idle`) is dead in every converged pass. The single pass runs from the
|
|
1962
2107
|
# un-narrowed post-predicate scope, so its arms stand in for that first iteration.
|
|
1963
2108
|
post_loop = join_break_scopes(post_loop, first_breaks, names) if node.begin_modifier?
|
|
1964
|
-
post_loop =
|
|
2109
|
+
post_loop = loop_exit(node, post_pred, post_loop)
|
|
1965
2110
|
[Type::Combinator.constant_of(nil), post_loop]
|
|
1966
2111
|
end
|
|
1967
2112
|
|
|
@@ -2048,6 +2193,12 @@ module Rigor
|
|
|
2048
2193
|
join_break_scopes(post_loop, arms, names)
|
|
2049
2194
|
end
|
|
2050
2195
|
|
|
2196
|
+
# A loop's continuation: a local its body and predicate only narrowed from `Dynamic[top]` re-joins to it
|
|
2197
|
+
# ({UntypedRejoin}, issue #1840), then the exit edge narrows.
|
|
2198
|
+
def loop_exit(node, post_pred, post_loop)
|
|
2199
|
+
narrow_loop_exit_edge(node, UntypedRejoin.apply(post_loop, post_pred, [node.predicate, node.statements]))
|
|
2200
|
+
end
|
|
2201
|
+
|
|
2051
2202
|
# Item 4 — loop-exit predicate narrowing. A `while pred` / `until pred` loop exits PRECISELY on the predicate's
|
|
2052
2203
|
# exit edge: `while` exits when `pred` is falsey, `until` when `pred` is truthy. So after the loop the
|
|
2053
2204
|
# predicate-assignment target carries the exit polarity — `until line = io.gets; …; end; line.foo` reads `line`
|
|
@@ -2379,6 +2530,12 @@ module Rigor
|
|
|
2379
2530
|
stores
|
|
2380
2531
|
end
|
|
2381
2532
|
|
|
2533
|
+
# A `for` loop's continuation, with a local the body only narrowed from `Dynamic[top]` re-joined to it
|
|
2534
|
+
# ({UntypedRejoin}, issue #1840). The index is written on every iteration.
|
|
2535
|
+
def for_exit(node, post_coll, continuation)
|
|
2536
|
+
UntypedRejoin.apply(continuation, post_coll, [node.index, node.statements])
|
|
2537
|
+
end
|
|
2538
|
+
|
|
2382
2539
|
# `for index in collection; body; end`. Unlike `each {}` blocks, `for` does NOT create a new variable scope: the
|
|
2383
2540
|
# index variable AND every local written in the body leak to the surrounding scope. The collection is evaluated
|
|
2384
2541
|
# once; the body runs zero or more times, so the post-loop scope is the join of the no-iteration scope (just
|
|
@@ -2405,7 +2562,7 @@ module Rigor
|
|
|
2405
2562
|
continuation = join_with_nil_injection(post_coll, body_scope)
|
|
2406
2563
|
pre_existing, body_first = loop_body_local_writes(node.statements, post_coll)
|
|
2407
2564
|
continuation = join_break_scopes(continuation, breaks, pre_existing + body_first)
|
|
2408
|
-
[Type::Combinator.constant_of(nil), continuation]
|
|
2565
|
+
[Type::Combinator.constant_of(nil), for_exit(node, post_coll, continuation)]
|
|
2409
2566
|
end
|
|
2410
2567
|
|
|
2411
2568
|
# Issue #1429 — a `for` body runs after the collection's `each`, which may be a project method, and after itself.
|
|
@@ -2565,7 +2722,7 @@ module Rigor
|
|
|
2565
2722
|
|
|
2566
2723
|
# A dead RHS is still evaluated and its scope still joins, so a write inside it nil-injects exactly as
|
|
2567
2724
|
# 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)
|
|
2725
|
+
joined_scope = UntypedRejoin.apply(join_with_nil_injection(left_scope, right_scope), left_scope, [node.right])
|
|
2569
2726
|
type = skipped_type
|
|
2570
2727
|
type = Type::Combinator.union(skipped_type, right_type) unless right_operand_dead?(node, left_type, left_scope)
|
|
2571
2728
|
return [type, joined_scope] unless edges
|
|
@@ -3013,7 +3170,7 @@ module Rigor
|
|
|
3013
3170
|
# observes the seed's union (not the pre-call narrowed value). Always-safe (only widens; no new facts). See
|
|
3014
3171
|
# [`docs/CURRENT_WORK.md`](../../../docs/CURRENT_WORK.md) § "Flow-folding" — G2 intervening-call case.
|
|
3015
3172
|
post_scope = invalidate_ivars_for_intervening_call(node, post_scope) if statement_call
|
|
3016
|
-
# C1 — regex match-data globals (`$~`, `$1..$
|
|
3173
|
+
# C1 — regex match-data globals (`$~`, `$1..$N`, `$&`, …) are narrowed to non-nil on a successful-match edge; a
|
|
3017
3174
|
# later call that itself runs a regex match rebinds them, so the narrowed facts must be dropped. We forget them
|
|
3018
3175
|
# only when the call may run a match in this frame ({#rebinds_match_globals?}). A call provably match-free on a
|
|
3019
3176
|
# known receiver — `$3.to_i`, `year < 50` — does NOT clobber, so the multi-statement `m = /…/ =~ s; …; use($2)`
|
|
@@ -3542,7 +3699,9 @@ module Rigor
|
|
|
3542
3699
|
end
|
|
3543
3700
|
|
|
3544
3701
|
def enter_call_block(node, block)
|
|
3545
|
-
block_entry = narrow_define_method_block_self(
|
|
3702
|
+
block_entry = narrow_define_method_block_self(
|
|
3703
|
+
node, with_repeated_or_write_marks(node, block, repeating_block_entry(node, block))
|
|
3704
|
+
)
|
|
3546
3705
|
# #319 — `Class.new do ... end` and friends evaluate their block as a CLASS BODY (`class_eval`
|
|
3547
3706
|
# semantics): `self` is the freshly created class, so a `def` inside defines an instance method on it
|
|
3548
3707
|
# and `attr_reader` runs as a class-level macro. Enter the block under the same `self_type` /
|
|
@@ -4142,7 +4301,7 @@ module Rigor
|
|
|
4142
4301
|
# The `break` scopes of one unrecorded pass from `converged` — for a capped fixpoint, whose widened binding no
|
|
4143
4302
|
# pass has run from.
|
|
4144
4303
|
def converged_break_arms(call_node, block, converged, targets)
|
|
4145
|
-
entry = block_pass_entry(call_node, block, converged)
|
|
4304
|
+
entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, converged))
|
|
4146
4305
|
sink, = collect_break_scopes { sub_eval(block, entry, **UNRECORDED) }
|
|
4147
4306
|
targeted_scopes(sink, targets)
|
|
4148
4307
|
end
|
|
@@ -4942,7 +5101,8 @@ module Rigor
|
|
|
4942
5101
|
# params / `;`-locals re-bound as usual) and returns the per-name exit binding for `names`. Used as the
|
|
4943
5102
|
# `BodyFixpoint` body-evaluator.
|
|
4944
5103
|
def block_exit_bindings(call_node, block, bindings, names)
|
|
4945
|
-
|
|
5104
|
+
entry = with_repeated_or_write_marks(call_node, block, block_pass_entry(call_node, block, bindings))
|
|
5105
|
+
_type, exit_scope = sub_eval(block, entry)
|
|
4946
5106
|
names.to_h { |name| [name, CapturedLocals.bound_type(exit_scope, name)] }
|
|
4947
5107
|
end
|
|
4948
5108
|
|
|
@@ -4958,6 +5118,38 @@ module Rigor
|
|
|
4958
5118
|
end
|
|
4959
5119
|
end
|
|
4960
5120
|
|
|
5121
|
+
# Issue #1336 — `entry` with the index `||=` sites of a repeating block marked as the generic block-return pass
|
|
5122
|
+
# marks them ({RepeatedOrWrites}, `Scope#with_repeated_or_writes`). The statement pass and every ADR-56
|
|
5123
|
+
# write-back pass type the body from one entry scope just as that pass does, so a slot an earlier run stored
|
|
5124
|
+
# into reads as a lone `Dynamic` there too, and the memoizing reading ({#index_compound_write_value}) answered
|
|
5125
|
+
# one site's rvalue: `last = (cache[:k] ||= :f).itself` beside a `cache[:k] ||= :e` wrote `:f` alone back
|
|
5126
|
+
# into `last`, and `last == :e` folded always-falsey. A call that runs its block at most once marks nothing.
|
|
5127
|
+
def with_repeated_or_write_marks(call_node, block, entry)
|
|
5128
|
+
sites = repeated_or_write_sites(call_node, block)
|
|
5129
|
+
sites.empty? ? entry : entry.with_repeated_or_writes(sites)
|
|
5130
|
+
end
|
|
5131
|
+
|
|
5132
|
+
# The sites {#with_repeated_or_write_marks} lays, once per block: a body that spells no `||=` — nearly every
|
|
5133
|
+
# one — answers none without the repetition question.
|
|
5134
|
+
def repeated_or_write_sites(call_node, block)
|
|
5135
|
+
memo = (@repeated_or_write_sites ||= {}.compare_by_identity)
|
|
5136
|
+
return memo[block] if memo.key?(block)
|
|
5137
|
+
|
|
5138
|
+
memo[block] = compute_repeated_or_write_sites(call_node, block)
|
|
5139
|
+
end
|
|
5140
|
+
|
|
5141
|
+
def compute_repeated_or_write_sites(call_node, block)
|
|
5142
|
+
body = block.body
|
|
5143
|
+
return NO_REPEATED_OR_WRITES if body.nil? || !RepeatedOrWrites.may_hold_or_write?(body)
|
|
5144
|
+
return NO_REPEATED_OR_WRITES if repeating_block_class(call_node).nil?
|
|
5145
|
+
|
|
5146
|
+
stores = CapturedLocals.content_mutations(block, scope, non_locals: true)
|
|
5147
|
+
RepeatedOrWrites.sites(block, stores, scope).at(nil)
|
|
5148
|
+
end
|
|
5149
|
+
|
|
5150
|
+
NO_REPEATED_OR_WRITES = [].freeze
|
|
5151
|
+
private_constant :NO_REPEATED_OR_WRITES
|
|
5152
|
+
|
|
4961
5153
|
NO_CAPTURE_BINDINGS = {}.freeze
|
|
4962
5154
|
private_constant :NO_CAPTURE_BINDINGS
|
|
4963
5155
|
|
|
@@ -5662,6 +5854,7 @@ module Rigor
|
|
|
5662
5854
|
class_context: class_context,
|
|
5663
5855
|
lexical_nesting: lexical_nesting,
|
|
5664
5856
|
converged_loop_recording: @converged_loop_recording,
|
|
5857
|
+
widen_clauses: @widen_clauses,
|
|
5665
5858
|
next_scope_sink: next_scope_sink,
|
|
5666
5859
|
operand_scope: operand_scope,
|
|
5667
5860
|
in_operand: in_operand,
|
|
@@ -5956,8 +6149,7 @@ module Rigor
|
|
|
5956
6149
|
# it is frozen, and the dispatcher already projects it (see `DataFolding`).
|
|
5957
6150
|
def struct_instance_projection(subject_type, method_name, scope, subject_node)
|
|
5958
6151
|
return nil unless subject_type.is_a?(Type::StructInstance)
|
|
5959
|
-
return nil unless MethodDispatcher::StructMaterialization.materialization_call?(subject_node,
|
|
5960
|
-
scope)
|
|
6152
|
+
return nil unless MethodDispatcher::StructMaterialization.materialization_call?(subject_node, scope)
|
|
5961
6153
|
|
|
5962
6154
|
case method_name
|
|
5963
6155
|
when :deconstruct then Type::Combinator.tuple_of(*subject_type.members.values)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
require_relative "../source/node_walker"
|
|
5
|
+
require_relative "captured_locals"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
# Issue #1840 — a branch merge that re-joins the narrowed edges of an untyped local gives it back its
|
|
10
|
+
# untyped binding.
|
|
11
|
+
#
|
|
12
|
+
# Flow narrowing concretizes a `Dynamic[top]` receiver (ADR-83): `x.nil?` splits it into `nil` and
|
|
13
|
+
# `Dynamic[top]`, and `when String` into `String` and `Dynamic[top]`. `Scope#join` unions the edges, so
|
|
14
|
+
# after both merges the local reads `Dynamic[top] | String | nil`. That union is not wrong as a set, but
|
|
15
|
+
# every member is a narrowing of the one untyped value, so it says nothing the pre-branch binding did
|
|
16
|
+
# not. It does, however, hold `nil` beside a nameable class, and `call.possible-nil-receiver` reports
|
|
17
|
+
# `x + "a"` on correct code although neither narrowing alone did.
|
|
18
|
+
#
|
|
19
|
+
# The rule: at a merge, a local whose pre-branch binding is exactly `Dynamic[top]`, that no merged arm
|
|
20
|
+
# writes, and whose joined binding is a union still carrying a `Dynamic` member, is bound to
|
|
21
|
+
# `Dynamic[top]` again.
|
|
22
|
+
#
|
|
23
|
+
# - The write test keeps a nil the program puts there: `x = nil if c` writes `x`, so the join stays
|
|
24
|
+
# `Dynamic[top] | nil` and still reports. It is syntactic, over every local-write form under the arms
|
|
25
|
+
# (a pattern capture and a block's write to the outer local included), so a doubtful arm keeps today's
|
|
26
|
+
# join. It does not see a written nil that a `nil?` edge stripped before the merge (`x = nil if c; if
|
|
27
|
+
# x.nil? … elsif x.is_a?(String) … end`): that merge's entry is exactly `Dynamic[top]`, and the outer
|
|
28
|
+
# join's `Dynamic[top] | nil` is one the possible-nil gate declines anyway (#1876).
|
|
29
|
+
# - The `Dynamic`-member test keeps a narrowing whose surviving arms no longer cover the untyped value:
|
|
30
|
+
# `case x when String then … when Integer then … else raise end` leaves `Integer | String`, which is
|
|
31
|
+
# what `x` is past it.
|
|
32
|
+
# - Only the `Dynamic[top]` facet: a faceted `Dynamic[T]` carries static knowledge of its own (a
|
|
33
|
+
# plugin's or a signature's `T`), so a `nil` its narrowing leaves may be one the facet promised. Those
|
|
34
|
+
# keep today's join.
|
|
35
|
+
#
|
|
36
|
+
# Restoring `Dynamic[top]` is always sound, since every narrowed edge is a subset of it; what it drops
|
|
37
|
+
# is only the nameable arms, which an untyped union never needed to dispatch.
|
|
38
|
+
module UntypedRejoin
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# `joined` with each qualifying local rebound to its `entry` binding. `arms` are the nodes whose
|
|
42
|
+
# evaluation produced the merged scopes (`nil` entries are skipped).
|
|
43
|
+
def apply(joined, entry, arms)
|
|
44
|
+
candidates = nil
|
|
45
|
+
joined.locals.each do |name, type|
|
|
46
|
+
next unless candidate?(entry.locals[name], type)
|
|
47
|
+
|
|
48
|
+
(candidates ||= []) << name
|
|
49
|
+
end
|
|
50
|
+
return joined if candidates.nil?
|
|
51
|
+
|
|
52
|
+
written = written_locals(arms)
|
|
53
|
+
kept = candidates.reject { |name| written.include?(name) }
|
|
54
|
+
return joined if kept.empty?
|
|
55
|
+
|
|
56
|
+
joined.with_rejoined_locals(kept.to_h { |name| [name, entry.locals[name]] })
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def candidate?(entry_type, joined_type)
|
|
60
|
+
entry_type.is_a?(Type::Dynamic) && entry_type.static_facet.is_a?(Type::Top) &&
|
|
61
|
+
joined_type.is_a?(Type::Union) && joined_type.members.any?(Type::Dynamic)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def written_locals(arms)
|
|
65
|
+
names = Set.new
|
|
66
|
+
arms.each do |arm|
|
|
67
|
+
Source::NodeWalker.each(arm) do |node|
|
|
68
|
+
names << node.name if CapturedLocals::LOCAL_WRITE_NODES.include?(node.class)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
names
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -6,6 +6,7 @@ require_relative "../cache/store"
|
|
|
6
6
|
require_relative "../analysis/runner"
|
|
7
7
|
require_relative "../analysis/incremental_session"
|
|
8
8
|
require_relative "../cache/incremental_snapshot"
|
|
9
|
+
require_relative "../effects/identity"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
module LanguageServer
|
|
@@ -24,6 +25,10 @@ module Rigor
|
|
|
24
25
|
# are small (the per-publish path is already ≈2ms once Environment is warm).
|
|
25
26
|
#
|
|
26
27
|
# Invalidation:
|
|
28
|
+
# - Every whole-project round (`#project_diagnostics`) re-reads the configuration and compares the
|
|
29
|
+
# incremental snapshot fingerprint and the `effects:` identity with the ones its session was primed with,
|
|
30
|
+
# re-baselining on any change — so no file watcher or `didChangeConfiguration` is needed for
|
|
31
|
+
# `.rigor.yml`, `sig/`, `pre_eval:` or lockfile edits (#1613).
|
|
27
32
|
# - `#invalidate!` drops the cached environment AND project scan + bumps the generation counter; the next
|
|
28
33
|
# reader rebuilds. Watched-file changes (`workspace/didChangeWatchedFiles`) and configuration refreshes
|
|
29
34
|
# (`workspace/didChangeConfiguration`) both trigger this — the next publish observes the new project
|
|
@@ -44,8 +49,13 @@ module Rigor
|
|
|
44
49
|
# invalidated never reach the editor.
|
|
45
50
|
attr_reader :configuration, :generation
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
# `configuration_loader` re-reads the configuration (e.g. `-> { Configuration.load(path) }`); a save round
|
|
53
|
+
# calls it so an edit to `.rigor.yml` (or a file it `includes:`) reaches the server without a restart. Nil
|
|
54
|
+
# keeps the configuration the context was built with.
|
|
55
|
+
def initialize(configuration:, configuration_loader: nil)
|
|
48
56
|
@configuration = configuration
|
|
57
|
+
@configuration_loader = configuration_loader
|
|
58
|
+
@primed_key = nil
|
|
49
59
|
@buffer_sources = {}
|
|
50
60
|
@disk_feature_files = nil
|
|
51
61
|
@environment_features = nil
|
|
@@ -110,16 +120,31 @@ module Rigor
|
|
|
110
120
|
# The session is never persisted. Its state lives as long as this context, which is what a long-running
|
|
111
121
|
# server needs, and writing it would race exactly the way the read-only {#cache_store} exists to avoid.
|
|
112
122
|
# Seeding is therefore one-directional: the terminal warms the server, not the reverse.
|
|
123
|
+
#
|
|
124
|
+
# Every round recomputes {Cache::IncrementalSnapshot.fingerprint} (the function the CLI's `--incremental`
|
|
125
|
+
# keys its snapshot on — one input list) and compares it with the one the session was primed with. A
|
|
126
|
+
# session's recheck only sees source-file digests, so an edit to a `pre_eval:` file, an auto-detected
|
|
127
|
+
# `sig/*.rbs`, a lockfile or `.rigor.yml` would otherwise keep serving answers computed from the old
|
|
128
|
+
# inputs until the server restarts (#1613). A change — including a fingerprint that newly cannot be
|
|
129
|
+
# computed — drops the session and the world built from the old inputs and re-baselines, so the round equals
|
|
130
|
+
# a cold run. The `effects:` identity rides along because `Configuration#to_h` omits `effects:`, so the
|
|
131
|
+
# fingerprint alone does not move on an `effects:` edit. A fingerprint that was ALREADY uncomputable at
|
|
132
|
+
# priming is not a change: that session keeps rechecking.
|
|
133
|
+
# {#generation} is deliberately NOT bumped: the round that notices the change is itself computed from the
|
|
134
|
+
# new inputs, and the publisher discards a round whose generation moved.
|
|
113
135
|
def project_diagnostics
|
|
136
|
+
reload_configuration
|
|
137
|
+
fingerprint = current_fingerprint
|
|
138
|
+
key = [fingerprint, Effects::Identity.digest(configuration: @configuration)]
|
|
139
|
+
rebaseline if @session_primed && key != @primed_key
|
|
114
140
|
session = (@incremental_session ||= build_incremental_session)
|
|
115
141
|
return session.recheck.diagnostics if @session_primed
|
|
116
142
|
|
|
117
143
|
@session_primed = true
|
|
144
|
+
@primed_key = key
|
|
118
145
|
diagnostics, = session.run_incremental(
|
|
119
146
|
snapshot: Cache::IncrementalSnapshot.new(root: @configuration.cache_path),
|
|
120
|
-
fingerprint:
|
|
121
|
-
configuration: @configuration, roots: @configuration.paths
|
|
122
|
-
),
|
|
147
|
+
fingerprint: fingerprint,
|
|
123
148
|
persist: false
|
|
124
149
|
)
|
|
125
150
|
diagnostics
|
|
@@ -153,6 +178,13 @@ module Rigor
|
|
|
153
178
|
# `workspace/didChangeConfiguration`.
|
|
154
179
|
def invalidate!
|
|
155
180
|
@generation += 1
|
|
181
|
+
reset_world
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
# Drops what was built from the previous inputs, without touching {#generation}.
|
|
187
|
+
def reset_world
|
|
156
188
|
@environment = nil
|
|
157
189
|
@environment_features = nil
|
|
158
190
|
@disk_feature_files = nil
|
|
@@ -161,12 +193,31 @@ module Rigor
|
|
|
161
193
|
# outlive them. The next save round primes a fresh one.
|
|
162
194
|
@incremental_session = nil
|
|
163
195
|
@session_primed = false
|
|
196
|
+
@primed_key = nil
|
|
164
197
|
# Cache store stays — it's content-addressed; a stale env build won't be served because the file
|
|
165
198
|
# digest mixed into the cache key has changed.
|
|
166
199
|
nil
|
|
167
200
|
end
|
|
168
201
|
|
|
169
|
-
|
|
202
|
+
# A changed configuration may also move `cache_path`, so the store goes too (unlike {#invalidate!}).
|
|
203
|
+
def rebaseline
|
|
204
|
+
reset_world
|
|
205
|
+
@cache_store = nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def current_fingerprint
|
|
209
|
+
Cache::IncrementalSnapshot.fingerprint(configuration: @configuration, roots: @configuration.paths)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# A load that raises (a half-typed `.rigor.yml`) keeps the previous configuration: the editor must not
|
|
213
|
+
# lose its diagnostics to a transient parse error, and the next round retries.
|
|
214
|
+
def reload_configuration
|
|
215
|
+
return unless @configuration_loader
|
|
216
|
+
|
|
217
|
+
@configuration = @configuration_loader.call
|
|
218
|
+
rescue StandardError
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
170
221
|
|
|
171
222
|
# The session shares this context's warm Environment, so a round does not rebuild the RBS universe. It
|
|
172
223
|
# deliberately does NOT share the prebuilt ProjectScan: a round runs after a save, i.e. after the file
|