rigortype 0.0.4 → 0.0.6

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +215 -134
  3. data/data/builtins/ruby_core/comparable.yml +87 -0
  4. data/data/builtins/ruby_core/complex.yml +505 -0
  5. data/data/builtins/ruby_core/date.yml +1737 -0
  6. data/data/builtins/ruby_core/enumerable.yml +557 -0
  7. data/data/builtins/ruby_core/file.yml +9 -0
  8. data/data/builtins/ruby_core/pathname.yml +1067 -0
  9. data/data/builtins/ruby_core/rational.yml +365 -0
  10. data/data/builtins/ruby_core/string.yml +9 -0
  11. data/data/builtins/ruby_core/time.yml +6 -4
  12. data/lib/rigor/cli.rb +1 -1
  13. data/lib/rigor/inference/builtins/comparable_catalog.rb +27 -0
  14. data/lib/rigor/inference/builtins/complex_catalog.rb +41 -0
  15. data/lib/rigor/inference/builtins/date_catalog.rb +98 -0
  16. data/lib/rigor/inference/builtins/enumerable_catalog.rb +27 -0
  17. data/lib/rigor/inference/builtins/pathname_catalog.rb +35 -0
  18. data/lib/rigor/inference/builtins/rational_catalog.rb +38 -0
  19. data/lib/rigor/inference/expression_typer.rb +285 -23
  20. data/lib/rigor/inference/method_dispatcher/block_folding.rb +322 -0
  21. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +197 -12
  22. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +99 -0
  23. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +95 -0
  24. data/lib/rigor/inference/method_dispatcher.rb +20 -8
  25. data/lib/rigor/inference/narrowing.rb +210 -1
  26. data/lib/rigor/inference/scope_indexer.rb +87 -11
  27. data/lib/rigor/inference/statement_evaluator.rb +5 -1
  28. data/lib/rigor/rbs_extended.rb +11 -6
  29. data/lib/rigor/type/integer_range.rb +4 -2
  30. data/lib/rigor/version.rb +1 -1
  31. data/sig/rigor/environment.rbs +4 -6
  32. data/sig/rigor/inference.rbs +2 -1
  33. data/sig/rigor/type.rbs +41 -41
  34. metadata +15 -1
@@ -8,6 +8,12 @@ require_relative "../builtins/hash_catalog"
8
8
  require_relative "../builtins/range_catalog"
9
9
  require_relative "../builtins/set_catalog"
10
10
  require_relative "../builtins/time_catalog"
11
+ require_relative "../builtins/date_catalog"
12
+ require_relative "../builtins/comparable_catalog"
13
+ require_relative "../builtins/enumerable_catalog"
14
+ require_relative "../builtins/rational_catalog"
15
+ require_relative "../builtins/complex_catalog"
16
+ require_relative "../builtins/pathname_catalog"
11
17
 
12
18
  module Rigor
13
19
  module Inference
@@ -109,9 +115,14 @@ module Rigor
109
115
  arg_sets = args.map { |a| numeric_set_of(a) }
110
116
  return nil if arg_sets.any?(&:nil?)
111
117
 
112
- case args.size
118
+ dispatch_by_arity(receiver_set, method_name, arg_sets)
119
+ end
120
+
121
+ def dispatch_by_arity(receiver_set, method_name, arg_sets)
122
+ case arg_sets.size
113
123
  when 0 then try_fold_unary(receiver_set, method_name)
114
124
  when 1 then try_fold_binary(receiver_set, method_name, arg_sets.first)
125
+ when 2 then try_fold_ternary(receiver_set, method_name, arg_sets)
115
126
  end
116
127
  end
117
128
 
@@ -220,6 +231,107 @@ module Rigor
220
231
  build_constant_type(results, source: receiver_values + arg_values)
221
232
  end
222
233
 
