rigortype 0.1.7 → 0.1.9

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +186 -513
  3. data/lib/rigor/analysis/check_rules.rb +23 -1
  4. data/lib/rigor/analysis/diagnostic.rb +17 -3
  5. data/lib/rigor/analysis/runner.rb +178 -3
  6. data/lib/rigor/analysis/worker_session.rb +14 -3
  7. data/lib/rigor/cli/annotate_command.rb +224 -0
  8. data/lib/rigor/cli/baseline_command.rb +36 -16
  9. data/lib/rigor/cli/prism_colorizer.rb +111 -0
  10. data/lib/rigor/cli/triage_command.rb +83 -0
  11. data/lib/rigor/cli/triage_renderer.rb +77 -0
  12. data/lib/rigor/cli.rb +71 -5
  13. data/lib/rigor/environment.rb +9 -1
  14. data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
  15. data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
  16. data/lib/rigor/inference/expression_typer.rb +300 -18
  17. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
  18. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +173 -10
  19. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
  20. data/lib/rigor/inference/method_dispatcher/math_folding.rb +149 -0
  21. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +20 -1
  22. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +33 -8
  23. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +81 -0
  24. data/lib/rigor/inference/method_dispatcher/set_folding.rb +81 -0
  25. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +316 -2
  26. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +126 -0
  27. data/lib/rigor/inference/method_dispatcher/time_folding.rb +56 -0
  28. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +67 -0
  29. data/lib/rigor/inference/method_dispatcher.rb +179 -4
  30. data/lib/rigor/inference/method_parameter_binder.rb +67 -10
  31. data/lib/rigor/inference/narrowing.rb +29 -10
  32. data/lib/rigor/inference/scope_indexer.rb +156 -6
  33. data/lib/rigor/inference/statement_evaluator.rb +43 -21
  34. data/lib/rigor/plugin/base.rb +39 -0
  35. data/lib/rigor/plugin/loader.rb +22 -1
  36. data/lib/rigor/plugin/manifest.rb +73 -10
  37. data/lib/rigor/plugin/protocol_contract.rb +185 -0
  38. data/lib/rigor/plugin/registry.rb +66 -0
  39. data/lib/rigor/scope.rb +46 -0
  40. data/lib/rigor/triage/catalogue.rb +296 -0
  41. data/lib/rigor/triage/hint.rb +27 -0
  42. data/lib/rigor/triage.rb +89 -0
  43. data/lib/rigor/type/constant.rb +29 -2
  44. data/lib/rigor/version.rb +1 -1
  45. data/sig/rigor/inference.rbs +1 -0
  46. data/sig/rigor/scope.rbs +6 -0
  47. metadata +16 -1
@@ -48,11 +48,21 @@ module Rigor
48
48
  module ConstantFolding # rubocop:disable Metrics/ModuleLength
49
49
  module_function
50
50
 
51
- NUMERIC_BINARY = Set[:+, :-, :*, :/, :%, :<, :<=, :>, :>=, :==, :!=, :<=>].freeze
52
- STRING_BINARY = Set[:+, :*, :==, :!=, :<, :<=, :>, :>=, :<=>].freeze
51
+ NUMERIC_BINARY = Set[
52
+ :+, :-, :*, :/, :%, :**, :&, :|, :^, :<<, :>>,
53
+ :<, :<=, :>, :>=, :==, :!=, :<=>,
54
+ :gcd, :lcm, :fdiv
55
+ ].freeze
56
+ STRING_BINARY = Set[
57
+ :+, :*, :==, :!=, :<, :<=, :>, :>=, :<=>,
58
+ :start_with?, :end_with?, :include?,
59
+ :delete_prefix, :delete_suffix,
60
+ :match?, :index, :rindex, :center, :ljust, :rjust
61
+ ].freeze
53
62
  SYMBOL_BINARY = Set[:==, :!=, :<=>, :<, :<=, :>, :>=].freeze
54
- BOOL_BINARY = Set[:&, :|, :^, :==, :!=].freeze
63
+ BOOL_BINARY = Set[:&, :|, :^, :==, :!=, :===].freeze
55
64
  NIL_BINARY = Set[:==, :!=].freeze
65
+ RATIONAL_BINARY = Set[:div, :modulo, :%, :remainder, :fdiv].freeze
56
66
 
57
67
  # v0.0.3 C — pure unary catalogue. Each method must:
