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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +373 -0
  3. data/README.md +152 -0
  4. data/exe/rigor +9 -0
  5. data/lib/rigor/analysis/check_rules.rb +503 -0
  6. data/lib/rigor/analysis/diagnostic.rb +35 -0
  7. data/lib/rigor/analysis/fact_store.rb +133 -0
  8. data/lib/rigor/analysis/result.rb +29 -0
  9. data/lib/rigor/analysis/runner.rb +119 -0
  10. data/lib/rigor/ast/type_node.rb +41 -0
  11. data/lib/rigor/ast.rb +22 -0
  12. data/lib/rigor/cli/type_of_command.rb +160 -0
  13. data/lib/rigor/cli/type_of_renderer.rb +88 -0
  14. data/lib/rigor/cli/type_scan_command.rb +160 -0
  15. data/lib/rigor/cli/type_scan_renderer.rb +165 -0
  16. data/lib/rigor/cli/type_scan_report.rb +32 -0
  17. data/lib/rigor/cli.rb +195 -0
  18. data/lib/rigor/configuration.rb +49 -0
  19. data/lib/rigor/environment/class_registry.rb +141 -0
  20. data/lib/rigor/environment/rbs_hierarchy.rb +64 -0
  21. data/lib/rigor/environment/rbs_loader.rb +244 -0
  22. data/lib/rigor/environment.rb +177 -0
  23. data/lib/rigor/inference/acceptance.rb +444 -0
  24. data/lib/rigor/inference/block_parameter_binder.rb +198 -0
  25. data/lib/rigor/inference/closure_escape_analyzer.rb +191 -0
  26. data/lib/rigor/inference/coverage_scanner.rb +85 -0
  27. data/lib/rigor/inference/expression_typer.rb +831 -0
  28. data/lib/rigor/inference/fallback.rb +35 -0
  29. data/lib/rigor/inference/fallback_tracer.rb +64 -0
  30. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +102 -0
  31. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +169 -0
  32. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +421 -0
  33. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +336 -0
  34. data/lib/rigor/inference/method_dispatcher.rb +213 -0
  35. data/lib/rigor/inference/method_parameter_binder.rb +257 -0
  36. data/lib/rigor/inference/multi_target_binder.rb +143 -0
  37. data/lib/rigor/inference/narrowing.rb +1008 -0
  38. data/lib/rigor/inference/rbs_type_translator.rb +219 -0
  39. data/lib/rigor/inference/scope_indexer.rb +468 -0
  40. data/lib/rigor/inference/statement_evaluator.rb +1017 -0
  41. data/lib/rigor/rbs_extended.rb +98 -0
  42. data/lib/rigor/scope.rb +340 -0
  43. data/lib/rigor/source/node_locator.rb +104 -0
  44. data/lib/rigor/source/node_walker.rb +37 -0
  45. data/lib/rigor/source.rb +15 -0
  46. data/lib/rigor/testing.rb +65 -0
  47. data/lib/rigor/trinary.rb +108 -0
  48. data/lib/rigor/type/accepts_result.rb +109 -0
  49. data/lib/rigor/type/bot.rb +57 -0
  50. data/lib/rigor/type/combinator.rb +148 -0
  51. data/lib/rigor/type/constant.rb +90 -0
  52. data/lib/rigor/type/dynamic.rb +60 -0
  53. data/lib/rigor/type/hash_shape.rb +246 -0
  54. data/lib/rigor/type/nominal.rb +83 -0
  55. data/lib/rigor/type/singleton.rb +65 -0
  56. data/lib/rigor/type/top.rb +56 -0
  57. data/lib/rigor/type/tuple.rb +84 -0
  58. data/lib/rigor/type/union.rb +65 -0
  59. data/lib/rigor/type.rb +23 -0
  60. data/lib/rigor/version.rb +5 -0
  61. data/lib/rigor.rb +29 -0
  62. data/sig/rigor/analysis/fact_store.rbs +51 -0
  63. data/sig/rigor/ast.rbs +11 -0
  64. data/sig/rigor/environment.rbs +59 -0
  65. data/sig/rigor/inference.rbs +151 -0
  66. data/sig/rigor/rbs_extended.rbs +22 -0
  67. data/sig/rigor/scope.rbs +49 -0
  68. data/sig/rigor/source.rbs +20 -0
  69. data/sig/rigor/testing.rbs +9 -0
  70. data/sig/rigor/trinary.rbs +29 -0
  71. data/sig/rigor/type.rbs +171 -0
  72. data/sig/rigor.rbs +70 -0
  73. 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