rigortype 0.0.1
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 +7 -0
- data/LICENSE +373 -0
- data/README.md +152 -0
- data/exe/rigor +9 -0
- data/lib/rigor/analysis/check_rules.rb +503 -0
- data/lib/rigor/analysis/diagnostic.rb +35 -0
- data/lib/rigor/analysis/fact_store.rb +133 -0
- data/lib/rigor/analysis/result.rb +29 -0
- data/lib/rigor/analysis/runner.rb +119 -0
- data/lib/rigor/ast/type_node.rb +41 -0
- data/lib/rigor/ast.rb +22 -0
- data/lib/rigor/cli/type_of_command.rb +160 -0
- data/lib/rigor/cli/type_of_renderer.rb +88 -0
- data/lib/rigor/cli/type_scan_command.rb +160 -0
- data/lib/rigor/cli/type_scan_renderer.rb +165 -0
- data/lib/rigor/cli/type_scan_report.rb +32 -0
- data/lib/rigor/cli.rb +195 -0
- data/lib/rigor/configuration.rb +49 -0
- data/lib/rigor/environment/class_registry.rb +141 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +64 -0
- data/lib/rigor/environment/rbs_loader.rb +244 -0
- data/lib/rigor/environment.rb +177 -0
- data/lib/rigor/inference/acceptance.rb +444 -0
- data/lib/rigor/inference/block_parameter_binder.rb +198 -0
- data/lib/rigor/inference/closure_escape_analyzer.rb +191 -0
- data/lib/rigor/inference/coverage_scanner.rb +85 -0
- data/lib/rigor/inference/expression_typer.rb +831 -0
- data/lib/rigor/inference/fallback.rb +35 -0
- data/lib/rigor/inference/fallback_tracer.rb +64 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +102 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +169 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +421 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +336 -0
- data/lib/rigor/inference/method_dispatcher.rb +213 -0
- data/lib/rigor/inference/method_parameter_binder.rb +257 -0
- data/lib/rigor/inference/multi_target_binder.rb +143 -0
- data/lib/rigor/inference/narrowing.rb +1008 -0
- data/lib/rigor/inference/rbs_type_translator.rb +219 -0
- data/lib/rigor/inference/scope_indexer.rb +468 -0
- data/lib/rigor/inference/statement_evaluator.rb +1017 -0
- data/lib/rigor/rbs_extended.rb +98 -0
- data/lib/rigor/scope.rb +340 -0
- data/lib/rigor/source/node_locator.rb +104 -0
- data/lib/rigor/source/node_walker.rb +37 -0
- data/lib/rigor/source.rb +15 -0
- data/lib/rigor/testing.rb +65 -0
- data/lib/rigor/trinary.rb +108 -0
- data/lib/rigor/type/accepts_result.rb +109 -0
- data/lib/rigor/type/bot.rb +57 -0
- data/lib/rigor/type/combinator.rb +148 -0
- data/lib/rigor/type/constant.rb +90 -0
- data/lib/rigor/type/dynamic.rb +60 -0
- data/lib/rigor/type/hash_shape.rb +246 -0
- data/lib/rigor/type/nominal.rb +83 -0
- data/lib/rigor/type/singleton.rb +65 -0
- data/lib/rigor/type/top.rb +56 -0
- data/lib/rigor/type/tuple.rb +84 -0
- data/lib/rigor/type/union.rb +65 -0
- data/lib/rigor/type.rb +23 -0
- data/lib/rigor/version.rb +5 -0
- data/lib/rigor.rb +29 -0
- data/sig/rigor/analysis/fact_store.rbs +51 -0
- data/sig/rigor/ast.rbs +11 -0
- data/sig/rigor/environment.rbs +59 -0
- data/sig/rigor/inference.rbs +151 -0
- data/sig/rigor/rbs_extended.rbs +22 -0
- data/sig/rigor/scope.rbs +49 -0
- data/sig/rigor/source.rbs +20 -0
- data/sig/rigor/testing.rbs +9 -0
- data/sig/rigor/trinary.rbs +29 -0
- data/sig/rigor/type.rbs +171 -0
- data/sig/rigor.rbs +70 -0
- metadata +260 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Slice 5 phase 2 shape-aware dispatch tier. Sits between
|
|
9
|
+
# {ConstantFolding} (which folds Constant-on-Constant arithmetic)
|
|
10
|
+
# and {RbsDispatch} (which projects shape carriers to their
|
|
11
|
+
# underlying nominal and resolves return types through RBS).
|
|
12
|
+
#
|
|
13
|
+
# The tier resolves a curated catalogue of element-access
|
|
14
|
+
# methods on `Rigor::Type::Tuple` and `Rigor::Type::HashShape`
|
|
15
|
+
# receivers, returning the *precise* member type rather than the
|
|
16
|
+
# projected `Array#[]` / `Hash#fetch` result. When the dispatch
|
|
17
|
+
# cannot prove which element will be returned (non-static key,
|
|
18
|
+
# out-of-range index, multi-arg `dig`, ...) the tier returns
|
|
19
|
+
# `nil` so the surrounding pipeline falls through to
|
|
20
|
+
# {RbsDispatch} and the projection-based answer.
|
|
21
|
+
#
|
|
22
|
+
# Catalogue (Slice 5 phase 2):
|
|
23
|
+
#
|
|
24
|
+
# - Tuple#`first`, Tuple#`last`, Tuple#`size`/`length`/`count`:
|
|
25
|
+
# no-arg, no-block.
|
|
26
|
+
# - Tuple#`[]`, Tuple#`fetch` with a single `Constant[Integer]`
|
|
27
|
+
# argument inside the tuple's bounds (negative indices are
|
|
28
|
+
# normalised by length). Tuple#`[]` also handles static
|
|
29
|
+
# Range and start-length slices, returning a sliced Tuple or
|
|
30
|
+
# `Constant[nil]` for statically nil slices.
|
|
31
|
+
# - Tuple#`dig` with a chain of `Constant[Integer]` /
|
|
32
|
+
# `Constant[Symbol|String]` arguments (Slice 5 phase 2 sub-
|
|
33
|
+
# phase 2). Each step recurses through the resolved member; a
|
|
34
|
+
# missing key/index along the chain collapses to `Constant[nil]`
|
|
35
|
+
# so the carrier surfaces through downstream narrowing. A
|
|
36
|
+
# non-shape intermediate falls through to the projection
|
|
37
|
+
# answer.
|
|
38
|
+
# - HashShape#`size`/`length`: no-arg.
|
|
39
|
+
# - HashShape#`[]`, HashShape#`fetch`, HashShape#`dig` with a
|
|
40
|
+
# single `Constant[Symbol|String]` argument matching one of
|
|
41
|
+
# the declared keys. `[]` and `dig` resolve missing keys to
|
|
42
|
+
# `Constant[nil]`; `fetch` (no default, no block) falls through
|
|
43
|
+
# on a miss because Ruby would raise `KeyError` and the
|
|
44
|
+
# analyzer prefers the conservative projection answer.
|
|
45
|
+
# - HashShape#`dig` with multi-arg chains (Slice 5 phase 2 sub-
|
|
46
|
+
# phase 2). Same chaining semantics as Tuple#`dig`.
|
|
47
|
+
# - HashShape#`values_at` with a list of `Constant[Symbol|String]`
|
|
48
|
+
# arguments (Slice 5 phase 2 sub-phase 2). The result is a
|
|
49
|
+
# `Tuple` whose elements are the per-key values
|
|
50
|
+
# (`Constant[nil]` for missing keys, mirroring Ruby's runtime
|
|
51
|
+
# behaviour).
|
|
52
|
+
#
|
|
53
|
+
# Methods that this tier does NOT yet handle (they fall through):
|
|
54
|
+
#
|
|
55
|
+
# - Iteration methods that bind block parameters (`each`, `map`,
|
|
56
|
+
# `select`, ...). Those land alongside the BlockNode-aware
|
|
57
|
+
# scope builder.
|
|
58
|
+
# - Tuple/HashShape mutation methods. These land with the future
|
|
59
|
+
# effect model so read-only entries and mutation invalidation
|
|
60
|
+
# have one place to report diagnostics.
|
|
61
|
+
#
|
|
62
|
+
# See docs/internal-spec/inference-engine.md (Slice 5 phase 2)
|
|
63
|
+
# and docs/adr/4-type-inference-engine.md for the slice
|
|
64
|
+
# rationale.
|
|
65
|
+
# rubocop:disable Metrics/ClassLength, Metrics/ModuleLength
|
|
66
|
+
module ShapeDispatch
|
|
67
|
+
module_function
|
|
68
|
+
|
|
69
|
+
TUPLE_HANDLERS = {
|
|
70
|
+
first: :tuple_first,
|
|
71
|
+
last: :tuple_last,
|
|
72
|
+
size: :tuple_size,
|
|
73
|
+
length: :tuple_size,
|
|
74
|
+
count: :tuple_size,
|
|
75
|
+
:[] => :tuple_index,
|
|
76
|
+
fetch: :tuple_index,
|
|
77
|
+
dig: :tuple_dig
|
|
78
|
+
}.freeze
|
|
79
|
+
|
|
80
|
+
HASH_SHAPE_HANDLERS = {
|
|
81
|
+
size: :hash_size,
|
|
82
|
+
length: :hash_size,
|
|
83
|
+
:[] => :hash_lookup,
|
|
84
|
+
fetch: :hash_lookup,
|
|
85
|
+
dig: :hash_dig,
|
|
86
|
+
values_at: :hash_values_at
|
|
87
|
+
}.freeze
|
|
88
|
+
|
|
89
|
+
# @return [Rigor::Type, nil] the precise element/value type, or
|
|
90
|
+
# `nil` to defer to the next dispatcher tier.
|
|
91
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
92
|
+
args ||= []
|
|
93
|
+
case receiver
|
|
94
|
+
when Type::Tuple then dispatch_tuple(receiver, method_name, args)
|
|
95
|
+
when Type::HashShape then dispatch_hash_shape(receiver, method_name, args)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class << self
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def dispatch_tuple(tuple, method_name, args)
|
|
103
|
+
handler = TUPLE_HANDLERS[method_name]
|
|
104
|
+
return nil unless handler
|
|
105
|
+
|
|
106
|
+
send(handler, tuple, method_name, args)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def dispatch_hash_shape(shape, method_name, args)
|
|
110
|
+
handler = HASH_SHAPE_HANDLERS[method_name]
|
|
111
|
+
return nil unless handler
|
|
112
|
+
|
|
113
|
+
send(handler, shape, method_name, args)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def tuple_first(tuple, _method_name, args)
|
|
117
|
+
return nil unless args.empty?
|
|
118
|
+
return Type::Combinator.constant_of(nil) if tuple.elements.empty?
|
|
119
|
+
|
|
120
|
+
tuple.elements.first
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def tuple_last(tuple, _method_name, args)
|
|
124
|
+
return nil unless args.empty?
|
|
125
|
+
return Type::Combinator.constant_of(nil) if tuple.elements.empty?
|
|
126
|
+
|
|
127
|
+
tuple.elements.last
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def tuple_size(tuple, _method_name, args)
|
|
131
|
+
return nil unless args.empty?
|
|
132
|
+
|
|
133
|
+
Type::Combinator.constant_of(tuple.elements.size)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# `tuple[i]`, `tuple[range]`, `tuple[start, length]`, and
|
|
137
|
+
# `tuple.fetch(i)` for static arguments. Out-of-range single
|
|
138
|
+
# indices still fall through because the same handler serves
|
|
139
|
+
# `fetch`, while statically nil slices can be represented
|
|
140
|
+
# precisely for `[]`.
|
|
141
|
+
def tuple_index(tuple, method_name, args)
|
|
142
|
+
case args.size
|
|
143
|
+
when 1 then tuple_single_index(tuple, method_name, args.first)
|
|
144
|
+
when 2 then tuple_start_length_slice(tuple, method_name, args)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def tuple_single_index(tuple, method_name, arg)
|
|
149
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
150
|
+
|
|
151
|
+
return tuple_range_slice(tuple, arg.value) if method_name == :[] && arg.value.is_a?(Range)
|
|
152
|
+
return nil unless arg.value.is_a?(Integer)
|
|
153
|
+
|
|
154
|
+
idx = normalise_index(arg.value, tuple.elements.size)
|
|
155
|
+
return nil unless idx
|
|
156
|
+
|
|
157
|
+
tuple.elements[idx]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def tuple_start_length_slice(tuple, method_name, args)
|
|
161
|
+
return nil unless method_name == :[]
|
|
162
|
+
|
|
163
|
+
start, length = args
|
|
164
|
+
return nil unless start.is_a?(Type::Constant) && length.is_a?(Type::Constant)
|
|
165
|
+
return nil unless start.value.is_a?(Integer) && length.value.is_a?(Integer)
|
|
166
|
+
|
|
167
|
+
tuple_slice(tuple.elements[start.value, length.value])
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def tuple_range_slice(tuple, range)
|
|
171
|
+
return nil unless integer_range?(range)
|
|
172
|
+
|
|
173
|
+
tuple_slice(tuple.elements[range])
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def tuple_slice(elements)
|
|
177
|
+
return Type::Combinator.constant_of(nil) if elements.nil?
|
|
178
|
+
|
|
179
|
+
Type::Combinator.tuple_of(*elements)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def integer_range?(range)
|
|
183
|
+
[range.begin, range.end].all? { |endpoint| endpoint.nil? || endpoint.is_a?(Integer) }
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# `tuple.dig(i, ...)` with a chain of static keys/indices.
|
|
187
|
+
# Each step recurses through the resolved member: a Tuple
|
|
188
|
+
# member dispatches `dig` on the remaining args, a HashShape
|
|
189
|
+
# member does the same, and a `Constant[nil]` member ends
|
|
190
|
+
# the chain at `Constant[nil]` (matching Ruby's `Array#dig`
|
|
191
|
+
# short-circuit on nil). Anything else along the chain
|
|
192
|
+
# falls through to the projection answer so the analyzer
|
|
193
|
+
# never invents a value it cannot prove.
|
|
194
|
+
def tuple_dig(tuple, _method_name, args)
|
|
195
|
+
return nil if args.empty?
|
|
196
|
+
|
|
197
|
+
step = tuple_dig_step(tuple, args.first)
|
|
198
|
+
return nil if step.nil?
|
|
199
|
+
|
|
200
|
+
chain_dig(step, args.drop(1))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def tuple_dig_step(tuple, arg)
|
|
204
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
205
|
+
return nil unless arg.value.is_a?(Integer)
|
|
206
|
+
|
|
207
|
+
idx = normalise_index(arg.value, tuple.elements.size)
|
|
208
|
+
return Type::Combinator.constant_of(nil) if idx.nil?
|
|
209
|
+
|
|
210
|
+
tuple.elements[idx]
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Returns the in-bounds non-negative index, or nil when the
|
|
214
|
+
# raw index falls outside `[-size, size)`.
|
|
215
|
+
def normalise_index(raw, size)
|
|
216
|
+
adjusted = raw.negative? ? raw + size : raw
|
|
217
|
+
return nil if adjusted.negative? || adjusted >= size
|
|
218
|
+
|
|
219
|
+
adjusted
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def hash_size(shape, _method_name, args)
|
|
223
|
+
return nil unless args.empty?
|
|
224
|
+
return nil unless shape.closed?
|
|
225
|
+
return nil unless shape.optional_keys.empty?
|
|
226
|
+
|
|
227
|
+
Type::Combinator.constant_of(shape.pairs.size)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# `shape[k]` and `shape.fetch(k)` for a static symbol/string
|
|
231
|
+
# key. Missing-key resolution depends on the method:
|
|
232
|
+
#
|
|
233
|
+
# - `[]` returns `nil` at runtime; we surface `Constant[nil]`
|
|
234
|
+
# so the carrier is visible to downstream narrowing.
|
|
235
|
+
# - `fetch` (no default, no block) raises `KeyError`; we let
|
|
236
|
+
# the projection answer apply because the runtime would
|
|
237
|
+
# not produce a value.
|
|
238
|
+
def hash_lookup(shape, method_name, args)
|
|
239
|
+
return nil unless args.size == 1
|
|
240
|
+
|
|
241
|
+
step = hash_dig_step(shape, args.first)
|
|
242
|
+
return nil if step.nil?
|
|
243
|
+
return nil if method_name == :fetch && optional_key_step?(shape, args.first)
|
|
244
|
+
return step unless missing_key_step?(shape, args.first)
|
|
245
|
+
|
|
246
|
+
return step if method_name == :[]
|
|
247
|
+
|
|
248
|
+
nil
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# `shape.dig(:a, :b, ...)` with a chain of static keys.
|
|
252
|
+
# Same recursion semantics as Tuple#`dig`: each step looks
|
|
253
|
+
# up the key, then `chain_dig` continues with the
|
|
254
|
+
# resolved value as the new receiver. Missing keys collapse
|
|
255
|
+
# to `Constant[nil]` (Ruby's `Hash#dig` short-circuits on
|
|
256
|
+
# nil too).
|
|
257
|
+
def hash_dig(shape, _method_name, args)
|
|
258
|
+
return nil if args.empty?
|
|
259
|
+
|
|
260
|
+
step = hash_dig_step(shape, args.first)
|
|
261
|
+
return nil if step.nil?
|
|
262
|
+
|
|
263
|
+
chain_dig(step, args.drop(1))
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Returns the per-step value type for a HashShape lookup
|
|
267
|
+
# (or `Constant[nil]` for a known-missing key). Returns
|
|
268
|
+
# `nil` when the argument is not a static symbol/string
|
|
269
|
+
# so the caller can fall through to the projection answer.
|
|
270
|
+
def hash_dig_step(shape, arg)
|
|
271
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
272
|
+
|
|
273
|
+
key = arg.value
|
|
274
|
+
return nil unless key.is_a?(Symbol) || key.is_a?(String)
|
|
275
|
+
|
|
276
|
+
if shape.pairs.key?(key)
|
|
277
|
+
value = shape.pairs[key]
|
|
278
|
+
return value unless shape.optional_key?(key)
|
|
279
|
+
|
|
280
|
+
return Type::Combinator.union(value, Type::Combinator.constant_of(nil))
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
Type::Combinator.constant_of(nil)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def optional_key_step?(shape, arg)
|
|
287
|
+
return false unless arg.is_a?(Type::Constant)
|
|
288
|
+
|
|
289
|
+
shape.optional_key?(arg.value)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def missing_key_step?(shape, arg)
|
|
293
|
+
return false unless arg.is_a?(Type::Constant)
|
|
294
|
+
|
|
295
|
+
!shape.pairs.key?(arg.value)
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# `shape.values_at(:a, :b, ...)` with a list of static
|
|
299
|
+
# keys. Returns a `Tuple` whose per-position values are
|
|
300
|
+
# the per-key value types (`Constant[nil]` for missing
|
|
301
|
+
# keys, mirroring Ruby's runtime behaviour). Falls through
|
|
302
|
+
# when any argument is not a static symbol/string.
|
|
303
|
+
def hash_values_at(shape, _method_name, args)
|
|
304
|
+
return nil if args.empty?
|
|
305
|
+
|
|
306
|
+
values = []
|
|
307
|
+
args.each do |arg|
|
|
308
|
+
step = hash_dig_step(shape, arg)
|
|
309
|
+
return nil if step.nil?
|
|
310
|
+
|
|
311
|
+
values << step
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
Type::Combinator.tuple_of(*values)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Continues a `dig` chain after the first step. Tuple and
|
|
318
|
+
# HashShape members re-dispatch into the catalogue;
|
|
319
|
+
# `Constant[nil]` short-circuits the chain (Hash#dig and
|
|
320
|
+
# Array#dig do the same at runtime); anything else falls
|
|
321
|
+
# through so the projection answer applies.
|
|
322
|
+
def chain_dig(receiver, args)
|
|
323
|
+
return receiver if args.empty?
|
|
324
|
+
|
|
325
|
+
case receiver
|
|
326
|
+
when Type::Tuple then tuple_dig(receiver, :dig, args)
|
|
327
|
+
when Type::HashShape then hash_dig(receiver, :dig, args)
|
|
328
|
+
when Type::Constant then receiver.value.nil? ? Type::Combinator.constant_of(nil) : nil
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
# rubocop:enable Metrics/ClassLength, Metrics/ModuleLength
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
require_relative "method_dispatcher/constant_folding"
|
|
5
|
+
require_relative "method_dispatcher/shape_dispatch"
|
|
6
|
+
require_relative "method_dispatcher/rbs_dispatch"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# Coordinates method dispatch for the inference engine.
|
|
11
|
+
#
|
|
12
|
+
# Given `(receiver_type, method_name, arg_types, block_type, environment)`,
|
|
13
|
+
# the dispatcher returns the inferred result type or `nil` when no
|
|
14
|
+
# rule matches. `nil` is a deliberately blunt "I don't know" signal:
|
|
15
|
+
# callers (today only `ExpressionTyper`) own the fail-soft fallback
|
|
16
|
+
# and decide whether to record a `FallbackTracer` event.
|
|
17
|
+
#
|
|
18
|
+
# Tiers (in order):
|
|
19
|
+
#
|
|
20
|
+
# 1. {ConstantFolding}: executes the Ruby operation directly when
|
|
21
|
+
# the receiver and argument are `Constant` carriers and the
|
|
22
|
+
# method is on the curated whitelist. Slice 2.
|
|
23
|
+
# 2. {ShapeDispatch}: returns the precise element/value type for a
|
|
24
|
+
# curated catalogue of `Tuple`/`HashShape` element-access
|
|
25
|
+
# methods (`first`, `last`, `[]` with a static integer/key,
|
|
26
|
+
# `fetch`, `dig`, `size`/`length`/`count`). Slice 5 phase 2.
|
|
27
|
+
# 3. {RbsDispatch}: looks up the receiver's class in the RBS
|
|
28
|
+
# environment carried by the scope and translates the method's
|
|
29
|
+
# return type into a Rigor::Type. Slice 4.
|
|
30
|
+
#
|
|
31
|
+
# `ShapeDispatch` deliberately runs *above* {RbsDispatch} so the
|
|
32
|
+
# precise per-position/per-key answer wins over the projected
|
|
33
|
+
# `Array#[]`/`Hash#fetch` answer; it falls through (`nil`) when
|
|
34
|
+
# the call cannot be proved against the static shape, in which
|
|
35
|
+
# case the projection answer from {RbsDispatch} applies.
|
|
36
|
+
#
|
|
37
|
+
# The dispatcher's public signature reserves space for `block_type:`
|
|
38
|
+
# and ADR-2 plugin extensions (later slices), so call sites added
|
|
39
|
+
# now do not have to be rewritten when those tiers arrive.
|
|
40
|
+
module MethodDispatcher
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
# @param receiver_type [Rigor::Type, nil] type of the receiver expression, or
|
|
44
|
+
# `nil` for an implicit-self call.
|
|
45
|
+
# @param method_name [Symbol]
|
|
46
|
+
# @param arg_types [Array<Rigor::Type>] positional argument types.
|
|
47
|
+
# @param block_type [Rigor::Type, nil] inferred return type of the
|
|
48
|
+
# accompanying `do ... end` / `{ ... }` block (Slice 6 phase C
|
|
49
|
+
# sub-phase 2). When non-nil, the dispatcher prefers an
|
|
50
|
+
# overload that declares a block, and binds the method's
|
|
51
|
+
# block-return type variable to `block_type` so a return type
|
|
52
|
+
# like `Array[U]` resolves to `Array[block_type]`.
|
|
53
|
+
# @param environment [Rigor::Environment, nil] required for
|
|
54
|
+
# RBS-backed dispatch; when nil only constant folding can fire.
|
|
55
|
+
# @return [Rigor::Type, nil] inferred result type, or `nil` for "no rule".
|
|
56
|
+
def dispatch(receiver_type:, method_name:, arg_types:, block_type: nil, environment: nil)
|
|
57
|
+
return nil if receiver_type.nil?
|
|
58
|
+
|
|
59
|
+
meta_result = try_meta_introspection(receiver_type, method_name)
|
|
60
|
+
return meta_result if meta_result
|
|
61
|
+
|
|
62
|
+
constant_result = ConstantFolding.try_fold(
|
|
63
|
+
receiver: receiver_type,
|
|
64
|
+
method_name: method_name,
|
|
65
|
+
args: arg_types
|
|
66
|
+
)
|
|
67
|
+
return constant_result if constant_result
|
|
68
|
+
|
|
69
|
+
shape_result = ShapeDispatch.try_dispatch(
|
|
70
|
+
receiver: receiver_type,
|
|
71
|
+
method_name: method_name,
|
|
72
|
+
args: arg_types
|
|
73
|
+
)
|
|
74
|
+
return shape_result if shape_result
|
|
75
|
+
|
|
76
|
+
rbs_result = RbsDispatch.try_dispatch(
|
|
77
|
+
receiver: receiver_type,
|
|
78
|
+
method_name: method_name,
|
|
79
|
+
args: arg_types,
|
|
80
|
+
environment: environment,
|
|
81
|
+
block_type: block_type
|
|
82
|
+
)
|
|
83
|
+
return rbs_result if rbs_result
|
|
84
|
+
|
|
85
|
+
# Slice 7 phase 10 — user-class ancestor fallback. When
|
|
86
|
+
# the receiver is `Nominal[T]` or `Singleton[T]` for a
|
|
87
|
+
# class not in the RBS environment (typically a
|
|
88
|
+
# user-defined class), retry the dispatch against the
|
|
89
|
+
# implicit ancestor: `Nominal[Object]` for instance
|
|
90
|
+
# receivers and `Singleton[Object]` for singleton
|
|
91
|
+
# receivers. This resolves Kernel intrinsics
|
|
92
|
+
# (`require`, `raise`, `puts`, ...) and Module/Class
|
|
93
|
+
# introspection (`attr_reader`, `private`, ...) on
|
|
94
|
+
# user classes without requiring the user to author
|
|
95
|
+
# their own RBS.
|
|
96
|
+
try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
100
|
+
return nil if environment.nil?
|
|
101
|
+
|
|
102
|
+
fallback_receiver = user_class_fallback_receiver(receiver_type, environment)
|
|
103
|
+
return nil if fallback_receiver.nil?
|
|
104
|
+
|
|
105
|
+
RbsDispatch.try_dispatch(
|
|
106
|
+
receiver: fallback_receiver,
|
|
107
|
+
method_name: method_name,
|
|
108
|
+
args: arg_types,
|
|
109
|
+
environment: environment,
|
|
110
|
+
block_type: block_type
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def user_class_fallback_receiver(receiver_type, environment)
|
|
115
|
+
loader = environment.rbs_loader
|
|
116
|
+
return nil if loader.nil?
|
|
117
|
+
|
|
118
|
+
case receiver_type
|
|
119
|
+
when Type::Nominal
|
|
120
|
+
return nil if loader.class_known?(receiver_type.class_name)
|
|
121
|
+
|
|
122
|
+
environment.nominal_for_name("Object")
|
|
123
|
+
when Type::Singleton
|
|
124
|
+
return nil if loader.class_known?(receiver_type.class_name)
|
|
125
|
+
|
|
126
|
+
environment.singleton_for_name("Class")
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Slice 7 phase 8 — meta-introspection shortcuts. The
|
|
131
|
+
# default `Object#class` RBS return type is `Class`, but
|
|
132
|
+
# for a receiver of known nominal identity we can do
|
|
133
|
+
# better: `instance_of(Foo).class` is `Singleton[Foo]`
|
|
134
|
+
# (the class object itself), which downstream dispatch
|
|
135
|
+
# uses to resolve `self.class.some_class_method`. The
|
|
136
|
+
# same logic answers `Foo.class` as `Singleton[Class]`
|
|
137
|
+
# (deliberate; calling `.class` on a class object yields
|
|
138
|
+
# `Class`, the metaclass). We also special-case `is_a?`-
|
|
139
|
+
# adjacent calls and the trivial `instance_of?(self)`
|
|
140
|
+
# later as the rule catalogue grows; for now only `class`
|
|
141
|
+
# is handled.
|
|
142
|
+
def try_meta_introspection(receiver_type, method_name)
|
|
143
|
+
case method_name
|
|
144
|
+
when :class then meta_class(receiver_type)
|
|
145
|
+
when :new then meta_new(receiver_type)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def meta_class(receiver_type)
|
|
150
|
+
case receiver_type
|
|
151
|
+
when Type::Nominal then Type::Combinator.singleton_of(receiver_type.class_name)
|
|
152
|
+
when Type::Constant then constant_metaclass(receiver_type.value)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# `Singleton[Foo].new` returns `Nominal[Foo]` (a fresh
|
|
157
|
+
# instance), regardless of whether Foo is in RBS. This
|
|
158
|
+
# short-circuits the Class.new generic-`instance`
|
|
159
|
+
# plumbing for user classes, so a discovered-class
|
|
160
|
+
# `ScanAccumulator.new` types as `Nominal[ScanAccumulator]`
|
|
161
|
+
# rather than `Class`.
|
|
162
|
+
def meta_new(receiver_type)
|
|
163
|
+
return nil unless receiver_type.is_a?(Type::Singleton)
|
|
164
|
+
|
|
165
|
+
Type::Combinator.nominal_of(receiver_type.class_name)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
CONSTANT_METACLASSES = {
|
|
169
|
+
Integer => "Integer", Float => "Float", String => "String",
|
|
170
|
+
Symbol => "Symbol", Range => "Range",
|
|
171
|
+
TrueClass => "TrueClass", FalseClass => "FalseClass",
|
|
172
|
+
NilClass => "NilClass"
|
|
173
|
+
}.freeze
|
|
174
|
+
private_constant :CONSTANT_METACLASSES
|
|
175
|
+
|
|
176
|
+
def constant_metaclass(value)
|
|
177
|
+
CONSTANT_METACLASSES.each do |klass, name|
|
|
178
|
+
return Type::Combinator.singleton_of(name) if value.is_a?(klass)
|
|
179
|
+
end
|
|
180
|
+
nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Returns the positional block parameter types declared by the
|
|
184
|
+
# receiving method's selected RBS overload, translated into
|
|
185
|
+
# `Rigor::Type`. Used by the StatementEvaluator's CallNode
|
|
186
|
+
# handler to bind block parameter names before evaluating the
|
|
187
|
+
# block body.
|
|
188
|
+
#
|
|
189
|
+
# The probe is best-effort: it returns an empty array whenever
|
|
190
|
+
# the receiver, environment, method definition, or selected
|
|
191
|
+
# overload does not provide statically declared block parameter
|
|
192
|
+
# types. Callers MUST treat the empty array as "no information";
|
|
193
|
+
# the binder falls back to `Dynamic[Top]` for every parameter
|
|
194
|
+
# slot in that case.
|
|
195
|
+
#
|
|
196
|
+
# @param receiver_type [Rigor::Type, nil]
|
|
197
|
+
# @param method_name [Symbol]
|
|
198
|
+
# @param arg_types [Array<Rigor::Type>]
|
|
199
|
+
# @param environment [Rigor::Environment, nil]
|
|
200
|
+
# @return [Array<Rigor::Type>]
|
|
201
|
+
def expected_block_param_types(receiver_type:, method_name:, arg_types:, environment: nil)
|
|
202
|
+
return [] if receiver_type.nil?
|
|
203
|
+
|
|
204
|
+
RbsDispatch.block_param_types(
|
|
205
|
+
receiver: receiver_type,
|
|
206
|
+
method_name: method_name,
|
|
207
|
+
args: arg_types,
|
|
208
|
+
environment: environment
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|