rigortype 0.0.6 → 0.0.8

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.
@@ -109,6 +109,16 @@ module Rigor
109
109
 
110
110
  # @return [Rigor::Type::Constant, Rigor::Type::Union, Rigor::Type::IntegerRange, nil]
111
111
  def try_fold(receiver:, method_name:, args:)
112
+ # v0.0.7 — `String#%` against a `Tuple` / `HashShape`
113
+ # argument runs Ruby's format-string engine when both
114
+ # sides are statically constant. The standard
115
+ # `numeric_set_of` path bails on Tuple / HashShape
116
+ # arguments because they are not scalar-Constant
117
+ # carriers, so the special-case sits ahead of the
118
+ # numeric path.
119
+ format_lift = try_fold_string_format(receiver, method_name, args)
120
+ return format_lift if format_lift
121
+
112
122
  receiver_set = numeric_set_of(receiver)
113
123
  return nil unless receiver_set
114
124
 
@@ -118,6 +128,59 @@ module Rigor
118
128
  dispatch_by_arity(receiver_set, method_name, arg_sets)
119
129
  end
120
130
 
131
+ # `Constant<String> % …` — runs the actual `String#%`
132
+ # operation when both sides are statically known. The
133
+ # argument may be:
134
+ # - A `Type::Constant` whose value is a scalar (Integer
135
+ # / Float / String / Symbol). Already handled by the
136
+ # numeric path; this method declines so the standard
137
+ # binary path picks it up.
138
+ # - A `Type::Tuple` whose elements are all `Constant`.
139
+ # Materialises the elements as a Ruby Array and runs
140
+ # the format.
141
+ # - A `Type::HashShape` with no optional keys whose
142
+ # values are all `Constant`. Materialises a Ruby Hash
143
+ # and runs the format. Symbol keys are kept as
144
+ # Symbols (matching Ruby's `%{key}` resolution).
145
+ # Anything else declines so the RBS tier widens.
146
+ # rubocop:disable Metrics/CyclomaticComplexity
147
+ def try_fold_string_format(receiver, method_name, args)
148
+ return nil unless method_name == :%
149
+ return nil unless args.size == 1
150
+ return nil unless receiver.is_a?(Type::Constant) && receiver.value.is_a?(String)
151
+
152
+ arg = args.first
153
+ ruby_arg = format_argument_value(arg)
154
+ return nil if ruby_arg.nil?
155
+
156
+ result = receiver.value % ruby_arg
157
+ return nil unless foldable_constant_value?(result)
158
+
159
+ Type::Combinator.constant_of(result)
160
+ rescue StandardError
161
+ nil
162
+ end
163
+ # rubocop:enable Metrics/CyclomaticComplexity
164
+
165
+ def format_argument_value(arg)
166
+ case arg
167
+ when Type::Tuple
168
+ return nil unless arg.elements.all?(Type::Constant)
169
+
170
+ arg.elements.map(&:value)
171
+ when Type::HashShape
172
+ hash_shape_format_value(arg)
173
+ end
174
+ end
175
+
176
+ def hash_shape_format_value(shape)
177
+ return nil unless shape.closed?
178
+ return nil unless shape.optional_keys.empty?
179
+ return nil unless shape.pairs.values.all?(Type::Constant)
180
+
181
+ shape.pairs.transform_values(&:value)
182
+ end
183
+
121
184
  def dispatch_by_arity(receiver_set, method_name, arg_sets)
122
185
  case arg_sets.size
123
186
  when 0 then try_fold_unary(receiver_set, method_name)
@@ -206,7 +269,17 @@ module Rigor
206
269
  [result]
207
270
  end
208
271
 
272
+ # rubocop:disable Metrics/CyclomaticComplexity
209
273
  def try_fold_unary_set(receiver_values, method_name)
274
+ range_lift = try_fold_range_constant_unary(receiver_values, method_name)
275
+ return range_lift if range_lift
276
+
277
+ string_lift = try_fold_string_array_unary(receiver_values, method_name)
278
+ return string_lift if string_lift
279
+
280
+ pathname_lift = try_fold_pathname_unary(receiver_values, method_name)
281
+ return pathname_lift if pathname_lift
282
+
210
283
  # Type-level allow check on every receiver. If one member's
211
284
  # type does not have the method in its allow list (e.g.
212
285
  # `Union[String, nil].nil?` — `:nil?` is not in
@@ -220,8 +293,68 @@ module Rigor
220
293
  end
221
294
  build_constant_type(results, source: receiver_values)
222
295
  end
