rigortype 0.0.2 → 0.0.4
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 +24 -7
- data/data/builtins/ruby_core/array.yml +1470 -0
- data/data/builtins/ruby_core/file.yml +501 -0
- data/data/builtins/ruby_core/hash.yml +936 -0
- data/data/builtins/ruby_core/io.yml +1594 -0
- data/data/builtins/ruby_core/numeric.yml +1809 -0
- data/data/builtins/ruby_core/range.yml +389 -0
- data/data/builtins/ruby_core/set.yml +594 -0
- data/data/builtins/ruby_core/string.yml +1850 -0
- data/data/builtins/ruby_core/time.yml +750 -0
- data/lib/rigor/analysis/check_rules.rb +97 -4
- data/lib/rigor/analysis/runner.rb +4 -0
- data/lib/rigor/builtins/imported_refinements.rb +251 -0
- data/lib/rigor/configuration.rb +6 -1
- data/lib/rigor/inference/acceptance.rb +324 -6
- data/lib/rigor/inference/builtins/array_catalog.rb +46 -0
- data/lib/rigor/inference/builtins/hash_catalog.rb +40 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +90 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +93 -0
- data/lib/rigor/inference/builtins/range_catalog.rb +46 -0
- data/lib/rigor/inference/builtins/set_catalog.rb +54 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +39 -0
- data/lib/rigor/inference/builtins/time_catalog.rb +64 -0
- data/lib/rigor/inference/expression_typer.rb +48 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +670 -16
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +144 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +215 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -7
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +240 -4
- data/lib/rigor/inference/method_dispatcher.rb +28 -21
- data/lib/rigor/inference/method_parameter_binder.rb +29 -4
- data/lib/rigor/inference/narrowing.rb +376 -4
- data/lib/rigor/inference/scope_indexer.rb +10 -2
- data/lib/rigor/inference/statement_evaluator.rb +213 -2
- data/lib/rigor/rbs_extended.rb +230 -15
- data/lib/rigor/scope.rb +14 -0
- data/lib/rigor/type/combinator.rb +159 -1
- data/lib/rigor/type/difference.rb +155 -0
- data/lib/rigor/type/integer_range.rb +137 -0
- data/lib/rigor/type/intersection.rb +135 -0
- data/lib/rigor/type/refined.rb +174 -0
- data/lib/rigor/type.rb +4 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/rbs_extended.rbs +14 -0
- data/sig/rigor/scope.rbs +1 -0
- data/sig/rigor/type.rbs +91 -1
- metadata +25 -1
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../type"
|
|
4
|
+
require_relative "../builtins/numeric_catalog"
|
|
5
|
+
require_relative "../builtins/string_catalog"
|
|
6
|
+
require_relative "../builtins/array_catalog"
|
|
7
|
+
require_relative "../builtins/hash_catalog"
|
|
8
|
+
require_relative "../builtins/range_catalog"
|
|
9
|
+
require_relative "../builtins/set_catalog"
|
|
10
|
+
require_relative "../builtins/time_catalog"
|
|
4
11
|
|
|
5
12
|
module Rigor
|
|
6
13
|
module Inference
|
|
7
14
|
module MethodDispatcher
|
|
8
|
-
# Slice 2 rule book that folds
|
|
9
|
-
# receivers into another `Constant`
|
|
15
|
+
# Slice 2 rule book that folds method calls on `Rigor::Type::Constant`
|
|
16
|
+
# receivers (and unions of them) into another `Constant` (or a small
|
|
17
|
+
# `Union[Constant, …]`) whenever:
|
|
10
18
|
#
|
|
11
|
-
# * the receiver is a recognised scalar literal,
|
|
12
|
-
#
|
|
19
|
+
# * the receiver is a recognised scalar literal, OR a `Union` whose
|
|
20
|
+
# members are all `Constant`,
|
|
21
|
+
# * arguments (zero or one) are likewise `Constant` or `Union[Constant…]`,
|
|
13
22
|
# * the method name is in the curated whitelist for the receiver's class,
|
|
14
23
|
# * the operation cannot accidentally explode the analyzer (we cap
|
|
15
|
-
# string-fold output at `STRING_FOLD_BYTE_LIMIT` bytes
|
|
16
|
-
#
|
|
24
|
+
# string-fold output at `STRING_FOLD_BYTE_LIMIT` bytes, the input
|
|
25
|
+
# cartesian product at `UNION_FOLD_INPUT_LIMIT`, and the deduped
|
|
26
|
+
# output union at `UNION_FOLD_OUTPUT_LIMIT`), and
|
|
27
|
+
# * the actual Ruby invocation does not raise on at least one
|
|
28
|
+
# receiver/argument combination.
|
|
17
29
|
#
|
|
18
30
|
# Anything else returns `nil`, signalling "no rule matched" so the
|
|
19
31
|
# caller (`ExpressionTyper`) falls back to `Dynamic[Top]` and records a
|
|
@@ -21,7 +33,7 @@ module Rigor
|
|
|
21
33
|
# behind this rule book, but the constant-folding semantics defined
|
|
22
34
|
# here MUST NOT regress: any value reachable by literal arithmetic at
|
|
23
35
|
# parse time is meant to be foldable independent of RBS data.
|
|
24
|
-
module ConstantFolding
|
|
36
|
+
module ConstantFolding # rubocop:disable Metrics/ModuleLength
|
|
25
37
|
module_function
|
|
26
38
|
|
|
27
39
|
NUMERIC_BINARY = Set[:+, :-, :*, :/, :%, :<, :<=, :>, :>=, :==, :!=, :<=>].freeze
|
|
@@ -30,25 +42,667 @@ module Rigor
|
|
|
30
42
|
BOOL_BINARY = Set[:&, :|, :^, :==, :!=].freeze
|
|
31
43
|
NIL_BINARY = Set[:==, :!=].freeze
|
|
32
44
|
|
|
45
|
+
# v0.0.3 C — pure unary catalogue. Each method must:
|
|
46
|
+
# - take zero arguments,
|
|
47
|
+
# - have no side effects,
|
|
48
|
+
# - never raise on the type's full domain (or be
|
|
49
|
+
# guarded by `safe?` below),
|
|
50
|
+
# - return a value safe to materialise as a
|
|
51
|
+
# `Constant` (no large strings, no host objects).
|
|
52
|
+
#
|
|
53
|
+
# The catalogue is the prerequisite for aggressive
|
|
54
|
+
# constant folding through user methods: once
|
|
55
|
+
# `Constant[3].odd?` folds to `Constant[true]`, the
|
|
56
|
+
# inter-procedural inference path landed in v0.0.2
|
|
57
|
+
# #5 carries the constant through the body of a
|
|
58
|
+
# user-defined `def is_odd(n) = n.odd?` so
|
|
59
|
+
# `Parity.new.is_odd(3)` types as `Constant[true]`
|
|
60
|
+
# rather than the RBS-widened `bool`.
|
|
61
|
+
INTEGER_UNARY = Set[
|
|
62
|
+
:odd?, :even?, :zero?, :positive?, :negative?,
|
|
63
|
+
:succ, :pred, :next, :abs, :magnitude,
|
|
64
|
+
:bit_length, :to_s, :to_i, :to_int, :to_f,
|
|
65
|
+
:inspect, :hash, :-@, :+@, :~
|
|
66
|
+
].freeze
|
|
67
|
+
FLOAT_UNARY = Set[
|
|
68
|
+
:zero?, :positive?, :negative?,
|
|
69
|
+
:nan?, :finite?, :infinite?,
|
|
70
|
+
:abs, :magnitude, :floor, :ceil, :round, :truncate,
|
|
71
|
+
:to_s, :to_i, :to_int, :to_f,
|
|
72
|
+
:inspect, :hash, :-@, :+@
|
|
73
|
+
].freeze
|
|
74
|
+
STRING_UNARY = Set[
|
|
75
|
+
:upcase, :downcase, :capitalize, :swapcase,
|
|
76
|
+
:reverse, :length, :size, :bytesize,
|
|
77
|
+
:empty?, :strip, :lstrip, :rstrip, :chomp,
|
|
78
|
+
:to_s, :to_str, :to_sym, :intern,
|
|
79
|
+
:inspect, :hash
|
|
80
|
+
].freeze
|
|
81
|
+
SYMBOL_UNARY = Set[
|
|
82
|
+
:to_s, :to_sym, :to_proc, :length, :size,
|
|
83
|
+
:empty?, :upcase, :downcase, :capitalize,
|
|
84
|
+
:swapcase, :inspect, :hash
|
|
85
|
+
].freeze
|
|
86
|
+
BOOL_UNARY = Set[:!, :to_s, :inspect, :hash, :&, :|, :^].freeze
|
|
87
|
+
NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect, :hash].freeze
|
|
88
|
+
|
|
33
89
|
STRING_FOLD_BYTE_LIMIT = 4096
|
|
34
90
|
|
|
35
|
-
#
|
|
91
|
+
# Input cartesian product hard cap. Keeps fold cost bounded even
|
|
92
|
+
# when the receiver and argument are both `Union[Constant…]`.
|
|
93
|
+
# 5 × 5 = 25 inputs is permitted; 6 × 6 = 36 is not. The user-
|
|
94
|
+
# facing payoff (a precise small enum) drops off fast past this
|
|
95
|
+
# range and CRuby method invocation cost adds up.
|
|
96
|
+
UNION_FOLD_INPUT_LIMIT = 32
|
|
97
|
+
|
|
98
|
+
# Output cardinality cap on the deduped result union. A single
|
|
99
|
+
# binary op on a small range can collapse: `[1,2,3] + [2,4,6]`
|
|
100
|
+
# produces 9 raw pairs but only 7 distinct sums. The output cap
|
|
101
|
+
# is what ultimately limits how wide an inferred type gets.
|
|
102
|
+
UNION_FOLD_OUTPUT_LIMIT = 8
|
|
103
|
+
|
|
104
|
+
# @return [Rigor::Type::Constant, Rigor::Type::Union, Rigor::Type::IntegerRange, nil]
|
|
36
105
|
def try_fold(receiver:, method_name:, args:)
|
|
37
|
-
|
|
38
|
-
return nil
|
|
106
|
+
receiver_set = numeric_set_of(receiver)
|
|
107
|
+
return nil unless receiver_set
|
|
108
|
+
|
|
109
|
+
arg_sets = args.map { |a| numeric_set_of(a) }
|
|
110
|
+
return nil if arg_sets.any?(&:nil?)
|
|
111
|
+
|
|
112
|
+
case args.size
|
|
113
|
+
when 0 then try_fold_unary(receiver_set, method_name)
|
|
114
|
+
when 1 then try_fold_binary(receiver_set, method_name, arg_sets.first)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Normalises an input type into one of:
|
|
119
|
+
# - `Array<Object>` for a `Constant` (1-element) or
|
|
120
|
+
# `Union[Constant…]` (n-element) — concrete values to enumerate.
|
|
121
|
+
# - `Type::IntegerRange` — bounded interval.
|
|
122
|
+
# - `nil` — the input shape is not foldable.
|
|
123
|
+
def numeric_set_of(type)
|
|
124
|
+
case type
|
|
125
|
+
when Type::Constant then [type.value]
|
|
126
|
+
when Type::Union
|
|
127
|
+
return nil unless type.members.all?(Type::Constant)
|
|
128
|
+
|
|
129
|
+
type.members.map(&:value)
|
|
130
|
+
when Type::IntegerRange then type
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def try_fold_unary(set, method_name)
|
|
135
|
+
case set
|
|
136
|
+
when Array then try_fold_unary_set(set, method_name)
|
|
137
|
+
when Type::IntegerRange then try_fold_unary_range(set, method_name)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def try_fold_binary(left, method_name, right)
|
|
142
|
+
return try_fold_divmod(left, right) if method_name == :divmod
|
|
143
|
+
|
|
144
|
+
if left.is_a?(Type::IntegerRange) || right.is_a?(Type::IntegerRange)
|
|
145
|
+
try_fold_binary_range(left, method_name, right)
|
|
146
|
+
else
|
|
147
|
+
try_fold_binary_set(left, method_name, right)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# `Integer#divmod` and `Float#divmod` return a 2-element array
|
|
152
|
+
# `[quotient, remainder]`. We project that into
|
|
153
|
+
# `Tuple[Constant[q], Constant[r]]` so downstream rules see
|
|
154
|
+
# the precise element types. Union/range receivers are
|
|
155
|
+
# widened per-position: each tuple slot carries the union of
|
|
156
|
+
# quotients (resp. remainders) over every safe input pair.
|
|
157
|
+
# Range inputs are not yet folded — they bail to nil.
|
|
158
|
+
def try_fold_divmod(left, right)
|
|
159
|
+
pairs = collect_divmod_pairs(left, right)
|
|
160
|
+
return nil unless pairs && !pairs.empty?
|
|
161
|
+
|
|
162
|
+
q_type = build_constant_type(pairs.map(&:first))
|
|
163
|
+
r_type = build_constant_type(pairs.map(&:last))
|
|
164
|
+
return nil unless q_type && r_type
|
|
165
|
+
|
|
166
|
+
Type::Combinator.tuple_of(q_type, r_type)
|
|
167
|
+
end
|
|
39
168
|
|
|
40
|
-
|
|
41
|
-
return nil unless
|
|
42
|
-
return nil
|
|
169
|
+
def collect_divmod_pairs(left, right)
|
|
170
|
+
return nil unless left.is_a?(Array) && right.is_a?(Array)
|
|
171
|
+
return nil if left.size * right.size > UNION_FOLD_INPUT_LIMIT
|
|
43
172
|
|
|
44
|
-
|
|
173
|
+
left.flat_map do |lv|
|
|
174
|
+
right.flat_map { |rv| invoke_divmod(lv, rv) || [] }
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Returns `[[quotient, remainder]]` (single-element array
|
|
179
|
+
# wrapping the tuple) on success; `nil` to signal "skip this
|
|
180
|
+
# pair". The wrapping mirrors `invoke_binary` so we can
|
|
181
|
+
# use `flat_map` and not lose legitimate 0/false elements.
|
|
182
|
+
def invoke_divmod(receiver_value, arg_value)
|
|
183
|
+
return nil unless receiver_value.is_a?(Numeric) && arg_value.is_a?(Numeric)
|
|
184
|
+
|
|
185
|
+
result = receiver_value.divmod(arg_value)
|
|
186
|
+
divmod_result_to_pair(result)
|
|
45
187
|
rescue StandardError
|
|
46
188
|
nil
|
|
47
189
|
end
|
|
48
190
|
|
|
191
|
+
def divmod_result_to_pair(result)
|
|
192
|
+
return nil unless result.is_a?(Array) && result.size == 2
|
|
193
|
+
return nil unless result.all? { |v| foldable_constant_value?(v) }
|
|
194
|
+
|
|
195
|
+
[result]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def try_fold_unary_set(receiver_values, method_name)
|
|
199
|
+
# Type-level allow check on every receiver. If one member's
|
|
200
|
+
# type does not have the method in its allow list (e.g.
|
|
201
|
+
# `Union[String, nil].nil?` — `:nil?` is not in
|
|
202
|
+
# `STRING_UNARY`), bail the fold so the RBS tier answers.
|
|
203
|
+
# Silently dropping the unsafe member would lie about the
|
|
204
|
+
# remaining receivers' behaviour.
|
|
205
|
+
return nil unless receiver_values.all? { |rv| unary_method_allowed?(rv, method_name) }
|
|
206
|
+
|
|
207
|
+
results = receiver_values.flat_map do |rv|
|
|
208
|
+
invoke_unary(rv, method_name) || []
|
|
209
|
+
end
|
|
210
|
+
build_constant_type(results, source: receiver_values)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def try_fold_binary_set(receiver_values, method_name, arg_values)
|
|
214
|
+
return nil if receiver_values.size * arg_values.size > UNION_FOLD_INPUT_LIMIT
|
|
215
|
+
return nil unless receiver_values.all? { |rv| binary_method_allowed?(rv, method_name) }
|
|
216
|
+
|
|
217
|
+
results = receiver_values.flat_map do |rv|
|
|
218
|
+
arg_values.flat_map { |av| invoke_binary(rv, method_name, av) || [] }
|
|
219
|
+
end
|
|
220
|
+
build_constant_type(results, source: receiver_values + arg_values)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def unary_method_allowed?(receiver_value, method_name)
|
|
224
|
+
unary_ops_for(receiver_value).include?(method_name) ||
|
|
225
|
+
catalog_allows?(receiver_value, method_name)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def binary_method_allowed?(receiver_value, method_name)
|
|
229
|
+
ops_for(receiver_value).include?(method_name) ||
|
|
230
|
+
catalog_allows?(receiver_value, method_name)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Builds a Constant or Union[Constant…] from a flat list of
|
|
234
|
+
# Ruby values. When the deduped set exceeds
|
|
235
|
+
# `UNION_FOLD_OUTPUT_LIMIT` and every result is an Integer,
|
|
236
|
+
# widens to the bounding `IntegerRange` instead of returning
|
|
237
|
+
# nil — that is the graceful escape valve for additions over
|
|
238
|
+
# disjoint integer ranges. The `source` array is used only as
|
|
239
|
+
# a hint that the result set's "Integer-ness" was already
|
|
240
|
+
# implied by the inputs (so the widening fallback only fires
|
|
241
|
+
# for arithmetic over integers).
|
|
242
|
+
def build_constant_type(values, source: nil)
|
|
243
|
+
return nil if values.empty?
|
|
244
|
+
|
|
245
|
+
unique = values.uniq
|
|
246
|
+
return collapse_constants(unique) if unique.size <= UNION_FOLD_OUTPUT_LIMIT
|
|
247
|
+
|
|
248
|
+
widen_to_integer_range(unique, source) ||
|
|
249
|
+
(raise_if_strict || nil)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def collapse_constants(values)
|
|
253
|
+
constants = values.map { |v| Type::Combinator.constant_of(v) }
|
|
254
|
+
constants.size == 1 ? constants.first : Type::Combinator.union(*constants)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Widening fallback: when every successful result is an
|
|
258
|
+
# Integer, return the bounding `IntegerRange` rather than
|
|
259
|
+
# losing the answer entirely. The fallback is also gated on
|
|
260
|
+
# the input set being all-integers, so a fold whose results
|
|
261
|
+
# happen to land on integers but whose receivers were Floats
|
|
262
|
+
# does not silently change shape.
|
|
263
|
+
def widen_to_integer_range(values, source)
|
|
264
|
+
return nil unless values.all?(Integer)
|
|
265
|
+
return nil if source && !source.all? { |v| v.is_a?(Integer) || v.is_a?(Type::IntegerRange) }
|
|
266
|
+
|
|
267
|
+
Type::Combinator.integer_range(values.min, values.max)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Reserved hook: present so future `:strict` modes can raise
|
|
271
|
+
# rather than silently returning nil. Today it always returns
|
|
272
|
+
# nil so behaviour is unchanged.
|
|
273
|
+
def raise_if_strict
|
|
274
|
+
nil
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# ----------------------------------------------------------------
|
|
278
|
+
# IntegerRange arithmetic and comparison.
|
|
279
|
+
# ----------------------------------------------------------------
|
|
280
|
+
|
|
281
|
+
RANGE_ADDITIVE = Set[:+, :-].freeze
|
|
282
|
+
RANGE_COMPARISON = Set[:<, :<=, :>, :>=, :==, :!=].freeze
|
|
283
|
+
|
|
284
|
+
# Per-operator dispatch table for binary range ops. Each
|
|
285
|
+
# value is a method symbol on `ConstantFolding` taking
|
|
286
|
+
# `(left, right)` and returning a `Type` or `nil`.
|
|
287
|
+
BINARY_RANGE_HANDLERS = {
|
|
288
|
+
:* => :range_multiply,
|
|
289
|
+
:/ => :range_divide,
|
|
290
|
+
:% => :range_modulo
|
|
291
|
+
}.freeze
|
|
292
|
+
private_constant :BINARY_RANGE_HANDLERS
|
|
293
|
+
|
|
294
|
+
def try_fold_binary_range(left, method_name, right)
|
|
295
|
+
l = ensure_integer_range(left)
|
|
296
|
+
r = ensure_integer_range(right)
|
|
297
|
+
return nil unless l && r
|
|
298
|
+
return range_additive(l, method_name, r) if RANGE_ADDITIVE.include?(method_name)
|
|
299
|
+
return range_comparison(l, method_name, r) if RANGE_COMPARISON.include?(method_name)
|
|
300
|
+
return send(BINARY_RANGE_HANDLERS[method_name], l, r) if BINARY_RANGE_HANDLERS.key?(method_name)
|
|
301
|
+
|
|
302
|
+
nil
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# Promotes an array-of-values input to an `IntegerRange` when
|
|
306
|
+
# every value is an `Integer`. Used so a mixed `Constant +
|
|
307
|
+
# IntegerRange` call can be reduced to range × range
|
|
308
|
+
# arithmetic. Returns `nil` for non-Integer arrays so a
|
|
309
|
+
# `Constant[Float]` does not silently degrade.
|
|
310
|
+
def ensure_integer_range(operand)
|
|
311
|
+
case operand
|
|
312
|
+
when Type::IntegerRange then operand
|
|
313
|
+
when Array
|
|
314
|
+
return nil unless operand.all?(Integer)
|
|
315
|
+
|
|
316
|
+
Type::Combinator.integer_range(operand.min, operand.max)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def range_additive(left, method_name, right)
|
|
321
|
+
lower, upper =
|
|
322
|
+
case method_name
|
|
323
|
+
when :+ then [left.lower + right.lower, left.upper + right.upper]
|
|
324
|
+
when :- then [left.lower - right.upper, left.upper - right.lower]
|
|
325
|
+
end
|
|
326
|
+
build_integer_range(lower, upper)
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Range × Range. Computes the four corner products with
|
|
330
|
+
# `safe_mul` so that `0 × ±∞` is treated as 0 rather than
|
|
331
|
+
# NaN — that captures the algebraic truth that the actual
|
|
332
|
+
# range elements are integers, never literal infinity.
|
|
333
|
+
def range_multiply(left, right)
|
|
334
|
+
corners = [
|
|
335
|
+
safe_mul(left.lower, right.lower),
|
|
336
|
+
safe_mul(left.lower, right.upper),
|
|
337
|
+
safe_mul(left.upper, right.lower),
|
|
338
|
+
safe_mul(left.upper, right.upper)
|
|
339
|
+
]
|
|
340
|
+
build_integer_range(corners.min, corners.max)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# 0 dominates: 0 × anything (including ±∞) is 0. Without this
|
|
344
|
+
# special case Ruby's `0 * Float::INFINITY` is `NaN`, which
|
|
345
|
+
# would corrupt `min`/`max`.
|
|
346
|
+
def safe_mul(left, right)
|
|
347
|
+
return 0 if left.zero? || right.zero?
|
|
348
|
+
|
|
349
|
+
left * right
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Range ÷ Range using Ruby's integer floor division. If the
|
|
353
|
+
# right range covers 0 the operation may raise
|
|
354
|
+
# `ZeroDivisionError`, so the fold bails (caller falls back
|
|
355
|
+
# to RBS-widened `Integer`). When both inputs are finite we
|
|
356
|
+
# compute the four corner quotients; the universal-on-one-side
|
|
357
|
+
# case is handled by treating ±∞ ÷ n as ±∞ and n ÷ ±∞ as 0.
|
|
358
|
+
def range_divide(left, right)
|
|
359
|
+
return nil if right.covers?(0)
|
|
360
|
+
|
|
361
|
+
corners = [
|
|
362
|
+
safe_div(left.lower, right.lower),
|
|
363
|
+
safe_div(left.lower, right.upper),
|
|
364
|
+
safe_div(left.upper, right.lower),
|
|
365
|
+
safe_div(left.upper, right.upper)
|
|
366
|
+
]
|
|
367
|
+
build_integer_range(corners.min, corners.max)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def safe_div(numer, denom)
|
|
371
|
+
return 0 if numer.zero?
|
|
372
|
+
return numer.positive? ^ denom.negative? ? Float::INFINITY : -Float::INFINITY if denom.zero?
|
|
373
|
+
return 0 if denom.infinite?
|
|
374
|
+
|
|
375
|
+
if numer.infinite?
|
|
376
|
+
return numer.positive? ^ denom.negative? ? Float::INFINITY : -Float::INFINITY
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
numer.to_i.div(denom.to_i).to_f
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Range % Range. Only the `(any range) % (positive constant n)`
|
|
383
|
+
# and `(any range) % (negative constant n)` cases are folded
|
|
384
|
+
# precisely — the former narrows to `int<0, n-1>`, the latter
|
|
385
|
+
# to `int<n+1, 0>`. Other shapes fall back to nil.
|
|
386
|
+
def range_modulo(_left, right)
|
|
387
|
+
return nil unless right.finite? && right.min == right.max
|
|
388
|
+
|
|
389
|
+
divisor = right.min
|
|
390
|
+
return nil if divisor.zero?
|
|
391
|
+
|
|
392
|
+
if divisor.positive?
|
|
393
|
+
build_integer_range(0, divisor - 1)
|
|
394
|
+
else
|
|
395
|
+
build_integer_range(divisor + 1, 0)
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Builds an `IntegerRange` from numeric `lower`/`upper`
|
|
400
|
+
# endpoints. Collapses single-point finite ranges to a
|
|
401
|
+
# `Constant` so downstream rules (which prefer the more
|
|
402
|
+
# specific carrier) see the most precise result.
|
|
403
|
+
def build_integer_range(lower, upper)
|
|
404
|
+
min = lower == -Float::INFINITY ? Type::IntegerRange::NEG_INFINITY : Integer(lower)
|
|
405
|
+
max = upper == Float::INFINITY ? Type::IntegerRange::POS_INFINITY : Integer(upper)
|
|
406
|
+
if min.is_a?(Integer) && max.is_a?(Integer) && min == max
|
|
407
|
+
Type::Combinator.constant_of(min)
|
|
408
|
+
else
|
|
409
|
+
Type::Combinator.integer_range(min, max)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def range_comparison(left, method_name, right)
|
|
414
|
+
decision = decide_range_comparison(left, method_name, right)
|
|
415
|
+
case decision
|
|
416
|
+
when :always_true then Type::Combinator.constant_of(true)
|
|
417
|
+
when :always_false then Type::Combinator.constant_of(false)
|
|
418
|
+
when :both then bool_union
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def bool_union
|
|
423
|
+
Type::Combinator.union(
|
|
424
|
+
Type::Combinator.constant_of(true),
|
|
425
|
+
Type::Combinator.constant_of(false)
|
|
426
|
+
)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
BOOL_INVERSE = {
|
|
430
|
+
always_true: :always_false,
|
|
431
|
+
always_false: :always_true,
|
|
432
|
+
both: :both
|
|
433
|
+
}.freeze
|
|
434
|
+
private_constant :BOOL_INVERSE
|
|
435
|
+
|
|
436
|
+
def decide_range_comparison(left, method_name, right)
|
|
437
|
+
case method_name
|
|
438
|
+
when :< then decide_lt(left, right)
|
|
439
|
+
when :<= then decide_le(left, right)
|
|
440
|
+
when :> then decide_lt(right, left)
|
|
441
|
+
when :>= then decide_le(right, left)
|
|
442
|
+
when :== then decide_eq(left, right)
|
|
443
|
+
when :!= then BOOL_INVERSE[decide_eq(left, right)]
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def decide_lt(left, right)
|
|
448
|
+
return :always_true if left.upper < right.lower
|
|
449
|
+
return :always_false if left.lower >= right.upper
|
|
450
|
+
|
|
451
|
+
:both
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def decide_le(left, right)
|
|
455
|
+
return :always_true if left.upper <= right.lower
|
|
456
|
+
return :always_false if left.lower > right.upper
|
|
457
|
+
|
|
458
|
+
:both
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def decide_eq(left, right)
|
|
462
|
+
return :always_true if left.finite? && right.finite? && left.min == left.max && left == right
|
|
463
|
+
return :always_false if left.upper < right.lower || right.upper < left.lower
|
|
464
|
+
|
|
465
|
+
:both
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# ----------------------------------------------------------------
|
|
469
|
+
# IntegerRange unary folds.
|
|
470
|
+
# ----------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
RANGE_UNARY_PREDICATES = Set[:zero?, :positive?, :negative?].freeze
|
|
473
|
+
RANGE_UNARY_SHIFTS = Set[:succ, :next, :pred].freeze
|
|
474
|
+
RANGE_UNARY_PARITY = Set[:even?, :odd?].freeze
|
|
475
|
+
|
|
476
|
+
# `(method_name) -> handler symbol` for the unary range
|
|
477
|
+
# surface that does not need extra context. The grouped
|
|
478
|
+
# categories (predicates / shifts / parity) stay separate
|
|
479
|
+
# because they share dispatch logic.
|
|
480
|
+
UNARY_RANGE_DIRECT = {
|
|
481
|
+
abs: :range_unary_abs,
|
|
482
|
+
magnitude: :range_unary_abs,
|
|
483
|
+
"-@": :range_unary_negate,
|
|
484
|
+
"+@": :range_unary_identity,
|
|
485
|
+
bit_length: :range_unary_bit_length
|
|
486
|
+
}.freeze
|
|
487
|
+
private_constant :UNARY_RANGE_DIRECT
|
|
488
|
+
|
|
489
|
+
def try_fold_unary_range(range, method_name)
|
|
490
|
+
return range_unary_predicate(range, method_name) if RANGE_UNARY_PREDICATES.include?(method_name)
|
|
491
|
+
return range_unary_shift(range, method_name) if RANGE_UNARY_SHIFTS.include?(method_name)
|
|
492
|
+
return range_unary_parity(range, method_name) if RANGE_UNARY_PARITY.include?(method_name)
|
|
493
|
+
return send(UNARY_RANGE_DIRECT[method_name], range) if UNARY_RANGE_DIRECT.key?(method_name)
|
|
494
|
+
|
|
495
|
+
nil
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def range_unary_negate(range)
|
|
499
|
+
build_integer_range(-range.upper, -range.lower)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def range_unary_identity(range)
|
|
503
|
+
range
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# `even?`/`odd?` on a single-point range collapses to an
|
|
507
|
+
# exact `Constant[bool]`. Any range spanning ≥ 2 integers
|
|
508
|
+
# contains both an even and an odd value, so the result is
|
|
509
|
+
# `Union[true, false]`.
|
|
510
|
+
def range_unary_parity(range, method_name)
|
|
511
|
+
if range.finite? && range.min == range.max
|
|
512
|
+
value = range.min.public_send(method_name)
|
|
513
|
+
Type::Combinator.constant_of(value)
|
|
514
|
+
else
|
|
515
|
+
bool_union
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# Integer#bit_length is non-negative and bounded by the
|
|
520
|
+
# bit_length of the wider endpoint. For half-open ranges the
|
|
521
|
+
# upper bound is unknown (any large integer is reachable), so
|
|
522
|
+
# we widen to non_negative_int. Negative endpoints map via
|
|
523
|
+
# `~n` semantics; using the magnitude is a safe upper bound.
|
|
524
|
+
def range_unary_bit_length(range)
|
|
525
|
+
return Type::Combinator.non_negative_int unless range.finite?
|
|
526
|
+
|
|
527
|
+
width = [range.min.bit_length, range.max.bit_length].max
|
|
528
|
+
build_integer_range(0, width)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def range_unary_predicate(range, method_name)
|
|
532
|
+
decision =
|
|
533
|
+
case method_name
|
|
534
|
+
when :zero? then decide_zero(range)
|
|
535
|
+
when :positive? then decide_positive(range)
|
|
536
|
+
when :negative? then decide_negative(range)
|
|
537
|
+
end
|
|
538
|
+
range_comparison_result(decision)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def range_comparison_result(decision)
|
|
542
|
+
case decision
|
|
543
|
+
when :always_true then Type::Combinator.constant_of(true)
|
|
544
|
+
when :always_false then Type::Combinator.constant_of(false)
|
|
545
|
+
when :both then bool_union
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def decide_zero(range)
|
|
550
|
+
return :always_true if range.finite? && range.min.zero? && range.max.zero?
|
|
551
|
+
return :always_false unless range.covers?(0)
|
|
552
|
+
|
|
553
|
+
:both
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def decide_positive(range)
|
|
557
|
+
return :always_true if range.lower.positive?
|
|
558
|
+
return :always_false if range.upper <= 0
|
|
559
|
+
|
|
560
|
+
:both
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def decide_negative(range)
|
|
564
|
+
return :always_true if range.upper.negative?
|
|
565
|
+
return :always_false if range.lower >= 0
|
|
566
|
+
|
|
567
|
+
:both
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def range_unary_shift(range, method_name)
|
|
571
|
+
delta = method_name == :pred ? -1 : 1
|
|
572
|
+
build_integer_range(range.lower + delta, range.upper + delta)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def range_unary_abs(range)
|
|
576
|
+
if range.lower >= 0
|
|
577
|
+
range
|
|
578
|
+
elsif range.upper <= 0
|
|
579
|
+
build_integer_range(-range.upper, -range.lower)
|
|
580
|
+
else
|
|
581
|
+
magnitude = [range.lower.abs, range.upper.abs].max
|
|
582
|
+
build_integer_range(0, magnitude)
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# ----------------------------------------------------------------
|
|
587
|
+
|
|
588
|
+
# Returns `[value]` on success, `nil` to signal "skip this pair".
|
|
589
|
+
# The 1-element-array shape lets callers distinguish a successful
|
|
590
|
+
# `false`/`nil` fold from a skipped pair when chaining via
|
|
591
|
+
# `flat_map`.
|
|
592
|
+
def invoke_binary(receiver_value, method_name, arg_value)
|
|
593
|
+
return nil unless safe?(receiver_value, method_name, arg_value)
|
|
594
|
+
|
|
595
|
+
result = receiver_value.public_send(method_name, arg_value)
|
|
596
|
+
foldable_constant_value?(result) ? [result] : nil
|
|
597
|
+
rescue StandardError
|
|
598
|
+
nil
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
# Returns `[value]` on success, `nil` to signal "skip". See
|
|
602
|
+
# `invoke_binary` for why we wrap.
|
|
603
|
+
def invoke_unary(receiver_value, method_name)
|
|
604
|
+
return nil unless unary_safe?(receiver_value, method_name)
|
|
605
|
+
return nil if string_unary_blow_up?(receiver_value, method_name)
|
|
606
|
+
|
|
607
|
+
result = receiver_value.public_send(method_name)
|
|
608
|
+
foldable_constant_value?(result) ? [result] : nil
|
|
609
|
+
rescue StandardError
|
|
610
|
+
nil
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def unary_safe?(receiver_value, method_name)
|
|
614
|
+
return true if unary_ops_for(receiver_value).include?(method_name)
|
|
615
|
+
|
|
616
|
+
catalog_allows?(receiver_value, method_name)
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Consults the offline numeric catalog (data/builtins/ruby_core/
|
|
620
|
+
# numeric.yml) as a superset of the hand-rolled unary/binary
|
|
621
|
+
# allow lists. The catalog's `leaf` / `trivial` /
|
|
622
|
+
# `leaf_when_numeric` entries promise the underlying CRuby
|
|
623
|
+
# implementation does not call back into user-redefinable
|
|
624
|
+
# Ruby methods, so executing them on a literal Integer/Float
|
|
625
|
+
# is safe regardless of monkey-patching.
|
|
626
|
+
def catalog_allows?(receiver_value, method_name)
|
|
627
|
+
catalog, class_name = catalog_for(receiver_value)
|
|
628
|
+
return false unless catalog
|
|
629
|
+
|
|
630
|
+
catalog.safe_for_folding?(class_name, method_name)
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# `(catalog, class_name)` per receiver class. The class_name
|
|
634
|
+
# is what each catalog's RBS-rooted entries are keyed by.
|
|
635
|
+
# `catalog_for` walks this table in declaration order so
|
|
636
|
+
# subclasses (Symbol < String) hit their dedicated entry
|
|
637
|
+
# before any base-class fallback would, and adding a new
|
|
638
|
+
# class is a one-line addition rather than another `when`
|
|
639
|
+
# arm on a growing case statement.
|
|
640
|
+
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"]]
|
|
650
|
+
].freeze
|
|
651
|
+
private_constant :CATALOG_BY_CLASS
|
|
652
|
+
|
|
653
|
+
# Returns `[catalog, class_name]` for receivers we have a
|
|
654
|
+
# catalog for; nil otherwise.
|
|
655
|
+
def catalog_for(receiver_value)
|
|
656
|
+
CATALOG_BY_CLASS.each do |klass, entry|
|
|
657
|
+
return entry if receiver_value.is_a?(klass)
|
|
658
|
+
end
|
|
659
|
+
nil
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def unary_ops_for(receiver_value)
|
|
663
|
+
case receiver_value
|
|
664
|
+
when Integer then INTEGER_UNARY
|
|
665
|
+
when Float then FLOAT_UNARY
|
|
666
|
+
when String then STRING_UNARY
|
|
667
|
+
when Symbol then SYMBOL_UNARY
|
|
668
|
+
when true, false then BOOL_UNARY
|
|
669
|
+
when nil then NIL_UNARY
|
|
670
|
+
else Set.new
|
|
671
|
+
end
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# `String#reverse` / `#swapcase` etc. produce a
|
|
675
|
+
# string the same size as the receiver; only the
|
|
676
|
+
# already-handled binary `:+` / `:*` paths can
|
|
677
|
+
# explode the output. No unary string method
|
|
678
|
+
# currently in the catalogue grows beyond the input
|
|
679
|
+
# size, so this hook is a no-op today — kept as a
|
|
680
|
+
# placeholder so future additions (e.g. `:succ` on
|
|
681
|
+
# very long strings) can be guarded without
|
|
682
|
+
# restructuring.
|
|
683
|
+
def string_unary_blow_up?(_receiver_value, _method_name)
|
|
684
|
+
false
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Scalar / String / Symbol values fold; everything
|
|
688
|
+
# else (Array, Hash, Proc, Range, ...) is held back
|
|
689
|
+
# because `Type::Constant` does not model those
|
|
690
|
+
# carriers and surfacing one would mis-type
|
|
691
|
+
# downstream calls. `Range`, `Array`, and friends
|
|
692
|
+
# have their own shape carriers; this method picks
|
|
693
|
+
# the conservative envelope of "values that already
|
|
694
|
+
# round-trip through `Type::Combinator.constant_of`".
|
|
695
|
+
def foldable_constant_value?(value)
|
|
696
|
+
case value
|
|
697
|
+
when Integer, Float, String, Symbol, true, false, nil then true
|
|
698
|
+
else false
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
|
|
49
702
|
def safe?(receiver_value, method_name, arg_value)
|
|
50
|
-
|
|
51
|
-
|
|
703
|
+
allowed = ops_for(receiver_value).include?(method_name) ||
|
|
704
|
+
catalog_allows?(receiver_value, method_name)
|
|
705
|
+
return false unless allowed
|
|
52
706
|
return false if integer_division_by_zero?(receiver_value, method_name, arg_value)
|
|
53
707
|
return false if string_blow_up?(receiver_value, method_name, arg_value)
|
|
54
708
|
|