58
68
  # - take zero arguments,
@@ -74,20 +84,23 @@ module Rigor
74
84
  :odd?, :even?, :zero?, :positive?, :negative?,
75
85
  :succ, :pred, :next, :abs, :magnitude,
76
86
  :bit_length, :to_s, :to_i, :to_int, :to_f,
87
+ :floor, :ceil, :round, :truncate, :chr,
77
88
  :inspect, :hash, :-@, :+@, :~
78
89
  ].freeze
79
90
  FLOAT_UNARY = Set[
80
91
  :zero?, :positive?, :negative?,
81
92
  :nan?, :finite?, :infinite?,
82
93
  :abs, :magnitude, :floor, :ceil, :round, :truncate,
94
+ :next_float, :prev_float,
83
95
  :to_s, :to_i, :to_int, :to_f,
84
96
  :inspect, :hash, :-@, :+@
85
97
  ].freeze
86
98
  STRING_UNARY = Set[
87
99
  :upcase, :downcase, :capitalize, :swapcase,
88
100
  :reverse, :length, :size, :bytesize,
89
- :empty?, :strip, :lstrip, :rstrip, :chomp,
101
+ :empty?, :strip, :lstrip, :rstrip, :chomp, :chop,
90
102
  :to_s, :to_str, :to_sym, :intern,
103
+ :to_i, :to_f, :ord, :chr, :hex, :oct, :succ, :next,
91
104
  :inspect, :hash
92
105
  ].freeze
93
106
  SYMBOL_UNARY = Set[
@@ -97,6 +110,11 @@ module Rigor
97
110
  ].freeze
98
111
  BOOL_UNARY = Set[:!, :to_s, :inspect, :hash, :&, :|, :^].freeze
99
112
  NIL_UNARY = Set[:nil?, :!, :to_s, :to_a, :to_h, :inspect, :hash].freeze
113
+ RATIONAL_UNARY = Set[
114
+ :zero?, :integer?, :real, :abs2,
115
+ :conj, :conjugate, :nonzero?
116
+ ].freeze
117
+ COMPLEX_UNARY = Set[:zero?, :nonzero?].freeze
100
118
 
101
119
  STRING_FOLD_BYTE_LIMIT = 4096
102
120
 
@@ -283,6 +301,18 @@ module Rigor
283
301
  pathname_lift = try_fold_pathname_unary(receiver_values, method_name)
284
302
  return pathname_lift if pathname_lift
285
303
 
304
+ regexp_lift = try_fold_regexp_array_unary(receiver_values, method_name)
305
+ return regexp_lift if regexp_lift
306
+
307
+ set_lift = try_fold_set_array_unary(receiver_values, method_name)
308
+ return set_lift if set_lift
309
+
310
+ integer_lift = try_fold_integer_array_unary(receiver_values, method_name)
311
+ return integer_lift if integer_lift
312
+
313
+ numeric_lift = try_fold_numeric_array_unary(receiver_values, method_name)
314
+ return numeric_lift if numeric_lift
315
+
286
316
  # Type-level allow check on every receiver. If one member's
287
317
  # type does not have the method in its allow list (e.g.
288
318
  # `Union[String, nil].nil?` — `:nil?` is not in
@@ -309,7 +339,7 @@ module Rigor
309
339
  # Only fires on a single-receiver Range with finite integer
310
340
  # endpoints; mixed unions fall through so the existing
311
341
  # union-of-Constants path keeps the rest of the arms.
312
- RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length].freeze
342
+ RANGE_FOLD_METHODS = Set[:to_a, :first, :last, :min, :max, :count, :size, :length, :entries, :minmax].freeze
313
343
  RANGE_TO_A_LIMIT = 16
314
344
  private_constant :RANGE_FOLD_METHODS, :RANGE_TO_A_LIMIT
315
345
 
@@ -326,10 +356,11 @@ module Rigor
326
356
 
327
357
  def range_constant_unary(range, method_name)
328
358
  case method_name
329
- when :to_a then range_to_a_tuple(range)
359
+ when :to_a, :entries then range_to_a_tuple(range)
330
360
  when :first, :min then range_endpoint_constant(range, :first)
331
361
  when :last, :max then range_endpoint_constant(range, :last)
332
362
  when :count, :size, :length then Type::Combinator.constant_of(range.to_a.size)
