rigortype 0.1.17 → 0.1.19
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 +159 -222
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules.rb +275 -44
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +207 -1200
- data/lib/rigor/analysis/worker_session.rb +60 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +708 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +21 -612
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +66 -7
- data/lib/rigor/environment/rbs_loader.rb +78 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1080 -105
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +11 -12
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +187 -55
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +142 -0
- data/lib/rigor/inference/narrowing.rb +330 -37
- data/lib/rigor/inference/scope_indexer.rb +770 -39
- data/lib/rigor/inference/statement_evaluator.rb +998 -68
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +517 -120
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +192 -0
- data/lib/rigor/plugin/registry.rb +264 -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 +60 -0
- data/lib/rigor/scope.rb +199 -204
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +34 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
- 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-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- 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 +37 -31
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- 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 +108 -36
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +6 -4
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +50 -29
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +1 -0
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +21 -3
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -6,6 +6,7 @@ require_relative "../flow_contribution"
|
|
|
6
6
|
require_relative "../flow_contribution/merger"
|
|
7
7
|
require_relative "../builtins/hkt_builtins"
|
|
8
8
|
require_relative "../builtins/static_return_refinements"
|
|
9
|
+
require_relative "flow_tracer"
|
|
9
10
|
require_relative "method_dispatcher/call_context"
|
|
10
11
|
require_relative "method_dispatcher/constant_folding"
|
|
11
12
|
require_relative "method_dispatcher/literal_string_folding"
|
|
@@ -13,7 +14,9 @@ require_relative "method_dispatcher/shape_dispatch"
|
|
|
13
14
|
require_relative "method_dispatcher/data_folding"
|
|
14
15
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
15
16
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
17
|
+
require_relative "method_dispatcher/reduce_folding"
|
|
16
18
|
require_relative "method_dispatcher/block_folding"
|
|
19
|
+
require_relative "method_dispatcher/array_to_h_folding"
|
|
17
20
|
require_relative "method_dispatcher/file_folding"
|
|
18
21
|
require_relative "method_dispatcher/shellwords_folding"
|
|
19
22
|
require_relative "method_dispatcher/math_folding"
|
|
@@ -73,9 +76,28 @@ module Rigor
|
|
|
73
76
|
# @param environment [Rigor::Environment, nil] required for
|
|
74
77
|
# RBS-backed dispatch; when nil only constant folding can fire.
|
|
75
78
|
# @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
|
|
76
|
-
def dispatch(receiver_type:, method_name:, arg_types:,
|
|
79
|
+
def dispatch(receiver_type:, method_name:, arg_types:,
|
|
77
80
|
block_type: nil, environment: nil,
|
|
78
81
|
call_node: nil, scope: nil)
|
|
82
|
+
result = resolve(
|
|
83
|
+
receiver_type: receiver_type, method_name: method_name, arg_types: arg_types,
|
|
84
|
+
block_type: block_type, environment: environment,
|
|
85
|
+
call_node: call_node, scope: scope
|
|
86
|
+
)
|
|
87
|
+
# `rigor trace` — record the dispatch outcome (resolved type, or
|
|
88
|
+
# the fail-soft `nil` the caller widens to `Dynamic[Top]`).
|
|
89
|
+
if FlowTracer.active?
|
|
90
|
+
FlowTracer.dispatch(
|
|
91
|
+
receiver: receiver_type, method_name: method_name, args: arg_types,
|
|
92
|
+
result: result, location: call_node&.location
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
result
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def resolve(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
99
|
+
block_type: nil, environment: nil,
|
|
100
|
+
call_node: nil, scope: nil)
|
|
79
101
|
return nil if receiver_type.nil?
|
|
80
102
|
|
|
81
103
|
# Build the call context once and thread it — unchanged —
|
|
@@ -217,7 +239,7 @@ module Rigor
|
|
|
217
239
|
# introspection (`attr_reader`, `private`, ...) on
|
|
218
240
|
# user classes without requiring the user to author
|
|
219
241
|
# their own RBS.
|
|
220
|
-
try_user_class_fallback(receiver_type,
|
|
242
|
+
try_user_class_fallback(receiver_type, environment, call_node, context)
|
|
221
243
|
end
|
|
222
244
|
|
|
223
245
|
# v0.1.3 — discovered-method dispatch tier. `scope` carries
|
|
@@ -254,7 +276,14 @@ module Rigor
|
|
|
254
276
|
class_name, kind = discovered_method_lookup(receiver_type)
|
|
255
277
|
return nil if class_name.nil?
|
|
256
278
|
return nil unless scope.discovered_method?(class_name, method_name, kind)
|
|
279
|
+
# Decline when a re-typable body is recorded for the method, so the
|
|
280
|
+
# downstream `ExpressionTyper` inference tier can fold a precise
|
|
281
|
+
# return instead of collapsing to `Dynamic[top]` here — instance
|
|
282
|
+
# bodies via `user_def_for`, singleton bodies (`def self.x` /
|
|
283
|
+
# `module_function`) via `singleton_def_for` (module-singleton
|
|
284
|
+
# call resolution, ADR-57 follow-up).
|
|
257
285
|
return nil if kind == :instance && scope.user_def_for(class_name, method_name)
|
|
286
|
+
return nil if kind == :singleton && scope.singleton_def_for(class_name, method_name)
|
|
258
287
|
|
|
259
288
|
Type::Combinator.untyped
|
|
260
289
|
end
|
|
@@ -298,12 +327,12 @@ module Rigor
|
|
|
298
327
|
end
|
|
299
328
|
|
|
300
329
|
# ADR-2 § "Flow Contribution Bundle" / v0.1.1 Track 2
|
|
301
|
-
# slice 7
|
|
302
|
-
#
|
|
303
|
-
#
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
#
|
|
330
|
+
# slice 7; ADR-52 WD3 — consults each loaded plugin's gated
|
|
331
|
+
# `dynamic_return` rules, wraps the contributed types as
|
|
332
|
+
# `FlowContribution` bundles, merges them through
|
|
333
|
+
# `FlowContribution::Merger`, and returns the merged
|
|
334
|
+
# `return_type` slot (or nil when no plugin contributed a
|
|
335
|
+
# return type).
|
|
307
336
|
#
|
|
308
337
|
# Plugins whose hook raises have their contribution
|
|
309
338
|
# silently dropped for this call so the dispatch chain
|
|
@@ -643,23 +672,15 @@ module Rigor
|
|
|
643
672
|
Type::Combinator.untyped
|
|
644
673
|
end
|
|
645
674
|
|
|
675
|
+
# ADR-52 WD1 — the per-dispatch plugins × owns_receivers ×
|
|
676
|
+
# `class_ordering` walk moved into the compiled contribution
|
|
677
|
+
# table: the union is built once per registry (almost always
|
|
678
|
+
# empty → O(1) false) and per-class verdicts memoise per run.
|
|
646
679
|
def plugin_owns_receiver?(class_name, environment)
|
|
647
680
|
registry = environment&.plugin_registry
|
|
648
681
|
return false if registry.nil? || registry.empty?
|
|
649
682
|
|
|
650
|
-
registry.
|
|
651
|
-
owns = plugin.manifest.owns_receivers # rigor:disable undefined-method
|
|
652
|
-
owns.any? { |owner| receiver_matches_owner?(class_name, owner, environment) }
|
|
653
|
-
end
|
|
654
|
-
end
|
|
655
|
-
|
|
656
|
-
def receiver_matches_owner?(class_name, owner, environment)
|
|
657
|
-
return true if class_name == owner
|
|
658
|
-
|
|
659
|
-
ordering = environment.class_ordering(class_name, owner)
|
|
660
|
-
%i[equal subclass].include?(ordering)
|
|
661
|
-
rescue StandardError
|
|
662
|
-
false
|
|
683
|
+
registry.contribution_index.owns_receiver?(class_name, environment)
|
|
663
684
|
end
|
|
664
685
|
|
|
665
686
|
def dep_source_class_name(receiver_type)
|
|
@@ -668,11 +689,11 @@ module Rigor
|
|
|
668
689
|
end
|
|
669
690
|
end
|
|
670
691
|
|
|
671
|
-
# ADR-37 slice 2 — gathers each plugin's return-type
|
|
672
|
-
# from
|
|
673
|
-
#
|
|
674
|
-
# `flow_contribution_for` escape valve
|
|
675
|
-
#
|
|
692
|
+
# ADR-37 slice 2 / ADR-52 WD3 — gathers each plugin's return-type
|
|
693
|
+
# contribution from the gated `dynamic_return` DSL, wrapped as a
|
|
694
|
+
# return-only `FlowContribution` for the shared merger. (The legacy
|
|
695
|
+
# ungated `flow_contribution_for` escape valve was deleted once its
|
|
696
|
+
# five users migrated.)
|
|
676
697
|
EMPTY_CONTRIBUTIONS = [].freeze
|
|
677
698
|
private_constant :EMPTY_CONTRIBUTIONS
|
|
678
699
|
|
|
@@ -682,31 +703,48 @@ module Rigor
|
|
|
682
703
|
#
|
|
683
704
|
# 1. Only the plugins that *structurally* implement a per-call path
|
|
684
705
|
# are visited — `registry.contribution_index.for_method_dispatch`
|
|
685
|
-
# is the registry-ordered subset
|
|
686
|
-
#
|
|
687
|
-
#
|
|
688
|
-
#
|
|
689
|
-
#
|
|
690
|
-
#
|
|
691
|
-
# per dispatch inside `dynamic_return_type`.
|
|
706
|
+
# is the registry-ordered subset declaring a `dynamic_return`.
|
|
707
|
+
# Iterating the subset in registry order, and gating each path by
|
|
708
|
+
# membership, yields the exact same contributions in the same
|
|
709
|
+
# order as visiting every plugin would (a skipped plugin's call
|
|
710
|
+
# returns nil/[] anyway). The receiver-class ancestry match still
|
|
711
|
+
# happens per dispatch inside `dynamic_return_type`.
|
|
692
712
|
# 2. Contributions accumulate lazily — allocate only when one
|
|
693
713
|
# actually appears, and share a frozen empty array otherwise. The
|
|
694
714
|
# caller treats the result as read-only (`.empty?` / `Merger.merge`).
|
|
715
|
+
# 3. ADR-52 WD1 — method-name gates compiled at registry build. The
|
|
716
|
+
# global gate makes the common "no plugin cares about this call"
|
|
717
|
+
# case a single Set probe; the per-plugin gate skips a plugin
|
|
718
|
+
# whose `dynamic_return` rules are all `methods:`-gated on other
|
|
719
|
+
# names. A pruned consultation could only have returned nil, so
|
|
720
|
+
# contribution order and content are unchanged.
|
|
695
721
|
def collect_plugin_contributions(registry, call_node, scope, receiver_type)
|
|
696
722
|
index = registry.contribution_index
|
|
697
723
|
relevant = index.for_method_dispatch
|
|
698
724
|
return EMPTY_CONTRIBUTIONS if relevant.empty?
|
|
699
725
|
|
|
726
|
+
# `call_node` is not always a CallNode — the `&:symbol` block
|
|
727
|
+
# path dispatches with the `Prism::BlockArgumentNode` itself
|
|
728
|
+
# (`ExpressionTyper#symbol_block_return_type`). A bare `.name`
|
|
729
|
+
# here raised, and the raise was silently absorbed by
|
|
730
|
+
# `block_return_type_for`'s rescue, nil-ing the block type and
|
|
731
|
+
# flipping `select(&:p)`-style calls onto their no-block
|
|
732
|
+
# Enumerator overloads (caught by the GitLab corpus gate).
|
|
733
|
+
name = call_node.respond_to?(:name) ? call_node.name : nil
|
|
734
|
+
return EMPTY_CONTRIBUTIONS unless index.dispatch_candidate?(name)
|
|
735
|
+
|
|
736
|
+
collect_gated_contributions(index, relevant, name, call_node, scope, receiver_type)
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# The post-gate walk, in registry order — the same order the
|
|
740
|
+
# ungated walk used.
|
|
741
|
+
def collect_gated_contributions(index, relevant, name, call_node, scope, receiver_type)
|
|
700
742
|
result = nil
|
|
701
743
|
relevant.each do |plugin|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
if index.dynamic?(plugin)
|
|
707
|
-
dynamic = plugin.dynamic_return_type(call_node: call_node, scope: scope, receiver_type: receiver_type)
|
|
708
|
-
(result ||= []) << FlowContribution.new(return_type: dynamic) if dynamic
|
|
709
|
-
end
|
|
744
|
+
next unless index.dynamic_candidate_for?(plugin, name)
|
|
745
|
+
|
|
746
|
+
dynamic = plugin.dynamic_return_type(call_node: call_node, scope: scope, receiver_type: receiver_type)
|
|
747
|
+
(result ||= []) << FlowContribution.new(return_type: dynamic) if dynamic
|
|
710
748
|
rescue StandardError
|
|
711
749
|
next
|
|
712
750
|
end
|
|
@@ -735,13 +773,37 @@ module Rigor
|
|
|
735
773
|
# filter the common cases first. Adding a precise tier is a
|
|
736
774
|
# one-line append here rather than another link in a hand-written
|
|
737
775
|
# `||` ladder.
|
|
738
|
-
|
|
739
|
-
ConstantFolding, LiteralStringFolding, ShapeDispatch
|
|
740
|
-
FileFolding, ShellwordsFolding, MathFolding, TimeFolding,
|
|
741
|
-
RegexpFolding, CGIFolding, URIFolding, SetFolding,
|
|
742
|
-
KernelDispatch, MethodFolding, BlockFolding
|
|
776
|
+
PRECISE_TIERS_HEAD = Ractor.make_shareable([
|
|
777
|
+
ConstantFolding, LiteralStringFolding, ShapeDispatch
|
|
743
778
|
].freeze)
|
|
744
|
-
private_constant :
|
|
779
|
+
private_constant :PRECISE_TIERS_HEAD
|
|
780
|
+
|
|
781
|
+
# ADR-53 re-review follow-up (gate-by-held-key applied to the
|
|
782
|
+
# built-in tiers): the eight stdlib singleton folders are mutually
|
|
783
|
+
# exclusive — each fires only on `Singleton[<its class>]`, the
|
|
784
|
+
# first check in every `try_dispatch` — so at most one can match a
|
|
785
|
+
# given receiver and their relative trial order was never
|
|
786
|
+
# observable. Compiling them into a class-name table turns eight
|
|
787
|
+
# no-op trials per call into one Hash read, skipped entirely when
|
|
788
|
+
# the receiver is not a `Singleton` (the overwhelmingly common
|
|
789
|
+
# case). The table sits where the eight sat in the old flat list:
|
|
790
|
+
# after ShapeDispatch, before KernelDispatch.
|
|
791
|
+
STDLIB_SINGLETON_FOLDERS = Ractor.make_shareable({
|
|
792
|
+
"File" => FileFolding,
|
|
793
|
+
"Shellwords" => ShellwordsFolding,
|
|
794
|
+
"Math" => MathFolding,
|
|
795
|
+
"Time" => TimeFolding,
|
|
796
|
+
"Regexp" => RegexpFolding,
|
|
797
|
+
"CGI" => CGIFolding,
|
|
798
|
+
"URI" => URIFolding,
|
|
799
|
+
"Set" => SetFolding
|
|
800
|
+
}.freeze)
|
|
801
|
+
private_constant :STDLIB_SINGLETON_FOLDERS
|
|
802
|
+
|
|
803
|
+
PRECISE_TIERS_TAIL = Ractor.make_shareable([
|
|
804
|
+
KernelDispatch, MethodFolding, ReduceFolding, ArrayToHFolding, BlockFolding
|
|
805
|
+
].freeze)
|
|
806
|
+
private_constant :PRECISE_TIERS_TAIL
|
|
745
807
|
|
|
746
808
|
def dispatch_precise_tiers(context)
|
|
747
809
|
# ADR-48 — Data value folding runs ahead of meta-introspection:
|
|
@@ -756,14 +818,25 @@ module Rigor
|
|
|
756
818
|
meta_result = try_meta_introspection(context.receiver, context.method_name, context.args)
|
|
757
819
|
return meta_result if meta_result
|
|
758
820
|
|
|
759
|
-
|
|
821
|
+
PRECISE_TIERS_HEAD.each do |tier|
|
|
822
|
+
result = tier.try_dispatch(context)
|
|
823
|
+
return result if result
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
receiver = context.receiver
|
|
827
|
+
if receiver.is_a?(Type::Singleton) && (folder = STDLIB_SINGLETON_FOLDERS[receiver.class_name])
|
|
828
|
+
result = folder.try_dispatch(context)
|
|
829
|
+
return result if result
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
PRECISE_TIERS_TAIL.each do |tier|
|
|
760
833
|
result = tier.try_dispatch(context)
|
|
761
834
|
return result if result
|
|
762
835
|
end
|
|
763
836
|
nil
|
|
764
837
|
end
|
|
765
838
|
|
|
766
|
-
def try_user_class_fallback(receiver_type,
|
|
839
|
+
def try_user_class_fallback(receiver_type, environment, call_node, context)
|
|
767
840
|
return nil if environment.nil?
|
|
768
841
|
|
|
769
842
|
fallback_receiver = user_class_fallback_receiver(receiver_type, environment)
|
|
@@ -788,14 +861,11 @@ module Rigor
|
|
|
788
861
|
# self / `self.`-receiver calls (`puts`, `raise`, `require`)
|
|
789
862
|
# keep resolving — those are the fallback's intended targets.
|
|
790
863
|
RbsDispatch.try_dispatch(
|
|
791
|
-
|
|
864
|
+
context.with(
|
|
792
865
|
receiver: fallback_receiver,
|
|
793
|
-
method_name: method_name,
|
|
794
|
-
args: arg_types,
|
|
795
|
-
environment: environment,
|
|
796
|
-
block_type: block_type,
|
|
797
866
|
self_type_override: receiver_type,
|
|
798
|
-
public_only: explicit_non_self_receiver?(call_node)
|
|
867
|
+
public_only: explicit_non_self_receiver?(call_node),
|
|
868
|
+
call_node: nil, scope: nil
|
|
799
869
|
)
|
|
800
870
|
)
|
|
801
871
|
end
|
|
@@ -891,18 +961,53 @@ module Rigor
|
|
|
891
961
|
set_lift = set_new_lift(receiver_type.class_name, arg_types)
|
|
892
962
|
return set_lift if set_lift
|
|
893
963
|
|
|
964
|
+
hash_lift = hash_new_lift(receiver_type.class_name, arg_types)
|
|
965
|
+
return hash_lift if hash_lift
|
|
966
|
+
|
|
894
967
|
regexp_lift = regexp_new_lift(receiver_type.class_name, arg_types)
|
|
895
968
|
return regexp_lift if regexp_lift
|
|
896
969
|
|
|
897
970
|
date_lift = date_new_lift(receiver_type.class_name, arg_types)
|
|
898
971
|
return date_lift if date_lift
|
|
899
972
|
|
|
973
|
+
struct_new_lift = struct_new_lift(receiver_type.class_name, arg_types)
|
|
974
|
+
return struct_new_lift if struct_new_lift
|
|
975
|
+
|
|
900
976
|
class_new_lift = class_new_lift(receiver_type.class_name, arg_types)
|
|
901
977
|
return class_new_lift if class_new_lift
|
|
902
978
|
|
|
903
979
|
Type::Combinator.nominal_of(receiver_type.class_name)
|
|
904
980
|
end
|
|
905
981
|
|
|
982
|
+
# `Struct.new(:a, :b)` synthesises an anonymous Struct *subclass*
|
|
983
|
+
# (a class object), not a Struct *instance* — so the chained
|
|
984
|
+
# idiom `Struct.new(:a, :b).new(1, 2)` must resolve `.new` again
|
|
985
|
+
# on a class-like carrier. The constant-bound form
|
|
986
|
+
# (`S = Struct.new(:a); S.new(1)`) already records `Singleton[S]`
|
|
987
|
+
# via `ScopeIndexer#record_meta_new_constant?`; this lift gives
|
|
988
|
+
# the *chained* (anonymous) position the same class-like carrier
|
|
989
|
+
# so the trailing `.new` dispatches instead of firing a spurious
|
|
990
|
+
# `undefined method 'new' for Struct`.
|
|
991
|
+
#
|
|
992
|
+
# The disambiguation mirrors `ScopeIndexer#struct_new_call?`: a
|
|
993
|
+
# call whose positionals are all `Constant<Symbol>` literals is a
|
|
994
|
+
# member-list class definition → `Singleton[Struct]`. The
|
|
995
|
+
# following `AnonStruct.new(1, 2)` carries non-symbol args, so it
|
|
996
|
+
# falls through this gate to `Nominal[Struct]` (a fresh instance)
|
|
997
|
+
# via the `meta_new` tail. ADR-48 deferred full Struct *value*
|
|
998
|
+
# folding (member-reader precision) on mutability grounds; this is
|
|
999
|
+
# the narrower `.new`-dispatch-only fix and contributes no member
|
|
1000
|
+
# layout, so `instance.a` stays at its RBS/Dynamic type.
|
|
1001
|
+
def struct_new_lift(class_name, arg_types)
|
|
1002
|
+
return nil unless class_name == "Struct"
|
|
1003
|
+
|
|
1004
|
+
positional = arg_types.grep_v(Type::HashShape)
|
|
1005
|
+
return nil if positional.empty?
|
|
1006
|
+
return nil unless positional.all? { |t| t.is_a?(Type::Constant) && t.value.is_a?(Symbol) }
|
|
1007
|
+
|
|
1008
|
+
Type::Combinator.singleton_of("Struct")
|
|
1009
|
+
end
|
|
1010
|
+
|
|
906
1011
|
# `Class.new` and `Class.new(Parent)` create a brand-new
|
|
907
1012
|
# anonymous class. Statically that class is representable as
|
|
908
1013
|
# the parent's singleton type — its singleton-method surface
|
|
@@ -989,6 +1094,33 @@ module Rigor
|
|
|
989
1094
|
type
|
|
990
1095
|
end
|
|
991
1096
|
|
|
1097
|
+
# `Hash.new(default)` — lifts the default value's type into the
|
|
1098
|
+
# Hash's value parameter so a subsequent `h[k]` read surfaces the
|
|
1099
|
+
# default type rather than `Dynamic[top]`. The common counter
|
|
1100
|
+
# idiom `h = Hash.new(0); h[k] += 1` then types the read as
|
|
1101
|
+
# `Integer`. The key parameter is left `untyped` (the default
|
|
1102
|
+
# carrier imposes no key constraint), so reads of any key resolve
|
|
1103
|
+
# through the value parameter. A value-pinned `Constant` default
|
|
1104
|
+
# (`0`) is widened to its nominal (`Integer`): the hash's values
|
|
1105
|
+
# mutate over its lifetime, so pinning the parameter to the
|
|
1106
|
+
# literal would be unsound for the aggregate.
|
|
1107
|
+
#
|
|
1108
|
+
# Only the single-argument default form folds. The zero-arg
|
|
1109
|
+
# (`Hash.new`) and the block form (`Hash.new { |h, k| … }`) keep
|
|
1110
|
+
# the bare `Nominal[Hash]` answer — the block's value type is not
|
|
1111
|
+
# available at this `:new` dispatch site, and conservatively
|
|
1112
|
+
# leaving the read as today's behaviour is precision-additive.
|
|
1113
|
+
def hash_new_lift(class_name, arg_types)
|
|
1114
|
+
return nil unless class_name == "Hash"
|
|
1115
|
+
return nil unless arg_types.size == 1
|
|
1116
|
+
|
|
1117
|
+
default = arg_types.first
|
|
1118
|
+
return nil if default.nil?
|
|
1119
|
+
|
|
1120
|
+
value = Type::Combinator.widen_value_pinned(default)
|
|
1121
|
+
Type::Combinator.nominal_of("Hash", type_args: [Type::Combinator.untyped, value])
|
|
1122
|
+
end
|
|
1123
|
+
|
|
992
1124
|
# `Range.new(b, e)` / `Range.new(b, e, excl)` — folds to
|
|
993
1125
|
# `Constant[Range]` when both endpoints are `Constant[Integer]`
|
|
994
1126
|
# or both are `Constant[String]`, and the optional third argument
|
|
@@ -32,6 +32,35 @@ module Rigor
|
|
|
32
32
|
# `#singleton_method`.
|
|
33
33
|
#
|
|
34
34
|
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
35
|
+
# Leaf-name extraction for a destructured positional parameter
|
|
36
|
+
# (`Prism::MultiTargetNode`). Stateless; lifted out of
|
|
37
|
+
# {MethodParameterBinder} so the binder's class length stays in
|
|
38
|
+
# budget.
|
|
39
|
+
module Destructure
|
|
40
|
+
module_function
|
|
41
|
+
|
|
42
|
+
# Collect every leaf local name a `MultiTargetNode` binds,
|
|
43
|
+
# recursing through nested destructures (`((a, b), c)`) and the
|
|
44
|
+
# splat slot (`(a, *rest)`). Targets without a `#name` (an
|
|
45
|
+
# index/call write target, vanishingly rare in a parameter
|
|
46
|
+
# position) are skipped — there is no local to bind.
|
|
47
|
+
def target_names(multi_target)
|
|
48
|
+
entries = multi_target.lefts + [multi_target.rest, *multi_target.rights].compact
|
|
49
|
+
entries.flat_map { |entry| names_for_entry(entry) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def names_for_entry(entry)
|
|
53
|
+
# A splat sub-target (`*rest` inside the destructure) wraps its
|
|
54
|
+
# real target in a `SplatNode#expression`; unwrap it.
|
|
55
|
+
entry = entry.expression if entry.is_a?(Prism::SplatNode) && entry.expression
|
|
56
|
+
return [] if entry.nil?
|
|
57
|
+
return target_names(entry) if entry.is_a?(Prism::MultiTargetNode)
|
|
58
|
+
return [entry.name] if entry.respond_to?(:name) && entry.name
|
|
59
|
+
|
|
60
|
+
[]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
35
64
|
class MethodParameterBinder
|
|
36
65
|
# @param environment [Rigor::Environment]
|
|
37
66
|
# @param class_path [String, nil] the qualified name of the class
|
|
@@ -112,14 +141,39 @@ module Rigor
|
|
|
112
141
|
|
|
113
142
|
def positional_slots(params_node)
|
|
114
143
|
slots = []
|
|
115
|
-
params_node.requireds.each_with_index
|
|
144
|
+
params_node.requireds.each_with_index do |p, i|
|
|
145
|
+
append_positional_slot(slots, :required_positional, p, i)
|
|
146
|
+
end
|
|
116
147
|
params_node.optionals.each_with_index { |p, i| slots << ParamSlot.new(:optional_positional, p.name, i) }
|
|
117
148
|
rest = params_node.rest
|
|
118
149
|
slots << ParamSlot.new(:rest_positional, rest.name, nil) if rest.respond_to?(:name) && rest&.name
|
|
119
|
-
params_node.posts.each_with_index
|
|
150
|
+
params_node.posts.each_with_index do |p, i|
|
|
151
|
+
append_positional_slot(slots, :trailing_positional, p, i)
|
|
152
|
+
end
|
|
120
153
|
slots
|
|
121
154
|
end
|
|
122
155
|
|
|
156
|
+
# A destructured positional parameter — `def f((a, b))` — is a
|
|
157
|
+
# `Prism::MultiTargetNode` in the `requireds`/`posts` list, not a
|
|
158
|
+
# `RequiredParameterNode`, so it has no `#name`. Bind each leaf
|
|
159
|
+
# sub-target local to `Dynamic[Top]` (a `:destructured_positional`
|
|
160
|
+
# slot with no RBS index) instead of crashing on a blind `.name`.
|
|
161
|
+
# Binding the names at all is what matters: it keeps the
|
|
162
|
+
# destructured locals present in the entry scope so the body's
|
|
163
|
+
# reads of them don't fall through to undefined-local noise. The
|
|
164
|
+
# element types are not cheaply available from the parameter list
|
|
165
|
+
# alone (no RBS function param maps onto a destructured slot), so
|
|
166
|
+
# `Dynamic[Top]` is the sound default.
|
|
167
|
+
def append_positional_slot(slots, kind, param, index)
|
|
168
|
+
if param.is_a?(Prism::MultiTargetNode)
|
|
169
|
+
Destructure.target_names(param).each do |name|
|
|
170
|
+
slots << ParamSlot.new(:destructured_positional, name, nil)
|
|
171
|
+
end
|
|
172
|
+
else
|
|
173
|
+
slots << ParamSlot.new(kind, param.name, index)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
123
177
|
def keyword_slots(params_node)
|
|
124
178
|
params_node.keywords.filter_map do |kw|
|
|
125
179
|
case kw
|
|
@@ -100,19 +100,62 @@ module Rigor
|
|
|
100
100
|
|
|
101
101
|
def decompose_tuple(tuple, front_count, back_count, rest_present:)
|
|
102
102
|
elements = tuple.elements
|
|
103
|
-
fronts = Array.new(front_count) { |i| elements
|
|
103
|
+
fronts = Array.new(front_count) { |i| slot_type(elements, i) }
|
|
104
104
|
if rest_present
|
|
105
105
|
middle_end = [elements.size - back_count, front_count].max
|
|
106
106
|
middle = elements[front_count...middle_end] || []
|
|
107
107
|
rest_type = Type::Combinator.tuple_of(*middle)
|
|
108
|
-
backs = Array.new(back_count) { |i| elements
|
|
108
|
+
backs = Array.new(back_count) { |i| slot_type(elements, middle_end + i) }
|
|
109
109
|
else
|
|
110
110
|
rest_type = nil
|
|
111
|
-
backs = Array.new(back_count) { |i| elements
|
|
111
|
+
backs = Array.new(back_count) { |i| slot_type(elements, front_count + i) }
|
|
112
112
|
end
|
|
113
113
|
[fronts, rest_type, backs]
|
|
114
114
|
end
|
|
115
115
|
|
|
116
|
+
# The per-slot type for index `i` of a tuple decomposition, FP-safely
|
|
117
|
+
# softened: a missing slot is `nil` (the runtime value of an
|
|
118
|
+
# over-destructured positional), and a PRESENT but nil-bearing slot
|
|
119
|
+
# (`X | nil`) is softened to its non-`nil` part — for a heterogeneous
|
|
120
|
+
# `Tuple` whose optional slot was made optional by flow.
|
|
121
|
+
#
|
|
122
|
+
# Rationale (ADR-57 slice 3 work-item 2): a destructure of a tuple
|
|
123
|
+
# element that flow typed as optional is almost always guarded by a
|
|
124
|
+
# CORRELATED invariant the flow engine cannot prove. The canonical case
|
|
125
|
+
# is haml's `parse_tag`, which returns `[..., last_line || @line.index
|
|
126
|
+
# + 1]` — a 9-tuple whose `last_line` slot widens to `Dynamic[top]?`
|
|
127
|
+
# through a loop-nested destructure; at the call site `..., last_line =
|
|
128
|
+
# parse_tag(text); raise(..., last_line - 1) if parse && value.empty?`
|
|
129
|
+
# the `last_line` is nil ONLY when an earlier element is too, and the
|
|
130
|
+
# guard short-circuits — but that correlation lives across slots, so
|
|
131
|
+
# per-slot flow sees `last_line` as nil-able and `last_line - 1` fires a
|
|
132
|
+
# spurious `possible nil receiver`. Manufacturing a `T?` for every
|
|
133
|
+
# destructured slot frightens working code; FP discipline (the program
|
|
134
|
+
# works) outranks the worst-case per-slot reading, so we drop the `nil`
|
|
135
|
+
# from a destructured slot and keep the non-`nil` constituent (a bare
|
|
136
|
+
# `nil` slot stays `nil` — there is nothing to soften). A pure non-
|
|
137
|
+
# optional element keeps its precise type unchanged.
|
|
138
|
+
def slot_type(elements, index)
|
|
139
|
+
element = elements[index]
|
|
140
|
+
return Type::Combinator.constant_of(nil) if element.nil?
|
|
141
|
+
|
|
142
|
+
soften_optional_slot(element)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def soften_optional_slot(element)
|
|
146
|
+
return element unless element.is_a?(Type::Union)
|
|
147
|
+
return element unless element.members.any? { |m| nil_literal?(m) }
|
|
148
|
+
|
|
149
|
+
non_nil = element.members.reject { |m| nil_literal?(m) }
|
|
150
|
+
return element if non_nil.empty? # a bare `nil` slot: nothing to soften
|
|
151
|
+
|
|
152
|
+
Type::Combinator.union(*non_nil)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def nil_literal?(member)
|
|
156
|
+
member.is_a?(Type::Constant) && member.value.nil?
|
|
157
|
+
end
|
|
158
|
+
|
|
116
159
|
def decompose_default(front_count, back_count, rest_present:)
|
|
117
160
|
[
|
|
118
161
|
Array.new(front_count) { Type::Combinator.untyped },
|
|
@@ -280,6 +280,148 @@ module Rigor
|
|
|
280
280
|
carriers = kinds.map { |name| Type::Combinator.nominal_of(name) }
|
|
281
281
|
carriers.size == 1 ? carriers.first : Type::Combinator.union(*carriers)
|
|
282
282
|
end
|
|
283
|
+
|
|
284
|
+
# ----------------------------------------------------------------
|
|
285
|
+
# ADR-56 slice C — receiver-content element-type JOIN.
|
|
286
|
+
#
|
|
287
|
+
# `widen_after_block` above forgets a literal-shape carrier's arity
|
|
288
|
+
# when a captured local is content-mutated inside a block, but it
|
|
289
|
+
# keeps only the SEED's element types — an unsound under-
|
|
290
|
+
# approximation for a non-empty seed (`out = [0]; arr.each { |x|
|
|
291
|
+
# out << x }` types `Array[0]` while the runtime array is
|
|
292
|
+
# `[0, 1, 2, 3]`). Slice C joins the appended/stored element (and
|
|
293
|
+
# key/value) types INTO the continuation collection's parameter, so
|
|
294
|
+
# the result is `Array[0 | Integer]` rather than `Array[0]`.
|
|
295
|
+
#
|
|
296
|
+
# Array content-mutators that append/store ELEMENTS. The appended
|
|
297
|
+
# element type is the call's argument type(s); `[]=`'s value is its
|
|
298
|
+
# LAST argument (the keys precede it). Subset of `ARRAY_MUTATORS`:
|
|
299
|
+
# only the element-INTRODUCING methods (removers / reorderers add no
|
|
300
|
+
# new element evidence and are already covered by the arity-forget).
|
|
301
|
+
ARRAY_CONTENT_ADDERS = %i[
|
|
302
|
+
<< push append prepend unshift concat insert []= fill replace
|
|
303
|
+
].to_set.freeze
|
|
304
|
+
|
|
305
|
+
# Hash content-mutators that store a key→value pair. For `[]=` /
|
|
306
|
+
# `store` the key is the first argument and the value the last.
|
|
307
|
+
HASH_CONTENT_ADDERS = %i[[]= store].to_set.freeze
|
|
308
|
+
|
|
309
|
+
# String content-mutators that append to the buffer. String carries
|
|
310
|
+
# no element parameter, so these contribute nothing to a join — they
|
|
311
|
+
# are listed so the orchestrator recognises them as content mutators
|
|
312
|
+
# (the binding already widens to `String` via normal typing); the
|
|
313
|
+
# join helpers below short-circuit on a non-collection pre-state.
|
|
314
|
+
STRING_CONTENT_ADDERS = %i[<< concat prepend insert replace].to_set.freeze
|
|
315
|
+
|
|
316
|
+
# Every method name that mutates a captured local's CONTENT — the
|
|
317
|
+
# union the orchestrator scans the block body for.
|
|
318
|
+
CONTENT_ADDERS = (ARRAY_CONTENT_ADDERS | HASH_CONTENT_ADDERS | STRING_CONTENT_ADDERS).freeze
|
|
319
|
+
|
|
320
|
+
# The element types a single content-mutator call introduces into an
|
|
321
|
+
# Array, given the per-argument types (already typed in the block
|
|
322
|
+
# body scope). `concat`/`replace` take collection arguments, so their
|
|
323
|
+
# element evidence is the arguments' OWN element types unioned; the
|
|
324
|
+
# rest append the argument values directly. Returns `[]` when no
|
|
325
|
+
# element evidence (e.g. a `<<` with no resolvable arg).
|
|
326
|
+
def array_added_elements(method_name, arg_types)
|
|
327
|
+
return [] if arg_types.empty?
|
|
328
|
+
|
|
329
|
+
case method_name
|
|
330
|
+
when :concat, :replace
|
|
331
|
+
arg_types.flat_map { |t| collection_element_types(t) }
|
|
332
|
+
when :insert
|
|
333
|
+
# `insert(index, *objs)` — first arg is the position.
|
|
334
|
+
arg_types.drop(1)
|
|
335
|
+
when :[]=
|
|
336
|
+
# `arr[i] = v` / `arr[i, n] = v` — value is the last argument.
|
|
337
|
+
[arg_types.last]
|
|
338
|
+
when :fill
|
|
339
|
+
# `fill(value)` — only the no-block single-value form adds a
|
|
340
|
+
# concrete element; block / range forms are conservatively
|
|
341
|
+
# ignored (the arity-forget already widened the binding).
|
|
342
|
+
arg_types.size == 1 ? arg_types : []
|
|
343
|
+
else # << push append prepend unshift
|
|
344
|
+
arg_types
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Builds the continuation Array type from the pre-state binding and
|
|
349
|
+
# the appended element types. The floor is `Array[Dynamic[top]]`
|
|
350
|
+
# (the sound empty-seed behaviour) when there is no element evidence
|
|
351
|
+
# at all.
|
|
352
|
+
def join_array_content(pre_state, added_elements)
|
|
353
|
+
seed_elements = collection_element_types(pre_state)
|
|
354
|
+
added = added_elements.compact
|
|
355
|
+
# The empty-seed floor element is `Dynamic[top]` (no element
|
|
356
|
+
# evidence). When real appended evidence exists that floor carries
|
|
357
|
+
# nothing, so drop it — an empty accumulator built by `out << x*2`
|
|
358
|
+
# reads `Array[Integer]`, not `Array[Integer | Dynamic[top]]`.
|
|
359
|
+
seed_elements = drop_dynamic(seed_elements) unless added.empty?
|
|
360
|
+
elements = seed_elements + added
|
|
361
|
+
return Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped]) if elements.empty?
|
|
362
|
+
|
|
363
|
+
Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.union(*elements)])
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Builds the continuation Hash type from the pre-state binding and a
|
|
367
|
+
# list of `[key_type, value_type]` pairs stored by `[]=` / `store`.
|
|
368
|
+
def join_hash_content(pre_state, added_pairs)
|
|
369
|
+
seed_keys, seed_values = hash_shape_key_values(pre_state)
|
|
370
|
+
added_keys = added_pairs.map(&:first).compact
|
|
371
|
+
added_values = added_pairs.map(&:last).compact
|
|
372
|
+
seed_keys = drop_dynamic(seed_keys) unless added_keys.empty?
|
|
373
|
+
seed_values = drop_dynamic(seed_values) unless added_values.empty?
|
|
374
|
+
keys = seed_keys + added_keys
|
|
375
|
+
values = seed_values + added_values
|
|
376
|
+
key_t = keys.empty? ? Type::Combinator.untyped : Type::Combinator.union(*keys)
|
|
377
|
+
value_t = values.empty? ? Type::Combinator.untyped : Type::Combinator.union(*values)
|
|
378
|
+
Type::Combinator.nominal_of("Hash", type_args: [key_t, value_t])
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Drops `Dynamic` (incl. `untyped`) constituents from a type list.
|
|
382
|
+
def drop_dynamic(types)
|
|
383
|
+
types.grep_v(Type::Dynamic)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Element types carried by a collection binding, regardless of which
|
|
387
|
+
# carrier holds them: a `Tuple` lists them, a `Nominal[Array, [E]]`
|
|
388
|
+
# has one element param, a bare `Array` / anything else yields none.
|
|
389
|
+
def collection_element_types(type)
|
|
390
|
+
case type
|
|
391
|
+
when Type::Tuple
|
|
392
|
+
type.elements
|
|
393
|
+
when Type::Nominal
|
|
394
|
+
type.class_name == "Array" ? type.type_args : []
|
|
395
|
+
when Type::Union
|
|
396
|
+
# A loop's single-pass join can union the widened collection with
|
|
397
|
+
# its un-widened literal seed (`Array[0] | [0]`); pull element
|
|
398
|
+
# evidence from every Array-ish member.
|
|
399
|
+
type.members.flat_map { |m| collection_element_types(m) }
|
|
400
|
+
else
|
|
401
|
+
[]
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# `[keys, values]` evidence from a Hash-ish pre-state binding —
|
|
406
|
+
# a `HashShape` (literal pairs) or a `Nominal[Hash, [K, V]]`.
|
|
407
|
+
def hash_shape_key_values(type)
|
|
408
|
+
case type
|
|
409
|
+
when Type::HashShape
|
|
410
|
+
return [[], []] if type.pairs.empty?
|
|
411
|
+
|
|
412
|
+
[[key_union_for(type.pairs.keys)], type.pairs.values]
|
|
413
|
+
when Type::Nominal
|
|
414
|
+
type.class_name == "Hash" && type.type_args.size == 2 ? [[type.type_args[0]], [type.type_args[1]]] : [[], []]
|
|
415
|
+
when Type::Union
|
|
416
|
+
type.members.each_with_object([[], []]) do |m, (ks, vs)|
|
|
417
|
+
mk, mv = hash_shape_key_values(m)
|
|
418
|
+
ks.concat(mk)
|
|
419
|
+
vs.concat(mv)
|
|
420
|
+
end
|
|
421
|
+
else
|
|
422
|
+
[[], []]
|
|
423
|
+
end
|
|
424
|
+
end
|
|
283
425
|
end
|
|
284
426
|
end
|
|
285
427
|
end
|