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,421 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+ require_relative "../rbs_type_translator"
5
+ require_relative "overload_selector"
6
+
7
+ module Rigor
8
+ module Inference
9
+ module MethodDispatcher
10
+ # Slice 4 dispatch tier that consults RBS method signatures.
11
+ # Sits behind {ConstantFolding}, so anything the constant folder
12
+ # already proves (e.g., `1 + 2 == 3`) keeps its full Constant
13
+ # precision; only the calls the folder cannot prove fall through
14
+ # to RBS.
15
+ #
16
+ # Phase 2b extends the dispatcher to recognise `Singleton[Foo]`
17
+ # receivers, routing those calls through `singleton_method`
18
+ # instead of `instance_method`. The constant `Foo` therefore now
19
+ # resolves to `Singleton[Foo]`, and `Foo.new` / `Foo.bar` look up
20
+ # the corresponding *class* methods.
21
+ #
22
+ # Phase 2c adds argument-typed overload selection: instead of
23
+ # always returning `method_types.first`, the dispatcher delegates
24
+ # to {OverloadSelector} which filters overloads by positional
25
+ # arity and consults `Rigor::Type#accepts` for each parameter.
26
+ # When no overload accepts the actual argument types, the
27
+ # selector falls back to the first overload so the existing
28
+ # phase-1/2b behavior is preserved.
29
+ #
30
+ # Phase 2d adds generics instantiation. Receivers carry an
31
+ # ordered `type_args` array on `Rigor::Type::Nominal`. The
32
+ # dispatcher zips the receiver's `type_args` against the class's
33
+ # declared type-parameter names (`Array` -> `[:Elem]`, `Hash` ->
34
+ # `[:K, :V]`, ...) to build a substitution map; that map is then
35
+ # threaded through {RbsTypeTranslator} so a return type like
36
+ # `::Array[Elem]` resolves to `Nominal["Array", [Integer]]`
37
+ # rather than degrading the variable to `Dynamic[Top]`. When
38
+ # arities mismatch (raw receiver, partial generics) the map is
39
+ # left empty and free variables degrade as before.
40
+ #
41
+ # Slice 5 phase 1 projects shape-carrying receivers onto their
42
+ # underlying nominal so the existing dispatch + substitution
43
+ # machinery works without duplication: `Tuple[Integer, String]`
44
+ # dispatches as `Array[Integer | String]`, and
45
+ # `HashShape{a: Integer}` dispatches as `Hash[Symbol, Integer]`.
46
+ # Tuple-aware refinements (e.g., `tuple[0]` returning the precise
47
+ # member) are deferred to Slice 5 phase 2.
48
+ #
49
+ # Remaining limitations:
50
+ #
51
+ # * `block_type:` is ignored; method types that constrain the
52
+ # block return type are not yet honored.
53
+ # * Keyword arguments are not threaded through call_arg_types,
54
+ # so overloads with required keywords are skipped (they cannot
55
+ # match the empty kwargs we send).
56
+ # * Method-level type parameters (e.g., `def foo[T]: (T) -> T`)
57
+ # are not bound; their variables remain `Dynamic[Top]` after
58
+ # substitution.
59
+ #
60
+ # See docs/adr/4-type-inference-engine.md for the broader plan.
61
+ # rubocop:disable Metrics/ModuleLength
62
+ module RbsDispatch
63
+ module_function
64
+
65
+ # @param receiver [Rigor::Type]
66
+ # @param method_name [Symbol]
67
+ # @param args [Array<Rigor::Type>]
68
+ # @param environment [Rigor::Environment]
69
+ # @param block_type [Rigor::Type, nil] inferred block return
70
+ # type, propagated from `MethodDispatcher.dispatch`. When
71
+ # non-nil, the selector prefers a block-bearing overload
72
+ # and binds the method-level type parameter that the
73
+ # block's return type references to `block_type` (Slice 6
74
+ # phase C sub-phase 2).
75
+ # @return [Rigor::Type, nil] inferred return type, or `nil`
76
+ # when no rule resolves (no class name, no method, dispatch
77
+ # on a Top/Dynamic[Top] receiver, etc.).
78
+ def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil)
79
+ return nil if environment.nil?
80
+ return nil unless environment.rbs_loader
81
+
82
+ dispatch_for(
83
+ receiver: receiver,
84
+ method_name: method_name,
85
+ args: args,
86
+ environment: environment,
87
+ block_type: block_type
88
+ )
89
+ end
90
+
91
+ # Slice 6 (Phase C sub-phase 1) probe: returns the positional
92
+ # block-parameter types declared by the receiving method's
93
+ # selected RBS overload, translated into `Rigor::Type`. Used
94
+ # by the StatementEvaluator to bind block parameter names
95
+ # before evaluating the block body.
96
+ #
97
+ # The probe shares the receiver descriptor / overload selector
98
+ # plumbing with `try_dispatch`; only the projection at the end
99
+ # differs (the block's positional params instead of the return
100
+ # type). Returns an empty array when:
101
+ #
102
+ # - the environment / RBS loader is missing,
103
+ # - the receiver does not project to a known class,
104
+ # - the method has no signature in RBS,
105
+ # - the selected overload has no `block:` clause, or
106
+ # - the block is `untyped` / `UntypedFunction` (no statically
107
+ # declared parameter types).
108
+ #
109
+ # This deliberately does NOT differentiate "no overload had a
110
+ # block" from "the block is untyped"; the binder treats both
111
+ # the same way (every parameter defaults to `Dynamic[Top]`).
112
+ # @return [Array<Rigor::Type>] positional block parameter types.
113
+ def block_param_types(receiver:, method_name:, args:, environment:)
114
+ return [] if environment.nil?
115
+ return [] unless environment.rbs_loader
116
+
117
+ probe_block_param_types(
118
+ receiver: receiver,
119
+ method_name: method_name,
120
+ args: args,
121
+ environment: environment
122
+ )
123
+ end
124
+
125
+ # rubocop:disable Metrics/ClassLength
126
+ class << self
127
+ private
128
+
129
+ def dispatch_for(receiver:, method_name:, args:, environment:, block_type:)
130
+ args ||= []
131
+ case receiver
132
+ when Type::Union
133
+ dispatch_union(receiver, method_name, args, environment, block_type)
134
+ else
135
+ dispatch_one(receiver, method_name, args, environment, block_type)
136
+ end
137
+ end
138
+
139
+ def dispatch_union(receiver, method_name, args, environment, block_type)
140
+ results = receiver.members.map do |member|
141
+ dispatch_one(member, method_name, args, environment, block_type)
142
+ end
143
+ return nil if results.any?(&:nil?)
144
+
145
+ Type::Combinator.union(*results)
146
+ end
147
+
148
+ def dispatch_one(receiver, method_name, args, environment, block_type)
149
+ descriptor = receiver_descriptor(receiver)
150
+ return nil unless descriptor
151
+
152
+ class_name, kind, receiver_args = descriptor
153
+ method_definition = lookup_method(environment, class_name, kind, method_name)
154
+ return nil unless method_definition
155
+
156
+ type_vars = build_type_vars(environment, class_name, receiver_args)
157
+ translate_return_type(
158
+ method_definition,
159
+ class_name: class_name,
160
+ kind: kind,
161
+ args: args,
162
+ type_vars: type_vars,
163
+ block_type: block_type
164
+ )
165
+ rescue StandardError
166
+ # Defensive: if RBS' definition builder raises on a broken
167
+ # hierarchy (e.g., partially loaded user signatures), the
168
+ # dispatcher MUST stay fail-soft.
169
+ nil
170
+ end
171
+
172
+ # Maps a Rigor::Type receiver to a
173
+ # `[class_name, kind, type_args]` triple where `kind` is
174
+ # either `:instance` or `:singleton` and `type_args` carries
175
+ # the receiver's generic instantiation (empty for raw or
176
+ # singleton receivers, since `Singleton[Foo]` carries no
177
+ # generic args today). Returns nil when the receiver does
178
+ # not correspond to a single concrete class.
179
+ #
180
+ # Slice 5 phase 1 projects Tuple/HashShape receivers to
181
+ # their underlying Array/Hash nominal so dispatch reuses the
182
+ # generic-typed pipeline.
183
+ def receiver_descriptor(receiver)
184
+ case receiver
185
+ when Type::Constant
186
+ [receiver.value.class.name, :instance, []]
187
+ when Type::Nominal
188
+ [receiver.class_name, :instance, receiver.type_args]
189
+ when Type::Singleton
190
+ [receiver.class_name, :singleton, []]
191
+ when Type::Tuple
192
+ ["Array", :instance, tuple_type_args(receiver)]
193
+ when Type::HashShape
194
+ ["Hash", :instance, hash_shape_type_args(receiver)]
195
+ when Type::Dynamic
196
+ receiver_descriptor(receiver.static_facet)
197
+ end
198
+ end
199
+
200
+ def tuple_type_args(tuple)
201
+ return [] if tuple.elements.empty?
202
+
203
+ [Type::Combinator.union(*tuple.elements)]
204
+ end
205
+
206
+ def hash_shape_type_args(shape)
207
+ return [] if shape.pairs.empty?
208
+
209
+ key_types = shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) }
210
+ value_types = shape.pairs.values
211
+ [
212
+ Type::Combinator.union(*key_types),
213
+ Type::Combinator.union(*value_types)
214
+ ]
215
+ end
216
+
217
+ def lookup_method(environment, class_name, kind, method_name)
218
+ case kind
219
+ when :instance
220
+ environment.rbs_loader.instance_method(
221
+ class_name: class_name,
222
+ method_name: method_name
223
+ )
224
+ when :singleton
225
+ environment.rbs_loader.singleton_method(
226
+ class_name: class_name,
227
+ method_name: method_name
228
+ )
229
+ end
230
+ end
231
+
232
+ # Slice 4 phase 2d substitution map. Zips the class's
233
+ # declared type-parameter names against the receiver's
234
+ # `type_args`. Returns an empty hash when either side is
235
+ # empty or when arities disagree -- in both cases free
236
+ # variables in the method's return type degrade to
237
+ # `Dynamic[Top]` per the translator's contract.
238
+ def build_type_vars(environment, class_name, receiver_args)
239
+ return {} if receiver_args.empty?
240
+
241
+ param_names = environment.rbs_loader.class_type_param_names(class_name)
242
+ return {} if param_names.empty?
243
+ return {} if param_names.size != receiver_args.size
244
+
245
+ param_names.zip(receiver_args).to_h
246
+ end
247
+
248
+ # rubocop:disable Metrics/ParameterLists
249
+ def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:)
250
+ instance_type = Type::Combinator.nominal_of(class_name)
251
+ self_type =
252
+ case kind
253
+ when :singleton then Type::Combinator.singleton_of(class_name)
254
+ else instance_type
255
+ end
256
+
257
+ method_type = OverloadSelector.select(
258
+ method_definition,
259
+ arg_types: args,
260
+ self_type: self_type,
261
+ instance_type: instance_type,
262
+ type_vars: type_vars,
263
+ block_required: !block_type.nil?
264
+ )
265
+ return nil unless method_type
266
+
267
+ full_type_vars = compose_block_type_vars(method_type, type_vars, block_type)
268
+
269
+ RbsTypeTranslator.translate(
270
+ method_type.type.return_type,
271
+ self_type: self_type,
272
+ instance_type: instance_type,
273
+ type_vars: full_type_vars
274
+ )
275
+ end
276
+ # rubocop:enable Metrics/ParameterLists
277
+
278
+ # When a block type is supplied, locate the method-level
279
+ # type parameter that the selected overload's block return
280
+ # type references and bind it to `block_type`. The
281
+ # contribution layers on top of the receiver-derived
282
+ # `type_vars` so a method like
283
+ # `def map[U] { (Elem) -> U } -> Array[U]` resolves
284
+ # `Elem` from the receiver and `U` from the block return
285
+ # type at the same call site. Anything outside this exact
286
+ # shape (no block clause, an `untyped` block, a non-
287
+ # variable block return type, a variable not declared in
288
+ # `type_params`) returns the original `type_vars` so
289
+ # fallbacks stay consistent.
290
+ def compose_block_type_vars(method_type, type_vars, block_type)
291
+ return type_vars if block_type.nil?
292
+
293
+ block_var_name = method_type_block_return_variable(method_type)
294
+ return type_vars if block_var_name.nil?
295
+
296
+ type_vars.merge(block_var_name => block_type)
297
+ end
298
+
299
+ def method_type_block_return_variable(method_type)
300
+ return_variable = block_return_variable(method_type)
301
+ return nil if return_variable.nil?
302
+
303
+ params = method_type.respond_to?(:type_params) ? method_type.type_params : []
304
+ return nil if params.nil?
305
+ return nil unless params.any? { |tp| tp.name == return_variable.name }
306
+
307
+ return_variable.name
308
+ end
309
+
310
+ def block_return_variable(method_type)
311
+ block = method_type.respond_to?(:block) ? method_type.block : nil
312
+ return nil if block.nil?
313
+
314
+ fun = block.type
315
+ return nil unless fun.respond_to?(:return_type)
316
+
317
+ return_type = fun.return_type
318
+ return_type.is_a?(RBS::Types::Variable) ? return_type : nil
319
+ end
320
+
321
+ # ----- block parameter probe (Phase C sub-phase 1) -----
322
+
323
+ def probe_block_param_types(receiver:, method_name:, args:, environment:)
324
+ args ||= []
325
+ case receiver
326
+ when Type::Union then probe_block_param_types_union(receiver, method_name, args, environment)
327
+ else probe_block_param_types_one(receiver, method_name, args, environment)
328
+ end
329
+ end
330
+
331
+ # For a union receiver we keep the conservative answer: only
332
+ # return block param types when every member resolves the
333
+ # same arity and types (otherwise the call sites would have
334
+ # to thread per-member binders, which the slice does not
335
+ # support yet). Mismatches degrade to the empty array so the
336
+ # binder defaults all params to Dynamic[Top].
337
+ def probe_block_param_types_union(receiver, method_name, args, environment)
338
+ results = receiver.members.map do |member|
339
+ probe_block_param_types_one(member, method_name, args, environment)
340
+ end
341
+ return [] if results.empty?
342
+ return [] unless results.all? { |r| r == results.first }
343
+
344
+ results.first
345
+ end
346
+
347
+ def probe_block_param_types_one(receiver, method_name, args, environment)
348
+ descriptor = receiver_descriptor(receiver)
349
+ return [] unless descriptor
350
+
351
+ class_name, kind, receiver_args = descriptor
352
+ method_definition = lookup_method(environment, class_name, kind, method_name)
353
+ return [] unless method_definition
354
+
355
+ type_vars = build_type_vars(environment, class_name, receiver_args)
356
+ extract_block_param_types(
357
+ method_definition,
358
+ class_name: class_name,
359
+ kind: kind,
360
+ args: args,
361
+ type_vars: type_vars
362
+ )
363
+ rescue StandardError
364
+ []
365
+ end
366
+
367
+ def extract_block_param_types(method_definition, class_name:, kind:, args:, type_vars:)
368
+ instance_type = Type::Combinator.nominal_of(class_name)
369
+ self_type =
370
+ case kind
371
+ when :singleton then Type::Combinator.singleton_of(class_name)
372
+ else instance_type
373
+ end
374
+
375
+ method_type = OverloadSelector.select(
376
+ method_definition,
377
+ arg_types: args,
378
+ self_type: self_type,
379
+ instance_type: instance_type,
380
+ type_vars: type_vars,
381
+ block_required: true
382
+ )
383
+ return [] unless method_type
384
+
385
+ block = method_type.respond_to?(:block) ? method_type.block : nil
386
+ return [] unless block
387
+
388
+ translate_block_positional_params(
389
+ block,
390
+ self_type: self_type,
391
+ instance_type: instance_type,
392
+ type_vars: type_vars
393
+ )
394
+ end
395
+
396
+ # `RBS::Types::Block#type` is normally an `RBS::Types::Function`
397
+ # carrying the block's parameter list; some signatures use
398
+ # `RBS::Types::UntypedFunction` (a `(?)` block) which exposes
399
+ # no parameter types -- we treat it as "no information" and
400
+ # return an empty array so the binder defaults every slot.
401
+ def translate_block_positional_params(block, self_type:, instance_type:, type_vars:)
402
+ fun = block.type
403
+ return [] unless fun.respond_to?(:required_positionals)
404
+
405
+ params = fun.required_positionals + fun.optional_positionals
406
+ params.map do |param|
407
+ RbsTypeTranslator.translate(
408
+ param.type,
409
+ self_type: self_type,
410
+ instance_type: instance_type,
411
+ type_vars: type_vars
412
+ )
413
+ end
414
+ end
415
+ end
416
+ # rubocop:enable Metrics/ClassLength
417
+ end
418
+ # rubocop:enable Metrics/ModuleLength
419
+ end
420
+ end
421
+ end