rigortype 0.0.3 → 0.0.5
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 -117
- 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/hash.yml +936 -0
- data/data/builtins/ruby_core/range.yml +389 -0
- data/data/builtins/ruby_core/rational.yml +365 -0
- data/data/builtins/ruby_core/set.yml +594 -0
- data/data/builtins/ruby_core/string.yml +9 -0
- data/data/builtins/ruby_core/time.yml +752 -0
- data/lib/rigor/analysis/check_rules.rb +11 -3
- data/lib/rigor/builtins/imported_refinements.rb +192 -10
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +181 -12
- 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/hash_catalog.rb +40 -0
- data/lib/rigor/inference/builtins/range_catalog.rb +46 -0
- data/lib/rigor/inference/builtins/rational_catalog.rb +38 -0
- data/lib/rigor/inference/builtins/set_catalog.rb +54 -0
- data/lib/rigor/inference/builtins/time_catalog.rb +64 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +145 -11
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +202 -1
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +95 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +135 -6
- data/lib/rigor/inference/method_dispatcher.rb +3 -1
- data/lib/rigor/inference/method_parameter_binder.rb +29 -4
- data/lib/rigor/inference/narrowing.rb +211 -0
- data/lib/rigor/inference/scope_indexer.rb +87 -11
- data/lib/rigor/inference/statement_evaluator.rb +6 -0
- data/lib/rigor/rbs_extended.rb +170 -14
- data/lib/rigor/type/combinator.rb +90 -0
- data/lib/rigor/type/integer_range.rb +4 -2
- data/lib/rigor/type/intersection.rb +135 -0
- data/lib/rigor/type/refined.rb +174 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +4 -6
- data/sig/rigor/inference.rbs +2 -1
- data/sig/rigor/rbs_extended.rbs +11 -0
- data/sig/rigor/type.rbs +75 -35
- metadata +22 -1
|
@@ -27,18 +27,25 @@ module Rigor
|
|
|
27
27
|
# - `a.downto(b) { |i| … }` yields the same domain `[b, a]`,
|
|
28
28
|
# just iterated in reverse. Lower bound from the
|
|
29
29
|
# argument, upper bound from the receiver.
|
|
30
|
-
module IteratorDispatch
|
|
30
|
+
module IteratorDispatch # rubocop:disable Metrics/ModuleLength
|
|
31
31
|
module_function
|
|
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)
|
|
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)
|
|
40
46
|
end
|
|
41
47
|
end
|
|
48
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
42
49
|
|
|
43
50
|
def times_block_params(receiver)
|
|
44
51
|
return nil unless integer_rooted?(receiver)
|
|
@@ -62,6 +69,200 @@ module Rigor
|
|
|
62
69
|
[build_index_range(lower_bound_of(end_arg), upper_bound_of(receiver))]
|
|
63
70
|
end
|
|
64
71
|
|
|
72
|
+
# Generalised iterator: every Enumerable-shaped collection
|
|
73
|
+
# in v0.0.4 yields `(element, index)` where the index is
|
|
74
|
+
# always `non-negative-int`. The element comes from the
|
|
75
|
+
# receiver's shape:
|
|
76
|
+
#
|
|
77
|
+
# - `Array[T]` / `Set[T]` / `Range[T]` → T
|
|
78
|
+
# - `Tuple[A, B, C]` → A | B | C
|
|
79
|
+
# (empty tuple cannot iterate, but we conservatively
|
|
80
|
+
# fall through to RBS so a missing rule never throws)
|
|
81
|
+
# - `Hash[K, V]` / `HashShape{...}` → Tuple[K, V]
|
|
82
|
+
# (Ruby yields `[key, value]` pairs as the element)
|
|
83
|
+
# - `Constant<Array>` / `Constant<Range>` / `Constant<Set>`
|
|
84
|
+
# → corresponding Constant element
|
|
85
|
+
#
|
|
86
|
+
# Receivers we cannot project (Top, Dynamic, unknown
|
|
87
|
+
# nominals, IO, …) decline so the RBS tier still answers
|
|
88
|
+
# — its element type is correct, only the index would
|
|
89
|
+
# widen to plain Integer.
|
|
90
|
+
def each_with_index_block_params(receiver)
|
|
91
|
+
element = element_type_of(receiver)
|
|
92
|
+
return nil if element.nil?
|
|
93
|
+
|
|
94
|
+
[element, Type::Combinator.non_negative_int]
|
|
95
|
+
end
|
|
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
|
+
|
|
190
|
+
ELEMENT_BY_NOMINAL = {
|
|
191
|
+
"Array" => :nominal_unary_element,
|
|
192
|
+
"Set" => :nominal_unary_element,
|
|
193
|
+
"Range" => :nominal_unary_element,
|
|
194
|
+
"Hash" => :nominal_hash_pair_element
|
|
195
|
+
}.freeze
|
|
196
|
+
private_constant :ELEMENT_BY_NOMINAL
|
|
197
|
+
|
|
198
|
+
def element_type_of(receiver)
|
|
199
|
+
case receiver
|
|
200
|
+
when Type::Tuple then tuple_element(receiver)
|
|
201
|
+
when Type::HashShape then hash_shape_pair_element(receiver)
|
|
202
|
+
when Type::Nominal then nominal_element(receiver)
|
|
203
|
+
when Type::Constant then constant_element(receiver)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def tuple_element(tuple)
|
|
208
|
+
return nil if tuple.elements.empty?
|
|
209
|
+
return tuple.elements.first if tuple.elements.size == 1
|
|
210
|
+
|
|
211
|
+
Type::Combinator.union(*tuple.elements)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def hash_shape_pair_element(shape)
|
|
215
|
+
return nil if shape.pairs.empty?
|
|
216
|
+
|
|
217
|
+
key = Type::Combinator.union(*shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) })
|
|
218
|
+
value = Type::Combinator.union(*shape.pairs.values)
|
|
219
|
+
Type::Combinator.tuple_of(key, value)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def nominal_element(nominal)
|
|
223
|
+
handler = ELEMENT_BY_NOMINAL[nominal.class_name]
|
|
224
|
+
return nil unless handler
|
|
225
|
+
|
|
226
|
+
send(handler, nominal)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def nominal_unary_element(nominal)
|
|
230
|
+
nominal.type_args.first
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def nominal_hash_pair_element(nominal)
|
|
234
|
+
key, value = nominal.type_args
|
|
235
|
+
return nil if key.nil? || value.nil?
|
|
236
|
+
|
|
237
|
+
Type::Combinator.tuple_of(key, value)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def constant_element(constant)
|
|
241
|
+
case constant.value
|
|
242
|
+
when Array
|
|
243
|
+
return nil if constant.value.empty?
|
|
244
|
+
|
|
245
|
+
Type::Combinator.union(*constant.value.map { |v| Type::Combinator.constant_of(v) })
|
|
246
|
+
when Range
|
|
247
|
+
range_constant_element(constant.value)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def range_constant_element(range)
|
|
252
|
+
beg = range.begin
|
|
253
|
+
en = range.end
|
|
254
|
+
return Type::Combinator.constant_of(beg) if beg.is_a?(Integer) && beg == en
|
|
255
|
+
|
|
256
|
+
if beg.is_a?(Integer) && en.is_a?(Integer)
|
|
257
|
+
upper = range.exclude_end? ? en - 1 : en
|
|
258
|
+
return build_index_range(beg, upper)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Mixed / non-integer ranges decline: the dispatcher
|
|
262
|
+
# falls through to RBS's element-type answer.
|
|
263
|
+
nil
|
|
264
|
+
end
|
|
265
|
+
|
|
65
266
|
# `Constant<Integer>`, `IntegerRange`, and `Nominal[Integer]`
|
|
66
267
|
# all participate. Non-integer types (Float, String, …) and
|
|
67
268
|
# `Top`/`Dynamic` decline so the RBS tier answers.
|
|
@@ -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
|
|
@@ -53,13 +53,22 @@ module Rigor
|
|
|
53
53
|
overloads = method_definition.method_types
|
|
54
54
|
return nil if overloads.empty?
|
|
55
55
|
|
|
56
|
+
# `rigor:v1:param: <name> <refinement>` annotations on
|
|
57
|
+
# this method override the RBS-declared parameter type
|
|
58
|
+
# at the matching name. The map is consumed inside
|
|
59
|
+
# `accepts_param?` so overload selection sees the
|
|
60
|
+
# tighter type when filtering candidates by argument
|
|
61
|
+
# compatibility.
|
|
62
|
+
param_overrides = RbsExtended.param_type_override_map(method_definition)
|
|
63
|
+
|
|
56
64
|
match = find_matching_overload(
|
|
57
65
|
overloads,
|
|
58
66
|
arg_types: arg_types,
|
|
59
67
|
self_type: self_type,
|
|
60
68
|
instance_type: instance_type,
|
|
61
69
|
type_vars: type_vars,
|
|
62
|
-
block_required: block_required
|
|
70
|
+
block_required: block_required,
|
|
71
|
+
param_overrides: param_overrides
|
|
63
72
|
)
|
|
64
73
|
return match if match
|
|
65
74
|
return overloads.find { |mt| overload_has_block?(mt) } if block_required
|
|
@@ -76,7 +85,8 @@ module Rigor
|
|
|
76
85
|
private
|
|
77
86
|
|
|
78
87
|
# rubocop:disable Metrics/ParameterLists
|
|
79
|
-
def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required
|
|
88
|
+
def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:,
|
|
89
|
+
param_overrides:)
|
|
80
90
|
overloads.find do |method_type|
|
|
81
91
|
next false if block_required && !OverloadSelector.overload_has_block?(method_type)
|
|
82
92
|
|
|
@@ -85,13 +95,15 @@ module Rigor
|
|
|
85
95
|
arg_types,
|
|
86
96
|
self_type: self_type,
|
|
87
97
|
instance_type: instance_type,
|
|
88
|
-
type_vars: type_vars
|
|
98
|
+
type_vars: type_vars,
|
|
99
|
+
param_overrides: param_overrides
|
|
89
100
|
)
|
|
90
101
|
end
|
|
91
102
|
end
|
|
92
103
|
# rubocop:enable Metrics/ParameterLists
|
|
93
104
|
|
|
94
|
-
|
|
105
|
+
# rubocop:disable Metrics/ParameterLists
|
|
106
|
+
def matches?(method_type, arg_types, self_type:, instance_type:, type_vars:, param_overrides:)
|
|
95
107
|
return false if method_type.respond_to?(:type_params) && rejects_keyword_required?(method_type)
|
|
96
108
|
|
|
97
109
|
fun = method_type.type
|
|
@@ -104,10 +116,12 @@ module Rigor
|
|
|
104
116
|
arg,
|
|
105
117
|
self_type: self_type,
|
|
106
118
|
instance_type: instance_type,
|
|
107
|
-
type_vars: type_vars
|
|
119
|
+
type_vars: type_vars,
|
|
120
|
+
param_overrides: param_overrides
|
|
108
121
|
)
|
|
109
122
|
end
|
|
110
123
|
end
|
|
124
|
+
# rubocop:enable Metrics/ParameterLists
|
|
111
125
|
|
|
112
126
|
# Slice 4 phase 2c does not pass keyword arguments through the
|
|
113
127
|
# call site (caller passes only positional `arg_types`). An
|
|
@@ -152,8 +166,9 @@ module Rigor
|
|
|
152
166
|
head
|
|
153
167
|
end
|
|
154
168
|
|
|
155
|
-
|
|
156
|
-
|
|
169
|
+
# rubocop:disable Metrics/ParameterLists
|
|
170
|
+
def accepts_param?(param, arg, self_type:, instance_type:, type_vars:, param_overrides:)
|
|
171
|
+
param_type = param_overrides[param.name] || RbsTypeTranslator.translate(
|
|
157
172
|
param.type,
|
|
158
173
|
self_type: self_type,
|
|
159
174
|
instance_type: instance_type,
|
|
@@ -162,6 +177,7 @@ module Rigor
|
|
|
162
177
|
result = param_type.accepts(arg, mode: :gradual)
|
|
163
178
|
result.yes? || result.maybe?
|
|
164
179
|
end
|
|
180
|
+
# rubocop:enable Metrics/ParameterLists
|
|
165
181
|
end
|
|
166
182
|
end
|
|
167
183
|
end
|
|
@@ -88,14 +88,25 @@ module Rigor
|
|
|
88
88
|
|
|
89
89
|
# @return [Rigor::Type, nil] the precise element/value type, or
|
|
90
90
|
# `nil` to defer to the next dispatcher tier.
|
|
91
|
+
# Per-carrier dispatch table. Adding a new carrier here
|
|
92
|
+
# is a one-row change; the helper methods stay private.
|
|
93
|
+
# Anonymous Type subclasses are not expected.
|
|
94
|
+
RECEIVER_HANDLERS = {
|
|
95
|
+
Type::Tuple => :dispatch_tuple,
|
|
96
|
+
Type::HashShape => :dispatch_hash_shape,
|
|
97
|
+
Type::Nominal => :dispatch_nominal_size,
|
|
98
|
+
Type::Difference => :dispatch_difference,
|
|
99
|
+
Type::Refined => :dispatch_refined,
|
|
100
|
+
Type::Intersection => :dispatch_intersection
|
|
101
|
+
}.freeze
|
|
102
|
+
private_constant :RECEIVER_HANDLERS
|
|
103
|
+
|
|
91
104
|
def try_dispatch(receiver:, method_name:, args:)
|
|
92
105
|
args ||= []
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
when Type::Difference then dispatch_difference(receiver, method_name, args)
|
|
98
|
-
end
|
|
106
|
+
handler = RECEIVER_HANDLERS[receiver.class]
|
|
107
|
+
return nil unless handler
|
|
108
|
+
|
|
109
|
+
send(handler, receiver, method_name, args)
|
|
99
110
|
end
|
|
100
111
|
|
|
101
112
|
# Tightens `Array#size` / `Array#length` / `String#length` /
|
|
@@ -220,6 +231,124 @@ module Rigor
|
|
|
220
231
|
Type::Combinator.positive_int
|
|
221
232
|
end
|
|
222
233
|
|
|
234
|
+
# Predicate-subset projections over a `Refined[base,
|
|
235
|
+
# predicate]` receiver. Today the catalogue is the
|
|
236
|
+
# String case-normalisation pair: `s.downcase` over a
|
|
237
|
+
# `lowercase-string` receiver folds to the same
|
|
238
|
+
# carrier (already lowercase), and `s.upcase` lifts a
|
|
239
|
+
# `lowercase-string` to `uppercase-string`. Symmetric
|
|
240
|
+
# rules apply with the predicates swapped. Numeric-
|
|
241
|
+
# string idempotence over `#downcase` / `#upcase` is
|
|
242
|
+
# also recognised because a numeric string equals its
|
|
243
|
+
# own case-normalisation.
|
|
244
|
+
#
|
|
245
|
+
# For methods this tier does not have a refinement-
|
|
246
|
+
# specific rule for, projection delegates to
|
|
247
|
+
# `dispatch_nominal_size` so size-returning calls on
|
|
248
|
+
# a `Refined[String, *]` still tighten to
|
|
249
|
+
# `non_negative_int`.
|
|
250
|
+
REFINED_STRING_PROJECTIONS = {
|
|
251
|
+
%i[lowercase downcase] => :refined_self,
|
|
252
|
+
%i[lowercase upcase] => :uppercase_string,
|
|
253
|
+
%i[uppercase upcase] => :refined_self,
|
|
254
|
+
%i[uppercase downcase] => :lowercase_string,
|
|
255
|
+
%i[numeric downcase] => :refined_self,
|
|
256
|
+
%i[numeric upcase] => :refined_self,
|
|
257
|
+
# Digit-only strings are case-invariant; the prefix
|
|
258
|
+
# letters in `0o…` / `0x…` are accepted by the
|
|
259
|
+
# predicate in either case so the predicate-subset
|
|
260
|
+
# is preserved across `#downcase` / `#upcase` even
|
|
261
|
+
# though the value-set element changes.
|
|
262
|
+
%i[decimal_int downcase] => :refined_self,
|
|
263
|
+
%i[decimal_int upcase] => :refined_self,
|
|
264
|
+
%i[octal_int downcase] => :refined_self,
|
|
265
|
+
%i[octal_int upcase] => :refined_self,
|
|
266
|
+
%i[hex_int downcase] => :refined_self,
|
|
267
|
+
%i[hex_int upcase] => :refined_self
|
|
268
|
+
}.freeze
|
|
269
|
+
private_constant :REFINED_STRING_PROJECTIONS
|
|
270
|
+
|
|
271
|
+
def dispatch_refined(refined, method_name, args)
|
|
272
|
+
base = refined.base
|
|
273
|
+
return nil unless base.is_a?(Type::Nominal)
|
|
274
|
+
|
|
275
|
+
if base.class_name == "String" && args.empty?
|
|
276
|
+
precise = refined_string_projection(refined, method_name)
|
|
277
|
+
return precise if precise
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
dispatch_nominal_size(base, method_name, args)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def refined_string_projection(refined, method_name)
|
|
284
|
+
handler = REFINED_STRING_PROJECTIONS[[refined.predicate_id, method_name]]
|
|
285
|
+
return nil unless handler
|
|
286
|
+
|
|
287
|
+
case handler
|
|
288
|
+
when :refined_self then refined
|
|
289
|
+
when :uppercase_string then Type::Combinator.uppercase_string
|
|
290
|
+
when :lowercase_string then Type::Combinator.lowercase_string
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Projects a method call over an `Intersection[M1, …]`
|
|
295
|
+
# receiver by collecting each member's projection and
|
|
296
|
+
# combining the results. The set-theoretic identity is
|
|
297
|
+
# `M(A ∩ B) ⊆ M(A) ∩ M(B)`, so the meet of the per-member
|
|
298
|
+
# projections is sound. Combining is best-effort:
|
|
299
|
+
#
|
|
300
|
+
# - If every result is a `Type::IntegerRange`, return
|
|
301
|
+
# their bounded-integer meet (max of lower bounds, min
|
|
302
|
+
# of upper bounds). This catches the common
|
|
303
|
+
# `(non_empty_string ∩ lowercase_string).size`
|
|
304
|
+
# pattern where one member projects to `positive-int`
|
|
305
|
+
# and the other to `non-negative-int`; the meet is
|
|
306
|
+
# `positive-int`.
|
|
307
|
+
# - Otherwise return the first non-nil result. A richer
|
|
308
|
+
# meet (e.g. of Difference + Refined results when both
|
|
309
|
+
# project) is left for a future slice; the carrier
|
|
310
|
+
# stays sound because every member's projection is
|
|
311
|
+
# already a superset of the true intersection.
|
|
312
|
+
#
|
|
313
|
+
# Returns nil when no member projects, so the caller
|
|
314
|
+
# falls through to the next dispatcher tier.
|
|
315
|
+
def dispatch_intersection(intersection, method_name, args)
|
|
316
|
+
results = intersection.members.filter_map do |member|
|
|
317
|
+
ShapeDispatch.try_dispatch(receiver: member, method_name: method_name, args: args)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
case results.size
|
|
321
|
+
when 0 then nil
|
|
322
|
+
when 1 then results.first
|
|
323
|
+
else combine_intersection_results(results)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def combine_intersection_results(results)
|
|
328
|
+
return narrow_integer_ranges(results) if results.all?(Type::IntegerRange)
|
|
329
|
+
|
|
330
|
+
results.first
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Compute the bounded-integer meet of two or more
|
|
334
|
+
# `IntegerRange` carriers. We compare via the numeric
|
|
335
|
+
# `lower` / `upper` accessors (`-Float::INFINITY` /
|
|
336
|
+
# `Float::INFINITY` for the symbolic ends), then map
|
|
337
|
+
# back to the symbolic-bound representation
|
|
338
|
+
# `IntegerRange.new` expects. The disjoint-meet case
|
|
339
|
+
# cannot arise from sound member-wise projections in
|
|
340
|
+
# v0.0.4 but is guarded defensively to keep the
|
|
341
|
+
# carrier total.
|
|
342
|
+
def narrow_integer_ranges(ranges)
|
|
343
|
+
numeric_low = ranges.map(&:lower).max
|
|
344
|
+
numeric_high = ranges.map(&:upper).min
|
|
345
|
+
return Type::Combinator.bot if numeric_low > numeric_high
|
|
346
|
+
|
|
347
|
+
min = numeric_low == -Float::INFINITY ? Type::IntegerRange::NEG_INFINITY : numeric_low.to_i
|
|
348
|
+
max = numeric_high == Float::INFINITY ? Type::IntegerRange::POS_INFINITY : numeric_high.to_i
|
|
349
|
+
Type::Combinator.integer_range(min, max)
|
|
350
|
+
end
|
|
351
|
+
|
|
223
352
|
def tuple_first(tuple, _method_name, args)
|
|
224
353
|
return nil unless args.empty?
|
|
225
354
|
return Type::Combinator.constant_of(nil) if tuple.elements.empty?
|
|
@@ -6,6 +6,7 @@ require_relative "method_dispatcher/shape_dispatch"
|
|
|
6
6
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
7
7
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
8
8
|
require_relative "method_dispatcher/file_folding"
|
|
9
|
+
require_relative "method_dispatcher/kernel_dispatch"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
module Inference
|
|
@@ -93,7 +94,8 @@ module Rigor
|
|
|
93
94
|
|
|
94
95
|
ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
95
96
|
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)
|
|
97
|
+
FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
98
|
+
KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types)
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
|
+
require_relative "../rbs_extended"
|
|
6
7
|
require_relative "rbs_type_translator"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
@@ -59,10 +60,13 @@ module Rigor
|
|
|
59
60
|
rbs_method = lookup_rbs_method(def_node)
|
|
60
61
|
return types unless rbs_method
|
|
61
62
|
|
|
62
|
-
method_types
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
apply_rbs_overloads(types, slots, rbs_method.method_types) unless rbs_method.method_types.empty?
|
|
64
|
+
# `rigor:v1:param: <name> <refinement>` annotations
|
|
65
|
+
# tighten the bound type for matching slots. Applied
|
|
66
|
+
# after the RBS-overload pass so the override is the
|
|
67
|
+
# authoritative answer regardless of what the RBS
|
|
68
|
+
# signature declared.
|
|
69
|
+
apply_param_overrides(types, slots, rbs_method)
|
|
66
70
|
types
|
|
67
71
|
end
|
|
68
72
|
|
|
@@ -165,6 +169,27 @@ module Rigor
|
|
|
165
169
|
end
|
|
166
170
|
end
|
|
167
171
|
|
|
172
|
+
# Reads the override map off the method's annotations and
|
|
173
|
+
# replaces the binding for any slot whose name appears in
|
|
174
|
+
# the map. Anonymous slots are skipped (no name to match).
|
|
175
|
+
# The override is used verbatim — no `:rest_*` re-wrapping —
|
|
176
|
+
# so authors who tighten a `*rest` parameter to e.g.
|
|
177
|
+
# `non-empty-array[Integer]` describe the parameter binding
|
|
178
|
+
# they actually want, not its element type.
|
|
179
|
+
def apply_param_overrides(types, slots, rbs_method)
|
|
180
|
+
override_map = RbsExtended.param_type_override_map(rbs_method)
|
|
181
|
+
return if override_map.empty?
|
|
182
|
+
|
|
183
|
+
slots.each do |slot|
|
|
184
|
+
next if slot.name.nil?
|
|
185
|
+
|
|
186
|
+
override = override_map[slot.name]
|
|
187
|
+
next if override.nil?
|
|
188
|
+
|
|
189
|
+
types[slot.name] = override
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
168
193
|
def collect_translated_types(method_types, slot)
|
|
169
194
|
rbs_types = method_types.flat_map do |mt|
|
|
170
195
|
t = rbs_type_for_slot(mt.type, slot)
|