rigortype 0.0.5 → 0.0.7

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.
@@ -13,6 +13,7 @@ require_relative "../builtins/comparable_catalog"
13
13
  require_relative "../builtins/enumerable_catalog"
14
14
  require_relative "../builtins/rational_catalog"
15
15
  require_relative "../builtins/complex_catalog"
16
+ require_relative "../builtins/pathname_catalog"
16
17
 
17
18
  module Rigor
18
19
  module Inference
@@ -108,6 +109,16 @@ module Rigor
108
109
 
109
110
  # @return [Rigor::Type::Constant, Rigor::Type::Union, Rigor::Type::IntegerRange, nil]
110
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
+
111
122
  receiver_set = numeric_set_of(receiver)
112
123
  return nil unless receiver_set
113
124
 
@@ -117,6 +128,59 @@ module Rigor
117
128
  dispatch_by_arity(receiver_set, method_name, arg_sets)
118
129
  end
119
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
+
120
184
  def dispatch_by_arity(receiver_set, method_name, arg_sets)
121
185
  case arg_sets.size
122
186
  when 0 then try_fold_unary(receiver_set, method_name)
@@ -205,7 +269,17 @@ module Rigor
205
269
  [result]
206
270
  end
207
271
 
272
+ # rubocop:disable Metrics/CyclomaticComplexity
208
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
+
209
283
  # Type-level allow check on every receiver. If one member's
210
284
  # type does not have the method in its allow list (e.g.
211
285
  # `Union[String, nil].nil?` — `:nil?` is not in
@@ -219,8 +293,68 @@ module Rigor
219
293
  end
220
294
  build_constant_type(results, source: receiver_values)
221
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
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?
222
346
 
347
+ Type::Combinator.constant_of(edge == :first ? values.first : values.last)
348
+ end
349
+
350
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
223
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
+
224
358
  return nil if receiver_values.size * arg_values.size > UNION_FOLD_INPUT_LIMIT
225
359
  return nil unless receiver_values.all? { |rv| binary_method_allowed?(rv, method_name) }
226
360
 
@@ -229,22 +363,204 @@ module Rigor
229
363
  end
230
364
  build_constant_type(results, source: receiver_values + arg_values)
231
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
232
479
 
233
480
  # 2-arg fold dispatch. Used by `Comparable#between?(min, max)`,
234
481
  # `Comparable#clamp(min, max)`, and `Integer#pow(exp, mod)` —
235
482
  # methods the catalog classifies `:leaf` but that the prior
236
- # 0/1-arg switch could not reach. Range receivers/args are
237
- # held back: a precise 2-arg range fold (e.g.
238
- # `int<0,10>.between?(0, 10)` `Constant[true]`) is a
239
- # follow-up; for now any IntegerRange operand bails to the
240
- # RBS tier.
483
+ # 0/1-arg switch could not reach.
484
+ #
485
+ # v0.0.6 IntegerRange-shaped receivers participate in
486
+ # `Comparable#between?` and `Comparable#clamp` folds.
487
+ # `int<a,b>.between?(min, max)` decides three-valued via
488
+ # the receiver's bounds against scalar args; `int<a,b>.clamp`
489
+ # narrows the receiver's bounds against the bracket. Other
490
+ # ternary methods over IntegerRange operands still decline.
241
491
  def try_fold_ternary(receiver_set, method_name, arg_sets)
242
- return nil if receiver_set.is_a?(Type::IntegerRange)
492
+ return try_fold_ternary_range(receiver_set, method_name, arg_sets) if receiver_set.is_a?(Type::IntegerRange)
243
493
  return nil if arg_sets.any?(Type::IntegerRange)
244
494
 
245
495
  try_fold_ternary_set(receiver_set, method_name, arg_sets)
246
496
  end
247
497
 
