rigortype 0.3.5 → 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 +5 -5
- 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 +442 -11
- data/docs/handbook/02-everyday-types.md +1 -1
- data/docs/handbook/04-tuples-and-shapes.md +1 -1
- data/docs/handbook/08-understanding-errors.md +1 -1
- data/docs/handbook/09-plugins.md +2 -2
- data/docs/handbook/10-sorbet.md +1 -1
- data/docs/handbook/README.md +2 -2
- data/docs/handbook/appendix-go.md +1 -1
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +1 -1
- data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +2 -2
- data/docs/handbook/appendix-typescript.md +4 -4
- data/docs/manual/02-cli-reference.md +29 -16
- data/docs/manual/03-configuration.md +1 -1
- data/docs/manual/04-diagnostics.md +12 -0
- data/docs/manual/05-inspecting-types.md +20 -8
- data/docs/manual/07-plugins.md +4 -0
- data/docs/manual/08-skills.md +1 -1
- data/docs/manual/11-ci.md +1 -1
- data/docs/manual/12-caching.md +1 -1
- data/docs/manual/15-type-protection-coverage.md +1 -1
- data/docs/manual/18-removing-dead-code.md +13 -4
- data/docs/manual/19-effect-labels.md +19 -12
- data/docs/manual/README.md +2 -2
- data/docs/manual/plugins/rigor-actionmailer.md +4 -4
- data/docs/manual/plugins/rigor-actionpack.md +61 -0
- data/docs/manual/plugins/rigor-activejob.md +3 -3
- data/docs/manual/plugins/rigor-activerecord.md +78 -5
- data/docs/manual/plugins/rigor-activesupport-core-ext.md +114 -12
- data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
- data/docs/manual/plugins/rigor-rspec.md +6 -2
- 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/diagnostic.rb +17 -6
- data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
- 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 +44 -16
- data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
- data/lib/rigor/analysis/reachability/scan.rb +27 -7
- data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
- data/lib/rigor/analysis/result.rb +32 -0
- data/lib/rigor/analysis/run_cache_key.rb +53 -1
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +93 -1
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +14 -8
- 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 +298 -38
- 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/file_digest.rb +20 -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/check_command.rb +67 -48
- data/lib/rigor/cli/coverage_command.rb +28 -24
- data/lib/rigor/cli/coverage_mutation.rb +13 -17
- data/lib/rigor/cli/coverage_scan.rb +47 -6
- data/lib/rigor/cli/doc_links.rb +100 -0
- data/lib/rigor/cli/docs_command.rb +32 -2
- data/lib/rigor/cli/effects_command.rb +27 -2
- data/lib/rigor/cli/effects_diff_renderer.rb +82 -12
- data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
- data/lib/rigor/cli/effects_snapshot_command.rb +52 -7
- 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/unused_command.rb +65 -16
- data/lib/rigor/cli.rb +22 -9
- data/lib/rigor/configuration.rb +7 -4
- data/lib/rigor/effects/definition_lines.rb +100 -0
- data/lib/rigor/effects/envelope_check.rb +18 -1
- data/lib/rigor/effects/liskov_check.rb +17 -8
- data/lib/rigor/effects/plugin_facts.rb +1 -1
- data/lib/rigor/effects/scanner.rb +2 -3
- data/lib/rigor/effects/signature_sources.rb +13 -2
- data/lib/rigor/effects/snapshot.rb +53 -21
- data/lib/rigor/effects/snapshot_diff.rb +26 -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/synthetic_method_scanner.rb +7 -0
- 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/rbs_extended/envelope_scanner.rb +9 -0
- 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
- data/skills/rigor-ci-setup/SKILL.md +2 -2
- data/skills/rigor-editor-setup/SKILL.md +2 -2
- data/skills/rigor-mcp-setup/SKILL.md +2 -2
- data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- metadata +73 -3
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# The element / key / value **content join** shared by every path that observes a collection
|
|
8
|
+
# being content-mutated in place.
|
|
9
|
+
#
|
|
10
|
+
# {MutationWidening} forgets what a mutation falsified — a literal's arity, and (for a
|
|
11
|
+
# slot-rewriting mutator) its value pinning. That half only ever subtracts. This module is the
|
|
12
|
+
# other half: it adds back what the mutation is known to have PUT in the collection, so the
|
|
13
|
+
# continuation carrier covers the mutated contents instead of only the seed's.
|
|
14
|
+
#
|
|
15
|
+
# Two callers share it, and they must agree:
|
|
16
|
+
#
|
|
17
|
+
# - the **block-capture** path (ADR-56 slice C) — `out = [0]; xs.each { |x| out << x }` types
|
|
18
|
+
# `out` as `Array[0 | Integer]`, not `Array[0]`;
|
|
19
|
+
# - the **straight-line** path (issue #560) — `u = [1, 2]; u.push(6)` types `u` as
|
|
20
|
+
# `Array[1 | 2 | Integer]`, not `Array[1 | 2]`. Before the join, `u.last` read back `1 | 2`,
|
|
21
|
+
# the constant-comparison fold turned `u.last == 6` into `Constant[false]`, and a correct
|
|
22
|
+
# program drew a false `flow.always-truthy-condition`. `mail`'s ragel tables are the same root
|
|
23
|
+
# seen from the precision side: `stack = []; stack[top] = cs` widened to `Array[untyped]`
|
|
24
|
+
# where the join reads `Array[Integer]` (issue #533 item 8).
|
|
25
|
+
module ContentJoin
|
|
26
|
+
# Array content-mutators that append/store ELEMENTS. The appended element type is the call's
|
|
27
|
+
# argument type(s); `[]=`'s value is its LAST argument (the keys precede it). Subset of
|
|
28
|
+
# {MutationWidening::ARRAY_MUTATORS}: only the element-INTRODUCING methods (removers /
|
|
29
|
+
# reorderers add no new element evidence and are already covered by the arity-forget).
|
|
30
|
+
ARRAY_CONTENT_ADDERS = %i[
|
|
31
|
+
<< push append prepend unshift concat insert []= fill replace
|
|
32
|
+
].to_set.freeze
|
|
33
|
+
|
|
34
|
+
# Hash content-mutators that store a key→value pair. For `[]=` / `store` the key is the first
|
|
35
|
+
# argument and the value the last.
|
|
36
|
+
HASH_CONTENT_ADDERS = %i[[]= store].to_set.freeze
|
|
37
|
+
|
|
38
|
+
# String content-mutators that append to the buffer. String carries no element parameter, so
|
|
39
|
+
# these contribute nothing to a join — they are listed so the orchestrator recognises them as
|
|
40
|
+
# content mutators (the binding already widens to `String` via normal typing); the join
|
|
41
|
+
# helpers below short-circuit on a non-collection pre-state.
|
|
42
|
+
STRING_CONTENT_ADDERS = %i[<< concat prepend insert replace].to_set.freeze
|
|
43
|
+
|
|
44
|
+
# Every method name that mutates a collection's CONTENT — the union the orchestrators scan a
|
|
45
|
+
# block body for, and the gate the straight-line path types its arguments behind.
|
|
46
|
+
CONTENT_ADDERS = (ARRAY_CONTENT_ADDERS | HASH_CONTENT_ADDERS | STRING_CONTENT_ADDERS).freeze
|
|
47
|
+
|
|
48
|
+
module_function
|
|
49
|
+
|
|
50
|
+
# The element types a single content-mutator call introduces into an Array, given the
|
|
51
|
+
# per-argument types (already typed by the caller in the scope the arguments are evaluated
|
|
52
|
+
# in). `concat`/`replace` take collection arguments, so their element evidence is the
|
|
53
|
+
# arguments' OWN element types unioned; the rest append the argument values directly. Returns
|
|
54
|
+
# `[]` when there is no element evidence (e.g. a `<<` with no resolvable arg).
|
|
55
|
+
def array_added_elements(method_name, arg_types)
|
|
56
|
+
return [] if arg_types.empty?
|
|
57
|
+
|
|
58
|
+
case method_name
|
|
59
|
+
when :concat, :replace
|
|
60
|
+
arg_types.flat_map { |t| collection_element_types(t) }
|
|
61
|
+
when :insert
|
|
62
|
+
# `insert(index, *objs)` — first arg is the position.
|
|
63
|
+
arg_types.drop(1)
|
|
64
|
+
when :[]=
|
|
65
|
+
# `arr[i] = v` / `arr[i, n] = v` — value is the last argument.
|
|
66
|
+
#
|
|
67
|
+
# The SPLICE forms (`arr[i, n] = other` / `arr[range] = other`) store `other`'s ELEMENTS,
|
|
68
|
+
# not `other` itself, so reading the value as one element over-widens: `a[0, 2] = [1, 2]`
|
|
69
|
+
# contributes `Array[Integer]` where `Integer` is the truth. Left alone deliberately —
|
|
70
|
+
# the answer is a superset either way, so it can only cost precision, and splitting the
|
|
71
|
+
# arities here would need the receiver's own element type to unwrap against. Revisit if a
|
|
72
|
+
# corpus site ever reads an element back through a splice-built array.
|
|
73
|
+
[arg_types.last]
|
|
74
|
+
when :fill
|
|
75
|
+
# `fill(value)` — only the no-block single-value form adds a concrete element; block /
|
|
76
|
+
# range forms are conservatively ignored (the arity-forget already widened the binding).
|
|
77
|
+
arg_types.size == 1 ? arg_types : []
|
|
78
|
+
else # << push append prepend unshift
|
|
79
|
+
arg_types
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Builds the continuation Array type from the pre-state binding and the appended element
|
|
84
|
+
# types. The floor is `Array[Dynamic[top]]` (the sound empty-seed behaviour) when there is no
|
|
85
|
+
# element evidence at all.
|
|
86
|
+
#
|
|
87
|
+
# **Every arm the seed carries survives, a `Dynamic` arm included.** A gradual seed element is
|
|
88
|
+
# a statement about what the collection ALREADY holds — a parameter declared `Array[untyped]`,
|
|
89
|
+
# a local seeded from a call whose signature returns the same, a literal `[x]` whose slot the
|
|
90
|
+
# engine could not type — and the stores a body adds are evidence about what the body put in,
|
|
91
|
+
# never about what was there first. Dropping the
|
|
92
|
+
# arm once concrete evidence appeared closed a declared `Array[untyped]` parameter to
|
|
93
|
+
# `Array[Integer]` under `[1, 2].each { a.push(rand(9)) }` and drew `undefined method 'upcase'`
|
|
94
|
+
# on `a.first.upcase`, which the declaration licenses (issue #586).
|
|
95
|
+
#
|
|
96
|
+
# What this method therefore never sees is the arity-forget's OWN floor: `widen_tuple` spells
|
|
97
|
+
# an empty `[]` as `Array[untyped]`, and read back through here that `untyped` would be
|
|
98
|
+
# indistinguishable from a declared one. Each caller keeps that floor out at its source by
|
|
99
|
+
# reading the seed from before the widening ran — the block seam from the pre-widen scope, the
|
|
100
|
+
# loop seam from the pre-body scope — so an empty literal contributes no element and `out = [];
|
|
101
|
+
# xs.each { out << x*2 }` still reads `Array[Integer]`. The straight-line seam passes the
|
|
102
|
+
# widened carrier, and adds the same `Dynamic` as its one-store floor anyway.
|
|
103
|
+
#
|
|
104
|
+
# **The rederived carrier replaces only the members it stands for** — see {#array_residue}.
|
|
105
|
+
def join_array_content(pre_state, added_elements)
|
|
106
|
+
elements = collection_element_types(pre_state) + added_elements.compact
|
|
107
|
+
element_t = elements.empty? ? Type::Combinator.untyped : Type::Combinator.union(*elements)
|
|
108
|
+
with_residue(Type::Combinator.nominal_of("Array", type_args: [element_t]), array_residue(pre_state))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Builds the continuation Hash type from the pre-state binding and a list of `[key_type,
|
|
112
|
+
# value_type]` pairs stored by `[]=` / `store`. The seed's key and value arms survive on the
|
|
113
|
+
# same terms as {#join_array_content}'s elements: a declared `Hash[untyped, untyped]` stays
|
|
114
|
+
# gradual on both sides however many pairs the body stores, and a pre-state member that is not
|
|
115
|
+
# a Hash carrier at all survives whole ({#hash_residue}).
|
|
116
|
+
def join_hash_content(pre_state, added_pairs)
|
|
117
|
+
seed_keys, seed_values = hash_shape_key_values(pre_state)
|
|
118
|
+
keys = seed_keys + added_pairs.map(&:first).compact
|
|
119
|
+
values = seed_values + added_pairs.map(&:last).compact
|
|
120
|
+
key_t = keys.empty? ? Type::Combinator.untyped : Type::Combinator.union(*keys)
|
|
121
|
+
value_t = values.empty? ? Type::Combinator.untyped : Type::Combinator.union(*values)
|
|
122
|
+
with_residue(Type::Combinator.nominal_of("Hash", type_args: [key_t, value_t]), hash_residue(pre_state))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# ----------------------------------------------------------------
|
|
126
|
+
# Union residue — the members the rederived carrier does NOT stand for (issue #631).
|
|
127
|
+
#
|
|
128
|
+
# A join REPLACES the binding with one freshly built `Nominal`, and that is the right answer
|
|
129
|
+
# only for the pre-state members the mutation actually applied to as a collection of that
|
|
130
|
+
# class. A `Union` seed can carry members that are not collection carriers at all, and those
|
|
131
|
+
# contribute no element / key / value evidence — `collection_element_types` and
|
|
132
|
+
# `hash_shape_key_values` both answer "nothing" for them. Before this rule they were simply
|
|
133
|
+
# gone:
|
|
134
|
+
#
|
|
135
|
+
# out = flag ? u : [2] # Dynamic[top] | [2]
|
|
136
|
+
# [1].each { out << 2 } # -> Array[2]; the whole-variable Dynamic arm dropped
|
|
137
|
+
# out.first.upcase # undefined method `upcase' for 2 -- on correct code
|
|
138
|
+
#
|
|
139
|
+
# This is WD2.9's rule ("a seed's own gradual arm survives the rederivation") one level out:
|
|
140
|
+
# there the surviving `Dynamic` was an ELEMENT of an Array carrier, here it is the whole
|
|
141
|
+
# variable. Both say the same thing — the body's stores are evidence about what the body put
|
|
142
|
+
# in, never about what the variable WAS — and the straight-line seam already reads the union
|
|
143
|
+
# through untouched, so the block and loop seams were the outliers.
|
|
144
|
+
#
|
|
145
|
+
# A member survives whole; it is never re-examined for element evidence, so nothing is
|
|
146
|
+
# double-counted. `Array.new`'s bare `Nominal[Array]` (no type args) IS a carrier and is
|
|
147
|
+
# absorbed, so #615's seed keeps closing rather than growing a second arm.
|
|
148
|
+
#
|
|
149
|
+
# **`nil` is the one member the mutation itself refutes, and it does not survive.** The rule
|
|
150
|
+
# above is a rule about ABSENCE of evidence — the seam cannot say a `Dynamic` or a foreign
|
|
151
|
+
# `Nominal` was mutated as an Array, so it must not rewrite it. `NilClass` defines no content
|
|
152
|
+
# mutator at all, so on every path where the body ran the binding was not nil; only the
|
|
153
|
+
# zero-iteration path keeps the arm, and that path is modelled upstream (the `while` base
|
|
154
|
+
# scope's nil-injection, slice A's `Constant[nil]` fixpoint seed) rather than here. Keeping it
|
|
155
|
+
# measured out as a pure cost: `r = nil; while …; r ||= []; r << x; end; r.each` gained a
|
|
156
|
+
# `call.possible-nil-receiver` on an idiom Rubyists write deliberately, while the genuinely
|
|
157
|
+
# live nil arm is already reported once — at the mutation site, where `r << x` draws the same
|
|
158
|
+
# diagnostic. This is the only member the join can refute without a method lookup it has no
|
|
159
|
+
# environment for; the general "the mutator is undefined on this member" rule would buy
|
|
160
|
+
# rarer shapes for machinery this seam does not have.
|
|
161
|
+
NON_SURVIVING_CLASSES = %w[NilClass].freeze
|
|
162
|
+
|
|
163
|
+
# Pre-state members the rederived Array does not stand for: a whole-variable `Dynamic`, a
|
|
164
|
+
# non-nil `Constant`, a foreign `Nominal`, a `Refined`. Mirrors {#collection_element_types}'s
|
|
165
|
+
# recursion so the absorbed set and the residue partition the union exactly. A `Difference` is
|
|
166
|
+
# absorbed with its base (`non-empty-array[T]` is an Array carrier) and otherwise kept whole,
|
|
167
|
+
# refinement and all.
|
|
168
|
+
def array_residue(type)
|
|
169
|
+
case type
|
|
170
|
+
when Type::Union then type.members.flat_map { |m| array_residue(m) }
|
|
171
|
+
when Type::Tuple then []
|
|
172
|
+
when Type::Nominal then type.class_name == "Array" ? [] : keep_member(type)
|
|
173
|
+
when Type::Difference then array_residue(type.base).empty? ? [] : [type]
|
|
174
|
+
else keep_member(type)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# The Hash-side twin of {#array_residue}, mirroring {#hash_shape_key_values}. A bare
|
|
179
|
+
# `Nominal[Hash]` with no type args is a carrier here even though it yields no key/value
|
|
180
|
+
# evidence — `Hash.new` must close like `Array.new`, not grow an arm.
|
|
181
|
+
def hash_residue(type)
|
|
182
|
+
case type
|
|
183
|
+
when Type::Union then type.members.flat_map { |m| hash_residue(m) }
|
|
184
|
+
when Type::HashShape then []
|
|
185
|
+
when Type::Nominal then type.class_name == "Hash" ? [] : keep_member(type)
|
|
186
|
+
when Type::Difference then hash_residue(type.base).empty? ? [] : [type]
|
|
187
|
+
else keep_member(type)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# `[type]`, unless the mutation refutes the member outright — see {NON_SURVIVING_CLASSES}.
|
|
192
|
+
def keep_member(type)
|
|
193
|
+
NON_SURVIVING_CLASSES.include?(evidence_class(type)) ? [] : [type]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def with_residue(carrier, residue)
|
|
197
|
+
residue.empty? ? carrier : Type::Combinator.union(*residue, carrier)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# ----------------------------------------------------------------
|
|
201
|
+
# Seed-admissibility — the straight-line join's signature gate (issue #560).
|
|
202
|
+
#
|
|
203
|
+
# A join is not free: it grows the carrier's element union, and a carrier that grows a member
|
|
204
|
+
# the enclosing method's HAND-WRITTEN signature does not admit turns a correct program into a
|
|
205
|
+
# `def.return-type-mismatch`. haml's temple builders are the corpus shape —
|
|
206
|
+
#
|
|
207
|
+
# def compile_html(node) # sig: (untyped) -> Array[:multi]
|
|
208
|
+
# temple = [:multi]
|
|
209
|
+
# temple << [:static, "<style>\n"]
|
|
210
|
+
# temple
|
|
211
|
+
# end
|
|
212
|
+
#
|
|
213
|
+
# — where joining the appended tuple precisely reads `Array[:multi | [:static, String]]` and
|
|
214
|
+
# draws a mismatch against `Array[:multi]` on eight sites. (PR #561 hit the same wall from the
|
|
215
|
+
# other direction and had to scope value-pin widening away from adders to avoid it.) A gradual
|
|
216
|
+
# member does NOT rescue that: `Array[:multi | [:static, String] | untyped]` is still rejected,
|
|
217
|
+
# because every non-`Dynamic` member is judged on its own. The gate is what keeps the foreign
|
|
218
|
+
# member out; the caller's gradual floor is a separate concern and neither substitutes for the
|
|
219
|
+
# other.
|
|
220
|
+
#
|
|
221
|
+
# So added evidence is admitted per member, against the class set the SEED already carries:
|
|
222
|
+
#
|
|
223
|
+
# - the seed's class set already admits the added member's class — the collection is
|
|
224
|
+
# homogeneous in the sense that matters, and the member joins as itself
|
|
225
|
+
# (`u = [1, 2]; u.push(6)` contributes `Integer`);
|
|
226
|
+
# - it does not — the collection is provably heterogeneous, and between the literal seed and
|
|
227
|
+
# the author's signature the engine has no ground to adjudicate. It contributes
|
|
228
|
+
# `Dynamic[top]` instead of a foreign precise member;
|
|
229
|
+
# - the seed carries nothing to contradict — an empty literal, or a slot the engine cannot type
|
|
230
|
+
# — and every member is admitted as itself.
|
|
231
|
+
#
|
|
232
|
+
# The second case only ever replaces a WRONG precise element type with a gradual one, so it is
|
|
233
|
+
# a soundness improvement paid for in opacity on exactly the sites that were lying.
|
|
234
|
+
#
|
|
235
|
+
# What this module does NOT decide is whether the resulting parameter may be CLOSED. That is
|
|
236
|
+
# the caller's call, and it turns on whether the caller saw every store —
|
|
237
|
+
# {MutationWidening#join_added_elements} carries the rule and the counter-example.
|
|
238
|
+
|
|
239
|
+
# `added`, with every member the seed's class set does not admit replaced by `Dynamic[top]`.
|
|
240
|
+
#
|
|
241
|
+
# A seed that carries nothing to contradict admits everything: an empty literal has no class
|
|
242
|
+
# set, and a `Dynamic` member means the engine could not type that slot, so it cannot rule
|
|
243
|
+
# anything out either.
|
|
244
|
+
#
|
|
245
|
+
# Both sides are flattened through their `Union` members before matching. Judging a union
|
|
246
|
+
# wholesale would floor `["a", 1]`-shaped evidence against an `Integer` seed even though its
|
|
247
|
+
# `Integer` half is admissible, and `evidence_class` has no answer for a `Union` at all — so
|
|
248
|
+
# the wholesale reading is strictly worse and no simpler.
|
|
249
|
+
def admissible_evidence(seed_members, added)
|
|
250
|
+
members = seed_members.flat_map { |m| union_members(m) }
|
|
251
|
+
return added if members.empty? || members.any?(Type::Dynamic)
|
|
252
|
+
|
|
253
|
+
classes = members.filter_map { |m| evidence_class(m) }.to_set
|
|
254
|
+
added.flat_map { |type| admit_members(union_members(type), classes) }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Each member of one added type, kept when its class is admitted and floored when it is not.
|
|
258
|
+
# A type whose members are all admitted returns them unchanged, so the common single-member
|
|
259
|
+
# case is `[type]`.
|
|
260
|
+
def admit_members(members, classes)
|
|
261
|
+
members.map { |m| classes.include?(evidence_class(m)) ? m : Type::Combinator.untyped }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The class name a type carrier commits its values to, or `nil` when it commits to none. A
|
|
265
|
+
# `nil` answer is never admitted: an unnameable carrier cannot be shown compatible with the
|
|
266
|
+
# seed, so it takes the gradual floor.
|
|
267
|
+
def evidence_class(type)
|
|
268
|
+
case type
|
|
269
|
+
when Type::Nominal then type.class_name
|
|
270
|
+
when Type::Constant then type.value.class.name
|
|
271
|
+
when Type::Tuple then "Array"
|
|
272
|
+
when Type::HashShape then "Hash"
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def union_members(type)
|
|
277
|
+
type.is_a?(Type::Union) ? type.members : [type]
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Element types carried by a collection binding, regardless of which carrier holds them: a
|
|
281
|
+
# `Tuple` lists them, a `Nominal[Array, [E]]` has one element param, a bare `Array` /
|
|
282
|
+
# anything else yields none.
|
|
283
|
+
#
|
|
284
|
+
# A `Difference` reads through to its base: `non-empty-array[T]` holds `T`s, and the seams
|
|
285
|
+
# that read a seed from BEFORE the arity-forget ran (see {#join_array_content}) meet the
|
|
286
|
+
# refinement carrier itself where they used to meet the base the widening had left. Declining
|
|
287
|
+
# it there would hand the continuation the widened base ALONE, with every appended arm missing
|
|
288
|
+
# — a wrong type, not a wide one.
|
|
289
|
+
def collection_element_types(type)
|
|
290
|
+
case type
|
|
291
|
+
when Type::Tuple
|
|
292
|
+
type.elements
|
|
293
|
+
when Type::Nominal
|
|
294
|
+
type.class_name == "Array" ? type.type_args : []
|
|
295
|
+
when Type::Union
|
|
296
|
+
# A loop's single-pass join can union the widened collection with its un-widened literal
|
|
297
|
+
# seed (`Array[0] | [0]`); pull element evidence from every Array-ish member.
|
|
298
|
+
type.members.flat_map { |m| collection_element_types(m) }
|
|
299
|
+
when Type::Difference
|
|
300
|
+
collection_element_types(type.base)
|
|
301
|
+
else
|
|
302
|
+
[]
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# `[keys, values]` evidence from a Hash-ish pre-state binding — a `HashShape` (literal pairs)
|
|
307
|
+
# or a `Nominal[Hash, [K, V]]`. A `Difference` (`non-empty-hash[K, V]`) reads through to its
|
|
308
|
+
# base, as {#collection_element_types} does for the Array side.
|
|
309
|
+
def hash_shape_key_values(type)
|
|
310
|
+
case type
|
|
311
|
+
when Type::HashShape
|
|
312
|
+
return [[], []] if type.pairs.empty?
|
|
313
|
+
|
|
314
|
+
[[key_union_for(type.pairs.keys)], type.pairs.values]
|
|
315
|
+
when Type::Nominal
|
|
316
|
+
type.class_name == "Hash" && type.type_args.size == 2 ? [[type.type_args[0]], [type.type_args[1]]] : [[], []]
|
|
317
|
+
when Type::Union
|
|
318
|
+
type.members.each_with_object([[], []]) do |m, (ks, vs)|
|
|
319
|
+
mk, mv = hash_shape_key_values(m)
|
|
320
|
+
ks.concat(mk)
|
|
321
|
+
vs.concat(mv)
|
|
322
|
+
end
|
|
323
|
+
when Type::Difference
|
|
324
|
+
hash_shape_key_values(type.base)
|
|
325
|
+
else
|
|
326
|
+
[[], []]
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Maps a literal Ruby key set to a union of the corresponding type carriers. Symbol / String /
|
|
331
|
+
# Integer / Float keys widen to their class nominal; the `true` / `false` / `nil` singleton
|
|
332
|
+
# keys keep their constant carrier (the constant IS the class's whole value set, and `nil`
|
|
333
|
+
# reads better than `NilClass` in a widened `Hash[K, V]`). We deliberately do NOT fold the
|
|
334
|
+
# widenable kinds to a `Constant<:k1> | Constant<:k2>` union — that would be a precision
|
|
335
|
+
# improvement that complicates the widening contract; the goal there is to LOSE precision, not
|
|
336
|
+
# to record a new fact set.
|
|
337
|
+
def key_union_for(keys)
|
|
338
|
+
carriers = keys.map do |key|
|
|
339
|
+
next Type::Combinator.constant_of(key) if [true, false, nil].include?(key)
|
|
340
|
+
|
|
341
|
+
Type::Combinator.nominal_of(key.class.name)
|
|
342
|
+
end.uniq
|
|
343
|
+
carriers.size == 1 ? carriers.first : Type::Combinator.union(*carriers)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
@@ -6,7 +6,7 @@ module Rigor
|
|
|
6
6
|
# the live node itself. When an incremental recheck rebuilds the discovery index from cached per-file seed
|
|
7
7
|
# bundles (ADR-85 WD2), the `def_nodes` / `singleton_def_nodes` tables carry handles for every unchanged
|
|
8
8
|
# file's methods: the file was never parsed this run, so there is no live node to store. A handle records
|
|
9
|
-
# the
|
|
9
|
+
# the four things needed to resolve, fingerprint, or scope it without the node:
|
|
10
10
|
#
|
|
11
11
|
# - `path` + `node_id` — the resolution key. The per-run parse memo ({Scope#resolve_def_handle}) parses the
|
|
12
12
|
# file once and returns the `Prism::DefNode` whose `node_id` matches (Prism `node_id` is stable across
|
|
@@ -16,8 +16,20 @@ module Rigor
|
|
|
16
16
|
# - `fingerprint` — the SHA-256 of the def's source slice, captured when the bundle was built. This lets
|
|
17
17
|
# `Runner#symbol_fingerprints` read the change-detection fingerprint (ADR-46 slice 4) off the handle
|
|
18
18
|
# without re-parsing the file — the one value-deref consumer besides the three accessor choke points.
|
|
19
|
+
# - `nesting` — the `Module.nesting` in force where the def is WRITTEN (issue #681's chain), `[]` for a
|
|
20
|
+
# top-level def (issue #716: that is Ruby's answer, not the absence of one), and nil only when no
|
|
21
|
+
# declaration walk recorded any. The cross-file `def_nestings` table is keyed by node IDENTITY, and
|
|
22
|
+
# {DefNodeResolver} hands back a node from its OWN parse, so no identity-keyed table built by the
|
|
23
|
+
# discovery walk can ever contain it. Travelling on the handle is what lets the resolver re-attach the
|
|
24
|
+
# chain to the node it mints, instead of the re-walk falling back to peeling the receiver's qualified
|
|
25
|
+
# name — which cannot tell a compact `class Admin::Maker` from the nested spelling, so an unchanged
|
|
26
|
+
# file's callee answered a different constant on the warm path than on a cold run ([#707]).
|
|
19
27
|
#
|
|
20
|
-
#
|
|
21
|
-
|
|
28
|
+
# A required member rather than a defaulted one: a writer that has a chain and forgets to pass it
|
|
29
|
+
# reintroduces exactly the divergence above, silently, so the constructor is where that must fail loudly.
|
|
30
|
+
#
|
|
31
|
+
# Marshal-clean by construction (Integer + Strings + a String array), so it rides the
|
|
32
|
+
# `IncrementalSnapshot` blob directly.
|
|
33
|
+
DefHandle = Data.define(:path, :node_id, :name, :fingerprint, :nesting)
|
|
22
34
|
end
|
|
23
35
|
end
|
|
@@ -28,15 +28,24 @@ module Rigor
|
|
|
28
28
|
private_constant :MEMO_KEY
|
|
29
29
|
|
|
30
30
|
# Installs a fresh per-run resolution memo, restoring the previous one on exit (always, even on a raise).
|
|
31
|
-
# `nodes` caches the resolved node per `(path, node_id)`; `indexes` caches each file's parse index
|
|
31
|
+
# `nodes` caches the resolved node per `(path, node_id)`; `indexes` caches each file's parse index;
|
|
32
|
+
# `nestings` re-attaches each handle's recorded `Module.nesting` to the node this module minted for it
|
|
33
|
+
# ({.rehydrated_nesting}).
|
|
32
34
|
def self.with_run
|
|
33
35
|
previous = Thread.current[MEMO_KEY]
|
|
34
|
-
Thread.current[MEMO_KEY] = { nodes: {}, indexes: {} }
|
|
36
|
+
Thread.current[MEMO_KEY] = { nodes: {}, indexes: {}, nestings: {}.compare_by_identity }
|
|
35
37
|
yield
|
|
36
38
|
ensure
|
|
37
39
|
Thread.current[MEMO_KEY] = previous
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
# Whether a per-run memo scope is in force. The rehydrated `Module.nesting` ({.rehydrated_nesting}) is
|
|
43
|
+
# recorded only inside one, so this names the precondition the internal spec's "within the resolver's
|
|
44
|
+
# per-run memo scope" clause states — and lets a spec assert that no handle-consuming entry point
|
|
45
|
+
# resolves outside it, which is the difference between the peel fallback being a documented edge and
|
|
46
|
+
# being silently reachable from production.
|
|
47
|
+
def self.run_scope? = !Thread.current[MEMO_KEY].nil?
|
|
48
|
+
|
|
40
49
|
# Resolves `handle` to a `Prism::DefNode` (the same object across the run for a given (path, node_id)), or
|
|
41
50
|
# nil. A non-handle argument is returned unchanged, so callers can pass a table value that is either a live
|
|
42
51
|
# node (cold / re-walked file) or a handle (unchanged file) without branching.
|
|
@@ -50,8 +59,58 @@ module Rigor
|
|
|
50
59
|
nodes = memo[:nodes]
|
|
51
60
|
return nodes[key] if nodes.key?(key)
|
|
52
61
|
|
|
53
|
-
nodes[key] = locate(handle, memo[:indexes])
|
|
62
|
+
node = nodes[key] = locate(handle, memo[:indexes])
|
|
63
|
+
record_nesting(memo, node, handle.nesting)
|
|
64
|
+
node
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Issue #707 — the recorded `Module.nesting` for a node this module minted from a {DefHandle}, or nil.
|
|
68
|
+
#
|
|
69
|
+
# This is a REHYDRATION of `Scope::DiscoveryIndex#discovered_def_nestings`, not a rival source for the
|
|
70
|
+
# same question, and the single reader ({Inference::ExpressionTyper#recorded_def_nesting}) may consult
|
|
71
|
+
# the table first and fall through here without shadowing a live answer. Two independent facts license
|
|
72
|
+
# that order, and the tempting third one is FALSE:
|
|
73
|
+
#
|
|
74
|
+
# 1. OBJECT PROVENANCE — no node can be a key in both. {.build_file_index} runs its OWN `Prism.parse`,
|
|
75
|
+
# and both tables are `compare_by_identity`, so a node this module mints is never the object any
|
|
76
|
+
# walk over the analyzer's own parse produced. This holds per NODE and needs nothing about files.
|
|
77
|
+
# 2. AGREEMENT — where both tables answer for the same DEF (through different objects), they answer the
|
|
78
|
+
# same chain. A bundle is only reused when the file's content SHA-256 matches
|
|
79
|
+
# ({Cache::FileDigest.hexdigest}, the digest tier, not the stat one), and `build_def_nestings` is a
|
|
80
|
+
# pure function of that file's AST, so identical bytes yield an identical chain.
|
|
81
|
+
#
|
|
82
|
+
# What is NOT true — and was asserted here before it was measured — is that a file takes exactly ONE of
|
|
83
|
+
# the two branches per run. It does so in the cross-file pre-pass only. An UNCHANGED file re-analysed as
|
|
84
|
+
# a dependent is served from its bundle there AND walked live by `ScopeIndexer#merge_def_node_tables`
|
|
85
|
+
# for its own per-file index, contributing keys to both tables in one `Runner#run`. Fact 1 is what makes
|
|
86
|
+
# that harmless, so a future change must preserve the separate parse, not the file-level split.
|
|
87
|
+
#
|
|
88
|
+
# `--verify-incremental` is the standing detector for the whole property: it compares the warm and cold
|
|
89
|
+
# diagnostics project-wide, so a disagreement surfaces as a reported incremental-only / full-only
|
|
90
|
+
# diagnostic rather than as a silently preferred answer.
|
|
91
|
+
#
|
|
92
|
+
# Outside a run scope ({.with_run} never entered) nothing is recorded and the reader keeps
|
|
93
|
+
# `Reflection.lexical_nesting_chain`'s peel fallback — the same gradual answer that path already gives.
|
|
94
|
+
# Every entry point that CONSUMES a handle runs inside `with_run`; `spec/rigor/inference/
|
|
95
|
+
# def_node_resolver_spec.rb` pins that, so a new one added outside a run scope fails there rather than
|
|
96
|
+
# silently reverting to the peel.
|
|
97
|
+
def self.rehydrated_nesting(node)
|
|
98
|
+
memo = Thread.current[MEMO_KEY]
|
|
99
|
+
memo && memo[:nestings][node]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Files the chain the bundle recorded against the node just minted for it. Issue #716 — an EMPTY chain
|
|
103
|
+
# is recorded like any other: it is the bundle saying the def is written at the top level, and dropping
|
|
104
|
+
# it would leave the warm path peeling the caller's namespace where a cold run resolves at the top
|
|
105
|
+
# level. Only `nil` — a bundle that recorded nothing for the def — is skipped, and
|
|
106
|
+
# `Cache::IncrementalSnapshot::SCHEMA` 17 is what keeps a pre-#716 blob (where `nil` MEANT top level)
|
|
107
|
+
# from being read as one.
|
|
108
|
+
def self.record_nesting(memo, node, nesting)
|
|
109
|
+
return if node.nil? || nesting.nil?
|
|
110
|
+
|
|
111
|
+
memo[:nestings][node] = nesting
|
|
54
112
|
end
|
|
113
|
+
private_class_method :record_nesting
|
|
55
114
|
|
|
56
115
|
# Finds the node for `handle` using a per-file `{node_id => DefNode}` + `{name => DefNode}` index cache.
|
|
57
116
|
def self.locate(handle, index_cache)
|