363
+ when :minmax then range_minmax_tuple(range)
333
364
  end
334
365
  end
335
366
 
@@ -348,6 +379,21 @@ module Rigor
348
379
  Type::Combinator.constant_of(edge == :first ? values.first : values.last)
349
380
  end
350
381
 
382
+ def range_minmax_tuple(range)
383
+ values = range.to_a
384
+ if values.empty?
385
+ return Type::Combinator.tuple_of(
386
+ Type::Combinator.constant_of(nil),
387
+ Type::Combinator.constant_of(nil)
388
+ )
389
+ end
390
+
391
+ Type::Combinator.tuple_of(
392
+ Type::Combinator.constant_of(values.first),
393
+ Type::Combinator.constant_of(values.last)
394
+ )
395
+ end
396
+
351
397
  def try_fold_binary_set(receiver_values, method_name, arg_values)
352
398
  string_lift = try_fold_string_array_binary(receiver_values, method_name, arg_values)
353
399
  return string_lift if string_lift
@@ -377,6 +423,30 @@ module Rigor
377
423
  :STRING_ARRAY_BINARY_METHODS,
378
424
  :STRING_ARRAY_LIFT_LIMIT
379
425
 
426
+ # `Constant<Regexp>#names` returns an Array of capture-group name
427
+ # strings. Lifted to a Tuple so downstream narrowing can project
428
+ # per-element types. The catalog classifies the C body as `:leaf`
429
+ # so it is safe to evaluate at fold time; no `$~` side effect.
430
+ REGEXP_ARRAY_UNARY_METHODS = Set[:names].freeze
431
+ private_constant :REGEXP_ARRAY_UNARY_METHODS
432
+
433
+ # `Constant<Set>#to_a` returns an Array of the set's elements.
434
+ # Ruby 3.2+ Set is C-implemented with a Hash as its backing store,
435
+ # so element ordering is deterministic (insertion order).
436
+ # The catalog marks `to_a` as `:dispatch` (it calls through to the
437
+ # internal hash), so this dedicated handler bypasses the catalog gate.
438
+ SET_ARRAY_UNARY_METHODS = Set[:to_a, :entries].freeze
439
+ private_constant :SET_ARRAY_UNARY_METHODS
440
+
441
+ # `Constant<Integer>#digits` returns the base-10 (or base-n with
442
+ # an argument — only the no-arg form is folded here) place
443
+ # values as a little-endian Array of Integers. Lifted to a
444
+ # Tuple so downstream rules see the precise per-position type.
445
+ # `digits` raises `Math::DomainError` on a negative receiver,
446
+ # so the negative case bails to the RBS tier.
447
+ INTEGER_ARRAY_UNARY_METHODS = Set[:digits].freeze
448
+ private_constant :INTEGER_ARRAY_UNARY_METHODS
449
+
380
450
  # v0.0.7 — `Constant<Pathname>` delegates to a curated set
381
451
  # of pure path-manipulation methods. Pathname is immutable
382
452
  # in Ruby (per its docstring) and the catalog classifies
@@ -446,6 +516,83 @@ module Rigor
446
516
  nil
447
517
  end
448
518
 
