rigortype 0.3.6 → 0.3.7
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/builtins/ruby_core/date.yml +6 -3
- data/data/builtins/ruby_core/enumerable.yml +1 -0
- data/data/builtins/ruby_core/exception.yml +2 -1
- data/data/builtins/ruby_core/file.yml +28 -14
- data/data/builtins/ruby_core/hash.yml +5 -2
- data/data/builtins/ruby_core/io.yml +33 -16
- data/data/builtins/ruby_core/random.yml +2 -1
- data/data/builtins/ruby_core/re.yml +6 -3
- data/data/builtins/ruby_core/struct.yml +2 -0
- data/data/builtins/ruby_core/time.yml +2 -1
- data/data/gem_overlay/activesupport/core_ext.rbs +409 -11
- data/docs/manual/02-cli-reference.md +16 -7
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activerecord.md +74 -1
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-sidekiq.md +22 -0
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +12 -0
- data/lib/rigor/analysis/check_rules/dead_version_guard_arms.rb +98 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -3
- data/lib/rigor/analysis/check_rules/published_constant_guard.rb +199 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +1 -2
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -2
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +6 -12
- data/lib/rigor/analysis/check_rules.rb +383 -48
- data/lib/rigor/analysis/crash_signature.rb +116 -0
- data/lib/rigor/analysis/dependency_recorder.rb +18 -3
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +28 -0
- data/lib/rigor/analysis/incremental_session.rb +44 -9
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +1 -2
- data/lib/rigor/analysis/reachability/graph.rb +14 -5
- data/lib/rigor/analysis/reachability/scan.rb +7 -3
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +41 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +34 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +15 -11
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +147 -10
- data/lib/rigor/analysis/worker_session.rb +10 -1
- data/lib/rigor/bleeding_edge.rb +7 -6
- data/lib/rigor/builtins/hkt_builtins.rb +45 -6
- data/lib/rigor/builtins/predefined_constant_refinements.rb +108 -59
- data/lib/rigor/builtins/static_return_refinements.rb +20 -1
- data/lib/rigor/cache/descriptor.rb +42 -2
- data/lib/rigor/cache/incremental_snapshot.rb +40 -3
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +31 -8
- data/lib/rigor/cache/store.rb +10 -1
- data/lib/rigor/cli/coverage_command.rb +23 -18
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/fused_protection_renderer.rb +19 -2
- data/lib/rigor/cli/fused_protection_report.rb +23 -2
- data/lib/rigor/cli/measurement_integrity_warning.rb +57 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +22 -2
- data/lib/rigor/cli/mutation_protection_report.rb +31 -2
- data/lib/rigor/cli/sig_gen_command.rb +18 -0
- data/lib/rigor/cli/type_of_command.rb +188 -41
- data/lib/rigor/cli/type_of_renderer.rb +72 -3
- data/lib/rigor/cli/type_scan_command.rb +10 -1
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/environment/bundle_sig_discovery.rb +64 -13
- data/lib/rigor/environment/lockfile_resolver.rb +15 -5
- data/lib/rigor/environment/missing_gem_constant_index.rb +45 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +16 -22
- data/lib/rigor/environment/rbs_loader.rb +525 -46
- data/lib/rigor/environment.rb +38 -15
- data/lib/rigor/inference/acceptance.rb +172 -20
- data/lib/rigor/inference/captured_locals.rb +62 -0
- data/lib/rigor/inference/content_join.rb +347 -0
- data/lib/rigor/inference/def_handle.rb +15 -3
- data/lib/rigor/inference/def_node_resolver.rb +62 -3
- data/lib/rigor/inference/expression_typer.rb +1020 -141
- data/lib/rigor/inference/fork_map.rb +6 -1
- data/lib/rigor/inference/hkt_reducer.rb +12 -1
- data/lib/rigor/inference/hkt_registry.rb +46 -0
- data/lib/rigor/inference/hkt_sugar_translator.rb +93 -0
- data/lib/rigor/inference/index_write_widening.rb +48 -0
- data/lib/rigor/inference/indexed_narrowing.rb +14 -0
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +7 -5
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +10 -0
- data/lib/rigor/inference/method_dispatcher/json_folding.rb +58 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +81 -38
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +54 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +1 -2
- data/lib/rigor/inference/method_dispatcher/singleton_mixin_dispatch.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +63 -10
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +104 -0
- data/lib/rigor/inference/method_dispatcher/universal_object_dispatch.rb +81 -0
- data/lib/rigor/inference/method_dispatcher.rb +155 -14
- data/lib/rigor/inference/method_parameter_binder.rb +3 -3
- data/lib/rigor/inference/mutation_widening.rb +244 -174
- data/lib/rigor/inference/narrowing.rb +331 -27
- data/lib/rigor/inference/parameter_inference_collector.rb +1 -0
- data/lib/rigor/inference/pre_eval_constants.rb +15 -0
- data/lib/rigor/inference/precision_scanner.rb +5 -2
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +89 -39
- data/lib/rigor/inference/scope_indexer.rb +1493 -140
- data/lib/rigor/inference/statement_evaluator.rb +276 -91
- data/lib/rigor/inference/struct_fold_safety.rb +186 -27
- data/lib/rigor/inference/version_guard.rb +229 -0
- data/lib/rigor/language_server/selection_range_provider.rb +1 -1
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/inflector.rb +14 -5
- data/lib/rigor/plugin/io_boundary.rb +107 -4
- data/lib/rigor/plugin/loader.rb +14 -0
- data/lib/rigor/protection/analysis_guard.rb +60 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +43 -38
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -4
- data/lib/rigor/protection/discovery_seed.rb +1 -1
- data/lib/rigor/protection/measurement_integrity.rb +36 -0
- data/lib/rigor/protection/mutation_scanner.rb +61 -6
- data/lib/rigor/protection/mutator.rb +121 -18
- data/lib/rigor/reflection.rb +182 -30
- data/lib/rigor/scope/discovery_index.rb +39 -1
- data/lib/rigor/scope.rb +385 -4
- data/lib/rigor/sig_gen/classification.rb +12 -1
- data/lib/rigor/sig_gen/generator.rb +254 -5
- data/lib/rigor/sig_gen/renderer.rb +1 -6
- data/lib/rigor/sig_gen/writer.rb +3 -0
- data/lib/rigor/source/constant_path.rb +79 -2
- data/lib/rigor/triage/catalogue.rb +1 -1
- data/lib/rigor/type/combinator.rb +10 -0
- data/lib/rigor/type/maybe.rb +47 -0
- data/lib/rigor/type/refined.rb +1 -2
- data/lib/rigor/type/result.rb +53 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/analyzer.rb +3 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +58 -8
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_index.rb +15 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +4 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/analyzer.rb +3 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +39 -14
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_index.rb +36 -3
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +6 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +21 -5
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +47 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_index.rb +13 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +242 -4
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/analyzer.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +7 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +62 -11
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_index.rb +15 -2
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +3 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +5 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +2 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +798 -22
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +156 -13
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +140 -40
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +2 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +20 -6
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_index.rb +14 -4
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -3
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +23 -6
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +264 -9
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +876 -23
- data/plugins/rigor-dry-monads/lib/rigor/plugin/dry_monads.rb +99 -0
- data/plugins/rigor-dry-monads/lib/rigor-dry-monads.rb +5 -0
- data/plugins/rigor-ethon/lib/rigor/plugin/ethon.rb +34 -0
- data/plugins/rigor-ethon/lib/rigor-ethon.rb +3 -0
- data/plugins/rigor-ethon/sig/ethon.rbs +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +198 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/binding_recognizer.rb +75 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/catalog.rb +64 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +173 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +46 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/types.rb +161 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +121 -0
- data/plugins/rigor-ffi/lib/rigor-ffi.rb +3 -0
- data/plugins/rigor-ffi/sig/ffi.rbs +67 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor/plugin/ffi_rzmq.rb +19 -0
- data/plugins/rigor-ffi-rzmq/lib/rigor-ffi-rzmq.rb +3 -0
- data/plugins/rigor-ffi-rzmq/sig/ffi_rzmq.rbs +29 -0
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +2 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +2 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +2 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +2 -1
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +156 -11
- data/plugins/rigor-rbnacl/lib/rigor/plugin/rbnacl.rb +30 -0
- data/plugins/rigor-rbnacl/lib/rigor-rbnacl.rb +3 -0
- data/plugins/rigor-rbnacl/sig/rbnacl.rbs +22 -0
- data/plugins/rigor-sassc/lib/rigor/plugin/sassc.rb +55 -0
- data/plugins/rigor-sassc/lib/rigor-sassc.rb +3 -0
- data/plugins/rigor-sassc/sig/sassc.rbs +24 -0
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers/analyzer.rb +32 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/analyzer.rb +3 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +63 -10
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_index.rb +15 -2
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +61 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +4 -2
- data/sig/rigor/analysis/baseline.rbs +1 -1
- data/sig/rigor/environment.rbs +3 -0
- data/sig/rigor/inference.rbs +4 -1
- data/sig/rigor/plugin/io_boundary.rbs +2 -0
- data/sig/rigor/reflection.rbs +3 -1
- data/sig/rigor/scope.rbs +25 -1
- data/sig/rigor/type.rbs +95 -1
- data/sig/rigor.rbs +16 -0
- metadata +69 -3
|
@@ -11,17 +11,21 @@ require_relative "../source/constant_path"
|
|
|
11
11
|
require_relative "anonymous_meta_class"
|
|
12
12
|
require_relative "block_parameter_binder"
|
|
13
13
|
require_relative "body_fixpoint"
|
|
14
|
+
require_relative "captured_locals"
|
|
14
15
|
require_relative "dynamic_origin"
|
|
15
16
|
require_relative "../analysis/check_rules/inferred_param_guard"
|
|
16
17
|
require_relative "struct_fold_safety"
|
|
17
18
|
require_relative "closure_escape_analyzer"
|
|
19
|
+
require_relative "content_join"
|
|
18
20
|
require_relative "indexed_narrowing"
|
|
21
|
+
require_relative "index_write_widening"
|
|
19
22
|
require_relative "method_dispatcher"
|
|
20
23
|
require_relative "method_parameter_binder"
|
|
21
24
|
require_relative "multi_target_binder"
|
|
22
25
|
require_relative "mutation_widening"
|
|
23
26
|
require_relative "narrowing"
|
|
24
27
|
require_relative "optimistic_origin"
|
|
28
|
+
require_relative "version_guard"
|
|
25
29
|
|
|
26
30
|
module Rigor
|
|
27
31
|
module Inference
|
|
@@ -52,7 +56,7 @@ module Rigor
|
|
|
52
56
|
#
|
|
53
57
|
# See docs/internal-spec/inference-engine.md for the public contract and docs/adr/4-type-inference-engine.md for the
|
|
54
58
|
# slice rationale.
|
|
55
|
-
# rubocop:disable Metrics/ClassLength
|
|
59
|
+
# rubocop:disable-next Metrics/ClassLength
|
|
56
60
|
class StatementEvaluator
|
|
57
61
|
# Hash-based dispatch keeps `evaluate` linear and lets future slices add control-flow node kinds without growing a
|
|
58
62
|
# single case statement past RuboCop's cyclomatic budget. Anonymous Prism subclasses are not expected.
|
|
@@ -76,7 +80,11 @@ module Rigor
|
|
|
76
80
|
Prism::GlobalVariableAndWriteNode => :eval_global_and_write,
|
|
77
81
|
Prism::GlobalVariableOperatorWriteNode => :eval_global_operator_write,
|
|
78
82
|
Prism::IndexOrWriteNode => :eval_index_or_write,
|
|
83
|
+
Prism::IndexAndWriteNode => :eval_index_write,
|
|
84
|
+
Prism::IndexOperatorWriteNode => :eval_index_write,
|
|
79
85
|
Prism::MultiWriteNode => :eval_multi_write,
|
|
86
|
+
Prism::ConstantWriteNode => :eval_constant_write,
|
|
87
|
+
Prism::ConstantPathWriteNode => :eval_constant_write,
|
|
80
88
|
Prism::IfNode => :eval_if,
|
|
81
89
|
Prism::UnlessNode => :eval_unless,
|
|
82
90
|
Prism::ElseNode => :eval_else,
|
|
@@ -128,6 +136,15 @@ module Rigor
|
|
|
128
136
|
# singleton-method RBS lookups.
|
|
129
137
|
ClassFrame = Data.define(:name, :singleton)
|
|
130
138
|
|
|
139
|
+
# Issue #652 — Ruby's `Module.nesting` for the body currently being evaluated, innermost first, built as
|
|
140
|
+
# the walk ENTERS each declaration rather than reconstructed from the qualified name afterwards. A
|
|
141
|
+
# compact `module A::B` contributes ONE entry, the nested `module A; module B` two, and both render the
|
|
142
|
+
# same `class_name`, so the distinction survives only if it is recorded here. `[]` is the file top level,
|
|
143
|
+
# and nothing is stamped for it — a scope with no recorded chain falls back to the name-peel, which
|
|
144
|
+
# answers the same thing at the top level and answers correctly for a body this walk never entered.
|
|
145
|
+
EMPTY_NESTING = [].freeze
|
|
146
|
+
private_constant :EMPTY_NESTING
|
|
147
|
+
|
|
131
148
|
# @param scope [Rigor::Scope]
|
|
132
149
|
# @param tracer [Rigor::Inference::FallbackTracer, nil]
|
|
133
150
|
# @param on_enter [#call, nil] optional `(node, scope) ->` callable
|
|
@@ -150,11 +167,12 @@ module Rigor
|
|
|
150
167
|
# `Integer`). Display-path only — `rigor check` leaves it off,
|
|
151
168
|
# keeping its diagnostics and wall-clock unchanged.
|
|
152
169
|
def initialize(scope:, tracer: nil, on_enter: nil, class_context: [].freeze,
|
|
153
|
-
converged_loop_recording: false)
|
|
170
|
+
lexical_nesting: EMPTY_NESTING, converged_loop_recording: false)
|
|
154
171
|
@scope = scope
|
|
155
172
|
@tracer = tracer
|
|
156
173
|
@on_enter = on_enter
|
|
157
174
|
@class_context = class_context.freeze
|
|
175
|
+
@lexical_nesting = lexical_nesting.freeze
|
|
158
176
|
@converged_loop_recording = converged_loop_recording
|
|
159
177
|
end
|
|
160
178
|
|
|
@@ -309,6 +327,48 @@ module Rigor
|
|
|
309
327
|
[rhs_type, post_rhs.with_global(node.name, rhs_type)]
|
|
310
328
|
end
|
|
311
329
|
|
|
330
|
+
# `Const = rvalue` / `A::Const = rvalue`. The pair is exactly what the default expression path produced before
|
|
331
|
+
# this handler existed — the expression typer types the rvalue and the scope comes back unchanged, a constant
|
|
332
|
+
# write binding no local — plus one walk the default path could not make. Issue #590: when the rvalue is a
|
|
333
|
+
# class-creating meta call with a literal block (`Const = Struct.new(:text) do … end` and its `Data.define` /
|
|
334
|
+
# `Class.new` / `Module.new` twins) the block is a CLASS BODY, `class_eval`'d on the class the call creates,
|
|
335
|
+
# and it is entered as one here — the constant-write counterpart of {#evaluate_block_if_present}'s #319 arm.
|
|
336
|
+
# Left to the default path the body was never walked at all, so `ScopeIndexer.propagate` handed every node
|
|
337
|
+
# inside the ENCLOSING scope: at file top level a nil `self_type`, which made `Scope#toplevel?` hold inside
|
|
338
|
+
# every `def` of the body and `call.unresolved-toplevel` fire on the struct's own member reads (and on
|
|
339
|
+
# `attr_reader`, the very macro #319 silenced at every other position); inside a module, the module's own
|
|
340
|
+
# `self` — a wrong receiver for every implicit-self call in the body.
|
|
341
|
+
def eval_constant_write(node)
|
|
342
|
+
result = [scope.type_of(node, tracer: tracer), scope]
|
|
343
|
+
context = meta_new_constant_body_context(node)
|
|
344
|
+
return result if context.nil?
|
|
345
|
+
|
|
346
|
+
call_node = node.value
|
|
347
|
+
enter_meta_class_body(call_node.block, build_block_entry_scope(call_node, call_node.block), context)
|
|
348
|
+
result
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# The class context a meta-new rvalue block is entered under, or nil when the rvalue is not that shape. The
|
|
352
|
+
# KEY must be the one `ScopeIndexer` filed the body's defs and member layout under, so the two passes agree —
|
|
353
|
+
# which is why the decision is delegated to the ScopeIndexer's own recognition rather than re-spelled here. A
|
|
354
|
+
# `Prism::ConstantWriteNode` whose rvalue `ScopeIndexer.meta_new_block_body` recognises is keyed by the
|
|
355
|
+
# constant's qualified name, one frame appended to the lexical context exactly as a `class Const` keyword body
|
|
356
|
+
# would be. Every other block form — a constant-PATH write (`A::B = Struct.new do … end`, which has no
|
|
357
|
+
# constant-keyed registration) or a shape the ScopeIndexer's stricter argument check rejects
|
|
358
|
+
# (`Const = Struct.new(*names) do … end`) — was registered under the call site's anonymous name and is
|
|
359
|
+
# entered under that.
|
|
360
|
+
def meta_new_constant_body_context(node)
|
|
361
|
+
call_node = node.value
|
|
362
|
+
return nil unless call_node.is_a?(Prism::CallNode) && call_node.block.is_a?(Prism::BlockNode)
|
|
363
|
+
|
|
364
|
+
if node.is_a?(Prism::ConstantWriteNode) && ScopeIndexer.meta_new_block_body(node)
|
|
365
|
+
return @class_context + [ClassFrame.new(name: node.name.to_s, singleton: false)]
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
anonymous = AnonymousMetaClass.name_for(call_node, scope.source_path)
|
|
369
|
+
anonymous && [ClassFrame.new(name: anonymous, singleton: false)]
|
|
370
|
+
end
|
|
371
|
+
|
|
312
372
|
# Slice 7 phase 3 — compound writes (||=, &&=, +=/-=/...) for every variable kind. Each handler:
|
|
313
373
|
# 1. Reads the current type from the appropriate scope
|
|
314
374
|
# binding map (or `Dynamic[Top]` when unbound).
|
|
@@ -432,12 +492,72 @@ module Rigor
|
|
|
432
492
|
|
|
433
493
|
key_node = first_index_argument(node)
|
|
434
494
|
address = key_node && IndexedNarrowing.stable_address(node.receiver, key_node)
|
|
495
|
+
# Issue #544 — a receiver with an untracked (Dynamic / Top) constituent can hold a caller-supplied
|
|
496
|
+
# slot value the `||=` keeps, so the recorded default would invent a fact; decline the record.
|
|
497
|
+
address = nil if address && !IndexedNarrowing.fully_tracked_receiver_type?(scope.type_of(node.receiver))
|
|
435
498
|
post = post_rhs
|
|
499
|
+
# Widen BEFORE recording the narrowing: rebinding the receiver drops the per-slot narrowings keyed on it, so
|
|
500
|
+
# the reverse order would trade this feature away for the widening. The two are complementary — the shape
|
|
501
|
+
# forgets that the collection is still empty, the narrowing remembers that THIS slot is now non-nil.
|
|
502
|
+
post = IndexWriteWidening.widen(node: node, current_scope: post,
|
|
503
|
+
arg_types: index_write_arg_types(node, result_type))
|
|
436
504
|
post = post.with_indexed_narrowing(*address, result_type) if address
|
|
437
505
|
|
|
438
506
|
[result_type, post]
|
|
439
507
|
end
|
|
440
508
|
|
|
509
|
+
# `h[k] &&= v` / `h[k] += v`. Neither had a handler, so both fell to `evaluate`'s default — typed as a pure
|
|
510
|
+
# expression, scope untouched — and the receiver never widened. They store through `[]=` exactly as
|
|
511
|
+
# `eval_index_or_write` does, so they take the same widening; the value itself is still typed by the expression
|
|
512
|
+
# typer (`type_of_assignment_write`), which is what the default did.
|
|
513
|
+
def eval_index_write(node)
|
|
514
|
+
stored = scope.type_of(node, tracer: tracer)
|
|
515
|
+
[stored,
|
|
516
|
+
IndexWriteWidening.widen(node: node, current_scope: scope,
|
|
517
|
+
arg_types: index_write_arg_types(node, stored))]
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# `[key_type, stored_value_type]` for an index-write node, shaped exactly like a `[]=` call's
|
|
521
|
+
# argument list so the widening seam can join it the same way (issue #560). The stored value is
|
|
522
|
+
# the node's OWN expression type — for `t[0] += 5` that is the compound machinery's already-computed
|
|
523
|
+
# `t[0] + 5`, which is the whole point: it is the value the mutation put in the slot, and the one
|
|
524
|
+
# the retained element evidence provably no longer covers. Returns `[]` when the key is unresolvable,
|
|
525
|
+
# which reproduces the pre-join widening.
|
|
526
|
+
# There is deliberately NO `rescue` here. `Scope#type_of` is a total query over well-formed Prism input,
|
|
527
|
+
# so a raise is an engine bug, and swallowing it would silently downgrade a live seam to "no evidence" —
|
|
528
|
+
# the join would quietly stop happening with nothing to show for it. Let it reach the runner's
|
|
529
|
+
# internal-error path, where it is visible.
|
|
530
|
+
def index_write_arg_types(node, stored_type)
|
|
531
|
+
key_node = first_index_argument(node)
|
|
532
|
+
return MutationWidening::NO_ARG_TYPES if key_node.nil? || stored_type.nil?
|
|
533
|
+
return MutationWidening::NO_ARG_TYPES unless MutationWidening.joinable_receiver?(node.receiver, scope)
|
|
534
|
+
|
|
535
|
+
[scope.type_of(key_node, tracer: tracer), stored_type]
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Argument types for a straight-line content mutator (`arr << x`, `h[k] = v`).
|
|
539
|
+
#
|
|
540
|
+
# **The two scopes are different on purpose.** `current_scope` is the post-call scope the widening will
|
|
541
|
+
# actually rewrite, so the receiver gate has to ask IT whether the binding is still a literal-shape
|
|
542
|
+
# carrier — asking the entry scope could green-light a join the widening then declines, or miss one it
|
|
543
|
+
# would make. The arguments, though, are evaluated BEFORE the call in Ruby's order, so their types come
|
|
544
|
+
# from the entry `scope`; that also matches how the block path types its own mutator arguments against
|
|
545
|
+
# the block-ENTRY scope. Between the two points the only edits to the scope are the block / escape /
|
|
546
|
+
# plugin-assertion helpers above, none of which can rebind a mutator call's argument expressions.
|
|
547
|
+
#
|
|
548
|
+
# Both gates are about cost, not correctness — the join declines on its own in either case.
|
|
549
|
+
# `Scope#type_of` builds a fresh `ExpressionTyper` per call and memoizes nothing, so typing arguments a
|
|
550
|
+
# join will not consume is pure overhead, and `<<` on a String buffer is one of the commonest calls
|
|
551
|
+
# there is. The content-adder table skips a non-adding mutator (`pop`, `sort!`); `joinable_receiver?`
|
|
552
|
+
# skips a receiver whose current binding is not a literal-shape carrier.
|
|
553
|
+
def mutator_arg_types(call_node, current_scope)
|
|
554
|
+
return MutationWidening::NO_ARG_TYPES unless ContentJoin::CONTENT_ADDERS.include?(call_node.name)
|
|
555
|
+
return MutationWidening::NO_ARG_TYPES unless MutationWidening.joinable_receiver?(call_node.receiver,
|
|
556
|
+
current_scope)
|
|
557
|
+
|
|
558
|
+
content_arg_types(call_node, scope)
|
|
559
|
+
end
|
|
560
|
+
|
|
441
561
|
def first_index_argument(node)
|
|
442
562
|
args = node.arguments
|
|
443
563
|
return nil if args.nil?
|
|
@@ -562,21 +682,33 @@ module Rigor
|
|
|
562
682
|
# non-falsey carriers like `Nominal[Integer]` (Integer is always truthy in Ruby — including 0) also collapse the
|
|
563
683
|
# dead else.
|
|
564
684
|
def live_branch_for_if(node, pred_type, post_pred)
|
|
565
|
-
|
|
566
|
-
case verdict
|
|
685
|
+
case branch_certainty(node.predicate, pred_type, post_pred)
|
|
567
686
|
when :truthy then eval_branch_or_nil(node.statements, post_pred)
|
|
568
687
|
when :falsey then eval_branch_or_nil(node.subsequent, post_pred)
|
|
569
688
|
end
|
|
570
689
|
end
|
|
571
690
|
|
|
572
691
|
def live_branch_for_unless(node, pred_type, post_pred)
|
|
573
|
-
|
|
574
|
-
case verdict
|
|
692
|
+
case branch_certainty(node.predicate, pred_type, post_pred)
|
|
575
693
|
when :truthy then eval_branch_or_nil(node.else_clause, post_pred)
|
|
576
694
|
when :falsey then eval_branch_or_nil(node.statements, post_pred)
|
|
577
695
|
end
|
|
578
696
|
end
|
|
579
697
|
|
|
698
|
+
# ADR-47 WD5 — a decidable **version guard** answers first (#627). `RUBY_VERSION >= "3.1"` and the
|
|
699
|
+
# `Gem::Version.new(…) <cmp> Gem::Version.new(…)` spellings fold from literals the analyzer can read, so the arm
|
|
700
|
+
# that cannot run on the Ruby being checked with is elided exactly as `if false`'s is: it is never evaluated, so
|
|
701
|
+
# it contributes no diagnostics and its writes do not join into the post-`if` scope. The guard expression's own
|
|
702
|
+
# type stays `bool`, which is what keeps `flow.always-truthy-condition` off it — a version guard is intentional,
|
|
703
|
+
# not a redundant condition. See {VersionGuard} for the folded shapes and the reference-Ruby premise.
|
|
704
|
+
def branch_certainty(predicate, pred_type, post_pred)
|
|
705
|
+
guard = VersionGuard.verdict(predicate)
|
|
706
|
+
return guard if guard
|
|
707
|
+
return nil if optimistic_carrier?(predicate, post_pred)
|
|
708
|
+
|
|
709
|
+
Narrowing.predicate_certainty(pred_type)
|
|
710
|
+
end
|
|
711
|
+
|
|
580
712
|
# ADR-101 — the branch elision MUST NOT conclude truthiness from a carrier whose nil-freeness rests on
|
|
581
713
|
# the `%a{implicitly-returns-nil}` that `RbsDispatch` reads past. Such a value is optimistic, not proof
|
|
582
714
|
# (see {Inference::OptimisticOrigin} and docs/internal-spec/inference-engine.md), so eliding an arm on
|
|
@@ -912,7 +1044,7 @@ module Rigor
|
|
|
912
1044
|
# writeback (a loop may content-mutate a collection without rebinding any local — `acc << x`), so apply it to
|
|
913
1045
|
# the single-pass join before returning.
|
|
914
1046
|
if names.empty?
|
|
915
|
-
fast = loop_content_writeback(node.statements, base_scope)
|
|
1047
|
+
fast = loop_content_writeback(node.statements, base_scope, pre_body: post_pred)
|
|
916
1048
|
return [Type::Combinator.constant_of(nil), narrow_loop_exit_edge(node, fast)]
|
|
917
1049
|
end
|
|
918
1050
|
|
|
@@ -941,9 +1073,9 @@ module Rigor
|
|
|
941
1073
|
post_loop = result.reduce(base_scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
942
1074
|
# ADR-56 slice C — loop-body receiver-content element-type join. A loop that content-mutates a collection (`acc
|
|
943
1075
|
# << n`) keeps only the seed's element types after the single-pass widen; join the appended/stored types into
|
|
944
|
-
# the continuation collection
|
|
945
|
-
#
|
|
946
|
-
loop_content_writeback(node.statements, post_loop)
|
|
1076
|
+
# the continuation collection. Pre-state comes from `post_pred` for a name the loop only content-mutates and
|
|
1077
|
+
# from `post_loop` for one it also rebinds, so composition still works — see {#loop_content_writeback}.
|
|
1078
|
+
loop_content_writeback(node.statements, post_loop, pre_body: post_pred, rebound: names)
|
|
947
1079
|
end
|
|
948
1080
|
|
|
949
1081
|
# Item 4 — loop-exit predicate narrowing. A `while pred` / `until pred` loop exits PRECISELY on the predicate's
|
|
@@ -1053,9 +1185,25 @@ module Rigor
|
|
|
1053
1185
|
# Joins loop-body content mutations into the continuation collection bindings. The mutator arguments are typed
|
|
1054
1186
|
# against `post_loop`, whose locals already carry the loop-body fixpoint widening (so an appended `n` that the
|
|
1055
1187
|
# loop decrements types `Integer`, not its entry `Constant[3]` — otherwise only the first iteration's value would
|
|
1056
|
-
# be captured, an unsound under-approximation).
|
|
1057
|
-
#
|
|
1058
|
-
|
|
1188
|
+
# be captured, an unsound under-approximation). A loop body shares the surrounding scope, so the receiver is any
|
|
1189
|
+
# `LocalVariableReadNode` (no depth filter).
|
|
1190
|
+
#
|
|
1191
|
+
# **Pre-state comes from `pre_body` — the scope as of the loop predicate — for every name the loop does not
|
|
1192
|
+
# REBIND.** `post_loop` derives from a body evaluation, so the straight-line join inside the body has already
|
|
1193
|
+
# written its own result into that binding, floor and all (issue #560: a straight-line seam sees one store, so it
|
|
1194
|
+
# never closes the parameter it feeds). Re-deriving on top of that is derivation on derived output: this seam
|
|
1195
|
+
# scans the WHOLE body and joins every store in it, so it wants the contents as they stood before the body ran
|
|
1196
|
+
# and re-adds the stores itself. Left on `post_loop`, ADR-56's own `acc = []; while …; acc.push(m); end` read
|
|
1197
|
+
# `Array[Dynamic[top] | Integer]` where it must read `Array[Integer]`.
|
|
1198
|
+
#
|
|
1199
|
+
# Scrubbing the floor back out of `post_loop` instead is NOT an option: once inside a union a `Dynamic` is
|
|
1200
|
+
# indistinguishable from a DECLARED one, and stripping it closes a declared `Array[Integer | untyped]` parameter
|
|
1201
|
+
# and draws a false `undefined method` on correct code. Telling the two apart needs provenance the carriers do
|
|
1202
|
+
# not have (issue #580); taking the pre-body binding sidesteps the question entirely.
|
|
1203
|
+
#
|
|
1204
|
+
# A name the loop rebinds keeps reading `post_loop`, which is what lets a local both rebound (slice B) and
|
|
1205
|
+
# content-mutated compose — the documented behaviour, and there the fixpoint's answer IS the right base.
|
|
1206
|
+
def loop_content_writeback(statements, post_loop, pre_body: nil, rebound: nil)
|
|
1059
1207
|
return post_loop if statements.nil?
|
|
1060
1208
|
|
|
1061
1209
|
mutations = Hash.new { |h, k| h[k] = [] }
|
|
@@ -1066,11 +1214,20 @@ module Rigor
|
|
|
1066
1214
|
return post_loop if mutations.empty?
|
|
1067
1215
|
|
|
1068
1216
|
mutations.reduce(post_loop) do |acc, (name, calls)|
|
|
1069
|
-
joined = join_content_for_local(name, calls, acc, post_loop)
|
|
1217
|
+
joined = join_content_for_local(name, calls, content_seed_scope(name, acc, pre_body, rebound), post_loop)
|
|
1070
1218
|
joined.nil? ? acc : acc.with_local(name, joined)
|
|
1071
1219
|
end
|
|
1072
1220
|
end
|
|
1073
1221
|
|
|
1222
|
+
# The scope a loop content join reads its PRE-STATE from: the pre-body scope for a name the loop only
|
|
1223
|
+
# content-mutates, and the accumulating post-loop scope for one the loop also rebinds. {#loop_content_writeback}
|
|
1224
|
+
# carries the why.
|
|
1225
|
+
def content_seed_scope(name, accumulated, pre_body, rebound)
|
|
1226
|
+
return accumulated if pre_body.nil? || rebound&.include?(name)
|
|
1227
|
+
|
|
1228
|
+
pre_body
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1074
1231
|
# Re-evaluates the loop body once from the converged fixpoint bindings, solely for the `on_enter` side effect of
|
|
1075
1232
|
# re-recording the body's per-node entry scopes. Gated behind the display-path-only `converged_loop_recording`
|
|
1076
1233
|
# flag so the check path neither pays the extra body evaluation nor risks any diagnostic drift.
|
|
@@ -1104,7 +1261,7 @@ module Rigor
|
|
|
1104
1261
|
pre_existing = []
|
|
1105
1262
|
body_first = []
|
|
1106
1263
|
Source::NodeWalker.each(statements) do |descendant|
|
|
1107
|
-
next unless LOCAL_WRITE_NODES.any? { |klass| descendant.is_a?(klass) }
|
|
1264
|
+
next unless CapturedLocals::LOCAL_WRITE_NODES.any? { |klass| descendant.is_a?(klass) }
|
|
1108
1265
|
|
|
1109
1266
|
name = descendant.name
|
|
1110
1267
|
if base_scope.locals.key?(name)
|
|
@@ -1305,10 +1462,19 @@ module Rigor
|
|
|
1305
1462
|
# lexical owner. The outer scope is unchanged on exit because Ruby's class definition does not bind any local in
|
|
1306
1463
|
# the enclosing scope. The class body's value is the value of its last statement (`Constant[nil]` for an empty
|
|
1307
1464
|
# body); we discard the body's post-scope.
|
|
1465
|
+
#
|
|
1466
|
+
# Issue #708 — a ROOTED header (`class ::Rooted::Bar`) re-anchors at the top level, so the frame stack
|
|
1467
|
+
# RESETS to that class alone rather than gaining a frame under the enclosure. `current_class_path` joins
|
|
1468
|
+
# the stack, so keeping the enclosing frames named the class `Outer::Rooted::Bar` and every `def` in it
|
|
1469
|
+
# registered under a name no caller writes. The nesting CHAIN is not reset the same way — Ruby leaves the
|
|
1470
|
+
# enclosing entries beneath the un-prefixed one — which is why {Source::ConstantPath.pushed_nesting} owns
|
|
1471
|
+
# that half rather than this one deriving it from the frames.
|
|
1308
1472
|
def eval_class_or_module(node)
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1473
|
+
path = node.constant_path
|
|
1474
|
+
frame = ClassFrame.new(name: Source::ConstantPath.qualified_name(path), singleton: false)
|
|
1475
|
+
new_context = Source::ConstantPath.rooted?(path) ? [frame] : @class_context + [frame]
|
|
1476
|
+
body_type, _body_scope = eval_class_body(node, new_context,
|
|
1477
|
+
Source::ConstantPath.pushed_nesting(@lexical_nesting, path))
|
|
1312
1478
|
[body_type, scope]
|
|
1313
1479
|
end
|
|
1314
1480
|
|
|
@@ -1316,6 +1482,13 @@ module Rigor
|
|
|
1316
1482
|
# class — the innermost frame flips to `singleton: true` so a nested `def foo` resolves through `singleton_method`
|
|
1317
1483
|
# rather than `instance_method`. For non-`self` expressions we cannot statically resolve the receiver, so we keep
|
|
1318
1484
|
# the existing context and accept that nested defs degrade to the `Dynamic[Top]` default.
|
|
1485
|
+
# Issue #652 — the body INHERITS this evaluator's chain. Ruby pushes the singleton class onto
|
|
1486
|
+
# `Module.nesting` and leaves the enclosing entries beneath it, so `class << Other` written inside
|
|
1487
|
+
# `module Admin` reads `Admin::Y`: the enclosing rung is live, and only the singleton rung itself
|
|
1488
|
+
# (`#<Class:Other>`) is one Rigor does not model, on either path
|
|
1489
|
+
# ([#662](https://github.com/rigortype/rigor/issues/662)). Discarding the chain here because
|
|
1490
|
+
# {#singleton_context_for} resets the FRAME STACK for the cross-class form would answer `Other::Y`
|
|
1491
|
+
# off the name-peel instead — a rung Ruby's lookup does not have at all.
|
|
1319
1492
|
def eval_singleton_class(node)
|
|
1320
1493
|
new_context = singleton_context_for(node)
|
|
1321
1494
|
body_type, _body_scope = eval_class_body(node, new_context)
|
|
@@ -1384,7 +1557,8 @@ module Rigor
|
|
|
1384
1557
|
# Flow-folding G1 / G2 — widen a local- or instance-variable binding when the call is an in-place mutator on it
|
|
1385
1558
|
# (e.g. `arms << x`, `@tags << hashtag`). Stops a literal-shape carrier (`Tuple` / `HashShape`) from outliving
|
|
1386
1559
|
# its justification when the value is mutated. Always-safe (loses precision, never invents facts).
|
|
1387
|
-
post_scope = MutationWidening.widen_after_call(call_node: node, current_scope: post_scope
|
|
1560
|
+
post_scope = MutationWidening.widen_after_call(call_node: node, current_scope: post_scope,
|
|
1561
|
+
arg_types: mutator_arg_types(node, post_scope))
|
|
1388
1562
|
# ADR-48 slice 4 — Struct member-setter re-typing. After `s.x = v` on a fold-safe StructInstance local, rebind
|
|
1389
1563
|
# `s` to a StructInstance with member `:x` replaced by the assigned type, so a later `s.x` folds to `v` and a
|
|
1390
1564
|
# sibling `s.y` stays precise. `call_type` is the setter's own result (the assigned value type). Sound only for
|
|
@@ -1402,11 +1576,16 @@ module Rigor
|
|
|
1402
1576
|
post_scope = widen_callee_escaped_argument_captures(node, post_scope)
|
|
1403
1577
|
# Same always-safe rationale as `widen_after_call` above — propagates outer-scope local / ivar widening from
|
|
1404
1578
|
# block body mutations (`items.each { |x| arr << x }`).
|
|
1579
|
+
#
|
|
1580
|
+
# The slice-C join below reads its SEED from here, before the widening runs: `widen_after_block` spells an
|
|
1581
|
+
# empty `[]` as `Array[untyped]`, and read back after the fact that `untyped` is indistinguishable from a
|
|
1582
|
+
# declared one (issue #586 — see {#content_writeback_block_captures}).
|
|
1583
|
+
pre_widen_scope = post_scope
|
|
1405
1584
|
post_scope = MutationWidening.widen_after_block(call_node: node, outer_scope: post_scope)
|
|
1406
1585
|
# ADR-56 slice C — receiver-content element-type join. Joins appended / stored element / key / value types into
|
|
1407
1586
|
# the continuation collection so `out = [0]; arr.each { |x| out << x }` types `Array[0 | Integer]`, not
|
|
1408
1587
|
# `Array[0]`. Same always-safe rationale (only widens).
|
|
1409
|
-
post_scope = content_writeback_block_captures(node, post_scope)
|
|
1588
|
+
post_scope = content_writeback_block_captures(node, post_scope, seed_scope: pre_widen_scope)
|
|
1410
1589
|
# Indexed-collection narrowing — drop any `receiver[key] ||= default` narrowing the analyzer recorded earlier
|
|
1411
1590
|
# when an intervening `[]=` writes the same slot or any other mutator runs against the receiver. Always-safe
|
|
1412
1591
|
# (only forgets; never invents).
|
|
@@ -1428,6 +1607,10 @@ module Rigor
|
|
|
1428
1607
|
# whose body we cannot prove match-free). A call provably match-free on a known receiver — `$3.to_i`, `year <
|
|
1429
1608
|
# 50` — does NOT clobber, so the multi-statement `m = /…/ =~ s; …; use($2)` stdlib idiom keeps its precision
|
|
1430
1609
|
# while a genuinely interposed match still invalidates.
|
|
1610
|
+
# The chain above is Scope-total by construction (every helper returns its input scope or a
|
|
1611
|
+
# combinator result); the `||=` is a runtime no-op that pins the INFERRED type back to Scope for
|
|
1612
|
+
# the negative rules when a helper's return widens to `Scope?` under call-site binding (#524).
|
|
1613
|
+
post_scope ||= scope
|
|
1431
1614
|
post_scope = post_scope.forget_match_globals if match_capable_call?(node)
|
|
1432
1615
|
[call_type, post_scope]
|
|
1433
1616
|
end
|
|
@@ -1466,12 +1649,14 @@ module Rigor
|
|
|
1466
1649
|
seed = current_scope.class_ivars_for(class_name)
|
|
1467
1650
|
return current_scope if seed.empty?
|
|
1468
1651
|
|
|
1469
|
-
|
|
1652
|
+
widened = current_scope
|
|
1653
|
+
seed.each do |ivar_name, seed_type|
|
|
1470
1654
|
local_type = current_scope.ivar(ivar_name)
|
|
1471
|
-
next
|
|
1655
|
+
next if local_type.nil? || local_type == seed_type
|
|
1472
1656
|
|
|
1473
|
-
|
|
1657
|
+
widened = widened.with_ivar(ivar_name, Type::Combinator.union(local_type, seed_type))
|
|
1474
1658
|
end
|
|
1659
|
+
widened
|
|
1475
1660
|
end
|
|
1476
1661
|
|
|
1477
1662
|
def intervening_call_candidate?(call_node)
|
|
@@ -1835,8 +2020,24 @@ module Rigor
|
|
|
1835
2020
|
anonymous = AnonymousMetaClass.name_for(node, scope.source_path)
|
|
1836
2021
|
return sub_eval(block, block_entry) if anonymous.nil?
|
|
1837
2022
|
|
|
1838
|
-
|
|
1839
|
-
|
|
2023
|
+
enter_meta_class_body(block, block_entry, [ClassFrame.new(name: anonymous, singleton: false)])
|
|
2024
|
+
end
|
|
2025
|
+
|
|
2026
|
+
# Enters a meta-new `block` as the body of the class `class_context` names: `self_type` is that class's
|
|
2027
|
+
# singleton, so a `def` inside binds an instance method on it through the ordinary
|
|
2028
|
+
# {#self_type_for_method_body} route, while `block_entry` keeps the outer locals visible. Shared by the two
|
|
2029
|
+
# positions such a block is reached from — a statement-level call ({#evaluate_block_if_present}) and a
|
|
2030
|
+
# constant-write rvalue ({#eval_constant_write}) — so the two cannot drift on what a class body's entry is.
|
|
2031
|
+
#
|
|
2032
|
+
# Issue #652 — the body keeps this evaluator's `Module.nesting`. A BLOCK never pushes a cref, however
|
|
2033
|
+
# much `class_eval` semantics make it behave like a class body in every other respect, so
|
|
2034
|
+
# `K = Class.new do … end` written inside `module Outer` reads `Outer::LABEL` at runtime and MUST NOT
|
|
2035
|
+
# record a `Outer::K` entry: that is a rung Ruby's constant lookup does not have. The `class_context`
|
|
2036
|
+
# frame is still pushed — it is what a nested `def` registers its method under — which is exactly the
|
|
2037
|
+
# divergence that makes the chain a separate record rather than a view of the frame stack.
|
|
2038
|
+
def enter_meta_class_body(block, block_entry, class_context)
|
|
2039
|
+
entry = block_entry.with_self_type(self_type_for_class_body(class_context))
|
|
2040
|
+
sub_eval(block, stamp_nesting(entry, @lexical_nesting), class_context: class_context)
|
|
1840
2041
|
end
|
|
1841
2042
|
|
|
1842
2043
|
# Slice 6 phase C sub-phase 3b/3c. When the call carries a block whose receiving method is NOT proven
|
|
@@ -2165,42 +2366,12 @@ module Rigor
|
|
|
2165
2366
|
# synthesise the union of the block's write types (which the current pass does not yet expose), we discard the
|
|
2166
2367
|
# narrowed binding altogether. A future sub-phase MAY refine this to the union of the block's actual writes.
|
|
2167
2368
|
def drop_captured_narrowing(block_node, base_scope)
|
|
2168
|
-
names =
|
|
2369
|
+
names = CapturedLocals.writes(block_node, base_scope)
|
|
2169
2370
|
return base_scope if names.empty?
|
|
2170
2371
|
|
|
2171
2372
|
names.reduce(base_scope) { |acc, name| acc.with_local(name, Type::Combinator.untyped) }
|
|
2172
2373
|
end
|
|
2173
2374
|
|
|
2174
|
-
# Names of outer locals the block body can REBIND, across every local-write form: plain `=`
|
|
2175
|
-
# (`LocalVariableWriteNode`), the operator / `||=` / `&&=` compound forms, and a multi-assign target (`x, y = ...`
|
|
2176
|
-
# → `LocalVariableTargetNode` under `MultiWriteNode`). Block-introduced names (parameters, numbered params,
|
|
2177
|
-
# `;`-locals) and names not bound in the outer scope are excluded — a write to either is not a captured rebind of
|
|
2178
|
-
# an outer variable.
|
|
2179
|
-
LOCAL_WRITE_NODES = [
|
|
2180
|
-
Prism::LocalVariableWriteNode,
|
|
2181
|
-
Prism::LocalVariableOperatorWriteNode,
|
|
2182
|
-
Prism::LocalVariableOrWriteNode,
|
|
2183
|
-
Prism::LocalVariableAndWriteNode,
|
|
2184
|
-
Prism::LocalVariableTargetNode
|
|
2185
|
-
].freeze
|
|
2186
|
-
private_constant :LOCAL_WRITE_NODES
|
|
2187
|
-
|
|
2188
|
-
def captured_local_writes(block_node, base_scope)
|
|
2189
|
-
body = block_node.body
|
|
2190
|
-
return [] if body.nil?
|
|
2191
|
-
|
|
2192
|
-
introduced = block_introduced_locals(block_node)
|
|
2193
|
-
outer_writes = []
|
|
2194
|
-
Source::NodeWalker.each(body) do |descendant|
|
|
2195
|
-
next unless LOCAL_WRITE_NODES.any? { |klass| descendant.is_a?(klass) }
|
|
2196
|
-
next if introduced.include?(descendant.name)
|
|
2197
|
-
next unless base_scope.locals.key?(descendant.name)
|
|
2198
|
-
|
|
2199
|
-
outer_writes << descendant.name
|
|
2200
|
-
end
|
|
2201
|
-
outer_writes.uniq
|
|
2202
|
-
end
|
|
2203
|
-
|
|
2204
2375
|
# ADR-56 slice A. For a `:non_escaping` block, fold the continuation binding of every outer local the body can
|
|
2205
2376
|
# rebind back into `post_scope`. The binding is a capped fixpoint (cap 3) over the block body re-evaluated under
|
|
2206
2377
|
# the running per-name assumption, joined with the pre-call binding (kept as a constituent so the 0-iteration path
|
|
@@ -2208,13 +2379,13 @@ module Rigor
|
|
|
2208
2379
|
# `Dynamic[top]` on non-convergence (matching `drop_captured_narrowing`).
|
|
2209
2380
|
#
|
|
2210
2381
|
# Fast path: a block writing no outer local leaves `post_scope` byte-identical (the overwhelming majority of
|
|
2211
|
-
# blocks), so this costs one extra `
|
|
2382
|
+
# blocks), so this costs one extra `CapturedLocals.writes` walk and nothing else.
|
|
2212
2383
|
def write_back_block_captures(call_node, post_scope)
|
|
2213
2384
|
block = call_node.block
|
|
2214
2385
|
return post_scope unless block.is_a?(Prism::BlockNode)
|
|
2215
2386
|
return post_scope unless classify_closure_escape(call_node) == :non_escaping
|
|
2216
2387
|
|
|
2217
|
-
names =
|
|
2388
|
+
names = CapturedLocals.writes(block, scope)
|
|
2218
2389
|
return post_scope if names.empty?
|
|
2219
2390
|
|
|
2220
2391
|
seed = names.to_h { |name| [name, scope.local(name)] }
|
|
@@ -2234,11 +2405,20 @@ module Rigor
|
|
|
2234
2405
|
# collection's parameter, so `out = [0]; arr.each { |x| out << x }` types `out` as `Array[0 | Integer]` (sound)
|
|
2235
2406
|
# rather than `Array[0]` (the B1 under-approximation: the runtime array is `[0, 1, 2, 3]`).
|
|
2236
2407
|
#
|
|
2237
|
-
# Pre-state is read from `
|
|
2238
|
-
#
|
|
2239
|
-
#
|
|
2240
|
-
#
|
|
2241
|
-
|
|
2408
|
+
# **Pre-state is read from `seed_scope` — the scope as it stood BEFORE `widen_after_block` ran.** The widening
|
|
2409
|
+
# spells an empty `[]` as `Array[untyped]`, and read back after the fact that `untyped` cannot be told from a
|
|
2410
|
+
# DECLARED one: the join used to drop every seed `Dynamic` once the body contributed concrete evidence, which
|
|
2411
|
+
# made `out = []; xs.each { out << x }` read `Array[Integer]` — and closed a parameter declared `Array[untyped]`
|
|
2412
|
+
# to `Array[Integer]` on the same rule, so `a.first.upcase` drew `undefined method` on code the declaration
|
|
2413
|
+
# licenses (issue #586). Read before the widening, an empty literal contributes no element and a declared
|
|
2414
|
+
# gradual arm is just another seed arm the join keeps. The pre-widen scope still carries the slice-A rebind
|
|
2415
|
+
# write-back — so a local that is BOTH rebound and content-mutated composes as before — and every other
|
|
2416
|
+
# post-call effect applied ahead of the widening; the pre-CALL `scope` would carry neither. The loop seam makes
|
|
2417
|
+
# the same choice with `pre_body`; see {#loop_content_writeback}.
|
|
2418
|
+
#
|
|
2419
|
+
# The block body is typed once for argument evidence; the floor is `Array[Dynamic[top]]` /
|
|
2420
|
+
# `Hash[untyped, untyped]` (the sound empty-seed behaviour). Always sound — only ever widens.
|
|
2421
|
+
def content_writeback_block_captures(call_node, post_scope, seed_scope:)
|
|
2242
2422
|
block = call_node.block
|
|
2243
2423
|
return post_scope unless block.is_a?(Prism::BlockNode)
|
|
2244
2424
|
return post_scope unless classify_closure_escape(call_node) == :non_escaping
|
|
@@ -2251,7 +2431,7 @@ module Rigor
|
|
|
2251
2431
|
|
|
2252
2432
|
entry = build_block_entry_scope(call_node, block)
|
|
2253
2433
|
mutations.reduce(post_scope) do |acc, (name, calls)|
|
|
2254
|
-
joined = join_content_for_local(name, calls,
|
|
2434
|
+
joined = join_content_for_local(name, calls, seed_scope, entry)
|
|
2255
2435
|
joined.nil? ? acc : acc.with_local(name, joined)
|
|
2256
2436
|
end
|
|
2257
2437
|
end
|
|
@@ -2308,7 +2488,7 @@ module Rigor
|
|
|
2308
2488
|
calls = []
|
|
2309
2489
|
Source::NodeWalker.each(body) do |descendant|
|
|
2310
2490
|
next unless descendant.is_a?(Prism::CallNode)
|
|
2311
|
-
next unless
|
|
2491
|
+
next unless ContentJoin::CONTENT_ADDERS.include?(descendant.name)
|
|
2312
2492
|
|
|
2313
2493
|
receiver = descendant.receiver
|
|
2314
2494
|
next unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
@@ -2320,7 +2500,11 @@ module Rigor
|
|
|
2320
2500
|
end
|
|
2321
2501
|
|
|
2322
2502
|
# Joins content evidence for a memo / param given its pre-state and a list of mutator calls, dispatching Array vs
|
|
2323
|
-
# Hash by the mutator set
|
|
2503
|
+
# Hash by the PRE-STATE's own evidence. The mutator set never picks the arm on its own: `[]=` is legal on Array,
|
|
2504
|
+
# Hash, and countless index-writable classes, so an index-write against a receiver the engine cannot shape must
|
|
2505
|
+
# not synthesize a hash carrier — mail's `compose_codepoints` mutated its untyped Array param through
|
|
2506
|
+
# integer/range index writes and returned `Hash[Integer | Range, …]` to its caller (issue #553). Dynamic in,
|
|
2507
|
+
# Dynamic out: a shapeless pre-state falls through to `join_array_param`, which declines it.
|
|
2324
2508
|
def join_content_for_param(calls, pre_state, block_entry)
|
|
2325
2509
|
return nil if pre_state.nil?
|
|
2326
2510
|
|
|
@@ -2328,7 +2512,7 @@ module Rigor
|
|
|
2328
2512
|
# String carries no element parameter; mutating `<<`/`concat` makes the constant value unsound (`s = "a"; s <<
|
|
2329
2513
|
# x` → runtime `"a…"`), so widen to the nominal base. Sound — only widens.
|
|
2330
2514
|
Type::Combinator.nominal_of("String")
|
|
2331
|
-
elsif hashish?(pre_state)
|
|
2515
|
+
elsif hashish?(pre_state)
|
|
2332
2516
|
join_hash_param(calls, pre_state, block_entry)
|
|
2333
2517
|
else
|
|
2334
2518
|
join_array_param(calls, pre_state, block_entry)
|
|
@@ -2339,7 +2523,7 @@ module Rigor
|
|
|
2339
2523
|
pairs = calls.flat_map { |c| hash_pair_types(c, block_entry) }
|
|
2340
2524
|
return nil if pairs.empty? && !hashish?(pre_state)
|
|
2341
2525
|
|
|
2342
|
-
|
|
2526
|
+
ContentJoin.join_hash_content(pre_state, pairs)
|
|
2343
2527
|
end
|
|
2344
2528
|
|
|
2345
2529
|
def join_array_param(calls, pre_state, block_entry)
|
|
@@ -2350,9 +2534,9 @@ module Rigor
|
|
|
2350
2534
|
# array-arity forget already widened the binding; contribute nothing.
|
|
2351
2535
|
next [] if index_write?(c)
|
|
2352
2536
|
|
|
2353
|
-
|
|
2537
|
+
ContentJoin.array_added_elements(c.name, content_arg_types(c, block_entry))
|
|
2354
2538
|
end
|
|
2355
|
-
|
|
2539
|
+
ContentJoin.join_array_content(pre_state, added)
|
|
2356
2540
|
end
|
|
2357
2541
|
|
|
2358
2542
|
# Walks the block body for content-mutator calls (`<<`, `push`, `[]=`, …) whose receiver is a captured outer local
|
|
@@ -2381,7 +2565,7 @@ module Rigor
|
|
|
2381
2565
|
# `[receiver_name, node]` when `node` is a content mutation whose receiver is a local variable satisfying `accept`
|
|
2382
2566
|
# (depth predicate), else `[nil, nil]`. Covers `[]=`-style CallNode mutators and the index-write node forms.
|
|
2383
2567
|
def content_mutation_target(node)
|
|
2384
|
-
is_call_mutator = node.is_a?(Prism::CallNode) &&
|
|
2568
|
+
is_call_mutator = node.is_a?(Prism::CallNode) && ContentJoin::CONTENT_ADDERS.include?(node.name)
|
|
2385
2569
|
return [nil, nil] unless is_call_mutator || index_write?(node)
|
|
2386
2570
|
|
|
2387
2571
|
receiver = node.receiver
|
|
@@ -2398,21 +2582,19 @@ module Rigor
|
|
|
2398
2582
|
join_content_for_param(calls, post_scope.local(name), block_entry)
|
|
2399
2583
|
end
|
|
2400
2584
|
|
|
2401
|
-
def hash_mutations?(calls)
|
|
2402
|
-
calls.any? do |c|
|
|
2403
|
-
index_write?(c) || (c.is_a?(Prism::CallNode) && MutationWidening::HASH_CONTENT_ADDERS.include?(c.name))
|
|
2404
|
-
end
|
|
2405
|
-
end
|
|
2406
|
-
|
|
2407
2585
|
def index_write?(node)
|
|
2408
2586
|
INDEX_WRITE_NODES.any? { |k| node.is_a?(k) }
|
|
2409
2587
|
end
|
|
2410
2588
|
|
|
2589
|
+
# A `Difference` (`non-empty-array[T]`) counts as its base. The seams read their seed from before the mutation
|
|
2590
|
+
# widening ran, so they meet the refinement carrier where they used to meet the `Array[T]` it widens to;
|
|
2591
|
+
# declining it would leave the continuation on that widened base with every appended arm missing.
|
|
2411
2592
|
def arrayish?(type)
|
|
2412
2593
|
case type
|
|
2413
2594
|
when Type::Tuple then true
|
|
2414
2595
|
when Type::Nominal then type.class_name == "Array"
|
|
2415
2596
|
when Type::Union then type.members.any? { |m| arrayish?(m) }
|
|
2597
|
+
when Type::Difference then arrayish?(type.base)
|
|
2416
2598
|
else false
|
|
2417
2599
|
end
|
|
2418
2600
|
end
|
|
@@ -2422,6 +2604,7 @@ module Rigor
|
|
|
2422
2604
|
when Type::HashShape then true
|
|
2423
2605
|
when Type::Nominal then type.class_name == "Hash"
|
|
2424
2606
|
when Type::Union then type.members.any? { |m| hashish?(m) }
|
|
2607
|
+
when Type::Difference then hashish?(type.base)
|
|
2425
2608
|
else false
|
|
2426
2609
|
end
|
|
2427
2610
|
end
|
|
@@ -2482,16 +2665,6 @@ module Rigor
|
|
|
2482
2665
|
names.to_h { |name| [name, exit_scope.local(name)] }
|
|
2483
2666
|
end
|
|
2484
2667
|
|
|
2485
|
-
# Names introduced by the block itself (parameters, numbered parameters via `BlockParameterBinder`, plus explicit
|
|
2486
|
-
# `;`-prefixed block-locals on `BlockParametersNode`). Writes to these names are local to the block and MUST NOT
|
|
2487
|
-
# be treated as captured rebinds of an outer local.
|
|
2488
|
-
def block_introduced_locals(block_node)
|
|
2489
|
-
introduced = Set.new(BlockParameterBinder.new.bind(block_node).keys)
|
|
2490
|
-
params_root = block_node.parameters
|
|
2491
|
-
params_root.locals.each { |loc| introduced << loc.name } if params_root.is_a?(Prism::BlockParametersNode)
|
|
2492
|
-
introduced
|
|
2493
|
-
end
|
|
2494
|
-
|
|
2495
2668
|
# `Prism::BlockNode` is reached through {#eval_call}; the handler runs the body under `scope`, which the caller
|
|
2496
2669
|
# has already augmented with the block's parameter bindings. Effects do not leak past the block (the outer
|
|
2497
2670
|
# eval_call returns the caller's scope unchanged), but the body's local writes are threaded through subsequent
|
|
@@ -2554,7 +2727,7 @@ module Rigor
|
|
|
2554
2727
|
|
|
2555
2728
|
# ----- def/class helpers -----
|
|
2556
2729
|
|
|
2557
|
-
def eval_class_body(node, new_context)
|
|
2730
|
+
def eval_class_body(node, new_context, new_nesting = @lexical_nesting)
|
|
2558
2731
|
return [Type::Combinator.constant_of(nil), scope] if node.body.nil?
|
|
2559
2732
|
|
|
2560
2733
|
# Class/module bodies run in a fresh scope: the outer scope's locals are NOT visible inside `class Foo; ...
|
|
@@ -2564,7 +2737,8 @@ module Rigor
|
|
|
2564
2737
|
fresh = build_fresh_body_scope
|
|
2565
2738
|
body_self = self_type_for_class_body(new_context)
|
|
2566
2739
|
fresh = fresh.with_self_type(body_self) if body_self
|
|
2567
|
-
|
|
2740
|
+
fresh = stamp_nesting(fresh, new_nesting)
|
|
2741
|
+
sub_eval(node.body, fresh, class_context: new_context, lexical_nesting: new_nesting)
|
|
2568
2742
|
end
|
|
2569
2743
|
|
|
2570
2744
|
def build_method_entry_scope(def_node) # rubocop:disable Metrics/AbcSize
|
|
@@ -2591,6 +2765,9 @@ module Rigor
|
|
|
2591
2765
|
fresh = build_fresh_body_scope
|
|
2592
2766
|
body_self = self_type_for_method_body(singleton: singleton)
|
|
2593
2767
|
fresh = fresh.with_self_type(body_self) if body_self
|
|
2768
|
+
# A `def` opens no new `Module.nesting` entry — the body resolves constants against the chain of the
|
|
2769
|
+
# declaration that encloses it, which is what the evaluator is already carrying (#652).
|
|
2770
|
+
fresh = stamp_nesting(fresh, @lexical_nesting)
|
|
2594
2771
|
fresh = seed_instance_ivars(fresh, singleton: singleton)
|
|
2595
2772
|
fresh = seed_class_cvars(fresh)
|
|
2596
2773
|
fresh = seed_program_globals(fresh)
|
|
@@ -2806,6 +2983,14 @@ module Rigor
|
|
|
2806
2983
|
end
|
|
2807
2984
|
end
|
|
2808
2985
|
|
|
2986
|
+
# Stamps a recorded chain onto a body-entry scope. An empty chain (a top-level body) and an unknown one
|
|
2987
|
+
# are both left unstamped, so `Reflection.lexical_nesting_chain` keeps its peel fallback for them.
|
|
2988
|
+
def stamp_nesting(body_scope, chain)
|
|
2989
|
+
return body_scope if chain.nil? || chain.empty?
|
|
2990
|
+
|
|
2991
|
+
body_scope.with_lexical_nesting(chain)
|
|
2992
|
+
end
|
|
2993
|
+
|
|
2809
2994
|
# The qualified name of the immediately-enclosing class (joining every nested `ClassFrame` with `::`). Returns
|
|
2810
2995
|
# `nil` for a top-level def with no enclosing class, which routes the parameter binder past RBS lookup.
|
|
2811
2996
|
def current_class_path
|
|
@@ -2859,12 +3044,13 @@ module Rigor
|
|
|
2859
3044
|
end
|
|
2860
3045
|
end
|
|
2861
3046
|
|
|
2862
|
-
def sub_eval(node, with_scope, class_context: @class_context)
|
|
3047
|
+
def sub_eval(node, with_scope, class_context: @class_context, lexical_nesting: @lexical_nesting)
|
|
2863
3048
|
StatementEvaluator.new(
|
|
2864
3049
|
scope: with_scope,
|
|
2865
3050
|
tracer: tracer,
|
|
2866
3051
|
on_enter: @on_enter,
|
|
2867
3052
|
class_context: class_context,
|
|
3053
|
+
lexical_nesting: lexical_nesting,
|
|
2868
3054
|
converged_loop_recording: @converged_loop_recording
|
|
2869
3055
|
).evaluate(node)
|
|
2870
3056
|
end
|
|
@@ -3119,6 +3305,5 @@ module Rigor
|
|
|
3119
3305
|
merged.to_a
|
|
3120
3306
|
end
|
|
3121
3307
|
end
|
|
3122
|
-
# rubocop:enable Metrics/ClassLength
|
|
3123
3308
|
end
|
|
3124
3309
|
end
|