234
+ # 2-arg fold dispatch. Used by `Comparable#between?(min, max)`,
235
+ # `Comparable#clamp(min, max)`, and `Integer#pow(exp, mod)` —
236
+ # methods the catalog classifies `:leaf` but that the prior
237
+ # 0/1-arg switch could not reach.
238
+ #
239
+ # v0.0.6 — IntegerRange-shaped receivers participate in
240
+ # `Comparable#between?` and `Comparable#clamp` folds.
241
+ # `int<a,b>.between?(min, max)` decides three-valued via
242
+ # the receiver's bounds against scalar args; `int<a,b>.clamp`
243
+ # narrows the receiver's bounds against the bracket. Other
244
+ # ternary methods over IntegerRange operands still decline.
245
+ def try_fold_ternary(receiver_set, method_name, arg_sets)
246
+ return try_fold_ternary_range(receiver_set, method_name, arg_sets) if receiver_set.is_a?(Type::IntegerRange)
247
+ return nil if arg_sets.any?(Type::IntegerRange)
248
+
249
+ try_fold_ternary_set(receiver_set, method_name, arg_sets)
250
+ end
251
+
252
+ # Receiver IntegerRange + two scalar `Constant<Integer>`
253
+ # args — the only IntegerRange-aware ternary fold today.
254
+ # `between?` returns Trinary truthiness over the bracket;
255
+ # `clamp` returns the intersected IntegerRange (or a
256
+ # collapsed Constant if the result pins a single point).
257
+ def try_fold_ternary_range(range, method_name, arg_sets)
258
+ return nil unless arg_sets.all?(Array)
259
+
260
+ min_arg = single_integer_arg(arg_sets[0])
261
+ max_arg = single_integer_arg(arg_sets[1])
262
+ return nil if min_arg.nil? || max_arg.nil?
263
+ return nil if min_arg > max_arg
264
+
265
+ case method_name
266
+ when :between? then range_between(range, min_arg, max_arg)
267
+ when :clamp then range_clamp(range, min_arg, max_arg)
268
+ end
269
+ end
270
+
271
+ def single_integer_arg(values)
272
+ return nil unless values.is_a?(Array) && values.size == 1
273
+
274
+ v = values.first
275
+ v.is_a?(Integer) ? v : nil
276
+ end
277
+
278
+ # `int<a,b>.between?(min, max)`:
279
+ # - Constant[true] when [a,b] ⊆ [min,max] (and finite).
280
+ # - Constant[false] when [a,b] ∩ [min,max] is empty.
281
+ # - bool union otherwise.
282
+ def range_between(range, min_arg, max_arg)
283
+ return Type::Combinator.constant_of(false) if range.upper < min_arg || range.lower > max_arg
284
+
285
+ return Type::Combinator.constant_of(true) if range.finite? && range.min >= min_arg && range.max <= max_arg
286
+
287
+ bool_union
288
+ end
289
+
290
+ # `int<a,b>.clamp(min, max)`:
291
+ # - new_lower = max(a, min), new_upper = min(b, max).
292
+ # - When new_lower > new_upper the bracket excluded the
293
+ # range entirely; the call still returns one of the
294
+ # bracket bounds at runtime, but Rigor is strictly less
295
+ # precise here than Ruby — decline so the RBS tier
296
+ # widens to plain Integer rather than the dispatcher
297
+ # inventing a value.
298
+ def range_clamp(range, min_arg, max_arg)
299
+ new_lower = clamp_lower_bound(range.lower, min_arg)
300
+ new_upper = clamp_upper_bound(range.upper, max_arg)
301
+ return nil if new_lower.is_a?(Integer) && new_upper.is_a?(Integer) && new_lower > new_upper
302
+
303
+ build_integer_range(new_lower, new_upper)
304
+ end
305
+
306
+ def clamp_lower_bound(range_lower, bracket_min)
307
+ return bracket_min if range_lower == -Float::INFINITY
308
+
309
+ [range_lower, bracket_min].max
310
+ end
311
+
312
+ def clamp_upper_bound(range_upper, bracket_max)
313
+ return bracket_max if range_upper == Float::INFINITY
314
+
315
+ [range_upper, bracket_max].min
316
+ end
317
+
318
+ def try_fold_ternary_set(receiver_values, method_name, arg_sets)
319
+ total = receiver_values.size * arg_sets[0].size * arg_sets[1].size
320
+ return nil if total > UNION_FOLD_INPUT_LIMIT
321
+ return nil unless receiver_values.all? { |rv| ternary_method_allowed?(rv, method_name) }
322
+
323
+ results = ternary_cartesian(receiver_values, method_name, arg_sets)
324
+ build_constant_type(results, source: receiver_values + arg_sets.flatten)
325
+ end
326
+
327
+ def ternary_cartesian(receiver_values, method_name, arg_sets)
328
+ receiver_values.flat_map do |rv|
329
+ arg_sets[0].flat_map do |av0|
330
+ arg_sets[1].flat_map { |av1| invoke_ternary(rv, method_name, av0, av1) || [] }
331
+ end
332
+ end
333
+ end
334
+
223
335
  def unary_method_allowed?(receiver_value, method_name)