519
+ # `Constant<Regexp>#names` — lift the Array[String] of named-capture
520
+ # group names to a Tuple[Constant[String]…]. Safe to evaluate at fold
521
+ # time: the C body reads only the regexp's internal names table,
522
+ # writes no global state, and always returns an Array of frozen Strings.
523
+ def try_fold_regexp_array_unary(receiver_values, method_name)
524
+ return nil unless REGEXP_ARRAY_UNARY_METHODS.include?(method_name)
525
+ return nil unless receiver_values.size == 1
526
+
527
+ receiver = receiver_values.first
528
+ return nil unless receiver.is_a?(Regexp)
529
+
530
+ lift_array_result(receiver.public_send(method_name))
531
+ rescue StandardError
532
+ nil
533
+ end
534
+
535
+ # `Constant<Set>#to_a` / `#entries` — lift the Array of set elements
536
+ # to a Tuple[Constant[…]…] when every element is a foldable scalar.
537
+ # Ruby 3.2+ Set is C-implemented; element order is deterministic
538
+ # (insertion order), so the result is stable across invocations.
539
+ def try_fold_set_array_unary(receiver_values, method_name)
540
+ return nil unless SET_ARRAY_UNARY_METHODS.include?(method_name)
541
+ return nil unless receiver_values.size == 1
542
+
543
+ receiver = receiver_values.first
544
+ return nil unless receiver.is_a?(::Set)
545
+
546
+ lift_array_result(receiver.to_a)
547
+ rescue StandardError
548
+ nil
549
+ end
550
+
551
+ # `Constant<Integer>#digits` — lift the Array of base-10 place
552
+ # values to a Tuple[Constant[Integer]…]. Safe to evaluate at
553
+ # fold time: the C body is pure arithmetic. Negative receivers
554
+ # raise `Math::DomainError`; the fold declines so the RBS tier
555
+ # answers with `Array[Integer]`.
556
+ def try_fold_integer_array_unary(receiver_values, method_name)
557
+ return nil unless INTEGER_ARRAY_UNARY_METHODS.include?(method_name)
558
+ return nil unless receiver_values.size == 1
559
+
560
+ receiver = receiver_values.first
561
+ return nil unless receiver.is_a?(Integer)
562
+ return nil if receiver.negative?
563
+
564
+ lift_array_result(receiver.digits)
565
+ rescue StandardError
566
+ nil
567
+ end
568
+
569
+ # `Constant<Complex>#rect` / `#rectangular` — lifts `[real, imaginary]`
570
+ # to `Tuple[Constant[re], Constant[im]]`. Both components are always
571
+ # numeric (Integer or Float for literal complexes), so they satisfy
572
+ # `foldable_constant_value?`.
573
+ #
574
+ # `Constant<Complex>#polar` — lifts `[abs, arg]` to
575
+ # `Tuple[Constant[Float], Constant[Float]]`. Evaluated at fold time
576
+ # via `Complex#polar` (which calls `Math.hypot` and `Math.atan2`).
577
+ # Deterministic: reads only the receiver's real and imaginary parts.
578
+ #
579
+ # Rational receivers also support `rect` / `rectangular` / `polar`:
580
+ # `Rational(r,1).rect` → `[r, 0]`, `Rational(r,1).polar` → `[abs, arg]`.
581
+ NUMERIC_ARRAY_UNARY_METHODS = Set[:rect, :rectangular, :polar].freeze
582
+ private_constant :NUMERIC_ARRAY_UNARY_METHODS
583
+
584
+ def try_fold_numeric_array_unary(receiver_values, method_name)
585
+ return nil unless NUMERIC_ARRAY_UNARY_METHODS.include?(method_name)
586
+ return nil unless receiver_values.size == 1
587
+
588
+ receiver = receiver_values.first
589
+ return nil unless receiver.is_a?(Complex) || receiver.is_a?(Rational)
590
+
591
+ lift_array_result(receiver.public_send(method_name))
592
+ rescue StandardError
593
+ nil
594
+ end
595
+
449
596
  # `Constant<String>#split(arg)` / `#scan(arg)` — lift the
450
597
  # Array result to a Tuple when both sides are statically
451
598
  # known and the cardinality fits.
@@ -1101,6 +1248,8 @@ module Rigor
1101
1248
  when Symbol then SYMBOL_UNARY
1102
1249
  when true, false then BOOL_UNARY
1103
1250
  when nil then NIL_UNARY
1251
+ when Rational then RATIONAL_UNARY
1252
+ when Complex then COMPLEX_UNARY
1104
1253
  else Set.new
1105
1254
  end
1106
1255
  end
@@ -1126,11 +1275,15 @@ module Rigor
1126
1275
  # have their own shape carriers; this method picks
1127
1276
  # the conservative envelope of "values that already
1128
1277
  # round-trip through `Type::Combinator.constant_of`".
1278
+ FOLDABLE_CONSTANT_CLASSES = [
1279
+ Integer, Float, Rational, Complex, String, Symbol,
1280
+ Regexp, Pathname, ::Set, Date, Time,
1281
+ TrueClass, FalseClass, NilClass
1282
+ ].freeze
1283
+ private_constant :FOLDABLE_CONSTANT_CLASSES
1284
+
1129
1285
  def foldable_constant_value?(value)
1130
- case value
1131
- when Integer, Float, Rational, Complex, String, Symbol, Regexp, Pathname, true, false, nil then true
1132
- else false
1133
- end
1286
+ FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
1134
1287
  end
1135
1288
 
