rigortype 0.1.17 → 0.1.18
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 +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules/rule_walk.rb +67 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +18 -1
- data/lib/rigor/analysis/check_rules.rb +34 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +580 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +318 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +160 -1190
- data/lib/rigor/analysis/worker_session.rb +47 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/check_command.rb +705 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli.rb +15 -614
- data/lib/rigor/configuration.rb +9 -6
- data/lib/rigor/environment/rbs_loader.rb +53 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/expression_typer.rb +28 -62
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +10 -11
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher.rb +115 -54
- data/lib/rigor/inference/narrowing.rb +60 -0
- data/lib/rigor/inference/scope_indexer.rb +75 -15
- data/lib/rigor/inference/statement_evaluator.rb +35 -52
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +282 -41
- data/lib/rigor/plugin/node_rule_walk.rb +147 -0
- data/lib/rigor/plugin/registry.rb +263 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +58 -0
- data/lib/rigor/scope.rb +67 -198
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/type/combinator.rb +5 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +70 -32
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +15 -21
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +35 -18
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +83 -36
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +1 -2
- data/sig/rigor/scope.rbs +41 -29
- data/sig/rigor/source.rbs +1 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- metadata +15 -2
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
data/lib/rigor/configuration.rb
CHANGED
|
@@ -70,12 +70,15 @@ module Rigor
|
|
|
70
70
|
"fold_platform_specific_paths" => false,
|
|
71
71
|
"cache" => {
|
|
72
72
|
"path" => ".rigor/cache",
|
|
73
|
-
# LRU eviction cap in bytes
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
|
|
73
|
+
# LRU eviction cap in bytes (ADR-54 WD3). The least-recently-used
|
|
74
|
+
# entries are removed at the end of a run when the total exceeds
|
|
75
|
+
# this limit. The 256 MB default exists to reap orphans — entries
|
|
76
|
+
# whose content key nothing references any more (an rbs gem bump,
|
|
77
|
+
# a signature change) and that no run would otherwise ever delete;
|
|
78
|
+
# a full active per-project set is ~2 MB, so the cap never touches
|
|
79
|
+
# live entries. Set explicitly to `null` to disable eviction
|
|
80
|
+
# (pre-WD3 behaviour: the cache grows until `--clear-cache`).
|
|
81
|
+
"max_bytes" => 268_435_456
|
|
79
82
|
},
|
|
80
83
|
"plugins_io" => {
|
|
81
84
|
"network" => "disabled",
|
|
@@ -559,29 +559,17 @@ module Rigor
|
|
|
559
559
|
# definition cannot be built (RBS may raise on broken hierarchies;
|
|
560
560
|
# we fail-soft and return nil so the caller can fall back).
|
|
561
561
|
#
|
|
562
|
-
#
|
|
563
|
-
#
|
|
564
|
-
#
|
|
565
|
-
#
|
|
566
|
-
#
|
|
567
|
-
#
|
|
562
|
+
# Built on demand from the (possibly cache-loaded) env; the
|
|
563
|
+
# in-memory `@instance_definition_cache` keeps the per-process
|
|
564
|
+
# short-circuit. ADR-54 WD1 retired the definitions disk blob:
|
|
565
|
+
# given a cached env, `Marshal.load`-ing every definition was
|
|
566
|
+
# measurably slower (and allocation-heavier) than rebuilding
|
|
567
|
+
# the ones a run actually touches.
|
|
568
568
|
def instance_definition(class_name)
|
|
569
569
|
key = class_name.to_s
|
|
570
570
|
return @instance_definition_cache[key] if @instance_definition_cache.key?(key)
|
|
571
571
|
|
|
572
|
-
@instance_definition_cache[key] =
|
|
573
|
-
cached_instance_definition(class_name)
|
|
574
|
-
else
|
|
575
|
-
build_instance_definition(class_name)
|
|
576
|
-
end
|
|
577
|
-
end
|
|
578
|
-
|
|
579
|
-
# Public uncached accessor used by the cache producer
|
|
580
|
-
# ({Rigor::Cache::RbsInstanceDefinitions}). Avoids the
|
|
581
|
-
# `private_method_called` round-trip a `loader.send(...)`
|
|
582
|
-
# callsite would require.
|
|
583
|
-
def uncached_instance_definition(class_name)
|
|
584
|
-
build_instance_definition(class_name)
|
|
572
|
+
@instance_definition_cache[key] = build_instance_definition(class_name)
|
|
585
573
|
end
|
|
586
574
|
|
|
587
575
|
# @return [RBS::Definition::Method, nil]
|
|
@@ -636,24 +624,14 @@ module Rigor
|
|
|
636
624
|
# those inherited from `Class` and `Module` for class types.
|
|
637
625
|
# Returns nil for unknown names and on RBS build errors (fail-soft).
|
|
638
626
|
#
|
|
639
|
-
#
|
|
640
|
-
#
|
|
641
|
-
#
|
|
642
|
-
# discipline as {#instance_definition}.
|
|
627
|
+
# Built on demand from the env with a per-process memo; the
|
|
628
|
+
# same on-demand discipline as {#instance_definition} (ADR-54
|
|
629
|
+
# WD1).
|
|
643
630
|
def singleton_definition(class_name)
|
|
644
631
|
key = class_name.to_s
|
|
645
632
|
return @singleton_definition_cache[key] if @singleton_definition_cache.key?(key)
|
|
646
633
|
|
|
647
|
-
@singleton_definition_cache[key] =
|
|
648
|
-
cached_singleton_definition(class_name)
|
|
649
|
-
else
|
|
650
|
-
build_singleton_definition(class_name)
|
|
651
|
-
end
|
|
652
|
-
end
|
|
653
|
-
|
|
654
|
-
# Public uncached accessor used by the cache producer.
|
|
655
|
-
def uncached_singleton_definition(class_name)
|
|
656
|
-
build_singleton_definition(class_name)
|
|
634
|
+
@singleton_definition_cache[key] = build_singleton_definition(class_name)
|
|
657
635
|
end
|
|
658
636
|
|
|
659
637
|
# @return [RBS::Definition::Method, nil] the class method on
|
|
@@ -762,18 +740,20 @@ module Rigor
|
|
|
762
740
|
end
|
|
763
741
|
|
|
764
742
|
# ADR-15 Phase 4b.x — eagerly drives every cached
|
|
765
|
-
# producer
|
|
766
|
-
#
|
|
767
|
-
#
|
|
743
|
+
# producer (plus the eager definitions tables, computed
|
|
744
|
+
# from the cached env since ADR-54 WD1) so a subsequent
|
|
745
|
+
# worker Ractor can serve all of its RBS queries without
|
|
746
|
+
# ever calling `RBS::EnvironmentLoader.new`.
|
|
768
747
|
# The loader path that calls `EnvironmentLoader.new`
|
|
769
748
|
# transitively reads a chain of non-`Ractor.shareable?`
|
|
770
749
|
# module constants
|
|
771
750
|
# (`RBS::EnvironmentLoader::DEFAULT_CORE_ROOT`,
|
|
772
751
|
# `RBS::Repository::DEFAULT_STDLIB_ROOT`,
|
|
773
752
|
# `Gem::Requirement::DefaultRequirement`, …) and trips
|
|
774
|
-
# `Ractor::IsolationError`. Pre-warming the
|
|
775
|
-
#
|
|
776
|
-
#
|
|
753
|
+
# `Ractor::IsolationError`. Pre-warming on the main
|
|
754
|
+
# Ractor — env blob loaded, derived tables built — keeps
|
|
755
|
+
# workers off that chain (`RBS::DefinitionBuilder` over
|
|
756
|
+
# an already-built env does not touch it).
|
|
777
757
|
#
|
|
778
758
|
# No-op when `cache_store` is nil — without a Store the
|
|
779
759
|
# worker has no choice but to build env via the loader,
|
|
@@ -793,6 +773,20 @@ module Rigor
|
|
|
793
773
|
self
|
|
794
774
|
end
|
|
795
775
|
|
|
776
|
+
# ADR-54 WD4 — the shared cache descriptor for every RBS-derived
|
|
777
|
+
# producer consulting this loader. Building it digests every
|
|
778
|
+
# `.rbs` file under `signature_paths` + the vendored gem sigs,
|
|
779
|
+
# and the result is identical across producers, so one build is
|
|
780
|
+
# memoised per loader (on `@state`, alongside `:env` — the env
|
|
781
|
+
# itself is loader-lifetime-memoised, so this adds no new
|
|
782
|
+
# staleness class).
|
|
783
|
+
def rbs_cache_descriptor
|
|
784
|
+
@state[:rbs_cache_descriptor] ||= begin
|
|
785
|
+
require_relative "../cache/rbs_descriptor"
|
|
786
|
+
Cache::RbsDescriptor.build(self)
|
|
787
|
+
end
|
|
788
|
+
end
|
|
789
|
+
|
|
796
790
|
# ADR-15 Phase 2b — return the loader's read-only
|
|
797
791
|
# query surface as a frozen, `Ractor.shareable?`
|
|
798
792
|
# {Reflection} value object. Built lazily on first
|
|
@@ -969,43 +963,34 @@ module Rigor
|
|
|
969
963
|
Cache::RbsEnvironment.fetch(loader: self, store: cache_store)
|
|
970
964
|
end
|
|
971
965
|
|
|
972
|
-
#
|
|
973
|
-
#
|
|
974
|
-
#
|
|
975
|
-
#
|
|
976
|
-
#
|
|
977
|
-
#
|
|
978
|
-
#
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
end
|
|
982
|
-
|
|
966
|
+
# Full `Hash<String, RBS::Definition>` tables for the
|
|
967
|
+
# {#prewarm} / {#reflection} consumers (ADR-15 Phase 2b/4b.x),
|
|
968
|
+
# which need every definition materialised up front. Built
|
|
969
|
+
# from the (cached) env via `RBS::DefinitionBuilder` — ADR-54
|
|
970
|
+
# WD1 retired the disk blobs these used to `Marshal.load`
|
|
971
|
+
# (building all definitions from a cached env is faster), so
|
|
972
|
+
# the eager-table cost is now a compute, not a load. Keys stay
|
|
973
|
+
# in `RBS::TypeName#to_s` form (top-level prefixed `"::Hash"`)
|
|
974
|
+
# — the shape {Environment::Reflection} documents.
|
|
983
975
|
def instance_definitions_table
|
|
984
|
-
@state[:instance_definitions_table] ||=
|
|
985
|
-
|
|
986
|
-
fetch_or_compute_producer(Cache::RbsInstanceDefinitions)
|
|
976
|
+
@state[:instance_definitions_table] ||= build_definitions_table do |name|
|
|
977
|
+
build_instance_definition(name)
|
|
987
978
|
end
|
|
988
979
|
end
|
|
989
980
|
|
|
990
|
-
def cached_singleton_definition(class_name)
|
|
991
|
-
singleton_definitions_table[normalise_class_key(class_name)]
|
|
992
|
-
end
|
|
993
|
-
|
|
994
981
|
def singleton_definitions_table
|
|
995
|
-
@state[:singleton_definitions_table] ||=
|
|
996
|
-
|
|
997
|
-
fetch_or_compute_producer(Cache::RbsSingletonDefinitions)
|
|
982
|
+
@state[:singleton_definitions_table] ||= build_definitions_table do |name|
|
|
983
|
+
build_singleton_definition(name)
|
|
998
984
|
end
|
|
999
985
|
end
|
|
1000
986
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
s.start_with?("::") ? s : "::#{s}"
|
|
987
|
+
def build_definitions_table
|
|
988
|
+
table = {}
|
|
989
|
+
each_known_class_name do |name|
|
|
990
|
+
definition = yield(name)
|
|
991
|
+
table[name] = definition if definition
|
|
992
|
+
end
|
|
993
|
+
table
|
|
1009
994
|
end
|
|
1010
995
|
|
|
1011
996
|
def builder
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -77,7 +77,7 @@ module Rigor
|
|
|
77
77
|
# @param plugin_registry [Rigor::Plugin::Registry, nil] v0.1.1
|
|
78
78
|
# Track 2 slice 7. The per-run plugin registry the
|
|
79
79
|
# inference engine consults at call sites for plugin
|
|
80
|
-
#
|
|
80
|
+
# `dynamic_return` rules. When nil (the
|
|
81
81
|
# default), no plugin-level return-type contribution
|
|
82
82
|
# participates — useful for tests, the `Environment.default`
|
|
83
83
|
# facade, and analyses that don't load plugins.
|
|
@@ -243,6 +243,12 @@ module Rigor
|
|
|
243
243
|
when Type::HashShape
|
|
244
244
|
accepts(self_type, project_hash_shape_to_nominal(other_type), mode: mode)
|
|
245
245
|
.with_reason("projected HashShape to Nominal[Hash]")
|
|
246
|
+
when Type::DataInstance
|
|
247
|
+
# ADR-48: a class-tagged member instance is exactly one value of
|
|
248
|
+
# its tagging class, so it projects to that class's nominal (the
|
|
249
|
+
# anonymous local-bound form projects to `Data` itself).
|
|
250
|
+
accepts(self_type, project_data_instance_to_nominal(other_type), mode: mode)
|
|
251
|
+
.with_reason("projected DataInstance to Nominal[#{other_type.class_name || 'Data'}]")
|
|
246
252
|
when Type::Difference, Type::Refined
|
|
247
253
|
# A refinement carrier's value set is a subset of its
|
|
248
254
|
# base. So if `self` (Nominal) accepts the base, it
|
|
@@ -376,6 +382,10 @@ module Rigor
|
|
|
376
382
|
end
|
|
377
383
|
end
|
|
378
384
|
|
|
385
|
+
def project_data_instance_to_nominal(instance)
|
|
386
|
+
Type::Combinator.nominal_of(instance.class_name || "Data")
|
|
387
|
+
end
|
|
388
|
+
|
|
379
389
|
def project_hash_shape_to_nominal(shape)
|
|
380
390
|
return Type::Combinator.nominal_of(Hash) if shape.pairs.empty?
|
|
381
391
|
|
|
@@ -8,6 +8,7 @@ require_relative "../analysis/self_call_resolution_recorder"
|
|
|
8
8
|
require_relative "block_parameter_binder"
|
|
9
9
|
require_relative "budget_trace"
|
|
10
10
|
require_relative "fallback"
|
|
11
|
+
require_relative "flow_tracer"
|
|
11
12
|
require_relative "indexed_narrowing"
|
|
12
13
|
require_relative "macro_block_self_type"
|
|
13
14
|
require_relative "method_dispatcher"
|
|
@@ -219,6 +220,15 @@ module Rigor
|
|
|
219
220
|
end
|
|
220
221
|
|
|
221
222
|
def type_of(node)
|
|
223
|
+
return untraced_type_of(node) unless FlowTracer.active?
|
|
224
|
+
|
|
225
|
+
# `rigor trace` — bracket the recursion with enter/result events.
|
|
226
|
+
# The tracer is observational only: the inferred type flows
|
|
227
|
+
# through unchanged (see FlowTracer's contract).
|
|
228
|
+
FlowTracer.trace_node(node) { untraced_type_of(node) }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def untraced_type_of(node)
|
|
222
232
|
# Slice A-declarations. ScopeIndexer pre-fills
|
|
223
233
|
# `scope.declared_types` for declaration-position nodes
|
|
224
234
|
# (`module Foo` / `class Bar` headers) with the qualified
|
|
@@ -668,26 +678,17 @@ module Rigor
|
|
|
668
678
|
end
|
|
669
679
|
|
|
670
680
|
# Returns `:truthy`, `:falsey`, or `nil` for an arbitrary
|
|
671
|
-
# predicate expression under three-valued logic.
|
|
672
|
-
#
|
|
673
|
-
#
|
|
674
|
-
#
|
|
675
|
-
# `
|
|
676
|
-
#
|
|
677
|
-
#
|
|
681
|
+
# predicate expression under three-valued logic.
|
|
682
|
+
# {Narrowing.predicate_certainty} owns the judgment (the same
|
|
683
|
+
# one `StatementEvaluator#live_branch_for_if` reads on the
|
|
684
|
+
# scope side): `Nominal[Integer]` (always truthy in Ruby),
|
|
685
|
+
# `Constant[nil]`, and `Constant[false]` fold one branch;
|
|
686
|
+
# `Union[true, false]`, `Dynamic[T]`, and `Top` keep both
|
|
687
|
+
# branches live.
|
|
678
688
|
def constant_predicate_polarity(predicate)
|
|
679
689
|
return nil if predicate.nil?
|
|
680
690
|
|
|
681
|
-
|
|
682
|
-
return nil if type.nil? || type.is_a?(Type::Bot)
|
|
683
|
-
|
|
684
|
-
truthy_bot = Narrowing.narrow_truthy(type).is_a?(Type::Bot)
|
|
685
|
-
falsey_bot = Narrowing.narrow_falsey(type).is_a?(Type::Bot)
|
|
686
|
-
|
|
687
|
-
return :falsey if truthy_bot && !falsey_bot
|
|
688
|
-
return :truthy if !truthy_bot && falsey_bot
|
|
689
|
-
|
|
690
|
-
nil
|
|
691
|
+
Narrowing.predicate_certainty(type_of(predicate))
|
|
691
692
|
end
|
|
692
693
|
|
|
693
694
|
def type_of_else(node)
|
|
@@ -818,29 +819,14 @@ module Rigor
|
|
|
818
819
|
# `:maybe` — the existing union fallback handles them.
|
|
819
820
|
def case_when_pattern_certainty(subject_type, pattern_node)
|
|
820
821
|
class_name = build_constant_path_name(pattern_node)
|
|
821
|
-
return class_pattern_certainty(subject_type, class_name) if class_name
|
|
822
|
+
return Narrowing.class_pattern_certainty(subject_type, class_name, environment: scope.environment) if class_name
|
|
822
823
|
|
|
823
824
|
literal = literal_pattern_value(pattern_node)
|
|
824
|
-
return
|
|
825
|
+
return Narrowing.value_pattern_certainty(subject_type, literal[:value]) if literal
|
|
825
826
|
|
|
826
827
|
:maybe
|
|
827
828
|
end
|
|
828
829
|
|
|
829
|
-
def class_pattern_certainty(subject_type, class_name)
|
|
830
|
-
env = scope.environment
|
|
831
|
-
truthy_bot = Narrowing.narrow_class(subject_type, class_name, environment: env).is_a?(Type::Bot)
|
|
832
|
-
falsey_bot = Narrowing.narrow_not_class(subject_type, class_name, environment: env).is_a?(Type::Bot)
|
|
833
|
-
|
|
834
|
-
return :no if truthy_bot && !falsey_bot
|
|
835
|
-
return :yes if !truthy_bot && falsey_bot
|
|
836
|
-
|
|
837
|
-
:maybe
|
|
838
|
-
end
|
|
839
|
-
|
|
840
|
-
VALUE_EQUALITY_CLASSES = [Integer, Float, Rational, Complex, String, Symbol,
|
|
841
|
-
TrueClass, FalseClass, NilClass].freeze
|
|
842
|
-
private_constant :VALUE_EQUALITY_CLASSES
|
|
843
|
-
|
|
844
830
|
# Returns `{ value: v }` when `pattern_node` types to a
|
|
845
831
|
# `Constant[v]` of a value-equality-safe class (so `===`
|
|
846
832
|
# reduces to `==`), else nil. Wrapped in a hash so a literal
|
|
@@ -849,18 +835,11 @@ module Rigor
|
|
|
849
835
|
def literal_pattern_value(pattern_node)
|
|
850
836
|
type = type_of(pattern_node)
|
|
851
837
|
return nil unless type.is_a?(Type::Constant)
|
|
852
|
-
return nil unless VALUE_EQUALITY_CLASSES.any? { |klass| type.value.is_a?(klass) }
|
|
838
|
+
return nil unless Narrowing::VALUE_EQUALITY_CLASSES.any? { |klass| type.value.is_a?(klass) }
|
|
853
839
|
|
|
854
840
|
{ value: type.value }
|
|
855
841
|
end
|
|
856
842
|
|
|
857
|
-
def literal_pattern_certainty(subject_type, pattern_value)
|
|
858
|
-
return :maybe unless subject_type.is_a?(Type::Constant)
|
|
859
|
-
return :maybe unless VALUE_EQUALITY_CLASSES.any? { |klass| subject_type.value.is_a?(klass) }
|
|
860
|
-
|
|
861
|
-
pattern_value == subject_type.value ? :yes : :no
|
|
862
|
-
end
|
|
863
|
-
|
|
864
843
|
# `when` clauses for `case` and `in` clauses for `case ... in` have
|
|
865
844
|
# the same body shape; we reuse one handler for both Prism node
|
|
866
845
|
# classes.
|
|
@@ -1565,7 +1544,7 @@ module Rigor
|
|
|
1565
1544
|
# nil when the parameter shape is too complex for the
|
|
1566
1545
|
# first-iteration binder (rest args, keyword args,
|
|
1567
1546
|
# block params, etc.).
|
|
1568
|
-
def build_user_method_body_scope(def_node, receiver, arg_types)
|
|
1547
|
+
def build_user_method_body_scope(def_node, receiver, arg_types)
|
|
1569
1548
|
params = def_node.parameters
|
|
1570
1549
|
required = params&.requireds || []
|
|
1571
1550
|
return nil unless params.nil? || user_method_param_shape_simple?(params)
|
|
@@ -1578,30 +1557,17 @@ module Rigor
|
|
|
1578
1557
|
locals = {}
|
|
1579
1558
|
required.each_with_index { |param, index| locals[param.name.to_sym] = arg_types[index] }
|
|
1580
1559
|
|
|
1581
|
-
# Construct the body scope in a SINGLE allocation
|
|
1560
|
+
# Construct the body scope in a SINGLE allocation — the previous
|
|
1582
1561
|
# `Scope.empty.with_*.with_*…` chain allocated a fresh frozen Scope
|
|
1583
|
-
# per field
|
|
1584
|
-
#
|
|
1585
|
-
#
|
|
1586
|
-
#
|
|
1587
|
-
# field defaults to the same empty binding the old chain left it at,
|
|
1588
|
-
# so the result is identical (ADR-44).
|
|
1562
|
+
# per field, run per user-method-call inference (ADR-44). The
|
|
1563
|
+
# discovery index is inherited whole by reference (ADR-53 Track A);
|
|
1564
|
+
# the hand-copied per-field list this replaces had silently dropped
|
|
1565
|
+
# `data_member_layouts` and `discovered_method_visibilities`.
|
|
1589
1566
|
Scope.new(
|
|
1590
1567
|
environment: scope.environment,
|
|
1591
1568
|
locals: locals.freeze,
|
|
1592
1569
|
self_type: receiver,
|
|
1593
|
-
|
|
1594
|
-
discovered_classes: scope.discovered_classes,
|
|
1595
|
-
in_source_constants: scope.in_source_constants,
|
|
1596
|
-
class_ivars: scope.class_ivars,
|
|
1597
|
-
class_cvars: scope.class_cvars,
|
|
1598
|
-
program_globals: scope.program_globals,
|
|
1599
|
-
discovered_methods: scope.discovered_methods,
|
|
1600
|
-
discovered_def_nodes: scope.discovered_def_nodes,
|
|
1601
|
-
discovered_def_sources: scope.discovered_def_sources,
|
|
1602
|
-
discovered_superclasses: scope.discovered_superclasses,
|
|
1603
|
-
discovered_includes: scope.discovered_includes,
|
|
1604
|
-
discovered_class_sources: scope.discovered_class_sources
|
|
1570
|
+
discovery: scope.discovery
|
|
1605
1571
|
)
|
|
1606
1572
|
end
|
|
1607
1573
|
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Inference
|
|
5
|
+
# Thread-local event recorder behind `rigor trace`: while a block runs
|
|
6
|
+
# under {record}, the inference engine emits a flat, ordered event
|
|
7
|
+
# stream describing HOW it typed the program — expression enter/result
|
|
8
|
+
# pairs, scope binds, union formation, and method-dispatch outcomes.
|
|
9
|
+
# The CLI replays that stream as a terminal animation (or dumps it as
|
|
10
|
+
# JSON); the engine itself never reads the events back, so recording
|
|
11
|
+
# is purely observational and MUST NOT change any inferred type.
|
|
12
|
+
#
|
|
13
|
+
# Modelled on {Analysis::DependencyRecorder}: thread-local state, a
|
|
14
|
+
# module-level activation count so the disabled fast path ({active?})
|
|
15
|
+
# is a plain integer read, and a frozen snapshot for consumers. The
|
|
16
|
+
# instrumented hot paths (`ExpressionTyper#type_of`,
|
|
17
|
+
# `Scope#with_local`, `Type::Combinator.union`,
|
|
18
|
+
# `MethodDispatcher.dispatch`) each guard their emit behind {active?},
|
|
19
|
+
# so a normal (non-tracing) run pays one integer comparison.
|
|
20
|
+
module FlowTracer
|
|
21
|
+
KEY = :__rigor_flow_tracer__
|
|
22
|
+
private_constant :KEY
|
|
23
|
+
|
|
24
|
+
# One animation-relevant moment.
|
|
25
|
+
#
|
|
26
|
+
# kind :enter | :result | :bind | :union | :dispatch
|
|
27
|
+
# depth expression-recursion depth at emit time (0 = statement level)
|
|
28
|
+
# location frozen Hash with :start_line/:start_column/:end_line/
|
|
29
|
+
# :end_column/:start_offset/:end_offset, or nil. Events
|
|
30
|
+
# without a node of their own (:bind, :union) inherit the
|
|
31
|
+
# innermost in-flight expression node's location so the
|
|
32
|
+
# replayer can still highlight the source being evaluated.
|
|
33
|
+
# stack frozen Array of short node-class names, outermost first
|
|
34
|
+
# data frozen kind-specific Hash (types pre-rendered as Strings
|
|
35
|
+
# via `describe(:short)` so events serialise to JSON as-is)
|
|
36
|
+
Event = Data.define(:kind, :depth, :location, :stack, :data)
|
|
37
|
+
|
|
38
|
+
# Mutable per-thread accumulator; only ever touched by the thread
|
|
39
|
+
# that activated it, so no locking is needed on the emit path.
|
|
40
|
+
class Recorder
|
|
41
|
+
attr_reader :events
|
|
42
|
+
|
|
43
|
+
def initialize
|
|
44
|
+
@events = []
|
|
45
|
+
@stack = []
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Brackets one `ExpressionTyper#type_of` recursion: emits :enter,
|
|
49
|
+
# runs the real inference, emits :result with the inferred type,
|
|
50
|
+
# and returns the type unchanged.
|
|
51
|
+
def node(node)
|
|
52
|
+
location = location_of(node)
|
|
53
|
+
name = short_name(node.class)
|
|
54
|
+
emit(:enter, location: location, data: { node: name })
|
|
55
|
+
@stack.push(node)
|
|
56
|
+
result = nil
|
|
57
|
+
begin
|
|
58
|
+
result = yield
|
|
59
|
+
ensure
|
|
60
|
+
@stack.pop
|
|
61
|
+
end
|
|
62
|
+
emit(:result, location: location, data: { node: name, type: FlowTracer.describe(result) })
|
|
63
|
+
result
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def emit(kind, location: nil, data: {})
|
|
67
|
+
@events << Event.new(
|
|
68
|
+
kind: kind,
|
|
69
|
+
depth: @stack.size,
|
|
70
|
+
location: (location || current_location)&.freeze,
|
|
71
|
+
stack: @stack.map { |n| short_name(n.class) }.freeze,
|
|
72
|
+
data: data.freeze
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def current_location
|
|
79
|
+
location_of(@stack.last)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def location_of(node)
|
|
83
|
+
return nil unless node.respond_to?(:location) && node.location
|
|
84
|
+
|
|
85
|
+
loc = node.location
|
|
86
|
+
{
|
|
87
|
+
start_line: loc.start_line, start_column: loc.start_column,
|
|
88
|
+
end_line: loc.end_line, end_column: loc.end_column,
|
|
89
|
+
start_offset: loc.start_offset, end_offset: loc.end_offset
|
|
90
|
+
}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def short_name(klass)
|
|
94
|
+
klass.name.to_s.split("::").last
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
@active_count = 0
|
|
99
|
+
@mutex = Mutex.new
|
|
100
|
+
|
|
101
|
+
module_function
|
|
102
|
+
|
|
103
|
+
# Activates recording on the current thread for the duration of the
|
|
104
|
+
# block and returns the frozen event list. Nests safely; restores
|
|
105
|
+
# the previous recorder on exit.
|
|
106
|
+
def record
|
|
107
|
+
previous = Thread.current[KEY]
|
|
108
|
+
recorder = Recorder.new
|
|
109
|
+
Thread.current[KEY] = recorder
|
|
110
|
+
@mutex.synchronize { @active_count += 1 }
|
|
111
|
+
yield
|
|
112
|
+
recorder.events.freeze
|
|
113
|
+
ensure
|
|
114
|
+
Thread.current[KEY] = previous
|
|
115
|
+
@mutex.synchronize { @active_count -= 1 }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Plain integer read (GVL-atomic) — the disabled fast path.
|
|
119
|
+
def active?
|
|
120
|
+
@active_count.positive?
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Brackets one expression-typing recursion. Falls through to the
|
|
124
|
+
# bare block when the current thread is not recording (another
|
|
125
|
+
# thread may have flipped {active?}).
|
|
126
|
+
def trace_node(node, &)
|
|
127
|
+
recorder = Thread.current[KEY]
|
|
128
|
+
return yield unless recorder
|
|
129
|
+
|
|
130
|
+
recorder.node(node, &)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# `Scope#with_local` — the moment a local enters the scope.
|
|
134
|
+
def bind(name, type)
|
|
135
|
+
Thread.current[KEY]&.emit(:bind, data: { name: name.to_s, type: describe(type) })
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# `Type::Combinator.union` — the moment branch types merge
|
|
139
|
+
# (including degenerate collapses like `1 | 1 → 1`).
|
|
140
|
+
def union(members, result)
|
|
141
|
+
Thread.current[KEY]&.emit(
|
|
142
|
+
:union,
|
|
143
|
+
data: { members: members.map { |m| describe(m) }.freeze, type: describe(result) }
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# `MethodDispatcher.dispatch` — resolution or the fail-soft `nil`
|
|
148
|
+
# ("no rule matched"; the caller will widen to `Dynamic[Top]`).
|
|
149
|
+
def dispatch(receiver:, method_name:, args:, result:, location: nil)
|
|
150
|
+
recorder = Thread.current[KEY]
|
|
151
|
+
return unless recorder
|
|
152
|
+
|
|
153
|
+
recorder.emit(
|
|
154
|
+
:dispatch,
|
|
155
|
+
location: location && location_hash(location),
|
|
156
|
+
data: {
|
|
157
|
+
receiver: describe(receiver), method: method_name.to_s,
|
|
158
|
+
args: args.map { |a| describe(a) }.freeze,
|
|
159
|
+
type: result && describe(result), resolved: !result.nil?
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def describe(type)
|
|
165
|
+
return "nil" if type.nil?
|
|
166
|
+
return type.describe(:short) if type.respond_to?(:describe)
|
|
167
|
+
|
|
168
|
+
type.inspect
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def location_hash(loc)
|
|
172
|
+
{
|
|
173
|
+
start_line: loc.start_line, start_column: loc.start_column,
|
|
174
|
+
end_line: loc.end_line, end_column: loc.end_column,
|
|
175
|
+
start_offset: loc.start_offset, end_offset: loc.end_offset
|
|
176
|
+
}
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -51,11 +51,16 @@ module Rigor
|
|
|
51
51
|
receiver_class_name = singleton_receiver_class_name(receiver_type)
|
|
52
52
|
return nil if receiver_class_name.nil?
|
|
53
53
|
|
|
54
|
-
verb
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
# ADR-52 WD1 — the verb-keyed table compiled at registry build
|
|
55
|
+
# replaces the per-call plugins × block_as_methods linear scan.
|
|
56
|
+
# Entries arrive in (plugin registration, declaration) order, so
|
|
57
|
+
# the first ancestry match below is the same entry the previous
|
|
58
|
+
# walk returned; the verb membership the old `matches?` checked
|
|
59
|
+
# is guaranteed by the table key.
|
|
60
|
+
entries = registry.contribution_index.block_entries_for(call_node.name)
|
|
61
|
+
entries.each do |entry|
|
|
62
|
+
if receiver_class_inherits_from?(receiver_class_name, entry.receiver_constraint, environment)
|
|
63
|
+
return instance_type_for(receiver_class_name, environment)
|
|
59
64
|
end
|
|
60
65
|
end
|
|
61
66
|
nil
|
|
@@ -73,12 +78,6 @@ module Rigor
|
|
|
73
78
|
receiver_type.class_name
|
|
74
79
|
end
|
|
75
80
|
|
|
76
|
-
def matches?(entry, verb, receiver_class_name, environment)
|
|
77
|
-
return false unless entry.verbs.include?(verb)
|
|
78
|
-
|
|
79
|
-
receiver_class_inherits_from?(receiver_class_name, entry.receiver_constraint, environment)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
81
|
def receiver_class_inherits_from?(class_name, constraint, environment)
|
|
83
82
|
return true if class_name == constraint
|
|
84
83
|
|
|
@@ -32,7 +32,16 @@ module Rigor
|
|
|
32
32
|
# matches, accept the first arity-and-gradual-accept match
|
|
33
33
|
# (the v0.1.1 behaviour). Alias / Interface / Intersection
|
|
34
34
|
# params still reach this pass, so call sites whose only
|
|
35
|
-
# candidate IS an alias-typed overload keep working.
|
|
35
|
+
# candidate IS an alias-typed overload keep working. One
|
|
36
|
+
# exclusion: an `untyped` argument does NOT gradually match
|
|
37
|
+
# a value-pinning param (`nil` / literal types — carriers
|
|
38
|
+
# that admit only specific values). Those overloads carry
|
|
39
|
+
# value-precise returns (`Kernel#Array: (nil) -> []`,
|
|
40
|
+
# `Regexp#=~: (nil) -> nil`) that would otherwise win purely
|
|
41
|
+
# by list position and inject false constants into the flow;
|
|
42
|
+
# they remain selectable when the argument PROVES the value
|
|
43
|
+
# (strict pass) or when no other overload matches (step 4's
|
|
44
|
+
# fallback picks the first overload regardless).
|
|
36
45
|
# 4. If no overload matches at all, fall back to
|
|
37
46
|
# `method_types.first` so existing call sites keep their
|
|
38
47
|
# phase 1 / 2b behavior. This preserves the fail-soft
|
|
@@ -393,9 +402,32 @@ module Rigor
|
|
|
393
402
|
instance_type: instance_type,
|
|
394
403
|
type_vars: type_vars
|
|
395
404
|
)
|
|
405
|
+
# An `untyped` arg gradually accepts against every param,
|
|
406
|
+
# so a value-pinning param would be "matched" with zero
|
|
407
|
+
# evidence and its value-precise return (`(nil) -> []`)
|
|
408
|
+
# would beat broader overloads purely by list position.
|
|
409
|
+
# Decline the pair; only the strict pass (where the arg
|
|
410
|
+
# proves the value) or the final first-overload fallback
|
|
411
|
+
# may select such an overload. (Pass 1 already skips
|
|
412
|
+
# untyped args entirely, so this only engages pass 2.)
|
|
413
|
+
return false if untyped_arg?(arg) && value_pinning?(param_type)
|
|
414
|
+
|
|
396
415
|
result = param_type.accepts(arg, mode: :gradual)
|
|
397
416
|
result.yes? || result.maybe?
|
|
398
417
|
end
|
|
418
|
+
|
|
419
|
+
# A type that admits only specific VALUES rather than a
|
|
420
|
+
# class of values: a `Constant` carrier (RBS `nil` and
|
|
421
|
+
# literal types both translate to one) or a union made up
|
|
422
|
+
# entirely of them (`true | false`, `1 | 2`, `nil?`-style
|
|
423
|
+
# optionals of literals).
|
|
424
|
+
def value_pinning?(type)
|
|
425
|
+
case type
|
|
426
|
+
when Type::Constant then true
|
|
427
|
+
when Type::Union then type.members.all? { |member| value_pinning?(member) }
|
|
428
|
+
else false
|
|
429
|
+
end
|
|
430
|
+
end
|
|
399
431
|
end
|
|
400
432
|
end
|
|
401
433
|
end
|