rigortype 0.1.8 → 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 +20 -0
- 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.rb +62 -4
- 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 +165 -6
- 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/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 +148 -1
- data/lib/rigor/inference/method_parameter_binder.rb +67 -10
- data/lib/rigor/inference/narrowing.rb +29 -10
- data/lib/rigor/inference/statement_evaluator.rb +3 -1
- 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/triage/catalogue.rb +2 -2
- data/lib/rigor/type/constant.rb +29 -2
- data/lib/rigor/version.rb +1 -1
- metadata +11 -1
|
@@ -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)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Folds `Regexp` class-method calls on statically known arguments.
|
|
9
|
+
#
|
|
10
|
+
# === Supported methods
|
|
11
|
+
#
|
|
12
|
+
# * `escape(str)` / `quote(str)` — escapes regexp meta-characters.
|
|
13
|
+
# Returns `Constant[String]`.
|
|
14
|
+
# * `new(str)` / `new(str, opts)` — constructs a Regexp at fold time
|
|
15
|
+
# when the pattern argument is a `Constant[String]`. The optional
|
|
16
|
+
# second argument may be a `Constant[Integer]` (flag bits), a
|
|
17
|
+
# `Constant[true/false]` (IGNORECASE shorthand), or absent.
|
|
18
|
+
# Returns `Constant[Regexp]`.
|
|
19
|
+
#
|
|
20
|
+
# === Non-constant / unsupported cases
|
|
21
|
+
#
|
|
22
|
+
# Returns `nil` (deferring to the next dispatcher tier) when:
|
|
23
|
+
# - the receiver is not `Singleton[Regexp]`,
|
|
24
|
+
# - the required pattern argument is not a `Constant[String]`,
|
|
25
|
+
# - the method is not in the supported set.
|
|
26
|
+
module RegexpFolding
|
|
27
|
+
REGEXP_ESCAPE_METHODS = Set[:escape, :quote].freeze
|
|
28
|
+
private_constant :REGEXP_ESCAPE_METHODS
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
33
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
34
|
+
return nil unless dispatch_target?(receiver)
|
|
35
|
+
return fold_escape(args) if REGEXP_ESCAPE_METHODS.include?(method_name)
|
|
36
|
+
return fold_new(args) if method_name == :new
|
|
37
|
+
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def dispatch_target?(receiver)
|
|
42
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "Regexp"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# `Regexp.escape(str)` / `.quote(str)` — one String arg.
|
|
46
|
+
def fold_escape(args)
|
|
47
|
+
return nil unless args.size == 1
|
|
48
|
+
|
|
49
|
+
arg = args.first
|
|
50
|
+
return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)
|
|
51
|
+
|
|
52
|
+
Type::Combinator.constant_of(Regexp.escape(arg.value))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# `Regexp.new(pattern)` / `Regexp.new(pattern, opts)` — constructs
|
|
56
|
+
# the pattern at inference time. Delegates to Ruby's real
|
|
57
|
+
# `Regexp.new` so all option forms (Integer flags, `true`/`false`,
|
|
58
|
+
# option strings) are handled without case-analysis; non-constant or
|
|
59
|
+
# invalid arguments decline through to the RBS tier.
|
|
60
|
+
def fold_new(args)
|
|
61
|
+
return nil if args.empty? || args.size > 2
|
|
62
|
+
|
|
63
|
+
pattern_arg = args.first
|
|
64
|
+
return nil unless pattern_arg.is_a?(Type::Constant) &&
|
|
65
|
+
pattern_arg.value.is_a?(String)
|
|
66
|
+
|
|
67
|
+
opts = args.size == 2 ? constant_value_or_nil(args[1]) : 0
|
|
68
|
+
return nil if args.size == 2 && opts.nil?
|
|
69
|
+
|
|
70
|
+
Type::Combinator.constant_of(Regexp.new(pattern_arg.value, opts))
|
|
71
|
+
rescue StandardError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def constant_value_or_nil(type)
|
|
76
|
+
type.is_a?(Type::Constant) ? type.value : nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Folds `Set` constructor calls into `Constant[Set]` when all
|
|
9
|
+
# arguments are statically known.
|
|
10
|
+
#
|
|
11
|
+
# Ruby 3.2+ implements Set in C (`set.c`). The constructor is
|
|
12
|
+
# deterministic — it reads only its arguments and writes nothing
|
|
13
|
+
# to global state.
|
|
14
|
+
#
|
|
15
|
+
# === Supported methods
|
|
16
|
+
#
|
|
17
|
+
# * `Set[]` — variadic constructor; each argument must be a
|
|
18
|
+
# `Constant[T]`. Returns `Constant[Set]`.
|
|
19
|
+
# * `Set.new` — zero-argument form; returns `Constant[Set.new]`.
|
|
20
|
+
# * `Set.new(tuple)` — the argument must be a `Tuple` whose
|
|
21
|
+
# every element is a `Constant[T]`. Returns `Constant[Set]`.
|
|
22
|
+
#
|
|
23
|
+
# === Non-constant / unsupported cases
|
|
24
|
+
#
|
|
25
|
+
# Returns `nil` (deferring to the next dispatcher tier) when:
|
|
26
|
+
# - the receiver is not `Singleton[Set]`,
|
|
27
|
+
# - the method is not `[]` or `new`,
|
|
28
|
+
# - any argument is non-constant or structurally opaque.
|
|
29
|
+
module SetFolding
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer.
|
|
33
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
34
|
+
return nil unless dispatch_target?(receiver)
|
|
35
|
+
|
|
36
|
+
case method_name
|
|
37
|
+
when :[] then fold_bracket(args)
|
|
38
|
+
when :new then fold_new(args)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def dispatch_target?(receiver)
|
|
43
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "Set"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# `Set["a", "b", "c"]` — all positional args must be Constant.
|
|
47
|
+
def fold_bracket(args)
|
|
48
|
+
values = args.map do |a|
|
|
49
|
+
return nil unless a.is_a?(Type::Constant)
|
|
50
|
+
|
|
51
|
+
a.value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Type::Combinator.constant_of(::Set.new(values))
|
|
55
|
+
rescue StandardError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# `Set.new` / `Set.new(tuple_of_constants)`.
|
|
60
|
+
def fold_new(args)
|
|
61
|
+
return Type::Combinator.constant_of(::Set.new) if args.empty?
|
|
62
|
+
return nil if args.size > 1
|
|
63
|
+
|
|
64
|
+
arg = args.first
|
|
65
|
+
values = case arg
|
|
66
|
+
when Type::Tuple
|
|
67
|
+
return nil unless arg.elements.all?(Type::Constant)
|
|
68
|
+
|
|
69
|
+
arg.elements.map(&:value)
|
|
70
|
+
else
|
|
71
|
+
return nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
Type::Combinator.constant_of(::Set.new(values))
|
|
75
|
+
rescue StandardError
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|