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,444 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# Shared dispatch table for `Rigor::Type#accepts(other, mode:)`.
|
|
8
|
+
#
|
|
9
|
+
# The acceptance query answers "is `other` passable to `self` at a
|
|
10
|
+
# method-parameter or assignment boundary?". It uses gradual-typing
|
|
11
|
+
# rules from docs/type-specification/value-lattice.md and the
|
|
12
|
+
# acceptance contract in docs/internal-spec/internal-type-api.md.
|
|
13
|
+
#
|
|
14
|
+
# Each concrete type's `accepts` method delegates here so the
|
|
15
|
+
# case-analysis stays in one place. Type instances remain thin value
|
|
16
|
+
# objects; routing logic lives in the inference layer.
|
|
17
|
+
#
|
|
18
|
+
# Slice 4 phase 2c implements the `:gradual` mode in full and
|
|
19
|
+
# reserves `:strict` for later slices (the entry point raises
|
|
20
|
+
# ArgumentError on strict for now). The table covers the leaf and
|
|
21
|
+
# combinator types added through phase 2b: Top, Bot, Dynamic,
|
|
22
|
+
# Nominal, Singleton, Constant, and Union.
|
|
23
|
+
#
|
|
24
|
+
# Slice 5 registers the shape carriers `Tuple` and `HashShape`.
|
|
25
|
+
# Tuple/HashShape acceptance compares per-position element types
|
|
26
|
+
# (covariant) and per-key entry types (depth covariant), including
|
|
27
|
+
# HashShape required/optional/closed-extra-key policy. When the
|
|
28
|
+
# receiver side is a
|
|
29
|
+
# generic `Nominal[Array, [E]]` or `Nominal[Hash, [K, V]]` the
|
|
30
|
+
# shape is projected to its underlying nominal so the existing
|
|
31
|
+
# generic-acceptance pipeline continues to apply; the converse
|
|
32
|
+
# direction (a Tuple receiver accepting a generic Array) stays
|
|
33
|
+
# conservative because the analyzer cannot verify arity from a
|
|
34
|
+
# raw nominal alone.
|
|
35
|
+
# rubocop:disable Metrics/ModuleLength
|
|
36
|
+
module Acceptance
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# @param self_type [Rigor::Type]
|
|
40
|
+
# @param other_type [Rigor::Type]
|
|
41
|
+
# @param mode [Symbol] `:gradual` (default) or `:strict`.
|
|
42
|
+
# @return [Rigor::Type::AcceptsResult]
|
|
43
|
+
def accepts(self_type, other_type, mode: :gradual)
|
|
44
|
+
raise ArgumentError, "Acceptance mode #{mode.inspect} is not implemented yet" unless mode == :gradual
|
|
45
|
+
|
|
46
|
+
return Type::AcceptsResult.yes(mode: mode, reasons: "Bot is the empty type") if other_type.is_a?(Type::Bot)
|
|
47
|
+
if other_type.is_a?(Type::Dynamic)
|
|
48
|
+
return Type::AcceptsResult.yes(mode: mode, reasons: "gradual: Dynamic[T] passes any boundary")
|
|
49
|
+
end
|
|
50
|
+
return accepts_union_other(self_type, other_type, mode) if other_type.is_a?(Type::Union)
|
|
51
|
+
|
|
52
|
+
accepts_one(self_type, other_type, mode)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Hash dispatch keeps `accepts_one` linear and lets future shape
|
|
56
|
+
# carriers register their handlers without re-tripping the
|
|
57
|
+
# cyclomatic budget on a growing `case` arm. Anonymous Type
|
|
58
|
+
# subclasses are not expected.
|
|
59
|
+
TYPE_HANDLERS = {
|
|
60
|
+
Type::Top => :accepts_top,
|
|
61
|
+
Type::Bot => :accepts_bot,
|
|
62
|
+
Type::Dynamic => :accepts_dynamic,
|
|
63
|
+
Type::Union => :accepts_union_self,
|
|
64
|
+
Type::Singleton => :accepts_singleton,
|
|
65
|
+
Type::Nominal => :accepts_nominal,
|
|
66
|
+
Type::Constant => :accepts_constant,
|
|
67
|
+
Type::Tuple => :accepts_tuple,
|
|
68
|
+
Type::HashShape => :accepts_hash_shape
|
|
69
|
+
}.freeze
|
|
70
|
+
private_constant :TYPE_HANDLERS
|
|
71
|
+
|
|
72
|
+
# rubocop:disable Metrics/ClassLength
|
|
73
|
+
class << self
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def accepts_one(self_type, other_type, mode)
|
|
77
|
+
handler = TYPE_HANDLERS[self_type.class]
|
|
78
|
+
return send(handler, self_type, other_type, mode) if handler
|
|
79
|
+
|
|
80
|
+
Type::AcceptsResult.maybe(mode: mode, reasons: "no rule for self=#{self_type.class}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def accepts_top(_self_type, _other_type, mode)
|
|
84
|
+
Type::AcceptsResult.yes(mode: mode, reasons: "Top is the universal type")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def accepts_bot(_self_type, other_type, mode)
|
|
88
|
+
# Other is not Bot here (handled in {.accepts}), so Bot rejects it.
|
|
89
|
+
Type::AcceptsResult.no(
|
|
90
|
+
mode: mode,
|
|
91
|
+
reasons: "Bot accepts only Bot, got #{other_type.class}"
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Dynamic[T] in gradual mode is liberally inhabited; any concrete
|
|
96
|
+
# other type is accepted because gradual consistency permits the
|
|
97
|
+
# crossing. (Other being Dynamic was handled in {.accepts}.)
|
|
98
|
+
def accepts_dynamic(_self_type, _other_type, mode)
|
|
99
|
+
Type::AcceptsResult.yes(
|
|
100
|
+
mode: mode,
|
|
101
|
+
reasons: "gradual: Dynamic[T] accepts any concrete type"
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Union[A,B].accepts(X) iff some member accepts X. Yes wins as
|
|
106
|
+
# soon as we find one; otherwise we surface "maybe" only when at
|
|
107
|
+
# least one member returned maybe (cannot rule out coverage),
|
|
108
|
+
# else "no".
|
|
109
|
+
def accepts_union_self(union, other_type, mode)
|
|
110
|
+
results = union.members.map { |m| accepts(m, other_type, mode: mode) }
|
|
111
|
+
|
|
112
|
+
if results.any?(&:yes?)
|
|
113
|
+
return Type::AcceptsResult.yes(
|
|
114
|
+
mode: mode,
|
|
115
|
+
reasons: "union has a member that accepts"
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if results.any?(&:maybe?)
|
|
120
|
+
Type::AcceptsResult.maybe(mode: mode, reasons: "no union member proved acceptance")
|
|
121
|
+
else
|
|
122
|
+
Type::AcceptsResult.no(
|
|
123
|
+
mode: mode,
|
|
124
|
+
reasons: "no union member accepts #{other_type.class}"
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# self.accepts(Union[Y, Z]) iff self accepts every Y_i. Strict
|
|
130
|
+
# AND across members: any "no" turns the whole result no, any
|
|
131
|
+
# "maybe" without a "no" gives maybe, all "yes" gives yes.
|
|
132
|
+
def accepts_union_other(self_type, union, mode)
|
|
133
|
+
results = union.members.map { |m| accepts(self_type, m, mode: mode) }
|
|
134
|
+
|
|
135
|
+
if results.any?(&:no?)
|
|
136
|
+
return Type::AcceptsResult.no(
|
|
137
|
+
mode: mode,
|
|
138
|
+
reasons: "a union member is rejected"
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if results.any?(&:maybe?)
|
|
143
|
+
Type::AcceptsResult.maybe(
|
|
144
|
+
mode: mode,
|
|
145
|
+
reasons: "a union member could not be proven accepted"
|
|
146
|
+
)
|
|
147
|
+
else
|
|
148
|
+
Type::AcceptsResult.yes(mode: mode, reasons: "every union member accepted")
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Singleton[C] only accepts another Singleton[D] where D is a
|
|
153
|
+
# subclass of (or equal to) C. Any other carrier (instance,
|
|
154
|
+
# constant, ...) is no, because the singleton type's inhabitants
|
|
155
|
+
# are the class objects themselves.
|
|
156
|
+
def accepts_singleton(self_type, other_type, mode)
|
|
157
|
+
unless other_type.is_a?(Type::Singleton)
|
|
158
|
+
return Type::AcceptsResult.no(
|
|
159
|
+
mode: mode,
|
|
160
|
+
reasons: "Singleton[#{self_type.class_name}] does not accept #{other_type.class}"
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
class_subtype_result(
|
|
165
|
+
target_name: self_type.class_name,
|
|
166
|
+
actual_name: other_type.class_name,
|
|
167
|
+
mode: mode,
|
|
168
|
+
kind: :singleton
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Nominal[C] accepts:
|
|
173
|
+
# - Nominal[D] when D <= C (Ruby class subtype) and the
|
|
174
|
+
# `type_args` are compatible (see {#accepts_nominal_args});
|
|
175
|
+
# - Constant[v] when v.is_a?(klass(C)). The type_args of self
|
|
176
|
+
# are ignored here because a Constant carries a concrete
|
|
177
|
+
# value, not a generic instantiation, and the analyzer has no
|
|
178
|
+
# way to refute the args from a literal alone.
|
|
179
|
+
# - Tuple[*] when self is the Array (or a supertype) family.
|
|
180
|
+
# The Tuple is projected to `Nominal[Array, [union(elements)]]`
|
|
181
|
+
# so the existing generic-arg machinery handles it.
|
|
182
|
+
# - HashShape{*} when self is the Hash (or a supertype) family,
|
|
183
|
+
# projected to `Nominal[Hash, [union(keys), union(values)]]`.
|
|
184
|
+
# - Singleton: never (wrong value kind).
|
|
185
|
+
def accepts_nominal(self_type, other_type, mode)
|
|
186
|
+
case other_type
|
|
187
|
+
when Type::Nominal
|
|
188
|
+
accepts_nominal_from_nominal(self_type, other_type, mode)
|
|
189
|
+
when Type::Constant
|
|
190
|
+
accepts_nominal_from_constant(self_type, other_type, mode)
|
|
191
|
+
when Type::Tuple
|
|
192
|
+
accepts(self_type, project_tuple_to_nominal(other_type), mode: mode)
|
|
193
|
+
.with_reason("projected Tuple to Nominal[Array]")
|
|
194
|
+
when Type::HashShape
|
|
195
|
+
accepts(self_type, project_hash_shape_to_nominal(other_type), mode: mode)
|
|
196
|
+
.with_reason("projected HashShape to Nominal[Hash]")
|
|
197
|
+
else
|
|
198
|
+
Type::AcceptsResult.no(
|
|
199
|
+
mode: mode,
|
|
200
|
+
reasons: "Nominal[#{self_type.class_name}] rejects #{other_type.class}"
|
|
201
|
+
)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def accepts_nominal_from_nominal(self_type, other_type, mode)
|
|
206
|
+
class_result = class_subtype_result(
|
|
207
|
+
target_name: self_type.class_name,
|
|
208
|
+
actual_name: other_type.class_name,
|
|
209
|
+
mode: mode,
|
|
210
|
+
kind: :instance
|
|
211
|
+
)
|
|
212
|
+
return class_result if class_result.no?
|
|
213
|
+
|
|
214
|
+
args_result = accepts_nominal_args(self_type, other_type, mode)
|
|
215
|
+
combine_results(class_result, args_result, mode)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def project_tuple_to_nominal(tuple)
|
|
219
|
+
if tuple.elements.empty?
|
|
220
|
+
Type::Combinator.nominal_of(Array)
|
|
221
|
+
else
|
|
222
|
+
Type::Combinator.nominal_of(
|
|
223
|
+
Array,
|
|
224
|
+
type_args: [Type::Combinator.union(*tuple.elements)]
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def project_hash_shape_to_nominal(shape)
|
|
230
|
+
return Type::Combinator.nominal_of(Hash) if shape.pairs.empty?
|
|
231
|
+
|
|
232
|
+
key_types = shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) }
|
|
233
|
+
value_types = shape.pairs.values
|
|
234
|
+
Type::Combinator.nominal_of(
|
|
235
|
+
Hash,
|
|
236
|
+
type_args: [
|
|
237
|
+
Type::Combinator.union(*key_types),
|
|
238
|
+
Type::Combinator.union(*value_types)
|
|
239
|
+
]
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Slice 4 phase 2d generic acceptance. Type arguments are
|
|
244
|
+
# treated covariantly element-wise (gradual default; declared
|
|
245
|
+
# variance lands in Slice 5+). When either side has no
|
|
246
|
+
# type_args we are lenient: the absent side is the "raw" form
|
|
247
|
+
# that historically meant "any instantiation", so we keep
|
|
248
|
+
# backward compatibility for call sites that have not yet
|
|
249
|
+
# learned to carry generics.
|
|
250
|
+
def accepts_nominal_args(self_type, other_type, mode)
|
|
251
|
+
shortcut = nominal_args_shortcut(self_type, other_type, mode)
|
|
252
|
+
return shortcut if shortcut
|
|
253
|
+
|
|
254
|
+
per_arg = self_type.type_args.zip(other_type.type_args).map do |formal, actual|
|
|
255
|
+
accepts(formal, actual, mode: mode)
|
|
256
|
+
end
|
|
257
|
+
combine_arg_results(per_arg, mode)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Returns an `AcceptsResult` for the universal short-circuits
|
|
261
|
+
# (raw self, raw other, arity mismatch) or `nil` when the full
|
|
262
|
+
# element-wise check still has to run.
|
|
263
|
+
def nominal_args_shortcut(self_type, other_type, mode)
|
|
264
|
+
return Type::AcceptsResult.yes(mode: mode, reasons: "self has no type_args") if self_type.type_args.empty?
|
|
265
|
+
if other_type.type_args.empty?
|
|
266
|
+
return Type::AcceptsResult.maybe(
|
|
267
|
+
mode: mode,
|
|
268
|
+
reasons: "other has no type_args; assuming compatible (raw)"
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
return nil if self_type.type_args.size == other_type.type_args.size
|
|
273
|
+
|
|
274
|
+
Type::AcceptsResult.no(
|
|
275
|
+
mode: mode,
|
|
276
|
+
reasons: "type_args arity mismatch: #{self_type.type_args.size} vs #{other_type.type_args.size}"
|
|
277
|
+
)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def combine_arg_results(per_arg, mode)
|
|
281
|
+
if per_arg.any?(&:no?)
|
|
282
|
+
return Type::AcceptsResult.no(mode: mode, reasons: "a type_arg is rejected (covariant)")
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
if per_arg.any?(&:maybe?)
|
|
286
|
+
Type::AcceptsResult.maybe(mode: mode, reasons: "a type_arg could not be proven accepted")
|
|
287
|
+
else
|
|
288
|
+
Type::AcceptsResult.yes(mode: mode, reasons: "every type_arg accepted (covariant)")
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def combine_results(class_result, args_result, mode)
|
|
293
|
+
combined_trinary = class_result.trinary.and(args_result.trinary)
|
|
294
|
+
Type::AcceptsResult.new(combined_trinary, mode: mode, reasons: class_result.reasons + args_result.reasons)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def accepts_nominal_from_constant(self_type, constant, mode)
|
|
298
|
+
ruby_class = resolve_class(self_type.class_name)
|
|
299
|
+
if ruby_class.nil?
|
|
300
|
+
return Type::AcceptsResult.maybe(
|
|
301
|
+
mode: mode,
|
|
302
|
+
reasons: "class #{self_type.class_name} not loadable; cannot prove from Constant"
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
if constant.value.is_a?(ruby_class)
|
|
307
|
+
Type::AcceptsResult.yes(mode: mode, reasons: "Constant value is_a?(#{self_type.class_name})")
|
|
308
|
+
else
|
|
309
|
+
Type::AcceptsResult.no(
|
|
310
|
+
mode: mode,
|
|
311
|
+
reasons: "Constant value is not a #{self_type.class_name}"
|
|
312
|
+
)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Constant[v] accepts only Constant[v'] with structurally equal
|
|
317
|
+
# value. Any other type is rejected (modulo the universal
|
|
318
|
+
# Bot/Dynamic short-circuits already applied upstream).
|
|
319
|
+
def accepts_constant(self_type, other_type, mode)
|
|
320
|
+
if other_type.is_a?(Type::Constant) && self_type == other_type
|
|
321
|
+
Type::AcceptsResult.yes(mode: mode, reasons: "structural literal match")
|
|
322
|
+
else
|
|
323
|
+
Type::AcceptsResult.no(
|
|
324
|
+
mode: mode,
|
|
325
|
+
reasons: "Constant[#{self_type.value.inspect}] rejects #{other_type.class}"
|
|
326
|
+
)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Tuple[A1..An] accepts:
|
|
331
|
+
# - Tuple[B1..Bn] when arities match and each Ai accepts Bi
|
|
332
|
+
# (covariant per-position).
|
|
333
|
+
# - Anything else: no (we cannot prove the arity from a generic
|
|
334
|
+
# nominal alone).
|
|
335
|
+
def accepts_tuple(self_type, other_type, mode)
|
|
336
|
+
unless other_type.is_a?(Type::Tuple)
|
|
337
|
+
return Type::AcceptsResult.no(
|
|
338
|
+
mode: mode,
|
|
339
|
+
reasons: "Tuple does not accept #{other_type.class}"
|
|
340
|
+
)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
if self_type.elements.size != other_type.elements.size
|
|
344
|
+
return Type::AcceptsResult.no(
|
|
345
|
+
mode: mode,
|
|
346
|
+
reasons: "tuple arity mismatch: #{self_type.elements.size} vs #{other_type.elements.size}"
|
|
347
|
+
)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
per_element = self_type.elements.zip(other_type.elements).map do |formal, actual|
|
|
351
|
+
accepts(formal, actual, mode: mode)
|
|
352
|
+
end
|
|
353
|
+
combine_arg_results(per_element, mode)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# HashShape{k1: T1, ...} accepts another HashShape when every
|
|
357
|
+
# required key of self is required on the other side and Ti
|
|
358
|
+
# accepts Ui (depth covariant). Optional keys may be absent on
|
|
359
|
+
# the other side; when present, their values are checked. A
|
|
360
|
+
# closed self rejects known or possible extra keys. Other
|
|
361
|
+
# types are rejected; the converse direction (a Nominal
|
|
362
|
+
# accepting a HashShape) is handled by `accepts_nominal` via
|
|
363
|
+
# projection.
|
|
364
|
+
def accepts_hash_shape(self_type, other_type, mode)
|
|
365
|
+
unless other_type.is_a?(Type::HashShape)
|
|
366
|
+
return Type::AcceptsResult.no(
|
|
367
|
+
mode: mode,
|
|
368
|
+
reasons: "HashShape does not accept #{other_type.class}"
|
|
369
|
+
)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
missing = self_type.required_keys.reject { |key| other_type.required_key?(key) }
|
|
373
|
+
return hash_shape_no(mode, "HashShape missing required keys: #{missing.inspect}") unless missing.empty?
|
|
374
|
+
|
|
375
|
+
if self_type.closed?
|
|
376
|
+
return hash_shape_no(mode, "HashShape closed target rejects open source") if other_type.open?
|
|
377
|
+
|
|
378
|
+
extra = other_type.pairs.keys - self_type.pairs.keys
|
|
379
|
+
unless extra.empty?
|
|
380
|
+
return hash_shape_no(mode, "HashShape closed target rejects extra keys: #{extra.inspect}")
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
per_entry = hash_shape_entry_results(self_type, other_type, mode)
|
|
385
|
+
combine_arg_results(per_entry, mode)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def hash_shape_entry_results(self_type, other_type, mode)
|
|
389
|
+
self_type.pairs.filter_map do |key, formal|
|
|
390
|
+
next unless other_type.pairs.key?(key)
|
|
391
|
+
|
|
392
|
+
accepts(formal, other_type.pairs.fetch(key), mode: mode)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def hash_shape_no(mode, reason)
|
|
397
|
+
Type::AcceptsResult.no(mode: mode, reasons: reason)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Slice 4 phase 2c uses Ruby's actual class hierarchy to answer
|
|
401
|
+
# "is D a subclass of C?". This works for any class loadable
|
|
402
|
+
# through Object.const_get -- core, stdlib, and live application
|
|
403
|
+
# classes. When either name fails to resolve we surface "maybe":
|
|
404
|
+
# the caller (overload selector) treats yes/maybe identically,
|
|
405
|
+
# so the conservative answer keeps overload coverage intact.
|
|
406
|
+
# Slice 5 will replace this with an RBS-driven hierarchy lookup
|
|
407
|
+
# so ahead-of-time type checking no longer relies on Ruby
|
|
408
|
+
# loading the application classes.
|
|
409
|
+
def class_subtype_result(target_name:, actual_name:, mode:, kind:)
|
|
410
|
+
return Type::AcceptsResult.yes(mode: mode, reasons: "exact name match") if target_name == actual_name
|
|
411
|
+
|
|
412
|
+
target_class = resolve_class(target_name)
|
|
413
|
+
actual_class = resolve_class(actual_name)
|
|
414
|
+
if target_class.nil? || actual_class.nil?
|
|
415
|
+
return Type::AcceptsResult.maybe(
|
|
416
|
+
mode: mode,
|
|
417
|
+
reasons: "subtype check unresolved (#{kind}: #{actual_name} <= #{target_name})"
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
if actual_class <= target_class
|
|
422
|
+
Type::AcceptsResult.yes(
|
|
423
|
+
mode: mode,
|
|
424
|
+
reasons: "#{actual_name} <= #{target_name} via Ruby hierarchy"
|
|
425
|
+
)
|
|
426
|
+
else
|
|
427
|
+
Type::AcceptsResult.no(
|
|
428
|
+
mode: mode,
|
|
429
|
+
reasons: "#{actual_name} is not a subclass of #{target_name}"
|
|
430
|
+
)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def resolve_class(name)
|
|
435
|
+
Object.const_get(name)
|
|
436
|
+
rescue NameError
|
|
437
|
+
nil
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
# rubocop:enable Metrics/ClassLength
|
|
441
|
+
end
|
|
442
|
+
# rubocop:enable Metrics/ModuleLength
|
|
443
|
+
end
|
|
444
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
require_relative "multi_target_binder"
|
|
7
|
+
|
|
8
|
+
module Rigor
|
|
9
|
+
module Inference
|
|
10
|
+
# Builds the entry scope of a block body by translating the block's
|
|
11
|
+
# parameter list into a `name -> Rigor::Type` map.
|
|
12
|
+
#
|
|
13
|
+
# The binder is the symmetric counterpart of {MethodParameterBinder}
|
|
14
|
+
# for `Prism::BlockNode`. The expected parameter types come from
|
|
15
|
+
# the receiving method's RBS signature
|
|
16
|
+
# ({Rigor::Inference::MethodDispatcher.expected_block_param_types});
|
|
17
|
+
# parameters that the signature does not cover (or that the binder
|
|
18
|
+
# cannot match by position) default to `Dynamic[Top]`. The default
|
|
19
|
+
# is the Slice 1 fail-soft answer for unknown values, so a block
|
|
20
|
+
# whose receiving method has no signature still binds every name
|
|
21
|
+
# into the scope (a block body whose `Local x` reads return
|
|
22
|
+
# `Dynamic[Top]` instead of falling through to the unbound-local
|
|
23
|
+
# `Dynamic[Top]` event is the same observable type, but the
|
|
24
|
+
# binding presence is what later slices need to attach narrowing
|
|
25
|
+
# facts to).
|
|
26
|
+
#
|
|
27
|
+
# MultiTargetNode parameters (`|(a, b), c|`) are bound by
|
|
28
|
+
# delegating each destructuring slot to
|
|
29
|
+
# {Rigor::Inference::MultiTargetBinder}, so a Tuple-shaped
|
|
30
|
+
# expected element type projects element-wise into the inner
|
|
31
|
+
# locals (Slice 6 phase C sub-phase 2). Numbered parameters
|
|
32
|
+
# (`_1`, `_2`, ...) are bound from `Prism::NumberedParametersNode`
|
|
33
|
+
# using the same per-position `expected_param_types:` array, so
|
|
34
|
+
# `[1, 2, 3].each { _1 + _2 }` sees `_1`/`_2` typed identically
|
|
35
|
+
# to their explicit `|x, y|` counterparts.
|
|
36
|
+
#
|
|
37
|
+
# Block-local declarations after `;` (e.g., `|x; y, z|`) are
|
|
38
|
+
# still skipped — they are explicitly block-local, so the outer
|
|
39
|
+
# scope MUST NOT observe them and the binder leaves them unbound.
|
|
40
|
+
#
|
|
41
|
+
# See docs/internal-spec/inference-engine.md for the binding contract.
|
|
42
|
+
# rubocop:disable Metrics/ClassLength
|
|
43
|
+
class BlockParameterBinder
|
|
44
|
+
# @param expected_param_types [Array<Rigor::Type>] positional block
|
|
45
|
+
# parameter types in order. Indices the binder cannot fill from
|
|
46
|
+
# this array (because the array is shorter than the parameter
|
|
47
|
+
# list, or because the slot is a kind we do not pull from the
|
|
48
|
+
# array) default to `Dynamic[Top]`.
|
|
49
|
+
def initialize(expected_param_types: [])
|
|
50
|
+
@expected_param_types = expected_param_types
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @param block_node [Prism::BlockNode]
|
|
54
|
+
# @return [Hash{Symbol => Rigor::Type}] ordered map from parameter
|
|
55
|
+
# name to bound type. Anonymous parameters are skipped;
|
|
56
|
+
# MultiTargetNode destructuring slots delegate to
|
|
57
|
+
# {MultiTargetBinder} and contribute every named local in
|
|
58
|
+
# declaration order. Numbered-parameter forms (`_1`, `_2`,
|
|
59
|
+
# ...) bind `:_1`, `:_2`, ... up to the maximum the block
|
|
60
|
+
# body refers to.
|
|
61
|
+
def bind(block_node)
|
|
62
|
+
params_root = block_node.parameters
|
|
63
|
+
return {} if params_root.nil?
|
|
64
|
+
|
|
65
|
+
case params_root
|
|
66
|
+
when Prism::NumberedParametersNode
|
|
67
|
+
bind_numbered_parameters(params_root)
|
|
68
|
+
when Prism::BlockParametersNode
|
|
69
|
+
bind_block_parameters(params_root)
|
|
70
|
+
else
|
|
71
|
+
{}
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
# `|_1, _2|` numbered-parameter form. Prism exposes the
|
|
78
|
+
# implicit count through `NumberedParametersNode#maximum`
|
|
79
|
+
# (the highest `_N` referenced in the body); we materialise
|
|
80
|
+
# bindings for `:_1` through `:_maximum` so the block body's
|
|
81
|
+
# `LocalVariableReadNode` lookups see the same types as the
|
|
82
|
+
# equivalent explicit `|x, y|` form would.
|
|
83
|
+
def bind_numbered_parameters(numbered_node)
|
|
84
|
+
bindings = {}
|
|
85
|
+
numbered_node.maximum.times do |i|
|
|
86
|
+
bindings[:"_#{i + 1}"] = positional_type_at(i)
|
|
87
|
+
end
|
|
88
|
+
bindings
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def bind_block_parameters(params_root)
|
|
92
|
+
params_node = params_root.parameters
|
|
93
|
+
return {} if params_node.nil?
|
|
94
|
+
|
|
95
|
+
bindings = {}
|
|
96
|
+
bind_positionals(params_node, bindings, 0)
|
|
97
|
+
bind_rest(params_node, bindings)
|
|
98
|
+
bind_keywords(params_node, bindings)
|
|
99
|
+
bind_keyword_rest(params_node, bindings)
|
|
100
|
+
bind_block_param(params_node, bindings)
|
|
101
|
+
bindings
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def bind_positionals(params_node, bindings, cursor)
|
|
105
|
+
cursor = bind_required_positionals(params_node, bindings, cursor)
|
|
106
|
+
cursor = bind_optional_positionals(params_node, bindings, cursor)
|
|
107
|
+
bind_trailing_positionals(params_node, bindings, cursor)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def bind_required_positionals(params_node, bindings, cursor)
|
|
111
|
+
params_node.requireds.each do |param|
|
|
112
|
+
bind_required_param(param, cursor, bindings)
|
|
113
|
+
cursor += 1
|
|
114
|
+
end
|
|
115
|
+
cursor
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def bind_optional_positionals(params_node, bindings, cursor)
|
|
119
|
+
params_node.optionals.each do |param|
|
|
120
|
+
bindings[param.name] = positional_type_at(cursor) if param.respond_to?(:name) && param.name
|
|
121
|
+
cursor += 1
|
|
122
|
+
end
|
|
123
|
+
cursor
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def bind_trailing_positionals(params_node, bindings, cursor)
|
|
127
|
+
params_node.posts.each do |param|
|
|
128
|
+
name = required_name(param)
|
|
129
|
+
bindings[name] = positional_type_at(cursor) if name
|
|
130
|
+
cursor += 1
|
|
131
|
+
end
|
|
132
|
+
cursor
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# `|*rest|` binds an Array of the leftover positional arguments.
|
|
136
|
+
# The expected-types array is per-position, not per-rest; we
|
|
137
|
+
# cannot reliably pick a single element type for rest, so we
|
|
138
|
+
# default to `Array[Dynamic[Top]]`. Slice C sub-phase 2 may
|
|
139
|
+
# tighten this when the receiving method's RBS rest type is
|
|
140
|
+
# available.
|
|
141
|
+
def bind_rest(params_node, bindings)
|
|
142
|
+
rest = params_node.rest
|
|
143
|
+
return unless rest.respond_to?(:name) && rest&.name
|
|
144
|
+
|
|
145
|
+
bindings[rest.name] = Type::Combinator.nominal_of("Array", type_args: [Type::Combinator.untyped])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def bind_keywords(params_node, bindings)
|
|
149
|
+
params_node.keywords.each do |kw|
|
|
150
|
+
case kw
|
|
151
|
+
when Prism::RequiredKeywordParameterNode, Prism::OptionalKeywordParameterNode
|
|
152
|
+
bindings[kw.name] = Type::Combinator.untyped
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def bind_keyword_rest(params_node, bindings)
|
|
158
|
+
kw_rest = params_node.keyword_rest
|
|
159
|
+
return unless kw_rest.respond_to?(:name) && kw_rest&.name
|
|
160
|
+
|
|
161
|
+
symbol_nominal = Type::Combinator.nominal_of("Symbol")
|
|
162
|
+
bindings[kw_rest.name] = Type::Combinator.nominal_of(
|
|
163
|
+
"Hash",
|
|
164
|
+
type_args: [symbol_nominal, Type::Combinator.untyped]
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def bind_block_param(params_node, bindings)
|
|
169
|
+
block = params_node.block
|
|
170
|
+
return unless block.respond_to?(:name) && block&.name
|
|
171
|
+
|
|
172
|
+
bindings[block.name] = Type::Combinator.nominal_of(Proc)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Required parameters in a block list can be either a plain
|
|
176
|
+
# `RequiredParameterNode` (named) or a `MultiTargetNode` (the
|
|
177
|
+
# `|(a, b), c|` destructuring form). Slice 6 phase C sub-phase 2
|
|
178
|
+
# delegates the latter to {MultiTargetBinder}, which decomposes
|
|
179
|
+
# the slot's expected Tuple element-wise and binds every named
|
|
180
|
+
# inner local. Other shapes (anonymous required parameters,
|
|
181
|
+
# forward arguments) are silently skipped.
|
|
182
|
+
def bind_required_param(param, cursor, bindings)
|
|
183
|
+
case param
|
|
184
|
+
when Prism::RequiredParameterNode
|
|
185
|
+
bindings[param.name] = positional_type_at(cursor)
|
|
186
|
+
when Prism::MultiTargetNode
|
|
187
|
+
nested = MultiTargetBinder.bind(param, positional_type_at(cursor))
|
|
188
|
+
bindings.merge!(nested)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def positional_type_at(index)
|
|
193
|
+
@expected_param_types[index] || Type::Combinator.untyped
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
# rubocop:enable Metrics/ClassLength
|
|
197
|
+
end
|
|
198
|
+
end
|