rigortype 0.1.19 → 0.2.0
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/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -23
- data/lib/rigor/analysis/check_rules/rule_walk.rb +3 -21
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +24 -15
- data/lib/rigor/analysis/check_rules.rb +492 -71
- data/lib/rigor/analysis/dependency_source_inference/index.rb +4 -7
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -18
- data/lib/rigor/analysis/dependency_source_inference.rb +3 -12
- data/lib/rigor/analysis/fact_store.rb +5 -4
- data/lib/rigor/analysis/rule_catalog.rb +153 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +17 -17
- data/lib/rigor/analysis/runner/project_pre_passes.rb +9 -8
- data/lib/rigor/analysis/runner.rb +17 -6
- data/lib/rigor/analysis/self_call_resolution_recorder.rb +3 -4
- data/lib/rigor/analysis/worker_session.rb +10 -14
- data/lib/rigor/builtins/predefined_constant_refinements.rb +151 -0
- data/lib/rigor/cache/store.rb +5 -3
- data/lib/rigor/cli/annotate_command.rb +28 -7
- data/lib/rigor/cli/baseline_command.rb +4 -3
- data/lib/rigor/cli/check_command.rb +115 -16
- data/lib/rigor/cli/coverage_command.rb +148 -16
- data/lib/rigor/cli/coverage_scan.rb +57 -0
- data/lib/rigor/cli/explain_command.rb +2 -0
- data/lib/rigor/cli/lsp_command.rb +3 -7
- data/lib/rigor/cli/mutation_protection_renderer.rb +63 -0
- data/lib/rigor/cli/mutation_protection_report.rb +73 -0
- data/lib/rigor/cli/options.rb +9 -0
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/protection_renderer.rb +63 -0
- data/lib/rigor/cli/protection_report.rb +68 -0
- data/lib/rigor/cli/sig_gen_command.rb +2 -1
- data/lib/rigor/cli/trace_command.rb +2 -1
- data/lib/rigor/cli/triage_command.rb +2 -1
- data/lib/rigor/cli/type_of_command.rb +1 -1
- data/lib/rigor/cli/type_scan_command.rb +2 -1
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/configuration/dependencies.rb +2 -4
- data/lib/rigor/configuration.rb +45 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +61 -13
- data/lib/rigor/environment/class_registry.rb +4 -3
- data/lib/rigor/environment/constant_type_cache_holder.rb +43 -0
- data/lib/rigor/environment/lockfile_resolver.rb +1 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +1 -2
- data/lib/rigor/environment/rbs_coverage_report.rb +2 -1
- data/lib/rigor/environment/rbs_loader.rb +49 -5
- data/lib/rigor/environment.rb +17 -7
- data/lib/rigor/flow_contribution/fact.rb +1 -1
- data/lib/rigor/flow_contribution.rb +3 -5
- data/lib/rigor/inference/acceptance.rb +17 -9
- data/lib/rigor/inference/block_parameter_binder.rb +2 -3
- data/lib/rigor/inference/builtins/comparable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +2 -2
- data/lib/rigor/inference/builtins/method_catalog.rb +19 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +9 -1
- data/lib/rigor/inference/expression_typer.rb +20 -28
- data/lib/rigor/inference/hkt_body.rb +8 -11
- data/lib/rigor/inference/hkt_body_parser.rb +10 -12
- data/lib/rigor/inference/hkt_registry.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/call_context.rb +1 -4
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +156 -21
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +9 -73
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +6 -7
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +10 -16
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +25 -13
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +93 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -3
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +24 -22
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +90 -15
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +303 -0
- data/lib/rigor/inference/method_dispatcher.rb +40 -48
- data/lib/rigor/inference/mutation_widening.rb +5 -11
- data/lib/rigor/inference/narrowing.rb +14 -16
- data/lib/rigor/inference/parameter_inference_collector.rb +367 -0
- data/lib/rigor/inference/project_patched_methods.rb +4 -7
- data/lib/rigor/inference/project_patched_scanner.rb +2 -13
- data/lib/rigor/inference/protection_scanner.rb +86 -0
- data/lib/rigor/inference/scope_indexer.rb +129 -55
- data/lib/rigor/inference/statement_evaluator.rb +244 -114
- data/lib/rigor/inference/struct_fold_safety.rb +181 -0
- data/lib/rigor/inference/synthetic_method.rb +7 -7
- data/lib/rigor/language_server/completion_provider.rb +6 -12
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -4
- data/lib/rigor/language_server/document_symbol_provider.rb +3 -3
- data/lib/rigor/language_server/hover_provider.rb +2 -3
- data/lib/rigor/language_server/hover_renderer.rb +2 -11
- data/lib/rigor/language_server/server.rb +9 -17
- data/lib/rigor/language_server.rb +4 -5
- data/lib/rigor/plugin/base.rb +10 -8
- data/lib/rigor/plugin/macro/block_as_method.rb +3 -4
- data/lib/rigor/plugin/macro/heredoc_template.rb +4 -7
- data/lib/rigor/plugin/macro/trait_registry.rb +3 -6
- data/lib/rigor/plugin/macro.rb +4 -5
- data/lib/rigor/plugin/manifest.rb +45 -66
- data/lib/rigor/plugin/registry.rb +6 -7
- data/lib/rigor/plugin/type_node_resolver.rb +6 -8
- data/lib/rigor/protection/mutation_scanner.rb +120 -0
- data/lib/rigor/protection/mutator.rb +246 -0
- data/lib/rigor/rbs_extended.rb +24 -36
- data/lib/rigor/reflection.rb +4 -7
- data/lib/rigor/scope/discovery_index.rb +14 -2
- data/lib/rigor/scope.rb +54 -11
- data/lib/rigor/sig_gen/observed_call.rb +3 -3
- data/lib/rigor/sig_gen/writer.rb +40 -2
- data/lib/rigor/source/constant_path.rb +62 -0
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/bound_method.rb +2 -11
- data/lib/rigor/type/combinator.rb +16 -3
- data/lib/rigor/type/constant.rb +2 -11
- data/lib/rigor/type/data_class.rb +2 -11
- data/lib/rigor/type/data_instance.rb +2 -11
- data/lib/rigor/type/hash_shape.rb +2 -11
- data/lib/rigor/type/integer_range.rb +2 -11
- data/lib/rigor/type/intersection.rb +2 -11
- data/lib/rigor/type/nominal.rb +2 -11
- data/lib/rigor/type/plain_lattice.rb +37 -0
- data/lib/rigor/type/refined.rb +72 -13
- data/lib/rigor/type/singleton.rb +2 -11
- data/lib/rigor/type/struct_class.rb +75 -0
- data/lib/rigor/type/struct_instance.rb +93 -0
- data/lib/rigor/type/tuple.rb +5 -15
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +3 -3
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +3 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +5 -13
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +11 -17
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +7 -10
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +3 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +6 -8
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +5 -7
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +1 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +9 -11
- data/plugins/rigor-dry-struct/lib/rigor/plugin/dry_struct.rb +8 -9
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +13 -12
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +3 -4
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/analyzer.rb +8 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +9 -11
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_index.rb +7 -8
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +7 -9
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +12 -13
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +15 -23
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +3 -3
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +3 -3
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +2 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +27 -11
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/devise_routes.rb +4 -6
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +12 -18
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +5 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -4
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_type_resolver.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +19 -14
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +0 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +5 -4
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/assertion_recognizer.rb +2 -3
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/method_signature.rb +7 -11
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sig_parser.rb +4 -5
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +6 -9
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/type_translator.rb +5 -15
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +28 -41
- data/sig/rigor/scope.rbs +9 -1
- data/sig/rigor/type.rbs +36 -1
- metadata +19 -1
|
@@ -6,8 +6,10 @@ require_relative "../reflection"
|
|
|
6
6
|
require_relative "../type"
|
|
7
7
|
require_relative "../analysis/fact_store"
|
|
8
8
|
require_relative "../source/node_walker"
|
|
9
|
+
require_relative "../source/constant_path"
|
|
9
10
|
require_relative "block_parameter_binder"
|
|
10
11
|
require_relative "body_fixpoint"
|
|
12
|
+
require_relative "struct_fold_safety"
|
|
11
13
|
require_relative "closure_escape_analyzer"
|
|
12
14
|
require_relative "indexed_narrowing"
|
|
13
15
|
require_relative "method_dispatcher"
|
|
@@ -100,6 +102,7 @@ module Rigor
|
|
|
100
102
|
Prism::CallNode => :eval_call,
|
|
101
103
|
Prism::BlockNode => :eval_block,
|
|
102
104
|
Prism::ReturnNode => :eval_return,
|
|
105
|
+
Prism::BreakNode => :eval_break,
|
|
103
106
|
Prism::MatchWriteNode => :eval_match_write
|
|
104
107
|
}.freeze
|
|
105
108
|
private_constant :HANDLERS
|
|
@@ -115,6 +118,19 @@ module Rigor
|
|
|
115
118
|
RETURN_SINK_KEY = :rigor_return_sink
|
|
116
119
|
private_constant :RETURN_SINK_KEY
|
|
117
120
|
|
|
121
|
+
# Thread-local sink (an Array of `[BreakNode, Scope]`) collecting the
|
|
122
|
+
# scope at each `break` reached while evaluating a loop body, so
|
|
123
|
+
# `eval_loop` / `eval_for` can join a `break`-path binding (`flag = true;
|
|
124
|
+
# break`) into the loop continuation that the fall-through would
|
|
125
|
+
# otherwise drop. Stacks like the return sink: a nested loop installs its
|
|
126
|
+
# own sink, restored on exit, so an inner loop's break does not leak to
|
|
127
|
+
# the outer one. A `break` inside a block / nested loop targets that
|
|
128
|
+
# inner construct, not the lexical loop — filtered out by the
|
|
129
|
+
# directly-targeting break set, see {#directly_targeting_breaks}.
|
|
130
|
+
# See docs/notes/20260615-loop-break-binding-propagation-design.md.
|
|
131
|
+
BREAK_SINK_KEY = :rigor_break_sink
|
|
132
|
+
private_constant :BREAK_SINK_KEY
|
|
133
|
+
|
|
118
134
|
# Lexical class frame: the `name:` field is the qualified class
|
|
119
135
|
# name as it would render in Ruby (e.g., `"Foo::Bar"`); the
|
|
120
136
|
# `singleton:` field is `true` for `class << self` frames so
|
|
@@ -365,9 +381,7 @@ module Rigor
|
|
|
365
381
|
end
|
|
366
382
|
|
|
367
383
|
# `receiver[key] ||= default` — the Redmine `Query#as_params`
|
|
368
|
-
# idiom
|
|
369
|
-
# "Indexed-collection narrowing through `Hash[k] ||= default`").
|
|
370
|
-
# After the `||=`, the next read at `receiver[key]` is known
|
|
384
|
+
# idiom. After the `||=`, the next read at `receiver[key]` is known
|
|
371
385
|
# non-nil; the next `<<` / `[]=` / other mutator runs against
|
|
372
386
|
# a Tuple / Hash carrier instead of the `Constant[nil]` an
|
|
373
387
|
# empty `HashShape{}` lookup would otherwise fold to.
|
|
@@ -480,10 +494,24 @@ module Rigor
|
|
|
480
494
|
# then-branch unconditionally exits (return / next /
|
|
481
495
|
# break / raise) and there is no else, the post-scope
|
|
482
496
|
# is the falsey edge of the predicate (subsequent
|
|
483
|
-
# statements observe the predicate-was-false world).
|
|
497
|
+
# statements observe the predicate-was-false world). The
|
|
498
|
+
# then-body is the *skipped* path, so the bare narrowing
|
|
499
|
+
# (no body assignments) is the correct continuation.
|
|
484
500
|
return [Type::Combinator.union(then_type, else_type), falsey_scope] \
|
|
485
501
|
if branch_terminates?(node.statements, then_type) && node.subsequent.nil?
|
|
486
|
-
|
|
502
|
+
# Symmetric case: the else / elsif-chain (`node.subsequent`)
|
|
503
|
+
# unconditionally exits, so the only surviving path is the
|
|
504
|
+
# then-branch that RAN. The continuation must therefore carry
|
|
505
|
+
# `then_scope` — the predicate-truthy narrowing PLUS the
|
|
506
|
+
# then-body's assignments — not the bare `truthy_scope`.
|
|
507
|
+
# Returning `truthy_scope` drops every local the then-body
|
|
508
|
+
# bound, leaving it unbound for any enclosing merge to
|
|
509
|
+
# spuriously nil-inject: e.g. the inner `elsif … else raise`
|
|
510
|
+
# of `if a then x=… elsif b then x=… else raise end` would
|
|
511
|
+
# return with `x` unbound, and the outer if's join would then
|
|
512
|
+
# read `x` as `… | nil` and fire a false `possible-nil-receiver`
|
|
513
|
+
# (liquid v5.x sweep, Event 3).
|
|
514
|
+
return [Type::Combinator.union(then_type, else_type), then_scope] \
|
|
487
515
|
if branch_terminates?(node.subsequent, else_type) && node.statements
|
|
488
516
|
|
|
489
517
|
[
|
|
@@ -518,10 +546,17 @@ module Rigor
|
|
|
518
546
|
else_type, else_scope = eval_branch_or_nil(node.else_clause, truthy_scope)
|
|
519
547
|
# Slice 7 phase 14 — same early-return narrowing as
|
|
520
548
|
# `if`: when the body unconditionally exits and there
|
|
521
|
-
# is no else, the post-scope is the truthy edge
|
|
549
|
+
# is no else, the post-scope is the truthy edge (the body
|
|
550
|
+
# is the skipped path, so the bare narrowing is correct).
|
|
522
551
|
return [Type::Combinator.union(then_type, else_type), truthy_scope] \
|
|
523
552
|
if branch_terminates?(node.statements, then_type) && node.else_clause.nil?
|
|
524
|
-
|
|
553
|
+
# Symmetric to the `if` else-exits fix: when the else-clause
|
|
554
|
+
# exits, the surviving path is the unless-body that RAN, so the
|
|
555
|
+
# continuation carries `then_scope` (the predicate-falsey
|
|
556
|
+
# narrowing PLUS the body's assignments), not the bare
|
|
557
|
+
# `falsey_scope` — otherwise body-bound locals are dropped and
|
|
558
|
+
# an enclosing merge nil-injects them.
|
|
559
|
+
return [Type::Combinator.union(then_type, else_type), then_scope] \
|
|
525
560
|
if branch_terminates?(node.else_clause, else_type) && node.statements
|
|
526
561
|
|
|
527
562
|
[
|
|
@@ -900,7 +935,11 @@ module Rigor
|
|
|
900
935
|
# widens `buf`'s Tuple), body-introduced locals' nil-injection, and
|
|
901
936
|
# the loop value itself. The fixpoint then OVERLAYS only the
|
|
902
937
|
# rebound-local bindings it corrects.
|
|
903
|
-
|
|
938
|
+
#
|
|
939
|
+
# The pass runs under a break sink so a `break`-path binding
|
|
940
|
+
# (`flag = true; break`) the fall-through `body_scope` drops is
|
|
941
|
+
# collected for the continuation join below.
|
|
942
|
+
break_targets, break_sink, body_scope = capture_loop_body_breaks(node.statements, post_pred)
|
|
904
943
|
base_scope = join_with_nil_injection(post_pred, body_scope)
|
|
905
944
|
|
|
906
945
|
rebound, body_first = loop_body_local_writes(node.statements, post_pred)
|
|
@@ -915,36 +954,41 @@ module Rigor
|
|
|
915
954
|
return [Type::Combinator.constant_of(nil), narrow_loop_exit_edge(node, fast)]
|
|
916
955
|
end
|
|
917
956
|
|
|
957
|
+
post_loop = converged_loop_scope(node, post_pred, base_scope, names, body_first)
|
|
958
|
+
# Recover `break`-path bindings the fall-through dropped (`flag = true;
|
|
959
|
+
# break` -> `flag` is `false | true`, not the stale `false`).
|
|
960
|
+
post_loop = join_break_scopes(post_loop, break_sink, break_targets, names)
|
|
961
|
+
post_loop = narrow_loop_exit_edge(node, post_loop)
|
|
962
|
+
[Type::Combinator.constant_of(nil), post_loop]
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
# The continuation scope for a loop whose body rebinds locals: the
|
|
966
|
+
# ADR-56 slice-B rebind fixpoint overlaid on `base_scope`, then the
|
|
967
|
+
# slice-C receiver-content writeback.
|
|
968
|
+
def converged_loop_scope(node, post_pred, base_scope, names, body_first)
|
|
918
969
|
# ADR-56 slice B — loop-body fixpoint. The body runs 0..N times and
|
|
919
970
|
# may compound (`d *= 2`), so the historical single body pass joined
|
|
920
971
|
# with the pre-loop scope kept stale folded constants
|
|
921
972
|
# (`d = 1; while …; d *= 2; end` → `1 | 2`, never reaching `4, 8`).
|
|
922
|
-
# Fold each body-written local's continuation binding through the
|
|
923
|
-
#
|
|
924
|
-
#
|
|
925
|
-
#
|
|
926
|
-
#
|
|
927
|
-
# 0-iteration path (the body may never run) degrades it to
|
|
928
|
-
# `T | nil`, matching the historical nil-injection treatment.
|
|
973
|
+
# Fold each body-written local's continuation binding through the same
|
|
974
|
+
# capped fixpoint slice A uses for non-escaping block captures. Seed:
|
|
975
|
+
# a pre-existing local seeds with its post-predicate binding; a local
|
|
976
|
+
# FIRST assigned inside the body seeds with `nil` so the 0-iteration
|
|
977
|
+
# path degrades it to `T | nil`, matching the nil-injection treatment.
|
|
929
978
|
result = loop_rebind_fixpoint(node, post_pred, names, body_first)
|
|
930
979
|
# Display-path re-record: the fixpoint's body re-evaluations fire
|
|
931
980
|
# `on_enter` with the cap-N INTERMEDIATE assumptions, so the
|
|
932
981
|
# last-visit-wins scope index would annotate loop-body lines with
|
|
933
|
-
# stale pre-convergence constants. One extra pass from the
|
|
934
|
-
#
|
|
935
|
-
# entry scopes post-writeback.
|
|
982
|
+
# stale pre-convergence constants. One extra pass from the converged
|
|
983
|
+
# bindings (result discarded) re-records the body's entry scopes.
|
|
936
984
|
record_converged_loop_body(node, post_pred, result, names, body_first)
|
|
937
985
|
post_loop = result.reduce(base_scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
938
|
-
# ADR-56 slice C — loop-body receiver-content element-type join. A
|
|
939
|
-
#
|
|
940
|
-
#
|
|
941
|
-
#
|
|
942
|
-
# `
|
|
943
|
-
|
|
944
|
-
# rebound and content-mutated composes.
|
|
945
|
-
post_loop = loop_content_writeback(node.statements, post_loop)
|
|
946
|
-
post_loop = narrow_loop_exit_edge(node, post_loop)
|
|
947
|
-
[Type::Combinator.constant_of(nil), post_loop]
|
|
986
|
+
# ADR-56 slice C — loop-body receiver-content element-type join. A loop
|
|
987
|
+
# that content-mutates a collection (`acc << n`) keeps only the seed's
|
|
988
|
+
# element types after the single-pass widen; join the appended/stored
|
|
989
|
+
# types into the continuation collection (pre-state read from
|
|
990
|
+
# `post_loop` so a local both rebound and content-mutated composes).
|
|
991
|
+
loop_content_writeback(node.statements, post_loop)
|
|
948
992
|
end
|
|
949
993
|
|
|
950
994
|
# Item 4 — loop-exit predicate narrowing. A `while pred` / `until pred`
|
|
@@ -983,6 +1027,87 @@ module Rigor
|
|
|
983
1027
|
found
|
|
984
1028
|
end
|
|
985
1029
|
|
|
1030
|
+
# A `break` inside one of these nested constructs targets the inner
|
|
1031
|
+
# construct (an inner loop, a block's method, a nested def), NOT the
|
|
1032
|
+
# lexical loop — so the directly-targeting break scan does not descend
|
|
1033
|
+
# into them.
|
|
1034
|
+
BREAK_BOUNDARY_NODES = [
|
|
1035
|
+
Prism::ForNode, Prism::WhileNode, Prism::UntilNode,
|
|
1036
|
+
Prism::BlockNode, Prism::LambdaNode, Prism::DefNode,
|
|
1037
|
+
Prism::ClassNode, Prism::ModuleNode, Prism::SingletonClassNode
|
|
1038
|
+
].freeze
|
|
1039
|
+
private_constant :BREAK_BOUNDARY_NODES
|
|
1040
|
+
|
|
1041
|
+
# The `BreakNode`s that lexically target THIS loop — reachable from the
|
|
1042
|
+
# body without crossing a nested loop / block / def boundary. An
|
|
1043
|
+
# identity-keyed Hash used as a membership set to filter the collected
|
|
1044
|
+
# break scopes (the thread-local sink also collects breaks from nested
|
|
1045
|
+
# blocks that did not install their own sink).
|
|
1046
|
+
def directly_targeting_breaks(statements)
|
|
1047
|
+
found = {}.compare_by_identity
|
|
1048
|
+
collect_direct_breaks(statements, found)
|
|
1049
|
+
found
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
def collect_direct_breaks(node, found)
|
|
1053
|
+
return if node.nil?
|
|
1054
|
+
|
|
1055
|
+
found[node] = true if node.is_a?(Prism::BreakNode)
|
|
1056
|
+
node.compact_child_nodes.each do |child|
|
|
1057
|
+
next if BREAK_BOUNDARY_NODES.any? { |klass| child.is_a?(klass) }
|
|
1058
|
+
|
|
1059
|
+
collect_direct_breaks(child, found)
|
|
1060
|
+
end
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
# Installs a fresh thread-local break sink around `yield` (a loop-body
|
|
1064
|
+
# evaluation), returning `[collected, yield_result]`. Stacks: the
|
|
1065
|
+
# previous sink is restored on exit so a nested loop's breaks do not
|
|
1066
|
+
# leak to the enclosing loop.
|
|
1067
|
+
def collect_break_scopes
|
|
1068
|
+
previous = Thread.current[BREAK_SINK_KEY]
|
|
1069
|
+
sink = []
|
|
1070
|
+
Thread.current[BREAK_SINK_KEY] = sink
|
|
1071
|
+
begin
|
|
1072
|
+
result = yield
|
|
1073
|
+
ensure
|
|
1074
|
+
Thread.current[BREAK_SINK_KEY] = previous
|
|
1075
|
+
end
|
|
1076
|
+
[sink, result]
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
# Runs a loop body's single pass under a break sink. Returns the
|
|
1080
|
+
# directly-targeting break set, the collected break scopes, and the
|
|
1081
|
+
# fall-through body scope — the three inputs the continuation's
|
|
1082
|
+
# {#join_break_scopes} needs. Shared by `eval_loop` and `eval_for`.
|
|
1083
|
+
def capture_loop_body_breaks(statements, entry)
|
|
1084
|
+
targets = directly_targeting_breaks(statements)
|
|
1085
|
+
sink, (_type, body_scope) = collect_break_scopes { sub_eval(statements, entry) }
|
|
1086
|
+
[targets, sink, body_scope]
|
|
1087
|
+
end
|
|
1088
|
+
|
|
1089
|
+
# Joins each directly-targeting break's body-written local bindings into
|
|
1090
|
+
# the loop continuation, so a `break`-path binding the fall-through
|
|
1091
|
+
# dropped is recovered (`flag = true; break` -> `flag` becomes `false |
|
|
1092
|
+
# true`). Only loop-body-written names are joined — an unchanged local
|
|
1093
|
+
# unions to itself; a break-only-written local is already present via the
|
|
1094
|
+
# fixpoint / nil-injection seed, so the union reflects its break value.
|
|
1095
|
+
def join_break_scopes(continuation, sink, targeting, names)
|
|
1096
|
+
return continuation if sink.empty? || names.empty?
|
|
1097
|
+
|
|
1098
|
+
breaks = sink.select { |(node, _scope)| targeting.key?(node) }
|
|
1099
|
+
breaks.reduce(continuation) do |cont, (_node, break_scope)|
|
|
1100
|
+
names.reduce(cont) do |acc, name|
|
|
1101
|
+
break_value = break_scope.local(name)
|
|
1102
|
+
next acc if break_value.nil?
|
|
1103
|
+
|
|
1104
|
+
current = acc.local(name)
|
|
1105
|
+
joined = current ? Type::Combinator.union(current, break_value) : break_value
|
|
1106
|
+
acc.with_local(name, joined)
|
|
1107
|
+
end
|
|
1108
|
+
end
|
|
1109
|
+
end
|
|
1110
|
+
|
|
986
1111
|
# Joins loop-body content mutations into the continuation collection
|
|
987
1112
|
# bindings. The mutator arguments are typed against `post_loop`, whose
|
|
988
1113
|
# locals already carry the loop-body fixpoint widening (so an
|
|
@@ -1107,11 +1232,19 @@ module Rigor
|
|
|
1107
1232
|
element_type = for_iteration_element_type(coll_type)
|
|
1108
1233
|
body_entry = bind_for_index(node.index, element_type, post_coll)
|
|
1109
1234
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1235
|
+
if node.statements.nil?
|
|
1236
|
+
return [Type::Combinator.constant_of(nil), join_with_nil_injection(post_coll, body_entry)]
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1239
|
+
# Run the body pass under a break sink so a `break`-path binding the
|
|
1240
|
+
# fall-through drops is recovered into the continuation (the `for`
|
|
1241
|
+
# sibling of `eval_loop`'s break join; `for` has no fixpoint, so the
|
|
1242
|
+
# single-pass join is the only continuation).
|
|
1243
|
+
break_targets, break_sink, body_scope = capture_loop_body_breaks(node.statements, body_entry)
|
|
1244
|
+
continuation = join_with_nil_injection(post_coll, body_scope)
|
|
1245
|
+
pre_existing, body_first = loop_body_local_writes(node.statements, post_coll)
|
|
1246
|
+
continuation = join_break_scopes(continuation, break_sink, break_targets, pre_existing + body_first)
|
|
1247
|
+
[Type::Combinator.constant_of(nil), continuation]
|
|
1115
1248
|
end
|
|
1116
1249
|
|
|
1117
1250
|
# `for x in coll` is semantically `coll.each { |x| ... }`. We
|
|
@@ -1278,7 +1411,7 @@ module Rigor
|
|
|
1278
1411
|
# (`Constant[nil]` for an empty body); we discard the body's
|
|
1279
1412
|
# post-scope.
|
|
1280
1413
|
def eval_class_or_module(node)
|
|
1281
|
-
name =
|
|
1414
|
+
name = Source::ConstantPath.qualified_name(node.constant_path)
|
|
1282
1415
|
new_context = @class_context + [ClassFrame.new(name: name, singleton: false)]
|
|
1283
1416
|
body_type, _body_scope = eval_class_body(node, new_context)
|
|
1284
1417
|
[body_type, scope]
|
|
@@ -1404,21 +1537,15 @@ module Rigor
|
|
|
1404
1537
|
# parameter (not for every self-call), and sound — only loses
|
|
1405
1538
|
# precision on the floored argument.
|
|
1406
1539
|
post_scope = widen_callee_escaped_argument_captures(node, post_scope)
|
|
1407
|
-
#
|
|
1408
|
-
#
|
|
1409
|
-
#
|
|
1410
|
-
# propagation step the outer `arr` keeps its pre-mutation
|
|
1411
|
-
# binding. Sound for the same reason — only ever LOSES
|
|
1412
|
-
# precision — so blindly applying is safe regardless of
|
|
1413
|
-
# whether the block actually runs.
|
|
1540
|
+
# Same always-safe rationale as `widen_after_call` above —
|
|
1541
|
+
# propagates outer-scope local / ivar widening from block body
|
|
1542
|
+
# mutations (`items.each { |x| arr << x }`).
|
|
1414
1543
|
post_scope = MutationWidening.widen_after_block(call_node: node, outer_scope: post_scope)
|
|
1415
|
-
# ADR-56 slice C — receiver-content element-type join.
|
|
1416
|
-
#
|
|
1417
|
-
#
|
|
1418
|
-
#
|
|
1419
|
-
#
|
|
1420
|
-
# parameter so `out = [0]; arr.each { |x| out << x }` types
|
|
1421
|
-
# `Array[0 | Integer]`, not `Array[0]`. Always sound — only widens.
|
|
1544
|
+
# ADR-56 slice C — receiver-content element-type join. Joins
|
|
1545
|
+
# appended / stored element / key / value types into the
|
|
1546
|
+
# continuation collection so `out = [0]; arr.each { |x| out << x }`
|
|
1547
|
+
# types `Array[0 | Integer]`, not `Array[0]`. Same always-safe
|
|
1548
|
+
# rationale (only widens).
|
|
1422
1549
|
post_scope = content_writeback_block_captures(node, post_scope)
|
|
1423
1550
|
# Indexed-collection narrowing — drop any
|
|
1424
1551
|
# `receiver[key] ||= default` narrowing the analyzer
|
|
@@ -1609,7 +1736,7 @@ module Rigor
|
|
|
1609
1736
|
args = matcher.arguments&.arguments || []
|
|
1610
1737
|
return nil unless args.size == 1
|
|
1611
1738
|
|
|
1612
|
-
class_name =
|
|
1739
|
+
class_name = Source::ConstantPath.qualified_name_or_nil(args.first)
|
|
1613
1740
|
return nil if class_name.nil?
|
|
1614
1741
|
|
|
1615
1742
|
{ local: local_name, kind: :class, class_name: class_name, exact: exact }
|
|
@@ -1665,35 +1792,6 @@ module Rigor
|
|
|
1665
1792
|
matcher.arguments.nil? || matcher.arguments.arguments.empty?
|
|
1666
1793
|
end
|
|
1667
1794
|
|
|
1668
|
-
# Decodes a `Prism::ConstantReadNode` /
|
|
1669
|
-
# `Prism::ConstantPathNode` into a colon-joined class
|
|
1670
|
-
# name string, or returns nil for any other node
|
|
1671
|
-
# shape. Mirrors the conservative envelope used by the
|
|
1672
|
-
# `is_a?` / `kind_of?` predicate narrower.
|
|
1673
|
-
def constant_node_name(node)
|
|
1674
|
-
case node
|
|
1675
|
-
when Prism::ConstantReadNode
|
|
1676
|
-
node.name.to_s
|
|
1677
|
-
when Prism::ConstantPathNode
|
|
1678
|
-
flatten_constant_path(node)
|
|
1679
|
-
end
|
|
1680
|
-
end
|
|
1681
|
-
|
|
1682
|
-
def flatten_constant_path(node)
|
|
1683
|
-
parts = []
|
|
1684
|
-
cursor = node
|
|
1685
|
-
while cursor.is_a?(Prism::ConstantPathNode)
|
|
1686
|
-
parts.unshift(cursor.name.to_s)
|
|
1687
|
-
cursor = cursor.parent
|
|
1688
|
-
end
|
|
1689
|
-
case cursor
|
|
1690
|
-
when Prism::ConstantReadNode then parts.unshift(cursor.name.to_s)
|
|
1691
|
-
when nil then nil # ::Foo absolute root — preserve as-is
|
|
1692
|
-
else return nil
|
|
1693
|
-
end
|
|
1694
|
-
parts.join("::")
|
|
1695
|
-
end
|
|
1696
|
-
|
|
1697
1795
|
# Slice 4b-2 (ADR-7 § "Slice 4-A/4-B") — applies the
|
|
1698
1796
|
# post-return facts the merger produces for an
|
|
1699
1797
|
# `RBS::Extended`-annotated call. Reads through
|
|
@@ -1702,9 +1800,8 @@ module Rigor
|
|
|
1702
1800
|
# rows for `:always` assert directives (the slice-4a
|
|
1703
1801
|
# routing places conditional asserts on `truthy_facts` /
|
|
1704
1802
|
# `falsey_facts`, which `Narrowing.predicate_scopes`
|
|
1705
|
-
# consumes).
|
|
1706
|
-
#
|
|
1707
|
-
# the same merger and land here.
|
|
1803
|
+
# consumes). Plugin `:always` assertions are handled by
|
|
1804
|
+
# the sibling `apply_plugin_assertions`, not this path.
|
|
1708
1805
|
def apply_rbs_extended_assertions(call_node, current_scope)
|
|
1709
1806
|
method_def = resolve_call_method(call_node, current_scope)
|
|
1710
1807
|
return current_scope if method_def.nil?
|
|
@@ -1770,16 +1867,9 @@ module Rigor
|
|
|
1770
1867
|
EMPTY_CONTRIBUTIONS = [].freeze
|
|
1771
1868
|
private_constant :EMPTY_CONTRIBUTIONS
|
|
1772
1869
|
|
|
1773
|
-
#
|
|
1774
|
-
#
|
|
1775
|
-
#
|
|
1776
|
-
# (`for_statement` = declares a `type_specifier`), gate each path
|
|
1777
|
-
# by membership AND by the ADR-52 WD1 method-name gates (every
|
|
1778
|
-
# `type_specifier` rule is `methods:`-gated, so the common
|
|
1779
|
-
# no-candidate case is a single Set probe; a pruned
|
|
1780
|
-
# consultation could only have returned `[]`), and accumulate
|
|
1781
|
-
# lazily (shared frozen empty array otherwise). Same contributions in
|
|
1782
|
-
# the same order as visiting every plugin; the caller is read-only.
|
|
1870
|
+
# Fast-exit guard: skip if no plugin declares a `type_specifier`, or if
|
|
1871
|
+
# no registered method-name gate matches the call. See
|
|
1872
|
+
# `collect_gated_statement_contributions` for the full consultation.
|
|
1783
1873
|
def collect_plugin_contributions(registry, call_node, current_scope)
|
|
1784
1874
|
index = registry.contribution_index
|
|
1785
1875
|
relevant = index.for_statement
|
|
@@ -1791,8 +1881,10 @@ module Rigor
|
|
|
1791
1881
|
collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
|
|
1792
1882
|
end
|
|
1793
1883
|
|
|
1794
|
-
#
|
|
1795
|
-
#
|
|
1884
|
+
# ADR-37 slice 2 / ADR-52 WD1 — post-gate walk in registry order.
|
|
1885
|
+
# Visits only plugins in `for_statement` (declare a `type_specifier`),
|
|
1886
|
+
# further gated by the method-name Set probe so the common no-candidate
|
|
1887
|
+
# case is a single lookup. Accumulates lazily; caller is read-only.
|
|
1796
1888
|
def collect_gated_statement_contributions(index, relevant, name, call_node, current_scope)
|
|
1797
1889
|
result = nil
|
|
1798
1890
|
relevant.each do |plugin|
|
|
@@ -2677,6 +2769,12 @@ module Rigor
|
|
|
2677
2769
|
source_path: scope.source_path
|
|
2678
2770
|
)
|
|
2679
2771
|
bindings = binder.bind(def_node)
|
|
2772
|
+
# ADR-67 WD3 — override an undeclared parameter with its call-site
|
|
2773
|
+
# inferred type (precision-additive; an RBS-declared parameter wins,
|
|
2774
|
+
# the table is empty on a normal `check` run). The inferred type lives
|
|
2775
|
+
# only as a body local, never as an RBS contract, so it cannot fire a
|
|
2776
|
+
# parameter-boundary diagnostic (WD1, satisfied by construction).
|
|
2777
|
+
bindings = seed_inferred_param_types(bindings, def_node, singleton)
|
|
2680
2778
|
|
|
2681
2779
|
# Method bodies do NOT see the outer scope's locals. They start
|
|
2682
2780
|
# from a fresh scope with the same environment, then receive
|
|
@@ -2690,9 +2788,47 @@ module Rigor
|
|
|
2690
2788
|
fresh = seed_instance_ivars(fresh, singleton: singleton)
|
|
2691
2789
|
fresh = seed_class_cvars(fresh)
|
|
2692
2790
|
fresh = seed_program_globals(fresh)
|
|
2791
|
+
# ADR-48 Struct slice 3 — install the method body's fold-safe-local set
|
|
2792
|
+
# so a member read off a mutation-free local folds during the in-body
|
|
2793
|
+
# walk (the call-return inference path is seeded separately).
|
|
2794
|
+
fresh = fresh.with_struct_fold_safe(
|
|
2795
|
+
StructFoldSafety.fold_safe_locals(
|
|
2796
|
+
def_node.body, ->(name) { scope.struct_member_layout(name)&.[](:members) }
|
|
2797
|
+
)
|
|
2798
|
+
)
|
|
2693
2799
|
bindings.reduce(fresh) { |acc, (name, type)| acc.with_local(name, type) }
|
|
2694
2800
|
end
|
|
2695
2801
|
|
|
2802
|
+
# ADR-67 WD3 — consult the call-site parameter-inference table for this
|
|
2803
|
+
# `def` and replace each undeclared (untyped) parameter binding with its
|
|
2804
|
+
# inferred type. Keyed by `[class_name, method_name, kind]`, reconstructed
|
|
2805
|
+
# from the lexical class path — the same triple
|
|
2806
|
+
# {Inference::ParameterInferenceCollector} records. An RBS-declared
|
|
2807
|
+
# parameter (a non-untyped binding) always wins. No-op when the table is
|
|
2808
|
+
# empty (the normal `check` path), so the seed is byte-identical there.
|
|
2809
|
+
def seed_inferred_param_types(bindings, def_node, singleton)
|
|
2810
|
+
inferred = scope.param_inferred_types
|
|
2811
|
+
return bindings if inferred.empty?
|
|
2812
|
+
|
|
2813
|
+
path = current_class_path
|
|
2814
|
+
return bindings if path.nil?
|
|
2815
|
+
|
|
2816
|
+
table = inferred[[path, def_node.name, singleton ? :singleton : :instance]]
|
|
2817
|
+
return bindings if table.nil? || table.empty?
|
|
2818
|
+
|
|
2819
|
+
merged = bindings.dup
|
|
2820
|
+
table.each do |name, type|
|
|
2821
|
+
merged[name] = type if merged.key?(name) && untyped_binding?(merged[name])
|
|
2822
|
+
end
|
|
2823
|
+
merged
|
|
2824
|
+
end
|
|
2825
|
+
|
|
2826
|
+
# True for the `Dynamic[Top]` carrier `MethodParameterBinder` leaves on an
|
|
2827
|
+
# undeclared parameter — the only bindings ADR-67 WD3 overrides.
|
|
2828
|
+
def untyped_binding?(type)
|
|
2829
|
+
type.is_a?(Type::Dynamic) && type.static_facet.is_a?(Type::Top)
|
|
2830
|
+
end
|
|
2831
|
+
|
|
2696
2832
|
def seed_instance_ivars(body_scope, singleton:)
|
|
2697
2833
|
return body_scope if singleton
|
|
2698
2834
|
|
|
@@ -2783,7 +2919,7 @@ module Rigor
|
|
|
2783
2919
|
when Prism::ConstantReadNode
|
|
2784
2920
|
receiver.name.to_s == prefix.last
|
|
2785
2921
|
when Prism::ConstantPathNode
|
|
2786
|
-
rendered =
|
|
2922
|
+
rendered = Source::ConstantPath.render(receiver)
|
|
2787
2923
|
return false unless rendered
|
|
2788
2924
|
|
|
2789
2925
|
path = rendered.split("::")
|
|
@@ -2823,25 +2959,6 @@ module Rigor
|
|
|
2823
2959
|
end
|
|
2824
2960
|
end
|
|
2825
2961
|
|
|
2826
|
-
def qualified_name_for(constant_path_node)
|
|
2827
|
-
case constant_path_node
|
|
2828
|
-
when Prism::ConstantReadNode
|
|
2829
|
-
constant_path_node.name.to_s
|
|
2830
|
-
when Prism::ConstantPathNode
|
|
2831
|
-
render_constant_path(constant_path_node)
|
|
2832
|
-
end
|
|
2833
|
-
end
|
|
2834
|
-
|
|
2835
|
-
def render_constant_path(node)
|
|
2836
|
-
prefix =
|
|
2837
|
-
case node.parent
|
|
2838
|
-
when Prism::ConstantReadNode then "#{node.parent.name}::"
|
|
2839
|
-
when Prism::ConstantPathNode then "#{render_constant_path(node.parent)}::"
|
|
2840
|
-
else ""
|
|
2841
|
-
end
|
|
2842
|
-
"#{prefix}#{node.name}"
|
|
2843
|
-
end
|
|
2844
|
-
|
|
2845
2962
|
def singleton_context_for(node)
|
|
2846
2963
|
case node.expression
|
|
2847
2964
|
when Prism::SelfNode
|
|
@@ -2880,7 +2997,7 @@ module Rigor
|
|
|
2880
2997
|
when Prism::ConstantReadNode
|
|
2881
2998
|
expression.name.to_s
|
|
2882
2999
|
when Prism::ConstantPathNode
|
|
2883
|
-
|
|
3000
|
+
Source::ConstantPath.render(expression)
|
|
2884
3001
|
end
|
|
2885
3002
|
end
|
|
2886
3003
|
|
|
@@ -2914,6 +3031,19 @@ module Rigor
|
|
|
2914
3031
|
[Type::Combinator.bot, scope]
|
|
2915
3032
|
end
|
|
2916
3033
|
|
|
3034
|
+
# A `break` transfers control to the loop exit (its flow value is `Bot`,
|
|
3035
|
+
# like `return`). It records the current scope into the active loop's
|
|
3036
|
+
# break sink so the loop join can recover a `break`-path binding the
|
|
3037
|
+
# fall-through would drop (`flag = true; break` -> `flag` is `false |
|
|
3038
|
+
# true` after the loop). nil sink = a `break` not inside an inferred
|
|
3039
|
+
# loop body (a block targeting a method, or top-level) — left to the
|
|
3040
|
+
# existing escaping-block / no-op handling.
|
|
3041
|
+
def eval_break(node)
|
|
3042
|
+
sink = Thread.current[BREAK_SINK_KEY]
|
|
3043
|
+
sink << [node, scope] if sink
|
|
3044
|
+
[Type::Combinator.bot, scope]
|
|
3045
|
+
end
|
|
3046
|
+
|
|
2917
3047
|
def record_return_value(node, sink)
|
|
2918
3048
|
args = node.arguments&.arguments || []
|
|
2919
3049
|
# `return` with no argument returns nil; `return a` records the
|