296
+ # rubocop:enable Metrics/CyclomaticComplexity
297
+
298
+ # v0.0.7 — `Constant<Range>#to_a` and the no-arg
299
+ # `first` / `last` / `min` / `max` short-circuit through a
300
+ # Range-specific arm that catalog dispatch cannot reach:
301
+ # - `to_a` returns an Array (not foldable through
302
+ # `foldable_constant_value?`) — lift to `Tuple[Constant…]`
303
+ # when the cardinality fits within `RANGE_TO_A_LIMIT`.
304
+ # - `first` / `last` / `min` / `max` are catalog-classified
305
+ # `:block_dependent` because of the optional-block forms,
306
+ # but the no-arg form is pure for finite integer ranges.
307
+ #
308
+ # Only fires on a single-receiver Range with finite integer
309
+ # endpoints; mixed unions fall through so the existing
310
+ # union-of-Constants path keeps the rest of the arms.
311
+ RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length].freeze
312
+ RANGE_TO_A_LIMIT = 16
313
+ private_constant :RANGE_FOLD_METHODS, :RANGE_TO_A_LIMIT
314
+
315
+ def try_fold_range_constant_unary(receiver_values, method_name)
316
+ return nil unless RANGE_FOLD_METHODS.include?(method_name)
317
+ return nil unless receiver_values.size == 1
223
318
 
319
+ range = receiver_values.first
320
+ return nil unless range.is_a?(Range)
321
+ return nil unless range.begin.is_a?(Integer) && range.end.is_a?(Integer)
322
+
323
+ range_constant_unary(range, method_name)
324
+ end
325
+
326
+ def range_constant_unary(range, method_name)
327
+ case method_name
328
+ when :to_a then range_to_a_tuple(range)
329
+ when :first, :min then range_endpoint_constant(range, :first)
330
+ when :last, :max then range_endpoint_constant(range, :last)
331
+ when :count, :size, :length then Type::Combinator.constant_of(range.to_a.size)
332
+ end
333
+ end
334
+
335
+ def range_to_a_tuple(range)
336
+ values = range.to_a
337
+ return Type::Combinator.tuple_of if values.empty?
338
+ return nil if values.size > RANGE_TO_A_LIMIT
339
+
340
+ Type::Combinator.tuple_of(*values.map { |v| Type::Combinator.constant_of(v) })
341
+ end
342
+
343
+ def range_endpoint_constant(range, edge)
344
+ values = range.to_a
345
+ return Type::Combinator.constant_of(nil) if values.empty?
346
+
347
+ Type::Combinator.constant_of(edge == :first ? values.first : values.last)
348
+ end
349
+
350
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
224
351
  def try_fold_binary_set(receiver_values, method_name, arg_values)
352
+ string_lift = try_fold_string_array_binary(receiver_values, method_name, arg_values)
353
+ return string_lift if string_lift
354
+
355
+ pathname_lift = try_fold_pathname_binary(receiver_values, method_name, arg_values)
356
+ return pathname_lift if pathname_lift
357
+
225
358
  return nil if receiver_values.size * arg_values.size > UNION_FOLD_INPUT_LIMIT
226
359
  return nil unless receiver_values.all? { |rv| binary_method_allowed?(rv, method_name) }
227
360
 
@@ -230,6 +363,119 @@ module Rigor
230
363
  end
231
364
  build_constant_type(results, source: receiver_values + arg_values)
232
365
  end
