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.
- checksums.yaml +4 -4
- data/README.md +186 -513
- data/lib/rigor/analysis/check_rules.rb +23 -1
- data/lib/rigor/analysis/diagnostic.rb +17 -3
- data/lib/rigor/analysis/runner.rb +178 -3
- data/lib/rigor/analysis/worker_session.rb +14 -3
- data/lib/rigor/cli/annotate_command.rb +224 -0
- data/lib/rigor/cli/baseline_command.rb +36 -16
- data/lib/rigor/cli/prism_colorizer.rb +111 -0
- data/lib/rigor/cli/triage_command.rb +83 -0
- data/lib/rigor/cli/triage_renderer.rb +77 -0
- data/lib/rigor/cli.rb +71 -5
- data/lib/rigor/environment.rb +9 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
- data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
- data/lib/rigor/inference/expression_typer.rb +300 -18
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +173 -10
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
- data/lib/rigor/inference/method_dispatcher/math_folding.rb +149 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +20 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +33 -8
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +316 -2
- data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +126 -0
- data/lib/rigor/inference/method_dispatcher/time_folding.rb +56 -0
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +179 -4
- data/lib/rigor/inference/method_parameter_binder.rb +67 -10
- data/lib/rigor/inference/narrowing.rb +29 -10
- data/lib/rigor/inference/scope_indexer.rb +156 -6
- data/lib/rigor/inference/statement_evaluator.rb +43 -21
- data/lib/rigor/plugin/base.rb +39 -0
- data/lib/rigor/plugin/loader.rb +22 -1
- data/lib/rigor/plugin/manifest.rb +73 -10
- data/lib/rigor/plugin/protocol_contract.rb +185 -0
- data/lib/rigor/plugin/registry.rb +66 -0
- data/lib/rigor/scope.rb +46 -0
- data/lib/rigor/triage/catalogue.rb +296 -0
- data/lib/rigor/triage/hint.rb +27 -0
- data/lib/rigor/triage.rb +89 -0
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/scope.rbs +6 -0
- 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[
|
|
52
|
-
|
|
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[:&, :|, :^, :==,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|