rigortype 0.0.5 → 0.0.7
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/data/builtins/ruby_core/pathname.yml +1067 -0
- data/lib/rigor/analysis/check_rules.rb +38 -41
- data/lib/rigor/builtins/imported_refinements.rb +93 -3
- data/lib/rigor/inference/builtins/pathname_catalog.rb +35 -0
- data/lib/rigor/inference/expression_typer.rb +310 -25
- data/lib/rigor/inference/method_dispatcher/block_folding.rb +322 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +325 -8
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +45 -4
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -9
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +409 -0
- data/lib/rigor/inference/method_dispatcher.rb +88 -18
- data/lib/rigor/inference/method_parameter_binder.rb +3 -5
- data/lib/rigor/inference/narrowing.rb +38 -6
- data/lib/rigor/inference/statement_evaluator.rb +5 -7
- data/lib/rigor/reflection.rb +203 -0
- data/lib/rigor/type/combinator.rb +244 -1
- data/lib/rigor/type/constant.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/sig/rigor/reflection.rbs +17 -0
- data/sig/rigor/type.rbs +5 -0
- metadata +6 -1
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Block-shaped fold dispatch (v0.0.6 phase 1).
|
|
9
|
+
#
|
|
10
|
+
# Sits ahead of `RbsDispatch.try_dispatch` and folds a small
|
|
11
|
+
# set of block-taking Enumerable methods when the inferred
|
|
12
|
+
# block return type is a Ruby-truthy or Ruby-falsey
|
|
13
|
+
# `Type::Constant`. The block-parameter typing for the same
|
|
14
|
+
# methods continues to be answered by `IteratorDispatch`
|
|
15
|
+
# (this module concerns the *return* of the call, not the
|
|
16
|
+
# block-param binding).
|
|
17
|
+
#
|
|
18
|
+
# The methods covered fall in two families:
|
|
19
|
+
#
|
|
20
|
+
# - **Filter-shaped** (`select` / `filter` / `reject` /
|
|
21
|
+
# `take_while` / `drop_while`): the block's truthiness
|
|
22
|
+
# selects the all-or-nothing endpoints — either the
|
|
23
|
+
# receiver's full shape (when every element is kept) or
|
|
24
|
+
# the empty-tuple carrier (when every element is dropped).
|
|
25
|
+
# - **Predicate-shaped** (`all?` / `any?` / `none?`): the
|
|
26
|
+
# block's truthiness combined with the receiver's
|
|
27
|
+
# emptiness collapses the call to a `Constant[bool]` in
|
|
28
|
+
# the cases where Ruby's actual semantics make it
|
|
29
|
+
# unconditional. Non-empty + truthy `any?` is `true`;
|
|
30
|
+
# non-empty + falsey `all?` is `false`; the empty-receiver
|
|
31
|
+
# "vacuous" answers (`[].all? { false } == true`,
|
|
32
|
+
# `[].any? { true } == false`, `[].none? { true } == true`)
|
|
33
|
+
# are likewise honoured.
|
|
34
|
+
#
|
|
35
|
+
# The dispatcher returns `nil` for any case that cannot be
|
|
36
|
+
# decided from the (receiver-shape, method, block-truthiness)
|
|
37
|
+
# tuple — element-wise block re-evaluation against
|
|
38
|
+
# `Constant<Array>` receivers (the `map` / `filter_map` /
|
|
39
|
+
# `flat_map` precision tier) is reserved for a later slice.
|
|
40
|
+
module BlockFolding # rubocop:disable Metrics/ModuleLength
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
FILTER_KEEP_ON_TRUTHY = Set[:select, :filter, :take_while].freeze
|
|
44
|
+
FILTER_KEEP_ON_FALSEY = Set[:reject, :drop_while].freeze
|
|
45
|
+
|
|
46
|
+
PREDICATE_METHODS = Set[:all?, :any?, :none?].freeze
|
|
47
|
+
|
|
48
|
+
# Methods whose answer is `nil` when the block always
|
|
49
|
+
# returns Ruby-falsey — `find` / `detect` short-circuit
|
|
50
|
+
# to nil when nothing matches, `find_index` / `index`
|
|
51
|
+
# likewise. These methods only fold on the falsey side
|
|
52
|
+
# for now; the truthy-block side requires per-position
|
|
53
|
+
# analysis (the index of the first kept element, or the
|
|
54
|
+
# element itself, depend on the receiver's shape and on
|
|
55
|
+
# which positions actually evaluate to truthy).
|
|
56
|
+
FALSEY_BLOCK_NIL_METHODS = Set[:find, :detect, :find_index, :index].freeze
|
|
57
|
+
|
|
58
|
+
# Block-taking `count` returns the number of elements
|
|
59
|
+
# for which the block is truthy. With a Constant-falsey
|
|
60
|
+
# block the answer is unconditionally `Constant[0]`;
|
|
61
|
+
# with a Constant-truthy block on a finitely-sized
|
|
62
|
+
# receiver it is `Constant[size]`.
|
|
63
|
+
COUNT_METHOD = :count
|
|
64
|
+
|
|
65
|
+
# @param receiver [Rigor::Type, nil]
|
|
66
|
+
# @param method_name [Symbol]
|
|
67
|
+
# @param args [Array<Rigor::Type>]
|
|
68
|
+
# @param block_type [Rigor::Type, nil] inferred return type of
|
|
69
|
+
# the call's block. `nil` means "no block at the call site"
|
|
70
|
+
# and disqualifies every rule here.
|
|
71
|
+
# @return [Rigor::Type, nil]
|
|
72
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
73
|
+
def try_fold(receiver:, method_name:, args:, block_type:)
|
|
74
|
+
return nil if receiver.nil? || block_type.nil?
|
|
75
|
+
|
|
76
|
+
truthiness = constant_truthiness(block_type)
|
|
77
|
+
return nil if truthiness.nil?
|
|
78
|
+
|
|
79
|
+
if PREDICATE_METHODS.include?(method_name)
|
|
80
|
+
fold_predicate(receiver, method_name, truthiness)
|
|
81
|
+
elsif filter_method?(method_name)
|
|
82
|
+
fold_filter(receiver, method_name, truthiness)
|
|
83
|
+
elsif FALSEY_BLOCK_NIL_METHODS.include?(method_name)
|
|
84
|
+
fold_falsey_nil_short_circuit(method_name, truthiness, args)
|
|
85
|
+
elsif method_name == COUNT_METHOD
|
|
86
|
+
fold_count(receiver, truthiness, args)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
90
|
+
|
|
91
|
+
def filter_method?(method_name)
|
|
92
|
+
FILTER_KEEP_ON_TRUTHY.include?(method_name) ||
|
|
93
|
+
FILTER_KEEP_ON_FALSEY.include?(method_name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Maps the block return type to `:truthy`, `:falsey`, or
|
|
97
|
+
# `nil` (inconclusive). Only `Type::Constant` answers
|
|
98
|
+
# decisively — `Union[true, false]`, `Nominal[…]`, or
|
|
99
|
+
# `Dynamic[T]` keep the dispatcher silent so the RBS
|
|
100
|
+
# tier still owns the call.
|
|
101
|
+
def constant_truthiness(block_type)
|
|
102
|
+
return nil unless block_type.is_a?(Type::Constant)
|
|
103
|
+
|
|
104
|
+
block_type.value ? :truthy : :falsey
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Filter-shaped methods collapse to either the receiver
|
|
108
|
+
# (every element kept) or the empty tuple (every element
|
|
109
|
+
# dropped). Tuple-shaped receivers widen to
|
|
110
|
+
# `Array[union of elements]` on the all-kept side because
|
|
111
|
+
# we cannot prove WHICH positional subset survives —
|
|
112
|
+
# Tuple's per-position semantics do not carry over to a
|
|
113
|
+
# filtered Array.
|
|
114
|
+
def fold_filter(receiver, method_name, truthiness)
|
|
115
|
+
return nil unless filter_receiver_known?(receiver)
|
|
116
|
+
|
|
117
|
+
keep_all = filter_keeps_all?(method_name, truthiness)
|
|
118
|
+
keep_all ? receiver_as_kept_array(receiver) : Type::Combinator.tuple_of
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def filter_keeps_all?(method_name, truthiness)
|
|
122
|
+
(FILTER_KEEP_ON_TRUTHY.include?(method_name) && truthiness == :truthy) ||
|
|
123
|
+
(FILTER_KEEP_ON_FALSEY.include?(method_name) && truthiness == :falsey)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def receiver_as_kept_array(receiver)
|
|
127
|
+
case receiver
|
|
128
|
+
when Type::Tuple then tuple_to_array(receiver)
|
|
129
|
+
else receiver
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def tuple_to_array(tuple)
|
|
134
|
+
return Type::Combinator.tuple_of if tuple.elements.empty?
|
|
135
|
+
return Type::Combinator.nominal_of("Array", type_args: [tuple.elements.first]) if tuple.elements.size == 1
|
|
136
|
+
|
|
137
|
+
element = Type::Combinator.union(*tuple.elements)
|
|
138
|
+
Type::Combinator.nominal_of("Array", type_args: [element])
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Predicate folds. The decision table mirrors Ruby's
|
|
142
|
+
# actual semantics on `Enumerable#all?` / `#any?` /
|
|
143
|
+
# `#none?` — see the table at the top of the module.
|
|
144
|
+
def fold_predicate(receiver, method_name, truthiness)
|
|
145
|
+
emptiness = receiver_emptiness(receiver)
|
|
146
|
+
decision = predicate_decision(method_name, truthiness, emptiness)
|
|
147
|
+
return nil if decision.nil?
|
|
148
|
+
|
|
149
|
+
case decision
|
|
150
|
+
when :always_true then Type::Combinator.constant_of(true)
|
|
151
|
+
when :always_false then Type::Combinator.constant_of(false)
|
|
152
|
+
when :bool then bool_union
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @return [:always_true, :always_false, :bool, nil]
|
|
157
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
158
|
+
def predicate_decision(method_name, truthiness, emptiness)
|
|
159
|
+
case method_name
|
|
160
|
+
when :all?
|
|
161
|
+
return :always_true if truthiness == :truthy
|
|
162
|
+
return :always_true if emptiness == :empty
|
|
163
|
+
return :always_false if emptiness == :non_empty
|
|
164
|
+
|
|
165
|
+
:bool
|
|
166
|
+
when :any?
|
|
167
|
+
return :always_false if truthiness == :falsey
|
|
168
|
+
return :always_true if emptiness == :non_empty
|
|
169
|
+
return :always_false if emptiness == :empty
|
|
170
|
+
|
|
171
|
+
:bool
|
|
172
|
+
when :none?
|
|
173
|
+
return :always_true if truthiness == :falsey
|
|
174
|
+
return :always_false if emptiness == :non_empty
|
|
175
|
+
return :always_true if emptiness == :empty
|
|
176
|
+
|
|
177
|
+
:bool
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
181
|
+
|
|
182
|
+
def bool_union
|
|
183
|
+
Type::Combinator.union(
|
|
184
|
+
Type::Combinator.constant_of(true),
|
|
185
|
+
Type::Combinator.constant_of(false)
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# @return [:empty, :non_empty, :unknown]
|
|
190
|
+
def receiver_emptiness(receiver)
|
|
191
|
+
case receiver
|
|
192
|
+
when Type::Tuple
|
|
193
|
+
receiver.elements.empty? ? :empty : :non_empty
|
|
194
|
+
when Type::HashShape
|
|
195
|
+
receiver.pairs.empty? ? :empty : :non_empty
|
|
196
|
+
when Type::Constant
|
|
197
|
+
constant_emptiness(receiver.value)
|
|
198
|
+
when Type::Difference
|
|
199
|
+
difference_emptiness(receiver)
|
|
200
|
+
else
|
|
201
|
+
:unknown
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def constant_emptiness(value)
|
|
206
|
+
# Only `Range` constants reach these folds: `Type::Constant`
|
|
207
|
+
# rejects Array / Hash literals (they become `Tuple` /
|
|
208
|
+
# `HashShape` carriers), and the remaining scalar
|
|
209
|
+
# constants (Integer / Float / Symbol / String / …)
|
|
210
|
+
# are not Enumerable receivers for the filter or
|
|
211
|
+
# predicate methods folded here.
|
|
212
|
+
return range_emptiness(value) if value.is_a?(Range)
|
|
213
|
+
|
|
214
|
+
:unknown
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def range_emptiness(range)
|
|
218
|
+
beg = range.begin
|
|
219
|
+
en = range.end
|
|
220
|
+
return :unknown unless beg.is_a?(Numeric) && en.is_a?(Numeric)
|
|
221
|
+
|
|
222
|
+
if range.exclude_end?
|
|
223
|
+
beg < en ? :non_empty : :empty
|
|
224
|
+
else
|
|
225
|
+
beg <= en ? :non_empty : :empty
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# `non-empty-array[T]` is encoded as
|
|
230
|
+
# `Difference[Array[T], Tuple[]]` — the imported built-in
|
|
231
|
+
# carrier for non-emptiness. Recognising it here lets
|
|
232
|
+
# `arr.any? { true }` fold to `Constant[true]` for
|
|
233
|
+
# callers who threaded the non-emptiness through their
|
|
234
|
+
# type signature.
|
|
235
|
+
def difference_emptiness(diff)
|
|
236
|
+
base = diff.base
|
|
237
|
+
removed = diff.removed
|
|
238
|
+
return :unknown unless removed.is_a?(Type::Tuple) && removed.elements.empty?
|
|
239
|
+
return :non_empty if array_or_hash_nominal?(base)
|
|
240
|
+
|
|
241
|
+
:unknown
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def array_or_hash_nominal?(type)
|
|
245
|
+
type.is_a?(Type::Nominal) && %w[Array Hash Set].include?(type.class_name)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Filter folds need at least a recognised collection
|
|
249
|
+
# carrier; `Top` / `Dynamic` / arbitrary nominals decline
|
|
250
|
+
# so the RBS tier answers (its `Array#select { … } -> Array[T]`
|
|
251
|
+
# projection is correct, just less precise on the empty
|
|
252
|
+
# endpoint).
|
|
253
|
+
def filter_receiver_known?(receiver)
|
|
254
|
+
case receiver
|
|
255
|
+
when Type::Tuple, Type::HashShape, Type::Constant, Type::Difference then true
|
|
256
|
+
when Type::Nominal then %w[Array Hash Set Range].include?(receiver.class_name)
|
|
257
|
+
else false
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# `find` / `detect` / `find_index` / `index` (block form)
|
|
262
|
+
# short-circuit to nil when the block is provably falsey.
|
|
263
|
+
# `index` and `find_index` also accept a non-block argument
|
|
264
|
+
# form (`arr.index(value)`); we decline whenever the call
|
|
265
|
+
# carries a positional argument so the RBS tier still
|
|
266
|
+
# answers the value-search variant correctly.
|
|
267
|
+
def fold_falsey_nil_short_circuit(_method_name, truthiness, args)
|
|
268
|
+
return nil unless args.empty?
|
|
269
|
+
return nil unless truthiness == :falsey
|
|
270
|
+
|
|
271
|
+
Type::Combinator.constant_of(nil)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# `count` with a block returns the count of elements
|
|
275
|
+
# for which the block is truthy. The non-block forms
|
|
276
|
+
# (`count` / `count(value)`) carry positional arguments
|
|
277
|
+
# and are handled by the RBS tier; this fold only fires
|
|
278
|
+
# when the block is the sole source of selection.
|
|
279
|
+
def fold_count(receiver, truthiness, args)
|
|
280
|
+
return nil unless args.empty?
|
|
281
|
+
return Type::Combinator.constant_of(0) if truthiness == :falsey
|
|
282
|
+
|
|
283
|
+
fold_count_truthy(receiver)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def fold_count_truthy(receiver)
|
|
287
|
+
size = finite_size(receiver)
|
|
288
|
+
return nil if size.nil?
|
|
289
|
+
|
|
290
|
+
Type::Combinator.constant_of(size)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Returns the receiver's known finite element count, or
|
|
294
|
+
# nil when the carrier does not pin a size. Tuple and
|
|
295
|
+
# HashShape are pinned by construction; `Constant<…>`
|
|
296
|
+
# exposes the literal's `.size`. Other shapes (Array[T],
|
|
297
|
+
# Range[T], Nominal) decline so the RBS tier widens.
|
|
298
|
+
def finite_size(receiver)
|
|
299
|
+
case receiver
|
|
300
|
+
when Type::Tuple then receiver.elements.size
|
|
301
|
+
when Type::HashShape then receiver.pairs.size
|
|
302
|
+
when Type::Constant then constant_size(receiver.value)
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def constant_size(value)
|
|
307
|
+
# Mirrors `constant_emptiness` — only `Range` produces
|
|
308
|
+
# a meaningful finite size for the methods folded here.
|
|
309
|
+
range_size(value) if value.is_a?(Range)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def range_size(range)
|
|
313
|
+
beg = range.begin
|
|
314
|
+
en = range.end
|
|
315
|
+
return nil unless beg.is_a?(Integer) && en.is_a?(Integer)
|
|
316
|
+
|
|
317
|
+
range.exclude_end? ? [en - beg, 0].max : [en - beg + 1, 0].max
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|