498
+ # Receiver IntegerRange + two scalar `Constant<Integer>`
499
+ # args — the only IntegerRange-aware ternary fold today.
500
+ # `between?` returns Trinary truthiness over the bracket;
501
+ # `clamp` returns the intersected IntegerRange (or a
502
+ # collapsed Constant if the result pins a single point).
503
+ def try_fold_ternary_range(range, method_name, arg_sets)
504
+ return nil unless arg_sets.all?(Array)
505
+
506
+ min_arg = single_integer_arg(arg_sets[0])
507
+ max_arg = single_integer_arg(arg_sets[1])
508
+ return nil if min_arg.nil? || max_arg.nil?
509
+ return nil if min_arg > max_arg
510
+
511
+ case method_name
512
+ when :between? then range_between(range, min_arg, max_arg)
513
+ when :clamp then range_clamp(range, min_arg, max_arg)
514
+ end
515
+ end
516
+
517
+ def single_integer_arg(values)
518
+ return nil unless values.is_a?(Array) && values.size == 1
519
+
520
+ v = values.first
521
+ v.is_a?(Integer) ? v : nil
522
+ end
523
+
524
+ # `int<a,b>.between?(min, max)`:
525
+ # - Constant[true] when [a,b] ⊆ [min,max] (and finite).
526
+ # - Constant[false] when [a,b] ∩ [min,max] is empty.
527
+ # - bool union otherwise.
528
+ def range_between(range, min_arg, max_arg)
529
+ return Type::Combinator.constant_of(false) if range.upper < min_arg || range.lower > max_arg
530
+
531
+ return Type::Combinator.constant_of(true) if range.finite? && range.min >= min_arg && range.max <= max_arg
532
+
533
+ bool_union
534
+ end
535
+
536
+ # `int<a,b>.clamp(min, max)`:
537
+ # - new_lower = max(a, min), new_upper = min(b, max).
538
+ # - When new_lower > new_upper the bracket excluded the
539
+ # range entirely; the call still returns one of the
540
+ # bracket bounds at runtime, but Rigor is strictly less
541
+ # precise here than Ruby — decline so the RBS tier
542
+ # widens to plain Integer rather than the dispatcher
543
+ # inventing a value.
544
+ def range_clamp(range, min_arg, max_arg)
545
+ new_lower = clamp_lower_bound(range.lower, min_arg)
546
+ new_upper = clamp_upper_bound(range.upper, max_arg)
547
+ return nil if new_lower.is_a?(Integer) && new_upper.is_a?(Integer) && new_lower > new_upper
548
+
549
+ build_integer_range(new_lower, new_upper)
550
+ end
551
+
552
+ def clamp_lower_bound(range_lower, bracket_min)
553
+ return bracket_min if range_lower == -Float::INFINITY
554
+
555
+ [range_lower, bracket_min].max
556
+ end
557
+
558
+ def clamp_upper_bound(range_upper, bracket_max)
559
+ return bracket_max if range_upper == Float::INFINITY
560
+
561
+ [range_upper, bracket_max].min
562
+ end
563
+
248
564
  def try_fold_ternary_set(receiver_values, method_name, arg_sets)
249
565
  total = receiver_values.size * arg_sets[0].size * arg_sets[1].size
250
566
  return nil if total > UNION_FOLD_INPUT_LIMIT
@@ -760,7 +1076,8 @@ module Rigor
760
1076
  [DateTime, [Builtins::DATE_CATALOG, "DateTime"]],
761
1077
  [Date, [Builtins::DATE_CATALOG, "Date"]],
762
1078
  [Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
763
- [Complex, [Builtins::COMPLEX_CATALOG, "Complex"]]
1079
+ [Complex, [Builtins::COMPLEX_CATALOG, "Complex"]],
1080
+ [Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]]
764
1081
  ].freeze
765
1082
  private_constant :CATALOG_BY_CLASS
766
1083
 
@@ -808,7 +1125,7 @@ module Rigor
808
1125
  # round-trip through `Type::Combinator.constant_of`".
809
1126
  def foldable_constant_value?(value)
810
1127
  case value
811
- 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
812
1129
  else false
813
1130
  end
814
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