224
336
  unary_ops_for(receiver_value).include?(method_name) ||
225
337
  catalog_allows?(receiver_value, method_name)
@@ -230,6 +342,13 @@ module Rigor
230
342
  catalog_allows?(receiver_value, method_name)
231
343
  end
232
344
 
345
+ # 2-arg methods have no hand-rolled allow list; the catalog
346
+ # is the sole gate. Adding a per-class arity-2 set is reserved
347
+ # for future cases that need it.
348
+ def ternary_method_allowed?(receiver_value, method_name)
349
+ catalog_allows?(receiver_value, method_name)
350
+ end
351
+
233
352
  # Builds a Constant or Union[Constant…] from a flat list of
234
353
  # Ruby values. When the deduped set exceeds
235
354
  # `UNION_FOLD_OUTPUT_LIMIT` and every result is an Integer,
@@ -598,6 +717,19 @@ module Rigor
598
717
  nil
599
718
  end
600
719
 
720
+ # Returns `[value]` on success, `nil` to signal "skip this triple".
721
+ # Mirrors `invoke_binary` but for the 2-argument shape; the wrap
722
+ # convention lets callers `flat_map` without losing
723
+ # legitimate `false`/`nil` folds.
724
+ def invoke_ternary(receiver_value, method_name, av0, av1)
725
+ return nil unless ternary_method_allowed?(receiver_value, method_name)
726
+
727
+ result = receiver_value.public_send(method_name, av0, av1)
728
+ foldable_constant_value?(result) ? [result] : nil
729
+ rescue StandardError
730
+ nil
731
+ end
732
+
601
733
  # Returns `[value]` on success, `nil` to signal "skip". See
602
734
  # `invoke_binary` for why we wrap.
603
735
  def invoke_unary(receiver_value, method_name)
@@ -623,11 +755,54 @@ module Rigor
623
755
  # implementation does not call back into user-redefinable
624
756
  # Ruby methods, so executing them on a literal Integer/Float
625
757
  # is safe regardless of monkey-patching.
758
+ #
759
+ # Resolution order:
760
+ #
761
+ # 1. Primary class catalog (e.g. NumericCatalog for an
762
+ # Integer receiver). When the catalog has an entry —
763
+ # even one classified `:dispatch` — that answer wins.
764
+ # The class's direct `rb_define_method` registration is
765
+ # authoritative; we MUST NOT fall through to a module
766
+ # catalog and risk over-folding.
767
+ # 2. Module catalogs (Comparable, Enumerable, …) that the
768
+ # receiver's class includes by ancestry. Reached only
769
+ # when the primary catalog has NO entry for the method
770
+ # — typically because the method is inherited purely
771
+ # through `include Comparable` / `include Enumerable`
772
+ # (e.g. `Integer#between?` / `Integer#clamp` are not in
773
+ # numeric.yml because the Init block does not
774
+ # `rb_define_method` them on Integer).
626
775
  def catalog_allows?(receiver_value, method_name)
