rigortype 0.1.8 → 0.1.10
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 +20 -0
- data/lib/rigor/analysis/runner.rb +67 -9
- data/lib/rigor/analysis/worker_session.rb +13 -4
- data/lib/rigor/cache/rbs_descriptor.rb +21 -2
- data/lib/rigor/cache/rbs_environment.rb +2 -1
- data/lib/rigor/cli/annotate_command.rb +274 -0
- data/lib/rigor/cli/baseline_command.rb +36 -16
- data/lib/rigor/cli/coverage_command.rb +126 -0
- data/lib/rigor/cli/coverage_renderer.rb +162 -0
- data/lib/rigor/cli/coverage_report.rb +75 -0
- data/lib/rigor/cli/mcp_command.rb +70 -0
- data/lib/rigor/cli/prism_colorizer.rb +111 -0
- data/lib/rigor/cli.rb +134 -6
- data/lib/rigor/environment/rbs_loader.rb +46 -5
- data/lib/rigor/environment/reporters.rb +3 -2
- data/lib/rigor/environment.rb +168 -5
- data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
- data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
- data/lib/rigor/inference/def_return_typer.rb +98 -0
- data/lib/rigor/inference/expression_typer.rb +308 -18
- data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +178 -10
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +62 -15
- 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/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 +431 -9
- 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 +148 -1
- data/lib/rigor/inference/method_parameter_binder.rb +67 -10
- data/lib/rigor/inference/narrowing.rb +29 -10
- data/lib/rigor/inference/precision_scanner.rb +131 -0
- data/lib/rigor/inference/statement_evaluator.rb +29 -3
- data/lib/rigor/mcp/loop.rb +43 -0
- data/lib/rigor/mcp/server.rb +263 -0
- data/lib/rigor/mcp.rb +16 -0
- data/lib/rigor/plugin/base.rb +67 -5
- data/lib/rigor/plugin/loader.rb +22 -1
- data/lib/rigor/plugin/manifest.rb +101 -10
- data/lib/rigor/plugin/protocol_contract.rb +185 -0
- data/lib/rigor/plugin/registry.rb +87 -0
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +51 -0
- data/lib/rigor/sig_gen/generator.rb +150 -75
- data/lib/rigor/triage/catalogue.rb +2 -2
- data/lib/rigor/type/combinator.rb +57 -0
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +39 -0
- data/sig/rigor/environment.rbs +3 -2
- data/sig/rigor/type.rbs +4 -0
- data/sig/rigor.rbs +2 -0
- metadata +42 -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.
|
|
@@ -841,6 +988,11 @@ module Rigor
|
|
|
841
988
|
magnitude: :range_unary_abs,
|
|
842
989
|
"-@": :range_unary_negate,
|
|
843
990
|
"+@": :range_unary_identity,
|
|
991
|
+
# `Integer#to_i` / `Integer#to_int` are identity operations —
|
|
992
|
+
# they return `self` — so the IntegerRange carrier is preserved.
|
|
993
|
+
# `positive-int.to_i → positive-int`, etc.
|
|
994
|
+
to_i: :range_unary_identity,
|
|
995
|
+
to_int: :range_unary_identity,
|
|
844
996
|
bit_length: :range_unary_bit_length
|
|
845
997
|
}.freeze
|
|
846
998
|
private_constant :UNARY_RANGE_DIRECT
|
|
@@ -1101,6 +1253,8 @@ module Rigor
|
|
|
1101
1253
|
when Symbol then SYMBOL_UNARY
|
|
1102
1254
|
when true, false then BOOL_UNARY
|
|
1103
1255
|
when nil then NIL_UNARY
|
|
1256
|
+
when Rational then RATIONAL_UNARY
|
|
1257
|
+
when Complex then COMPLEX_UNARY
|
|
1104
1258
|
else Set.new
|
|
1105
1259
|
end
|
|
1106
1260
|
end
|
|
@@ -1126,11 +1280,15 @@ module Rigor
|
|
|
1126
1280
|
# have their own shape carriers; this method picks
|
|
1127
1281
|
# the conservative envelope of "values that already
|
|
1128
1282
|
# round-trip through `Type::Combinator.constant_of`".
|
|
1283
|
+
FOLDABLE_CONSTANT_CLASSES = [
|
|
1284
|
+
Integer, Float, Rational, Complex, String, Symbol,
|
|
1285
|
+
Regexp, Pathname, ::Set, Date, Time,
|
|
1286
|
+
TrueClass, FalseClass, NilClass
|
|
1287
|
+
].freeze
|
|
1288
|
+
private_constant :FOLDABLE_CONSTANT_CLASSES
|
|
1289
|
+
|
|
1129
1290
|
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
|
|
1291
|
+
FOLDABLE_CONSTANT_CLASSES.any? { |klass| value.is_a?(klass) }
|
|
1134
1292
|
end
|
|
1135
1293
|
|
|
1136
1294
|
def safe?(receiver_value, method_name, arg_value)
|
|
@@ -1150,6 +1308,7 @@ module Rigor
|
|
|
1150
1308
|
when Symbol then SYMBOL_BINARY
|
|
1151
1309
|
when true, false then BOOL_BINARY
|
|
1152
1310
|
when nil then NIL_BINARY
|
|
1311
|
+
when Rational then RATIONAL_BINARY
|
|
1153
1312
|
else Set.new
|
|
1154
1313
|
end
|
|
1155
1314
|
end
|
|
@@ -1169,10 +1328,19 @@ module Rigor
|
|
|
1169
1328
|
case method_name
|
|
1170
1329
|
when :+ then string_concat_blow_up?(receiver_value, arg_value)
|
|
1171
1330
|
when :* then string_repeat_blow_up?(receiver_value, arg_value)
|
|
1331
|
+
when :center, :ljust, :rjust then string_pad_blow_up?(arg_value)
|
|
1172
1332
|
else false
|
|
1173
1333
|
end
|
|
1174
1334
|
end
|
|
1175
1335
|
|
|
1336
|
+
# `"x".center(width)` / `#ljust` / `#rjust` produce a string
|
|
1337
|
+
# of `max(width, len)` characters. A literal `width` far
|
|
1338
|
+
# larger than the receiver would materialise a huge Constant;
|
|
1339
|
+
# cap it at the same byte limit the concat / repeat paths use.
|
|
1340
|
+
def string_pad_blow_up?(arg_value)
|
|
1341
|
+
arg_value.is_a?(Integer) && arg_value > STRING_FOLD_BYTE_LIMIT
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1176
1344
|
def string_concat_blow_up?(receiver_value, arg_value)
|
|
1177
1345
|
arg_value.is_a?(String) &&
|
|
1178
1346
|
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`
|
|
@@ -65,6 +65,11 @@ module Rigor
|
|
|
65
65
|
# == ""`); the carrier collapses from `non-empty-literal-string`
|
|
66
66
|
# down to plain `literal-string`.
|
|
67
67
|
LITERAL_PRESERVING_METHODS = %i[strip lstrip rstrip chomp chop scrub].freeze
|
|
68
|
+
# Methods that preserve literal-bearing AND non-empty-string-ness.
|
|
69
|
+
# Unlike `LITERAL_PRESERVING_METHODS` (strip/chomp/etc.) these do
|
|
70
|
+
# not reduce the string — they transform characters without
|
|
71
|
+
# removing any, so a non-empty receiver stays non-empty.
|
|
72
|
+
NON_EMPTY_LITERAL_PRESERVING_METHODS = %i[upcase downcase capitalize swapcase reverse].freeze
|
|
68
73
|
# v0.1.1 Track 1 slice 5c — width-padding methods. `center`
|
|
69
74
|
# / `ljust` / `rjust` take a `width` Integer plus an
|
|
70
75
|
# optional literal padding `String`. When the receiver
|
|
@@ -72,28 +77,32 @@ module Rigor
|
|
|
72
77
|
# literal-bearing, the result is literal-bearing too.
|
|
73
78
|
WIDTH_PADDING_METHODS = %i[center ljust rjust].freeze
|
|
74
79
|
private_constant :CONCAT_METHODS, :FORMAT_METHODS,
|
|
75
|
-
:LITERAL_PRESERVING_METHODS, :
|
|
80
|
+
:LITERAL_PRESERVING_METHODS, :NON_EMPTY_LITERAL_PRESERVING_METHODS,
|
|
81
|
+
:WIDTH_PADDING_METHODS
|
|
76
82
|
|
|
77
83
|
def try_dispatch(receiver:, method_name:, args:, **)
|
|
78
84
|
return fold_array_join(receiver, args) if method_name == :join
|
|
79
85
|
return fold_format(args) if FORMAT_METHODS.include?(method_name)
|
|
80
|
-
|
|
81
86
|
return nil unless Type::Combinator.literal_string_compatible?(receiver)
|
|
82
|
-
|
|
83
87
|
return fold_string_percent(args) if method_name == :%
|
|
84
|
-
if args.empty?
|
|
85
|
-
return LITERAL_PRESERVING_METHODS.include?(method_name) ? Type::Combinator.literal_string : nil
|
|
86
|
-
end
|
|
88
|
+
return fold_no_arg(receiver, method_name) if args.empty?
|
|
87
89
|
return fold_width_pad(args) if WIDTH_PADDING_METHODS.include?(method_name)
|
|
88
90
|
return nil unless args.size == 1
|
|
89
91
|
|
|
90
92
|
if CONCAT_METHODS.include?(method_name)
|
|
91
|
-
fold_concat(args.first)
|
|
93
|
+
fold_concat(receiver, args.first)
|
|
92
94
|
elsif method_name == :*
|
|
93
|
-
fold_repeat(args.first)
|
|
95
|
+
fold_repeat(receiver, args.first)
|
|
94
96
|
end
|
|
95
97
|
end
|
|
96
98
|
|
|
99
|
+
def fold_no_arg(receiver, method_name)
|
|
100
|
+
return Type::Combinator.literal_string if LITERAL_PRESERVING_METHODS.include?(method_name)
|
|
101
|
+
return non_empty_literal_result(receiver) if NON_EMPTY_LITERAL_PRESERVING_METHODS.include?(method_name)
|
|
102
|
+
|
|
103
|
+
nil
|
|
104
|
+
end
|
|
105
|
+
|
|
97
106
|
# `String#center` / `#ljust` / `#rjust` — first argument is
|
|
98
107
|
# the target width (Integer-typed), optional second
|
|
99
108
|
# argument is the padding string (must be literal-bearing
|
|
@@ -111,19 +120,39 @@ module Rigor
|
|
|
111
120
|
Type::Combinator.literal_string
|
|
112
121
|
end
|
|
113
122
|
|
|
114
|
-
def fold_concat(
|
|
115
|
-
return nil unless Type::Combinator.literal_string_compatible?(
|
|
123
|
+
def fold_concat(receiver, arg)
|
|
124
|
+
return nil unless Type::Combinator.literal_string_compatible?(arg)
|
|
125
|
+
|
|
126
|
+
if Type::Combinator.non_empty_string_compatible?(receiver) ||
|
|
127
|
+
Type::Combinator.non_empty_string_compatible?(arg)
|
|
128
|
+
return Type::Combinator.non_empty_literal_string
|
|
129
|
+
end
|
|
116
130
|
|
|
117
131
|
Type::Combinator.literal_string
|
|
118
132
|
end
|
|
119
133
|
|
|
120
|
-
def fold_repeat(
|
|
121
|
-
return nil unless integer_typed?(
|
|
122
|
-
return nil if known_negative_integer?(
|
|
134
|
+
def fold_repeat(receiver, arg)
|
|
135
|
+
return nil unless integer_typed?(arg)
|
|
136
|
+
return nil if known_negative_integer?(arg)
|
|
137
|
+
return Type::Combinator.constant_of("") if known_zero_integer?(arg)
|
|
138
|
+
|
|
139
|
+
if Type::Combinator.non_empty_string_compatible?(receiver) && known_positive_integer?(arg)
|
|
140
|
+
return Type::Combinator.non_empty_literal_string
|
|
141
|
+
end
|
|
123
142
|
|
|
124
143
|
Type::Combinator.literal_string
|
|
125
144
|
end
|
|
126
145
|
|
|
146
|
+
# Returns `non_empty_literal_string` when the receiver is provably
|
|
147
|
+
# non-empty; otherwise collapses to plain `literal_string`.
|
|
148
|
+
def non_empty_literal_result(receiver)
|
|
149
|
+
if Type::Combinator.non_empty_string_compatible?(receiver)
|
|
150
|
+
Type::Combinator.non_empty_literal_string
|
|
151
|
+
else
|
|
152
|
+
Type::Combinator.literal_string
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
127
156
|
# `[lit, lit].join(sep)` — receiver must be a Tuple
|
|
128
157
|
# whose every element is literal-bearing; separator
|
|
129
158
|
# (when given) must be literal-bearing too. Multi-arg
|
|
@@ -205,9 +234,27 @@ module Rigor
|
|
|
205
234
|
type.is_a?(Type::Constant) && type.value.is_a?(Integer) && type.value.negative?
|
|
206
235
|
end
|
|
207
236
|
|
|
208
|
-
|
|
237
|
+
def known_zero_integer?(type)
|
|
238
|
+
case type
|
|
239
|
+
when Type::Constant then type.value.is_a?(Integer) && type.value.zero?
|
|
240
|
+
when Type::IntegerRange then type.lower.zero? && type.upper.zero?
|
|
241
|
+
else false
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def known_positive_integer?(type)
|
|
246
|
+
case type
|
|
247
|
+
when Type::Constant then type.value.is_a?(Integer) && type.value.positive?
|
|
248
|
+
when Type::IntegerRange then type.lower >= 1
|
|
249
|
+
else false
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
private_class_method :fold_no_arg, :fold_concat, :fold_repeat, :fold_array_join,
|
|
209
254
|
:fold_format, :fold_string_percent, :fold_width_pad,
|
|
210
|
-
:literal_or_constant?,
|
|
255
|
+
:non_empty_literal_result, :literal_or_constant?,
|
|
256
|
+
:integer_typed?, :known_negative_integer?,
|
|
257
|
+
:known_zero_integer?, :known_positive_integer?
|
|
211
258
|
end
|
|
212
259
|
end
|
|
213
260
|
end
|
|
@@ -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
|