366
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
367
+
368
+ # v0.0.7 — `Constant<String>#chars` / `bytes` / `lines` /
369
+ # `split` (no-arg) return a Ruby Array of foldable
370
+ # scalars; `foldable_constant_value?` rejects Array
371
+ # results, so the standard unary path declines. Lift the
372
+ # Array to a per-position `Tuple[Constant…]` directly,
373
+ # capped at `STRING_ARRAY_LIFT_LIMIT` to keep the result
374
+ # bounded for long strings.
375
+ STRING_ARRAY_UNARY_METHODS = Set[:chars, :bytes, :lines, :split].freeze
376
+ STRING_ARRAY_BINARY_METHODS = Set[:split, :scan].freeze
377
+ STRING_ARRAY_LIFT_LIMIT = 32
378
+ private_constant :STRING_ARRAY_UNARY_METHODS,
379
+ :STRING_ARRAY_BINARY_METHODS,
380
+ :STRING_ARRAY_LIFT_LIMIT
381
+
382
+ # v0.0.7 — `Constant<Pathname>` delegates to a curated set
383
+ # of pure path-manipulation methods. Pathname is immutable
384
+ # in Ruby (per its docstring) and the catalog classifies
385
+ # most methods `:dispatch` because the C body delegates to
386
+ # File / Dir / FileTest. The methods listed here are
387
+ # filesystem-independent — they read only `@path` — so
388
+ # invoking them at fold time produces a deterministic
389
+ # result regardless of the host filesystem state.
390
+ #
391
+ # Filesystem-touching methods (`exist?`, `file?`, `read`,
392
+ # `stat`, …) are intentionally NOT folded: their answer
393
+ # depends on the analysis machine's filesystem, which is
394
+ # neither stable nor relevant to the analyzed program.
395
+ PATHNAME_PURE_UNARY = Set[
396
+ :to_s, :to_path, :to_str,
397
+ :basename, :dirname, :extname, :cleanpath,
398
+ :parent, :sub_ext, :root?, :absolute?, :relative?,
399
+ :hash, :inspect
400
+ ].freeze
401
+ PATHNAME_PURE_BINARY = Set[
402
+ :+, :join, :sub_ext, :<=>, :==, :eql?, :===,
403
+ :relative_path_from
404
+ ].freeze
405
+ private_constant :PATHNAME_PURE_UNARY, :PATHNAME_PURE_BINARY
406
+
407
+ def try_fold_pathname_unary(receiver_values, method_name)
408
+ return nil unless PATHNAME_PURE_UNARY.include?(method_name)
409
+ return nil unless receiver_values.size == 1
410
+
411
+ receiver = receiver_values.first
412
+ return nil unless receiver.is_a?(Pathname)
413
+
414
+ result = receiver.public_send(method_name)
415
+ return nil unless foldable_constant_value?(result)
416
+
417
+ Type::Combinator.constant_of(result)
418
+ rescue StandardError
419
+ nil
420
+ end
421
+
422
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
423
+ def try_fold_pathname_binary(receiver_values, method_name, arg_values)
424
+ return nil unless PATHNAME_PURE_BINARY.include?(method_name)
425
+ return nil unless receiver_values.size == 1 && arg_values.size == 1
426
+
427
+ receiver = receiver_values.first
428
+ arg = arg_values.first
429
+ return nil unless receiver.is_a?(Pathname)
430
+ return nil unless arg.is_a?(Pathname) || arg.is_a?(String)
431
+
432
+ result = receiver.public_send(method_name, arg)
433
+ return nil unless foldable_constant_value?(result)
434
+
435
+ Type::Combinator.constant_of(result)
436
+ rescue StandardError
437
+ nil
438
+ end
439
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
440
+
441
+ def try_fold_string_array_unary(receiver_values, method_name)
442
+ return nil unless STRING_ARRAY_UNARY_METHODS.include?(method_name)
443
+ return nil unless receiver_values.size == 1
444
+
445
+ receiver = receiver_values.first
446
+ return nil unless receiver.is_a?(String)
447
+
448
+ lift_array_result(receiver.public_send(method_name))
449
+ rescue StandardError
450
+ nil
451
+ end
452
+
453
+ # `Constant<String>#split(arg)` / `#scan(arg)` — lift the
454
+ # Array result to a Tuple when both sides are statically
455
+ # known and the cardinality fits.
456
+ # rubocop:disable Metrics/CyclomaticComplexity
457
+ def try_fold_string_array_binary(receiver_values, method_name, arg_values)
458
+ return nil unless STRING_ARRAY_BINARY_METHODS.include?(method_name)
459
+ return nil unless receiver_values.size == 1 && arg_values.size == 1
460
+
461
+ receiver = receiver_values.first
462
+ arg = arg_values.first
463
+ return nil unless receiver.is_a?(String)
464
+ return nil unless arg.is_a?(String) || arg.is_a?(Regexp)
465
+
466
+ lift_array_result(receiver.public_send(method_name, arg))
467
+ rescue StandardError
468
+ nil
469
+ end
470
+ # rubocop:enable Metrics/CyclomaticComplexity
471
+
472
+ def lift_array_result(result)
473
+ return nil unless result.is_a?(Array)
474
+ return nil if result.size > STRING_ARRAY_LIFT_LIMIT
475
+ return nil unless result.all? { |v| foldable_constant_value?(v) }
476
+
477
+ Type::Combinator.tuple_of(*result.map { |v| Type::Combinator.constant_of(v) })
478
+ end
233
479
 
234
480
  # 2-arg fold dispatch. Used by `Comparable#between?(min, max)`,
235
481
  # `Comparable#clamp(min, max)`, and `Integer#pow(exp, mod)` —
@@ -879,7 +1125,7 @@ module Rigor
879
1125
  # round-trip through `Type::Combinator.constant_of`".
