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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +186 -513
  3. data/lib/rigor/analysis/check_rules.rb +20 -0
  4. data/lib/rigor/cli/annotate_command.rb +224 -0
  5. data/lib/rigor/cli/baseline_command.rb +36 -16
  6. data/lib/rigor/cli/prism_colorizer.rb +111 -0
  7. data/lib/rigor/cli.rb +62 -4
  8. data/lib/rigor/environment.rb +9 -1
  9. data/lib/rigor/inference/builtins/method_catalog.rb +17 -1
  10. data/lib/rigor/inference/builtins/time_catalog.rb +10 -1
  11. data/lib/rigor/inference/expression_typer.rb +165 -6
  12. data/lib/rigor/inference/method_dispatcher/cgi_folding.rb +109 -0
  13. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +173 -10
  14. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +53 -1
  15. data/lib/rigor/inference/method_dispatcher/math_folding.rb +149 -0
  16. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +20 -1
  17. data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +81 -0
  18. data/lib/rigor/inference/method_dispatcher/set_folding.rb +81 -0
  19. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +316 -2
  20. data/lib/rigor/inference/method_dispatcher/shellwords_folding.rb +126 -0
  21. data/lib/rigor/inference/method_dispatcher/time_folding.rb +56 -0
  22. data/lib/rigor/inference/method_dispatcher/uri_folding.rb +67 -0
  23. data/lib/rigor/inference/method_dispatcher.rb +148 -1
  24. data/lib/rigor/inference/method_parameter_binder.rb +67 -10
  25. data/lib/rigor/inference/narrowing.rb +29 -10
  26. data/lib/rigor/inference/statement_evaluator.rb +3 -1
  27. data/lib/rigor/plugin/base.rb +39 -0
  28. data/lib/rigor/plugin/loader.rb +22 -1
  29. data/lib/rigor/plugin/manifest.rb +73 -10
  30. data/lib/rigor/plugin/protocol_contract.rb +185 -0
  31. data/lib/rigor/plugin/registry.rb +66 -0
  32. data/lib/rigor/triage/catalogue.rb +2 -2
  33. data/lib/rigor/type/constant.rb +29 -2
  34. data/lib/rigor/version.rb +1 -1
  35. 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
- 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)
@@ -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