1136
1289
  def safe?(receiver_value, method_name, arg_value)
@@ -1150,6 +1303,7 @@ module Rigor
1150
1303
  when Symbol then SYMBOL_BINARY
1151
1304
  when true, false then BOOL_BINARY
1152
1305
  when nil then NIL_BINARY
1306
+ when Rational then RATIONAL_BINARY
1153
1307
  else Set.new
1154
1308
  end
1155
1309
  end
@@ -1169,10 +1323,19 @@ module Rigor
1169
1323
  case method_name
1170
1324
  when :+ then string_concat_blow_up?(receiver_value, arg_value)
1171
1325
  when :* then string_repeat_blow_up?(receiver_value, arg_value)
1326
+ when :center, :ljust, :rjust then string_pad_blow_up?(arg_value)
1172
1327
  else false
1173
1328
  end
1174
1329
  end
1175
1330
 
1331
+ # `"x".center(width)` / `#ljust` / `#rjust` produce a string
1332
+ # of `max(width, len)` characters. A literal `width` far
1333
+ # larger than the receiver would materialise a huge Constant;
1334
+ # cap it at the same byte limit the concat / repeat paths use.
1335
+ def string_pad_blow_up?(arg_value)
1336
+ arg_value.is_a?(Integer) && arg_value > STRING_FOLD_BYTE_LIMIT
1337
+ end
1338
+
1176
1339
  def string_concat_blow_up?(receiver_value, arg_value)
1177
1340
  arg_value.is_a?(String) &&
1178
1341
  receiver_value.bytesize + arg_value.bytesize > STRING_FOLD_BYTE_LIMIT
@@ -57,11 +57,63 @@ module Rigor
57
57
  return nil if receiver.nil?
58
58
  return try_array(args) if method_name == :Array
59
59
  return try_numeric_constructor(method_name, args) if NUMERIC_CONSTRUCTORS.key?(method_name)
60
- return try_integer_from_refinement(args) if method_name == :Integer
60
+ return try_integer(args) if method_name == :Integer
61
+ return try_float(args) if method_name == :Float
61
62
 
62
63
  nil
63
64
  end
64
65
 
66
+ # `Kernel#Integer(arg)` / `Integer(arg, base)`. Two folding
67
+ # paths, tried in order:
68
+ #
69
+ # 1. A `Refined[String, predicate]` argument whose predicate
70
+ # is a digit-only carrier narrows to `non-negative-int`
71
+ # (see {try_integer_from_refinement}).
72
+ # 2. A `Constant` String or Numeric argument — optionally
73
+ # with a `Constant[Integer]` base — runs the actual
74
+ # `Integer()` conversion and lifts the result to
75
+ # `Constant[Integer]`.
76
+ def try_integer(args)
77
+ refined = try_integer_from_refinement(args)
78
+ return refined if refined
79
+
80
+ try_integer_constant(args)
81
+ end
82
+
83
+ # Constant-folding path for `Integer()`. A non-parseable
84
+ # string raises `ArgumentError` (or `TypeError` for a base
85
+ # against a non-string) at fold time; the handler declines
86
+ # so the RBS tier answers with the widened `Integer`.
87
+ def try_integer_constant(args)
88
+ return nil unless [1, 2].include?(args.size)
89
+ return nil unless args.all?(Type::Constant)
90
+
91
+ values = args.map(&:value)
92
+ return nil unless values[0].is_a?(String) || values[0].is_a?(Numeric)
93
+ return nil if values.size == 2 && !values[1].is_a?(Integer)
94
+
95
+ Type::Combinator.constant_of(Integer(*values))
96
+ rescue ArgumentError, TypeError
97
+ nil
98
+ end
99
+
100
+ # `Kernel#Float(arg)` — folds a `Constant` String or Numeric
101
+ # argument to `Constant[Float]`. A non-parseable string
102
+ # raises `ArgumentError` at fold time; the handler declines.
103
+ def try_float(args)
104
+ return nil unless args.size == 1
105
+
106
+ arg = args.first
107
+ return nil unless arg.is_a?(Type::Constant)
108
+
109
+ value = arg.value
110
+ return nil unless value.is_a?(String) || value.is_a?(Numeric)
111
+
112
+ Type::Combinator.constant_of(Float(value))
113
+ rescue ArgumentError, TypeError
114
+ nil
115
+ end
116
+
65
117
  # `Kernel#Integer(s)` over a `Refined[String, predicate]`
