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.
@@ -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