880
1126
  def foldable_constant_value?(value)
881
1127
  case value
882
- when Integer, Float, String, Symbol, true, false, nil then true
1128
+ when Integer, Float, Rational, Complex, String, Symbol, Regexp, Pathname, true, false, nil then true
883
1129
  else false
884
1130
  end
885
1131
  end
@@ -30,18 +30,59 @@ module Rigor
30
30
  module KernelDispatch
31
31
  module_function
32
32
 
33
+ # `Kernel#Rational` / `Kernel#Complex` constructor folds.
34
+ # When every argument is a `Type::Constant` whose value is
35
+ # numeric, we can run the actual Ruby constructor and lift
36
+ # the result into a `Constant<Rational>` / `Constant<Complex>`.
37
+ # The factory accepts the same shapes as Ruby:
38
+ # `Rational(a)`, `Rational(a, b)`, `Complex(a)`, `Complex(a, b)`.
39
+ NUMERIC_CONSTRUCTORS = {
40
+ Rational: ->(*args) { Rational(*args) },
41
+ Complex: ->(*args) { Complex(*args) }
42
+ }.freeze
43
+ private_constant :NUMERIC_CONSTRUCTORS
44
+
33
45
  def try_dispatch(receiver:, method_name:, args:)
34
- return nil unless method_name == :Array
35
- return nil if args.length != 1
36
46
  return nil if receiver.nil?
47
+ return try_array(args) if method_name == :Array
48
+ return try_numeric_constructor(method_name, args) if NUMERIC_CONSTRUCTORS.key?(method_name)
49
+
50
+ nil
51
+ end
52
+
53
+ def try_array(args)
54
+ return nil if args.length != 1
37
55
 
38
- arg = args.first
39
- element = element_type_of(arg)
56
+ element = element_type_of(args.first)
40
57
  return nil if element.nil?
41
58
 
42
59
  Type::Combinator.nominal_of("Array", type_args: [element])
43
60
  end
44
61
 
62
+ # `Rational(int)` / `Rational(num, den)` and `Complex(re)`
63
+ # / `Complex(re, im)` fold when every arg is a numeric
64
+ # Constant. The actual Ruby constructor runs at fold time
65
+ # (host-side), so the result respects Ruby's normalisation
66
+ # (`Rational(2, 4)` → `Rational(1, 2)`).
67
+ def try_numeric_constructor(method_name, args)
68
+ return nil unless [1, 2].include?(args.size)
69
+ return nil unless args.all? { |arg| numeric_constant?(arg) }
70
+
71
+ values = args.map(&:value)
72
+ result = NUMERIC_CONSTRUCTORS[method_name].call(*values)
73
+ Type::Combinator.constant_of(result)
74
+ rescue StandardError
75
+ nil
76
+ end
77
+
78
+ def numeric_constant?(type)
79
+ type.is_a?(Type::Constant) &&
80
+ (type.value.is_a?(Integer) ||
81
+ type.value.is_a?(Float) ||
82
+ type.value.is_a?(Rational) ||
83
+ type.value.is_a?(Complex))
84
+ end
85
+
45
86
  # Computes the element type the argument contributes to the
46
87
  # `Array(arg)` result, mirroring Ruby's coercion contract:
47
88
  #
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../reflection"
3
4
  require_relative "../../type"
4
5
  require_relative "../../rbs_extended"
5
6
  require_relative "../rbs_type_translator"
@@ -218,15 +219,9 @@ module Rigor
218
219
  def lookup_method(environment, class_name, kind, method_name)
219
220
  case kind
220
221
  when :instance
221
- environment.rbs_loader.instance_method(
222
- class_name: class_name,
223
- method_name: method_name
224
- )
222
+ Rigor::Reflection.instance_method_definition(class_name, method_name, environment: environment)
225
223
  when :singleton
226
- environment.rbs_loader.singleton_method(
227
- class_name: class_name,
228
- method_name: method_name
229
- )
224
+ Rigor::Reflection.singleton_method_definition(class_name, method_name, environment: environment)
230
225
  end
231
226
  end
232
227
 
@@ -239,7 +234,7 @@ module Rigor
239
234
  def build_type_vars(environment, class_name, receiver_args)
240
235
  return {} if receiver_args.empty?
241
236
 
242
- param_names = environment.rbs_loader.class_type_param_names(class_name)
237
+ param_names = Rigor::Reflection.class_type_param_names(class_name, environment: environment)
243
238
  return {} if param_names.empty?
244
239
  return {} if param_names.size != receiver_args.size
245
240