66
118
  # whose predicate is in {INTEGER_REFINEMENT_PREDICATES}.
67
119
  # Mirrors the `String#to_i` projection in `ShapeDispatch`
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # Folds `Math` module-function calls on statically known numeric
9
+ # constants.
10
+ #
11
+ # `Math` is a pure, side-effect-free module whose functions are
12
+ # deterministic over their inputs — the same number always
13
+ # produces the same result. When every relevant argument is a
14
+ # `Constant` carrying a `Numeric` value, the analyzer evaluates
15
+ # the call at inference time and returns the concrete result.
16
+ #
17
+ # === Supported methods
18
+ #
19
+ # * Single-argument transcendental functions (`sqrt`, `cbrt`,
20
+ # `exp`, `log2`, `log10`, `log1p`, `expm1`, `sin`, `cos`,
21
+ # `tan`, `asin`, `acos`, `atan`, `sinh`, `cosh`, `tanh`,
22
+ # `asinh`, `acosh`, `atanh`, `erf`, `erfc`, `gamma`) — return
23
+ # `Constant[Float]`.
24
+ #
25
+ # * Two-argument functions (`atan2`, `hypot`, `ldexp`) — return
26
+ # `Constant[Float]`.
27
+ #
28
+ # * `log(x)` / `log(x, base)` — variadic; folds the 1- and
29
+ # 2-argument forms to `Constant[Float]`.
30
+ #
31
+ # * `frexp(x)` / `lgamma(x)` — return a two-element array, lifted
32
+ # to `Tuple[Constant[Float], Constant[Integer]]`.
33
+ #
34
+ # === Non-constant / unsupported cases
35
+ #
36
+ # Any call where the receiver is not `Singleton[Math]`, an
37
+ # argument is not a numeric `Constant`, the method is not in the
38
+ # supported set, or the Ruby call raises `Math::DomainError` /
39
+ # `RangeError` (domain-error inputs like `Math.sqrt(-1)`) returns
40
+ # `nil`, deferring to the next dispatcher tier.
41
+ #
42
+ # An infinite or NaN result (`Math.log(0.0)` → `-Infinity`) is
43
+ # still folded — `Constant[Float]` carries those values, matching
44
+ # `ConstantFolding`'s treatment of `Float / 0`.
45
+ module MathFolding
46
+ MATH_UNARY = Set[
47
+ :sqrt, :cbrt, :exp, :log2, :log10, :log1p, :expm1,
48
+ :sin, :cos, :tan, :asin, :acos, :atan,
49
+ :sinh, :cosh, :tanh, :asinh, :acosh, :atanh,
50
+ :erf, :erfc, :gamma
51
+ ].freeze
52
+ MATH_BINARY = Set[:atan2, :hypot, :ldexp].freeze
53
+ MATH_TUPLE_UNARY = Set[:frexp, :lgamma].freeze
54
+
55
+ private_constant :MATH_UNARY, :MATH_BINARY, :MATH_TUPLE_UNARY
56
+
57
+ module_function
58
+
59
+ # @return [Rigor::Type, nil] folded result, or nil to defer.
60
+ def try_dispatch(receiver:, method_name:, args:)
61
+ return nil unless dispatch_target?(receiver)
62
+
63
+ # `log` is variadic (1 or 2 args), so it cannot live in the
64
+ # fixed-arity sets above.
65
+ return fold_log(args) if method_name == :log
66
+ return fold_unary(method_name, args) if MATH_UNARY.include?(method_name)
67
+ return fold_binary(method_name, args) if MATH_BINARY.include?(method_name)
68
+ return fold_tuple_unary(method_name, args) if MATH_TUPLE_UNARY.include?(method_name)
69
+
70
+ nil
71
+ end
72
+
73
+ def dispatch_target?(receiver)
74
+ receiver.is_a?(Type::Singleton) && receiver.class_name == "Math"
75
+ end
76
+
77
+ # Unwraps a numeric `Constant` argument to its Ruby value.
78
+ # Returns nil for any non-`Constant` or non-`Numeric` carrier.
79
+ def numeric_constant(arg)
80
+ return nil unless arg.is_a?(Type::Constant)
81
+
82
+ value = arg.value
83
+ value.is_a?(Numeric) ? value : nil
84
+ end
85
+
86
+ def fold_unary(method_name, args)
87
+ return nil unless args.size == 1
88
+
89
+ x = numeric_constant(args.first)
90
+ return nil if x.nil?
91
+
92
+ fold_float_result(Math.public_send(method_name, x))
93
+ rescue Math::DomainError, RangeError
94
+ nil
95
+ end
96
+
97
+ def fold_binary(method_name, args)
98
+ return nil unless args.size == 2
99
+
100
+ a = numeric_constant(args[0])
101
+ b = numeric_constant(args[1])
102
+ return nil if a.nil? || b.nil?
103
+
104
+ fold_float_result(Math.public_send(method_name, a, b))
105
+ rescue Math::DomainError, RangeError
106
+ nil
107
+ end
108
+
109
+ # `Math.log(x)` and `Math.log(x, base)` — the only variadic
110
+ # `Math` function.
111
+ def fold_log(args)
112
+ return nil unless [1, 2].include?(args.size)
113
+
114
+ values = args.map { |a| numeric_constant(a) }
115
+ return nil if values.any?(&:nil?)
116
+
117
+ fold_float_result(Math.log(*values))
118
+ rescue Math::DomainError, RangeError
119
+ nil
120
+ end
121
+
122
+ # `Math.frexp` / `Math.lgamma` return a two-element array;
123
+ # lift it to `Tuple[Constant[Float], Constant[Integer]]`.
124
+ def fold_tuple_unary(method_name, args)
125
+ return nil unless args.size == 1
126
+
127
+ x = numeric_constant(args.first)
128
+ return nil if x.nil?
129
+
130
+ result = Math.public_send(method_name, x)
131
+ return nil unless result.is_a?(Array) && result.size == 2
132
+
133
+ Type::Combinator.tuple_of(
134
+ Type::Combinator.constant_of(result[0]),
135
+ Type::Combinator.constant_of(result[1])
136
+ )
137
+ rescue Math::DomainError, RangeError
138
+ nil
139
+ end
140
+
141
+ def fold_float_result(result)
142
+ return nil unless result.is_a?(Float)
143
+
144
+ Type::Combinator.constant_of(result)
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -122,13 +122,30 @@ module Rigor
122
122
  return match if match