627
776
  catalog, class_name = catalog_for(receiver_value)
628
- return false unless catalog
777
+ return catalog.safe_for_folding?(class_name, method_name) if catalog&.method_entry(class_name, method_name)
629
778
 
630
- catalog.safe_for_folding?(class_name, method_name)
779
+ module_catalogs_for(receiver_value).any? do |mod_catalog, mod_name|
780
+ mod_catalog.method_entry(mod_name, method_name) &&
781
+ mod_catalog.safe_for_folding?(mod_name, method_name)
782
+ end
783
+ end
784
+
785
+ # `(Module, catalog, class_name)` triples consulted as a
786
+ # fallthrough when the primary class catalog has no entry.
787
+ # Each triple's Module is matched against the receiver
788
+ # class's ancestor chain at lookup time; the catalog
789
+ # corresponds to the module-mode YAML at
790
+ # `data/builtins/ruby_core/<topic>.yml`.
791
+ MODULE_CATALOGS = [
792
+ [Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
793
+ [Enumerable, Builtins::ENUMERABLE_CATALOG, "Enumerable"]
794
+ ].freeze
795
+ private_constant :MODULE_CATALOGS
796
+
797
+ # Returns the `(catalog, class_name)` pairs for every
798
+ # registered module that is in the receiver's ancestor
799
+ # chain. The receiver's class's `Module#ancestors` is
800
+ # cached by Ruby; the `Set` membership check is cheap.
801
+ def module_catalogs_for(receiver_value)
802
+ ancestors = Set.new(receiver_value.class.ancestors)
803
+ MODULE_CATALOGS.filter_map do |mod, catalog, class_name|
804
+ [catalog, class_name] if ancestors.include?(mod)
805
+ end
631
806
  end
632
807
 
633
808
  # `(catalog, class_name)` per receiver class. The class_name
@@ -637,16 +812,26 @@ module Rigor
637
812
  # before any base-class fallback would, and adding a new
638
813
  # class is a one-line addition rather than another `when`
639
814
  # arm on a growing case statement.
815
+ # Subclass-before-superclass ordering: `DateTime < Date`,
816
+ # so the `DateTime` row MUST come before the `Date` row.
817
+ # Otherwise a `DateTime` receiver would match the `Date`
818
+ # arm first and the catalog would consult the Date entry
819
+ # in `DATE_CATALOG` for the wrong class.
640
820
  CATALOG_BY_CLASS = [
641
- [Integer, [Builtins::NumericCatalog, "Integer"]],
642
- [Float, [Builtins::NumericCatalog, "Float"]],
643
- [String, [Builtins::STRING_CATALOG, "String"]],
644
- [Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
645
- [Array, [Builtins::ARRAY_CATALOG, "Array"]],
646
- [Hash, [Builtins::HASH_CATALOG, "Hash"]],
647
- [Range, [Builtins::RANGE_CATALOG, "Range"]],
648
- [::Set, [Builtins::SET_CATALOG, "Set"]],
649
- [Time, [Builtins::TIME_CATALOG, "Time"]]
821
+ [Integer, [Builtins::NumericCatalog, "Integer"]],
822
+ [Float, [Builtins::NumericCatalog, "Float"]],
823
+ [String, [Builtins::STRING_CATALOG, "String"]],
824
+ [Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
825
+ [Array, [Builtins::ARRAY_CATALOG, "Array"]],
826
+ [Hash, [Builtins::HASH_CATALOG, "Hash"]],
827
+ [Range, [Builtins::RANGE_CATALOG, "Range"]],
828
+ [::Set, [Builtins::SET_CATALOG, "Set"]],
829
+ [Time, [Builtins::TIME_CATALOG, "Time"]],
830
+ [DateTime, [Builtins::DATE_CATALOG, "DateTime"]],
831
+ [Date, [Builtins::DATE_CATALOG, "Date"]],
832
+ [Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
833
+ [Complex, [Builtins::COMPLEX_CATALOG, "Complex"]],
834
+ [Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]]
650
835
  ].freeze
651
836
  private_constant :CATALOG_BY_CLASS
652
837
 
@@ -32,14 +32,20 @@ module Rigor
32
32
 
33
33
  # @return [Array<Rigor::Type>, nil] block-param types, or
34
34
  # nil to fall through to the next tier.
35
+ # rubocop:disable Metrics/CyclomaticComplexity
35
36
  def block_param_types(receiver:, method_name:, args:)
36
37
  case method_name
37
38
  when :times then times_block_params(receiver)
38
39
  when :upto then upto_block_params(receiver, args.first)
39
40
  when :downto then downto_block_params(receiver, args.first)
40
41
  when :each_with_index then each_with_index_block_params(receiver)
42
+ when :each_with_object then each_with_object_block_params(receiver, args.first)
43
+ when :inject, :reduce then inject_block_params(receiver, args)
44
+ when :group_by, :partition then single_element_block_params(receiver)
45
+ when :each_slice, :each_cons then slice_block_params(receiver)
41
46
  end
42
47
  end
48
+ # rubocop:enable Metrics/CyclomaticComplexity
43
49
 
44
50
  def times_block_params(receiver)
45
51
  return nil unless integer_rooted?(receiver)
@@ -88,6 +94,99 @@ module Rigor
88
94
  [element, Type::Combinator.non_negative_int]
89
95
  end
90
96
 
97
+ # `each_with_object(memo) { |elem, memo_inner| … }` yields
98
+ # `(element, memo)` where `memo` is the second argument's
99
+ # type (passed by reference and threaded across iterations
100
+ # at runtime — Rigor reflects that by binding the block's
101
+ # second parameter to whatever the call site supplied).
102
+ # When the call has no memo argument the dispatcher
103
+ # declines so the user's RBS / overload selector decides.
104
+ def each_with_object_block_params(receiver, memo_arg)
105
+ return nil if memo_arg.nil?
106
+
107
+ element = element_type_of(receiver)
108
+ return nil if element.nil?
109
+
110
+ [element, memo_arg]
111
+ end
112
+
113
+ # `inject(seed) { |memo, elem| … }` and `reduce` accept
114
+ # three call shapes:
115
+ #
116
+ # - `(seed) { |memo, elem| … }` — block params `[seed, element]`.
117
+ # The memo's static type is the seed's; we cannot prove the
118
+ # block return type here without round-tripping through the
119
+ # block analyser, so the binding is the seed's type and
120
+ # downstream inference widens as needed.
121
+ # - `() { |memo, elem| … }` — the first iteration uses the
122
+ # first element as the memo, so `[element, element]` is the
123
+ # sound binding.
124
+ # - `(seed, :sym)` / `(:sym)` — Symbol method-name forms have
125
+ # no block. `inject` with a Symbol final arg is recognised
126
+ # and declined (returns nil) so the dispatcher does not
127
+ # pretend a block existed.
128
+ def inject_block_params(receiver, args)
129
+ element = element_type_of(receiver)
130
+ return nil if element.nil?
131
+
132
+ case args.size
133
+ when 0
134
+ [element, element]
135
+ when 1
136
+ seed = args.first
137
+ return nil if symbol_constant?(seed)
138
+
139
+ [seed, element]
140
+ when 2
141
+ # `inject(seed, :sym)` — Symbol-call form, no block.
142
+ return nil if symbol_constant?(args[1])
143
+
144
+ [args[0], element]
145
+ end
146
+ end
147
+
148
+ def symbol_constant?(type)
149
+ type.is_a?(Type::Constant) && type.value.is_a?(Symbol)
150
+ end
151
+
152
+ # Element-yielding Enumerable methods covered as a v0.0.5
153
+ # placeholder. RBS already binds the block parameter
154
+ # correctly for plain `Array[T]` / `Set[T]` / `Range[T]`
155
+ # receivers via generic substitution; this tier exists so
156
+ # Tuple- and HashShape-shaped receivers reach the block
157
+ # body with the precise per-position element union /
158
+ # `Tuple[K, V]` pair rather than the projected
159
+ # `Array[union]` / `Hash[K, V]` widening.
160
+ #
161
+ # NOTE (v0.0.5): the per-method coverage here (group_by,
162
+ # partition, each_slice, each_cons) is intentionally
163
+ # narrow. The longer-term direction is to move
164
+ # Enumerable-aware projections into a plugin tier modelled
165
+ # after PHPStan's extension API (ADR-2). The placeholders
166
+ # below stay until the plugin surface is in place; once it
167
+ # ships, this dispatcher loses these arms and the
168
+ # equivalent rules move into a built-in plugin loaded at
169
+ # boot.
170
+ def single_element_block_params(receiver)
171
+ element = element_type_of(receiver)
172
+ return nil if element.nil?
173
+
174
+ [element]
175
+ end
176
+
177
+ # `each_slice(n) { |slice| … }` and `each_cons(n) { |window| … }`
178
+ # both yield an `Array[element]` once per iteration. The
179
+ # tier ignores the slice-size argument (a Constant<Integer>
180
+ # `n` could in principle bound the slice's length, but a
181
+ # tighter Tuple-of-`n` carrier is reserved for the plugin
182
+ # tier per the NOTE above).
183
+ def slice_block_params(receiver)
184
+ element = element_type_of(receiver)
185
+ return nil if element.nil?
186
+
187
+ [Type::Combinator.nominal_of("Array", type_args: [element])]
188
+ end
189
+
91
190
  ELEMENT_BY_NOMINAL = {
92
191
  "Array" => :nominal_unary_element,
93
192
  "Set" => :nominal_unary_element,
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # Kernel intrinsic shape-folding — precision tier for the
9
+ # `Kernel` module-functions whose return type is a function
10
+ # of the argument's *shape*, not just its class.
11
+ #
12
+ # Today the only catalogued intrinsic is `Kernel#Array`. The
13
+ # default RBS sig is `Array(untyped) -> Array[untyped]`, which
14
+ # collapses to `Array[Dynamic[top]]` for every caller. This
15
+ # tier short-circuits with a precise answer when the argument's
16
+ # type lattice tells us what the result element type MUST be:
17
+ #
18
+ # Array(Constant[nil]) -> Array[bot] # `[]`
19
+ # Array(Nominal["Array",[E]]) -> Array[E] # already an Array
20
+ # Array(Tuple[T1,T2,…]) -> Array[T1|T2|…]
21
+ # Array(Union[A,B,…]) -> distribute, then unify
22
+ # Array(other Nominal[T]) -> Array[Nominal[T]]
23
+ #
24
+ # For receiver shapes we cannot prove (`Top`, `Dynamic`, …)
25
+ # the tier returns nil and the RBS tier answers with the
26
+ # generic `Array[untyped]` envelope.
27
+ #
28
+ # See `docs/type-specification/value-lattice.md` for the
29
+ # union-distribution contract this tier mirrors.
30
+ module KernelDispatch
31
+ module_function
32
+
33
+ def try_dispatch(receiver:, method_name:, args:)
34
+ return nil unless method_name == :Array
35
+ return nil if args.length != 1
36
+ return nil if receiver.nil?
37
+
38
+ arg = args.first
39
+ element = element_type_of(arg)
40
+ return nil if element.nil?
41
+
42
+ Type::Combinator.nominal_of("Array", type_args: [element])
43
+ end
44
+
45
+ # Computes the element type the argument contributes to the
46
+ # `Array(arg)` result, mirroring Ruby's coercion contract:
47
+ #
48
+ # - `nil` becomes `[]` (element type Bot — the empty array
49
+ # contributes no inhabitants).
50
+ # - An existing `Array[E]` is returned as-is, so its element
51
+ # type is `E`.
52
+ # - A `Tuple[T1, T2, …]` is materialised as `Array[T1|T2|…]`
53
+ # (every tuple inhabitant is a tuple, hence Array-like).
54
+ # - Any other value `v` becomes `[v]`, so the element type
55
+ # is the value's own type.
56
+ #
57
+ # Returns nil for receiver shapes the tier cannot prove
58
+ # (Top, Dynamic, Bot in pre-coercion position) so the
59
+ # caller falls back to the RBS-tier envelope.
60
+ def element_type_of(type)
61
+ case type
62
+ when Type::Union
63
+ distribute_over_union(type)
64
+ when Type::Constant
65
+ type.value.nil? ? Type::Combinator.bot : type
66
+ when Type::Nominal
67
+ array_element_or_self(type)
68
+ when Type::Tuple
69
+ tuple_element_union(type)
70
+ end
71
+ end
72
+
73
+ def distribute_over_union(union)
74
+ contributions = union.members.map { |member| element_type_of(member) }
75
+ return nil if contributions.any?(&:nil?)
76
+
77
+ Type::Combinator.union(*contributions)
78
+ end
79
+
80
+ def array_element_or_self(nominal)
81
+ return nominal unless nominal.class_name == "Array"
82
+ return Type::Combinator.untyped if nominal.type_args.empty?
83
+
84
+ Type::Combinator.union(*nominal.type_args)
85
+ end
86
+
87
+ def tuple_element_union(tuple)
88
+ return Type::Combinator.bot if tuple.elements.empty?
89
+
90
+ Type::Combinator.union(*tuple.elements)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -5,7 +5,9 @@ require_relative "method_dispatcher/constant_folding"
5
5
  require_relative "method_dispatcher/shape_dispatch"
6
6
  require_relative "method_dispatcher/rbs_dispatch"
7
7
  require_relative "method_dispatcher/iterator_dispatch"
8
+ require_relative "method_dispatcher/block_folding"
8
9
  require_relative "method_dispatcher/file_folding"
10
+ require_relative "method_dispatcher/kernel_dispatch"
9
11
 
10
12
  module Rigor
11
13
  module Inference
@@ -58,7 +60,7 @@ module Rigor
58
60
  def dispatch(receiver_type:, method_name:, arg_types:, block_type: nil, environment: nil)
59
61
  return nil if receiver_type.nil?
60
62
 
61
- precise = dispatch_precise_tiers(receiver_type, method_name, arg_types)
63
+ precise = dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type)
62
64
  return precise if precise
63
65
 
64
66
  rbs_result = RbsDispatch.try_dispatch(
@@ -82,18 +84,28 @@ module Rigor
82
84
  end
83
85
 
84
86
  # Runs the precision tiers (constant fold, shape dispatch,
85
- # file-path fold) in order and returns the first non-nil
86
- # answer. Each tier owns its own receiver/argument shape
87
- # checks; a tier that does not recognise the receiver returns
88
- # nil so the next tier can try. The RBS tier sits below this
89
- # chain and is invoked by the outer `dispatch` method.
90
- def dispatch_precise_tiers(receiver_type, method_name, arg_types)
87
+ # file-path fold, block fold) in order and returns the first
88
+ # non-nil answer. Each tier owns its own receiver/argument
89
+ # shape checks; a tier that does not recognise the receiver
90
+ # returns nil so the next tier can try. The RBS tier sits
91
+ # below this chain and is invoked by the outer `dispatch`
92
+ # method.
93
+ #
94
+ # `BlockFolding` runs last among the precision tiers because
95
+ # its rules apply only to block-taking calls, so the cheaper
96
+ # arity-based fold tiers above it filter out the common
97
+ # cases first. When `block_type` is nil the tier is a no-op.
98
+ def dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type = nil)
91
99
  meta_result = try_meta_introspection(receiver_type, method_name)
92
100
  return meta_result if meta_result
93
101
 
94
102
  ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
95
103
  ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
96
- FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types)
104
+ FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
105
+ KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
106
+ BlockFolding.try_fold(
107
+ receiver: receiver_type, method_name: method_name, args: arg_types, block_type: block_type
108
+ )
97
109
  end
98
110
 
99
111
  def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)