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.
- checksums.yaml +4 -4
- data/README.md +215 -134
- data/data/builtins/ruby_core/comparable.yml +87 -0
- data/data/builtins/ruby_core/complex.yml +505 -0
- data/data/builtins/ruby_core/date.yml +1737 -0
- data/data/builtins/ruby_core/enumerable.yml +557 -0
- data/data/builtins/ruby_core/file.yml +9 -0
- data/data/builtins/ruby_core/pathname.yml +1067 -0
- data/data/builtins/ruby_core/rational.yml +365 -0
- data/data/builtins/ruby_core/string.yml +9 -0
- data/data/builtins/ruby_core/time.yml +6 -4
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/inference/builtins/comparable_catalog.rb +27 -0
- data/lib/rigor/inference/builtins/complex_catalog.rb +41 -0
- data/lib/rigor/inference/builtins/date_catalog.rb +98 -0
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +27 -0
- data/lib/rigor/inference/builtins/pathname_catalog.rb +35 -0
- data/lib/rigor/inference/builtins/rational_catalog.rb +38 -0
- data/lib/rigor/inference/expression_typer.rb +285 -23
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +322 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +197 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +99 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +95 -0
- data/lib/rigor/inference/method_dispatcher.rb +20 -8
- data/lib/rigor/inference/narrowing.rb +210 -1
- data/lib/rigor/inference/scope_indexer.rb +87 -11
- data/lib/rigor/inference/statement_evaluator.rb +5 -1
- data/lib/rigor/rbs_extended.rb +11 -6
- data/lib/rigor/type/integer_range.rb +4 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +4 -6
- data/sig/rigor/inference.rbs +2 -1
- data/sig/rigor/type.rbs +41 -41
- 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
|
-
|
|
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
|
|
777
|
+
return catalog.safe_for_folding?(class_name, method_name) if catalog&.method_entry(class_name, method_name)
|
|
629
778
|
|
|
630
|
-
|
|
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,
|
|
642
|
-
[Float,
|
|
643
|
-
[String,
|
|
644
|
-
[Symbol,
|
|
645
|
-
[Array,
|
|
646
|
-
[Hash,
|
|
647
|
-
[Range,
|
|
648
|
-
[::Set,
|
|
649
|
-
[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
|
|
86
|
-
# answer. Each tier owns its own receiver/argument
|
|
87
|
-
# checks; a tier that does not recognise the receiver
|
|
88
|
-
# nil so the next tier can try. The RBS tier sits
|
|
89
|
-
# chain and is invoked by the outer `dispatch`
|
|
90
|
-
|
|
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)
|