123
123
  return overloads.find { |mt| overload_has_block?(mt) } if block_required
124
124
 
125
- overloads.first
125
+ # No block at the call site: prefer an overload that does
126
+ # not REQUIRE a block over `overloads.first`. Methods like
127
+ # `Array#filter` / `Enumerable#map` declare the block-
128
+ # bearing overload first (`() { ... } -> Array[Elem]`) and
129
+ # the bare-call overload second (`() -> Enumerator[...]`).
130
+ # Without this, a no-block `[1, 2].filter` would adopt the
131
+ # block overload's `Array[Elem]` return when the call
132
+ # actually yields an `Enumerator`.
133
+ overloads.find { |mt| !overload_requires_block?(mt) } || overloads.first
126
134
  end
127
135
 
128
136
  def overload_has_block?(method_type)
129
137
  method_type.respond_to?(:block) && method_type.block
130
138
  end
131
139
 
140
+ # True when the overload declares a block that the caller
141
+ # MUST supply (`{ ... }` in RBS). An optional block
142
+ # (`?{ ... }`) does NOT count — that overload is a valid
143
+ # match for a block-less call.
144
+ def overload_requires_block?(method_type)
145
+ block = overload_has_block?(method_type)
146
+ !!block && block.required
147
+ end
148
+
132
149
  class << self
133
150
  private
134
151
 
@@ -163,6 +180,7 @@ module Rigor
163
180
 
164
181
  overloads.find do |method_type|
165
182
  next false if block_required && !OverloadSelector.overload_has_block?(method_type)
183
+ next false if !block_required && OverloadSelector.overload_requires_block?(method_type)
166
184
  next false if strict && !strictly_typed_params?(method_type, arg_types.size)
167
185
 
168
186
  matches?(
@@ -199,6 +217,7 @@ module Rigor
199
217
  def find_matching_overload_via_aliases(overloads, arg_types:, block_required:)
200
218
  overloads.find do |method_type|
201
219
  next false if block_required && !OverloadSelector.overload_has_block?(method_type)
220
+ next false if !block_required && OverloadSelector.overload_requires_block?(method_type)
202
221
 
203
222
  fun = method_type.type
204
223
  next false unless arity_compatible?(fun, arg_types.size)
@@ -85,10 +85,17 @@ module Rigor
85
85
  # `Bundler::URI::Generic` per `Kernel#dup: () -> self`
86
86
  # rather than `Object`. Defaults to nil (compute self
87
87
  # from the resolved class_name as before).
88
+ # @param public_only [Boolean] when true, a method whose RBS
89
+ # accessibility is `:private` does not resolve (the call
90
+ # yields `nil`, i.e. "no rule"). Set by the explicit-
91
+ # non-`self`-receiver user-class fallback so a call like
92
+ # `Favourite.select(...)` does not adopt the private
93
+ # `Kernel#select` signature.
88
94
  # @return [Rigor::Type, nil] inferred return type, or `nil`
89
95
  # when no rule resolves (no class name, no method, dispatch
90
96
  # on a Top/Dynamic[Top] receiver, etc.).
91
- def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil, self_type_override: nil)
97
+ def try_dispatch(receiver:, method_name:, args:, environment:, block_type: nil, self_type_override: nil,
98
+ public_only: false)
92
99
  return nil if environment.nil?
93
100
  return nil unless environment.rbs_loader
94
101
 
@@ -98,7 +105,8 @@ module Rigor
98
105
  args: args,
99
106
  environment: environment,
100
107
  block_type: block_type,
101
- self_type_override: self_type_override
108
+ self_type_override: self_type_override,
109
+ public_only: public_only
102
110
  )
103
111
  end
104
112
 
@@ -140,32 +148,39 @@ module Rigor
140
148
  class << self
141
149
  private
142
150
 
143
- def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil)
151
+ def dispatch_for(receiver:, method_name:, args:, environment:, block_type:, self_type_override: nil,
152
+ public_only: false)
144
153
  args ||= []
145
154
  case receiver
146
155
  when Type::Union
147
- dispatch_union(receiver, method_name, args, environment, block_type, self_type_override)
156
+ dispatch_union(receiver, method_name, args, environment, block_type, self_type_override,
157
+ public_only: public_only)
148
158
  else
149
- dispatch_one(receiver, method_name, args, environment, block_type, self_type_override)
159
+ dispatch_one(receiver, method_name, args, environment, block_type, self_type_override,
160
+ public_only: public_only)
150
161
  end
151
162
  end
152
163
 
153
- def dispatch_union(receiver, method_name, args, environment, block_type, self_type_override = nil)
164
+ def dispatch_union(receiver, method_name, args, environment, block_type, self_type_override = nil,
165
+ public_only: false)
154
166
  results = receiver.members.map do |member|
155
- dispatch_one(member, method_name, args, environment, block_type, self_type_override)
167
+ dispatch_one(member, method_name, args, environment, block_type, self_type_override,
168
+ public_only: public_only)
156
169
  end
157
170
  return nil if results.any?(&:nil?)
158
171
 
159
172
  Type::Combinator.union(*results)
160
173
  end
161
174
 
162
- def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil)
175
+ def dispatch_one(receiver, method_name, args, environment, block_type, self_type_override = nil,
176
+ public_only: false)
163
177
  descriptor = receiver_descriptor(receiver)
164
178
  return nil unless descriptor
165
179
 
166
180
  class_name, kind, receiver_args = descriptor
167
181
  method_definition = lookup_method(environment, class_name, kind, method_name)
168
182
  return nil unless method_definition
183
+ return nil if public_only && method_private?(method_definition)
169
184
 
170
185
  type_vars = build_type_vars(environment, class_name, receiver_args)
171
186
  translate_return_type(
@@ -242,6 +257,16 @@ module Rigor
242
257
  ]
243
258
  end
244
259
 
260
+ # True when the RBS method definition is `private`. A call
261
+ # with an explicit, non-`self` receiver cannot reach a
262
+ # private method (Ruby raises `NoMethodError`), so the
263
+ # explicit-receiver user-class fallback uses this to reject
264
+ # private signatures rather than return a wrong type.
265
+ def method_private?(method_definition)
266
+ method_definition.respond_to?(:accessibility) &&
267
+ method_definition.accessibility == :private
268
+ end
269
+
245
270
  def lookup_method(environment, class_name, kind, method_name)
246
271
  case kind
247
272
  when :instance