rigortype 0.0.3 → 0.0.4
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 +24 -7
- data/data/builtins/ruby_core/hash.yml +936 -0
- data/data/builtins/ruby_core/range.yml +389 -0
- data/data/builtins/ruby_core/set.yml +594 -0
- data/data/builtins/ruby_core/time.yml +750 -0
- data/lib/rigor/analysis/check_rules.rb +11 -3
- data/lib/rigor/builtins/imported_refinements.rb +192 -10
- data/lib/rigor/inference/acceptance.rb +181 -12
- data/lib/rigor/inference/builtins/hash_catalog.rb +40 -0
- data/lib/rigor/inference/builtins/range_catalog.rb +46 -0
- data/lib/rigor/inference/builtins/set_catalog.rb +54 -0
- data/lib/rigor/inference/builtins/time_catalog.rb +64 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +28 -8
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +103 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +23 -7
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +135 -6
- data/lib/rigor/inference/method_parameter_binder.rb +29 -4
- data/lib/rigor/inference/narrowing.rb +2 -0
- data/lib/rigor/inference/statement_evaluator.rb +2 -0
- data/lib/rigor/rbs_extended.rb +167 -16
- data/lib/rigor/type/combinator.rb +90 -0
- data/lib/rigor/type/intersection.rb +135 -0
- data/lib/rigor/type/refined.rb +174 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/rbs_extended.rbs +11 -0
- data/sig/rigor/type.rbs +40 -0
- metadata +11 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Time` catalog. Singleton — load once, consult during dispatch.
|
|
9
|
+
#
|
|
10
|
+
# Time is a pure-C built-in: the Init block in
|
|
11
|
+
# `references/ruby/time.c` registers the bulk of the surface,
|
|
12
|
+
# and the Ruby-side prelude `references/ruby/timev.rb`
|
|
13
|
+
# contributes the class-side constructors (`Time.now`,
|
|
14
|
+
# `Time.at`, `Time.new`) through Primitive cexpr stubs.
|
|
15
|
+
#
|
|
16
|
+
# Time receivers are not lifted to a `Constant` carrier today
|
|
17
|
+
# (there is no `Time` literal node — the closest is
|
|
18
|
+
# `Time.now` / `Time.new(...)`, which produce `Nominal[Time]`).
|
|
19
|
+
# The catalog wiring therefore mostly governs:
|
|
20
|
+
#
|
|
21
|
+
# 1. The size-projection-equivalent reader surface (`#year`,
|
|
22
|
+
# `#month`, `#hour`, `#sec`, `#wday`, …) — RBS-declared
|
|
23
|
+
# `Integer` is preserved through dispatch.
|
|
24
|
+
# 2. The blocklist below, which keeps the indirect-mutator
|
|
25
|
+
# methods that the C-body classifier mis-flagged as
|
|
26
|
+
# `:leaf` from ever folding through a hypothetical future
|
|
27
|
+
# `Constant<Time>` carrier.
|
|
28
|
+
#
|
|
29
|
+
# The blocklist captures the false-positive `:leaf` entries
|
|
30
|
+
# whose helper functions the regex classifier did not
|
|
31
|
+
# recognise as mutators.
|
|
32
|
+
TIME_CATALOG = MethodCatalog.new(
|
|
33
|
+
path: File.expand_path(
|
|
34
|
+
"../../../../data/builtins/ruby_core/time.yml",
|
|
35
|
+
__dir__
|
|
36
|
+
),
|
|
37
|
+
mutating_selectors: {
|
|
38
|
+
"Time" => Set[
|
|
39
|
+
# `time_init_copy` writes the `timew` and `vtm` slots on
|
|
40
|
+
# the receiver via `time_set_timew` / `time_set_vtm`.
|
|
41
|
+
# Classed `:leaf` because those setters are not in the
|
|
42
|
+
# mutator regex's helper list. Blocked for symmetry with
|
|
43
|
+
# String / Array / Range / Set initialize_copy entries.
|
|
44
|
+
:initialize_copy,
|
|
45
|
+
# `time_localtime_m` -> `time_localtime` calls
|
|
46
|
+
# `time_modify(time)` to mark the receiver mutable
|
|
47
|
+
# before rewriting its `vtm` cache and `tzmode`. The
|
|
48
|
+
# docstring is explicit ("converts time to local time
|
|
49
|
+
# in place"). The C-body classifier mis-flagged it as
|
|
50
|
+
# `:leaf` because `time_modify` is not in its mutator
|
|
51
|
+
# regex.
|
|
52
|
+
:localtime,
|
|
53
|
+
# `time_gmtime` (registered as both `gmtime` and `utc`
|
|
54
|
+
# against `rb_cTime`) follows the same in-place pattern
|
|
55
|
+
# as `time_localtime`: `time_modify(time)` then a
|
|
56
|
+
# `time_set_vtm` write and `TZMODE_SET_UTC`. Both
|
|
57
|
+
# selectors share the cfunc, so both must be blocked.
|
|
58
|
+
:gmtime, :utc
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -4,6 +4,10 @@ require_relative "../../type"
|
|
|
4
4
|
require_relative "../builtins/numeric_catalog"
|
|
5
5
|
require_relative "../builtins/string_catalog"
|
|
6
6
|
require_relative "../builtins/array_catalog"
|
|
7
|
+
require_relative "../builtins/hash_catalog"
|
|
8
|
+
require_relative "../builtins/range_catalog"
|
|
9
|
+
require_relative "../builtins/set_catalog"
|
|
10
|
+
require_relative "../builtins/time_catalog"
|
|
7
11
|
|
|
8
12
|
module Rigor
|
|
9
13
|
module Inference
|
|
@@ -626,17 +630,33 @@ module Rigor
|
|
|
626
630
|
catalog.safe_for_folding?(class_name, method_name)
|
|
627
631
|
end
|
|
628
632
|
|
|
633
|
+
# `(catalog, class_name)` per receiver class. The class_name
|
|
634
|
+
# is what each catalog's RBS-rooted entries are keyed by.
|
|
635
|
+
# `catalog_for` walks this table in declaration order so
|
|
636
|
+
# subclasses (Symbol < String) hit their dedicated entry
|
|
637
|
+
# before any base-class fallback would, and adding a new
|
|
638
|
+
# class is a one-line addition rather than another `when`
|
|
639
|
+
# arm on a growing case statement.
|
|
640
|
+
CATALOG_BY_CLASS = [
|
|
641
|
+
[Integer, [Builtins::NumericCatalog, "Integer"]],
|
|
642
|
+
[Float, [Builtins::NumericCatalog, "Float"]],
|
|
643
|
+
[String, [Builtins::STRING_CATALOG, "String"]],
|
|
644
|
+
[Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
|
|
645
|
+
[Array, [Builtins::ARRAY_CATALOG, "Array"]],
|
|
646
|
+
[Hash, [Builtins::HASH_CATALOG, "Hash"]],
|
|
647
|
+
[Range, [Builtins::RANGE_CATALOG, "Range"]],
|
|
648
|
+
[::Set, [Builtins::SET_CATALOG, "Set"]],
|
|
649
|
+
[Time, [Builtins::TIME_CATALOG, "Time"]]
|
|
650
|
+
].freeze
|
|
651
|
+
private_constant :CATALOG_BY_CLASS
|
|
652
|
+
|
|
629
653
|
# Returns `[catalog, class_name]` for receivers we have a
|
|
630
|
-
# catalog for; nil otherwise.
|
|
631
|
-
# catalog's RBS-rooted entries are keyed by.
|
|
654
|
+
# catalog for; nil otherwise.
|
|
632
655
|
def catalog_for(receiver_value)
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
when Float then [Builtins::NumericCatalog, "Float"]
|
|
636
|
-
when String then [Builtins::STRING_CATALOG, "String"]
|
|
637
|
-
when Symbol then [Builtins::STRING_CATALOG, "Symbol"]
|
|
638
|
-
when Array then [Builtins::ARRAY_CATALOG, "Array"]
|
|
656
|
+
CATALOG_BY_CLASS.each do |klass, entry|
|
|
657
|
+
return entry if receiver_value.is_a?(klass)
|
|
639
658
|
end
|
|
659
|
+
nil
|
|
640
660
|
end
|
|
641
661
|
|
|
642
662
|
def unary_ops_for(receiver_value)
|
|
@@ -27,7 +27,7 @@ module Rigor
|
|
|
27
27
|
# - `a.downto(b) { |i| … }` yields the same domain `[b, a]`,
|
|
28
28
|
# just iterated in reverse. Lower bound from the
|
|
29
29
|
# argument, upper bound from the receiver.
|
|
30
|
-
module IteratorDispatch
|
|
30
|
+
module IteratorDispatch # rubocop:disable Metrics/ModuleLength
|
|
31
31
|
module_function
|
|
32
32
|
|
|
33
33
|
# @return [Array<Rigor::Type>, nil] block-param types, or
|
|
@@ -37,6 +37,7 @@ module Rigor
|
|
|
37
37
|
when :times then times_block_params(receiver)
|
|
38
38
|
when :upto then upto_block_params(receiver, args.first)
|
|
39
39
|
when :downto then downto_block_params(receiver, args.first)
|
|
40
|
+
when :each_with_index then each_with_index_block_params(receiver)
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
@@ -62,6 +63,107 @@ module Rigor
|
|
|
62
63
|
[build_index_range(lower_bound_of(end_arg), upper_bound_of(receiver))]
|
|
63
64
|
end
|
|
64
65
|
|
|
66
|
+
# Generalised iterator: every Enumerable-shaped collection
|
|
67
|
+
# in v0.0.4 yields `(element, index)` where the index is
|
|
68
|
+
# always `non-negative-int`. The element comes from the
|
|
69
|
+
# receiver's shape:
|
|
70
|
+
#
|
|
71
|
+
# - `Array[T]` / `Set[T]` / `Range[T]` → T
|
|
72
|
+
# - `Tuple[A, B, C]` → A | B | C
|
|
73
|
+
# (empty tuple cannot iterate, but we conservatively
|
|
74
|
+
# fall through to RBS so a missing rule never throws)
|
|
75
|
+
# - `Hash[K, V]` / `HashShape{...}` → Tuple[K, V]
|
|
76
|
+
# (Ruby yields `[key, value]` pairs as the element)
|
|
77
|
+
# - `Constant<Array>` / `Constant<Range>` / `Constant<Set>`
|
|
78
|
+
# → corresponding Constant element
|
|
79
|
+
#
|
|
80
|
+
# Receivers we cannot project (Top, Dynamic, unknown
|
|
81
|
+
# nominals, IO, …) decline so the RBS tier still answers
|
|
82
|
+
# — its element type is correct, only the index would
|
|
83
|
+
# widen to plain Integer.
|
|
84
|
+
def each_with_index_block_params(receiver)
|
|
85
|
+
element = element_type_of(receiver)
|
|
86
|
+
return nil if element.nil?
|
|
87
|
+
|
|
88
|
+
[element, Type::Combinator.non_negative_int]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
ELEMENT_BY_NOMINAL = {
|
|
92
|
+
"Array" => :nominal_unary_element,
|
|
93
|
+
"Set" => :nominal_unary_element,
|
|
94
|
+
"Range" => :nominal_unary_element,
|
|
95
|
+
"Hash" => :nominal_hash_pair_element
|
|
96
|
+
}.freeze
|
|
97
|
+
private_constant :ELEMENT_BY_NOMINAL
|
|
98
|
+
|
|
99
|
+
def element_type_of(receiver)
|
|
100
|
+
case receiver
|
|
101
|
+
when Type::Tuple then tuple_element(receiver)
|
|
102
|
+
when Type::HashShape then hash_shape_pair_element(receiver)
|
|
103
|
+
when Type::Nominal then nominal_element(receiver)
|
|
104
|
+
when Type::Constant then constant_element(receiver)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def tuple_element(tuple)
|
|
109
|
+
return nil if tuple.elements.empty?
|
|
110
|
+
return tuple.elements.first if tuple.elements.size == 1
|
|
111
|
+
|
|
112
|
+
Type::Combinator.union(*tuple.elements)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def hash_shape_pair_element(shape)
|
|
116
|
+
return nil if shape.pairs.empty?
|
|
117
|
+
|
|
118
|
+
key = Type::Combinator.union(*shape.pairs.keys.map { |k| Type::Combinator.constant_of(k) })
|
|
119
|
+
value = Type::Combinator.union(*shape.pairs.values)
|
|
120
|
+
Type::Combinator.tuple_of(key, value)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def nominal_element(nominal)
|
|
124
|
+
handler = ELEMENT_BY_NOMINAL[nominal.class_name]
|
|
125
|
+
return nil unless handler
|
|
126
|
+
|
|
127
|
+
send(handler, nominal)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def nominal_unary_element(nominal)
|
|
131
|
+
nominal.type_args.first
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def nominal_hash_pair_element(nominal)
|
|
135
|
+
key, value = nominal.type_args
|
|
136
|
+
return nil if key.nil? || value.nil?
|
|
137
|
+
|
|
138
|
+
Type::Combinator.tuple_of(key, value)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def constant_element(constant)
|
|
142
|
+
case constant.value
|
|
143
|
+
when Array
|
|
144
|
+
return nil if constant.value.empty?
|
|
145
|
+
|
|
146
|
+
Type::Combinator.union(*constant.value.map { |v| Type::Combinator.constant_of(v) })
|
|
147
|
+
when Range
|
|
148
|
+
range_constant_element(constant.value)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def range_constant_element(range)
|
|
153
|
+
beg = range.begin
|
|
154
|
+
en = range.end
|
|
155
|
+
return Type::Combinator.constant_of(beg) if beg.is_a?(Integer) && beg == en
|
|
156
|
+
|
|
157
|
+
if beg.is_a?(Integer) && en.is_a?(Integer)
|
|
158
|
+
upper = range.exclude_end? ? en - 1 : en
|
|
159
|
+
return build_index_range(beg, upper)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Mixed / non-integer ranges decline: the dispatcher
|
|
163
|
+
# falls through to RBS's element-type answer.
|
|
164
|
+
nil
|
|
165
|
+
end
|
|
166
|
+
|
|
65
167
|
# `Constant<Integer>`, `IntegerRange`, and `Nominal[Integer]`
|
|
66
168
|
# all participate. Non-integer types (Float, String, …) and
|
|
67
169
|
# `Top`/`Dynamic` decline so the RBS tier answers.
|
|
@@ -53,13 +53,22 @@ module Rigor
|
|
|
53
53
|
overloads = method_definition.method_types
|
|
54
54
|
return nil if overloads.empty?
|
|
55
55
|
|
|
56
|
+
# `rigor:v1:param: <name> <refinement>` annotations on
|
|
57
|
+
# this method override the RBS-declared parameter type
|
|
58
|
+
# at the matching name. The map is consumed inside
|
|
59
|
+
# `accepts_param?` so overload selection sees the
|
|
60
|
+
# tighter type when filtering candidates by argument
|
|
61
|
+
# compatibility.
|
|
62
|
+
param_overrides = RbsExtended.param_type_override_map(method_definition)
|
|
63
|
+
|
|
56
64
|
match = find_matching_overload(
|
|
57
65
|
overloads,
|
|
58
66
|
arg_types: arg_types,
|
|
59
67
|
self_type: self_type,
|
|
60
68
|
instance_type: instance_type,
|
|
61
69
|
type_vars: type_vars,
|
|
62
|
-
block_required: block_required
|
|
70
|
+
block_required: block_required,
|
|
71
|
+
param_overrides: param_overrides
|
|
63
72
|
)
|
|
64
73
|
return match if match
|
|
65
74
|
return overloads.find { |mt| overload_has_block?(mt) } if block_required
|
|
@@ -76,7 +85,8 @@ module Rigor
|
|
|
76
85
|
private
|
|
77
86
|
|
|
78
87
|
# rubocop:disable Metrics/ParameterLists
|
|
79
|
-
def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required
|
|
88
|
+
def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:,
|
|
89
|
+
param_overrides:)
|
|
80
90
|
overloads.find do |method_type|
|
|
81
91
|
next false if block_required && !OverloadSelector.overload_has_block?(method_type)
|
|
82
92
|
|
|
@@ -85,13 +95,15 @@ module Rigor
|
|
|
85
95
|
arg_types,
|
|
86
96
|
self_type: self_type,
|
|
87
97
|
instance_type: instance_type,
|
|
88
|
-
type_vars: type_vars
|
|
98
|
+
type_vars: type_vars,
|
|
99
|
+
param_overrides: param_overrides
|
|
89
100
|
)
|
|
90
101
|
end
|
|
91
102
|
end
|
|
92
103
|
# rubocop:enable Metrics/ParameterLists
|
|
93
104
|
|
|
94
|
-
|
|
105
|
+
# rubocop:disable Metrics/ParameterLists
|
|
106
|
+
def matches?(method_type, arg_types, self_type:, instance_type:, type_vars:, param_overrides:)
|
|
95
107
|
return false if method_type.respond_to?(:type_params) && rejects_keyword_required?(method_type)
|
|
96
108
|
|
|
97
109
|
fun = method_type.type
|
|
@@ -104,10 +116,12 @@ module Rigor
|
|
|
104
116
|
arg,
|
|
105
117
|
self_type: self_type,
|
|
106
118
|
instance_type: instance_type,
|
|
107
|
-
type_vars: type_vars
|
|
119
|
+
type_vars: type_vars,
|
|
120
|
+
param_overrides: param_overrides
|
|
108
121
|
)
|
|
109
122
|
end
|
|
110
123
|
end
|
|
124
|
+
# rubocop:enable Metrics/ParameterLists
|
|
111
125
|
|
|
112
126
|
# Slice 4 phase 2c does not pass keyword arguments through the
|
|
113
127
|
# call site (caller passes only positional `arg_types`). An
|
|
@@ -152,8 +166,9 @@ module Rigor
|
|
|
152
166
|
head
|
|
153
167
|
end
|
|
154
168
|
|
|
155
|
-
|
|
156
|
-
|
|
169
|
+
# rubocop:disable Metrics/ParameterLists
|
|
170
|
+
def accepts_param?(param, arg, self_type:, instance_type:, type_vars:, param_overrides:)
|
|
171
|
+
param_type = param_overrides[param.name] || RbsTypeTranslator.translate(
|
|
157
172
|
param.type,
|
|
158
173
|
self_type: self_type,
|
|
159
174
|
instance_type: instance_type,
|
|
@@ -162,6 +177,7 @@ module Rigor
|
|
|
162
177
|
result = param_type.accepts(arg, mode: :gradual)
|
|
163
178
|
result.yes? || result.maybe?
|
|
164
179
|
end
|
|
180
|
+
# rubocop:enable Metrics/ParameterLists
|
|
165
181
|
end
|
|
166
182
|
end
|
|
167
183
|
end
|
|
@@ -88,14 +88,25 @@ module Rigor
|
|
|
88
88
|
|
|
89
89
|
# @return [Rigor::Type, nil] the precise element/value type, or
|
|
90
90
|
# `nil` to defer to the next dispatcher tier.
|
|
91
|
+
# Per-carrier dispatch table. Adding a new carrier here
|
|
92
|
+
# is a one-row change; the helper methods stay private.
|
|
93
|
+
# Anonymous Type subclasses are not expected.
|
|
94
|
+
RECEIVER_HANDLERS = {
|
|
95
|
+
Type::Tuple => :dispatch_tuple,
|
|
96
|
+
Type::HashShape => :dispatch_hash_shape,
|
|
97
|
+
Type::Nominal => :dispatch_nominal_size,
|
|
98
|
+
Type::Difference => :dispatch_difference,
|
|
99
|
+
Type::Refined => :dispatch_refined,
|
|
100
|
+
Type::Intersection => :dispatch_intersection
|
|
101
|
+
}.freeze
|
|
102
|
+
private_constant :RECEIVER_HANDLERS
|
|
103
|
+
|
|
91
104
|
def try_dispatch(receiver:, method_name:, args:)
|
|
92
105
|
args ||= []
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
when Type::Difference then dispatch_difference(receiver, method_name, args)
|
|
98
|
-
end
|
|
106
|
+
handler = RECEIVER_HANDLERS[receiver.class]
|
|
107
|
+
return nil unless handler
|
|
108
|
+
|
|
109
|
+
send(handler, receiver, method_name, args)
|
|
99
110
|
end
|
|
100
111
|
|
|
101
112
|
# Tightens `Array#size` / `Array#length` / `String#length` /
|
|
@@ -220,6 +231,124 @@ module Rigor
|
|
|
220
231
|
Type::Combinator.positive_int
|
|
221
232
|
end
|
|
222
233
|
|
|
234
|
+
# Predicate-subset projections over a `Refined[base,
|
|
235
|
+
# predicate]` receiver. Today the catalogue is the
|
|
236
|
+
# String case-normalisation pair: `s.downcase` over a
|
|
237
|
+
# `lowercase-string` receiver folds to the same
|
|
238
|
+
# carrier (already lowercase), and `s.upcase` lifts a
|
|
239
|
+
# `lowercase-string` to `uppercase-string`. Symmetric
|
|
240
|
+
# rules apply with the predicates swapped. Numeric-
|
|
241
|
+
# string idempotence over `#downcase` / `#upcase` is
|
|
242
|
+
# also recognised because a numeric string equals its
|
|
243
|
+
# own case-normalisation.
|
|
244
|
+
#
|
|
245
|
+
# For methods this tier does not have a refinement-
|
|
246
|
+
# specific rule for, projection delegates to
|
|
247
|
+
# `dispatch_nominal_size` so size-returning calls on
|
|
248
|
+
# a `Refined[String, *]` still tighten to
|
|
249
|
+
# `non_negative_int`.
|
|
250
|
+
REFINED_STRING_PROJECTIONS = {
|
|
251
|
+
%i[lowercase downcase] => :refined_self,
|
|
252
|
+
%i[lowercase upcase] => :uppercase_string,
|
|
253
|
+
%i[uppercase upcase] => :refined_self,
|
|
254
|
+
%i[uppercase downcase] => :lowercase_string,
|
|
255
|
+
%i[numeric downcase] => :refined_self,
|
|
256
|
+
%i[numeric upcase] => :refined_self,
|
|
257
|
+
# Digit-only strings are case-invariant; the prefix
|
|
258
|
+
# letters in `0o…` / `0x…` are accepted by the
|
|
259
|
+
# predicate in either case so the predicate-subset
|
|
260
|
+
# is preserved across `#downcase` / `#upcase` even
|
|
261
|
+
# though the value-set element changes.
|
|
262
|
+
%i[decimal_int downcase] => :refined_self,
|
|
263
|
+
%i[decimal_int upcase] => :refined_self,
|
|
264
|
+
%i[octal_int downcase] => :refined_self,
|
|
265
|
+
%i[octal_int upcase] => :refined_self,
|
|
266
|
+
%i[hex_int downcase] => :refined_self,
|
|
267
|
+
%i[hex_int upcase] => :refined_self
|
|
268
|
+
}.freeze
|
|
269
|
+
private_constant :REFINED_STRING_PROJECTIONS
|
|
270
|
+
|
|
271
|
+
def dispatch_refined(refined, method_name, args)
|
|
272
|
+
base = refined.base
|
|
273
|
+
return nil unless base.is_a?(Type::Nominal)
|
|
274
|
+
|
|
275
|
+
if base.class_name == "String" && args.empty?
|
|
276
|
+
precise = refined_string_projection(refined, method_name)
|
|
277
|
+
return precise if precise
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
dispatch_nominal_size(base, method_name, args)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def refined_string_projection(refined, method_name)
|
|
284
|
+
handler = REFINED_STRING_PROJECTIONS[[refined.predicate_id, method_name]]
|
|
285
|
+
return nil unless handler
|
|
286
|
+
|
|
287
|
+
case handler
|
|
288
|
+
when :refined_self then refined
|
|
289
|
+
when :uppercase_string then Type::Combinator.uppercase_string
|
|
290
|
+
when :lowercase_string then Type::Combinator.lowercase_string
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Projects a method call over an `Intersection[M1, …]`
|
|
295
|
+
# receiver by collecting each member's projection and
|
|
296
|
+
# combining the results. The set-theoretic identity is
|
|
297
|
+
# `M(A ∩ B) ⊆ M(A) ∩ M(B)`, so the meet of the per-member
|
|
298
|
+
# projections is sound. Combining is best-effort:
|
|
299
|
+
#
|
|
300
|
+
# - If every result is a `Type::IntegerRange`, return
|
|
301
|
+
# their bounded-integer meet (max of lower bounds, min
|
|
302
|
+
# of upper bounds). This catches the common
|
|
303
|
+
# `(non_empty_string ∩ lowercase_string).size`
|
|
304
|
+
# pattern where one member projects to `positive-int`
|
|
305
|
+
# and the other to `non-negative-int`; the meet is
|
|
306
|
+
# `positive-int`.
|
|
307
|
+
# - Otherwise return the first non-nil result. A richer
|
|
308
|
+
# meet (e.g. of Difference + Refined results when both
|
|
309
|
+
# project) is left for a future slice; the carrier
|
|
310
|
+
# stays sound because every member's projection is
|
|
311
|
+
# already a superset of the true intersection.
|
|
312
|
+
#
|
|
313
|
+
# Returns nil when no member projects, so the caller
|
|
314
|
+
# falls through to the next dispatcher tier.
|
|
315
|
+
def dispatch_intersection(intersection, method_name, args)
|
|
316
|
+
results = intersection.members.filter_map do |member|
|
|
317
|
+
ShapeDispatch.try_dispatch(receiver: member, method_name: method_name, args: args)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
case results.size
|
|
321
|
+
when 0 then nil
|
|
322
|
+
when 1 then results.first
|
|
323
|
+
else combine_intersection_results(results)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def combine_intersection_results(results)
|
|
328
|
+
return narrow_integer_ranges(results) if results.all?(Type::IntegerRange)
|
|
329
|
+
|
|
330
|
+
results.first
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# Compute the bounded-integer meet of two or more
|
|
334
|
+
# `IntegerRange` carriers. We compare via the numeric
|
|
335
|
+
# `lower` / `upper` accessors (`-Float::INFINITY` /
|
|
336
|
+
# `Float::INFINITY` for the symbolic ends), then map
|
|
337
|
+
# back to the symbolic-bound representation
|
|
338
|
+
# `IntegerRange.new` expects. The disjoint-meet case
|
|
339
|
+
# cannot arise from sound member-wise projections in
|
|
340
|
+
# v0.0.4 but is guarded defensively to keep the
|
|
341
|
+
# carrier total.
|
|
342
|
+
def narrow_integer_ranges(ranges)
|
|
343
|
+
numeric_low = ranges.map(&:lower).max
|
|
344
|
+
numeric_high = ranges.map(&:upper).min
|
|
345
|
+
return Type::Combinator.bot if numeric_low > numeric_high
|
|
346
|
+
|
|
347
|
+
min = numeric_low == -Float::INFINITY ? Type::IntegerRange::NEG_INFINITY : numeric_low.to_i
|
|
348
|
+
max = numeric_high == Float::INFINITY ? Type::IntegerRange::POS_INFINITY : numeric_high.to_i
|
|
349
|
+
Type::Combinator.integer_range(min, max)
|
|
350
|
+
end
|
|
351
|
+
|
|
223
352
|
def tuple_first(tuple, _method_name, args)
|
|
224
353
|
return nil unless args.empty?
|
|
225
354
|
return Type::Combinator.constant_of(nil) if tuple.elements.empty?
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "prism"
|
|
4
4
|
|
|
5
5
|
require_relative "../type"
|
|
6
|
+
require_relative "../rbs_extended"
|
|
6
7
|
require_relative "rbs_type_translator"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
@@ -59,10 +60,13 @@ module Rigor
|
|
|
59
60
|
rbs_method = lookup_rbs_method(def_node)
|
|
60
61
|
return types unless rbs_method
|
|
61
62
|
|
|
62
|
-
method_types
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
apply_rbs_overloads(types, slots, rbs_method.method_types) unless rbs_method.method_types.empty?
|
|
64
|
+
# `rigor:v1:param: <name> <refinement>` annotations
|
|
65
|
+
# tighten the bound type for matching slots. Applied
|
|
66
|
+
# after the RBS-overload pass so the override is the
|
|
67
|
+
# authoritative answer regardless of what the RBS
|
|
68
|
+
# signature declared.
|
|
69
|
+
apply_param_overrides(types, slots, rbs_method)
|
|
66
70
|
types
|
|
67
71
|
end
|
|
68
72
|
|
|
@@ -165,6 +169,27 @@ module Rigor
|
|
|
165
169
|
end
|
|
166
170
|
end
|
|
167
171
|
|
|
172
|
+
# Reads the override map off the method's annotations and
|
|
173
|
+
# replaces the binding for any slot whose name appears in
|
|
174
|
+
# the map. Anonymous slots are skipped (no name to match).
|
|
175
|
+
# The override is used verbatim — no `:rest_*` re-wrapping —
|
|
176
|
+
# so authors who tighten a `*rest` parameter to e.g.
|
|
177
|
+
# `non-empty-array[Integer]` describe the parameter binding
|
|
178
|
+
# they actually want, not its element type.
|
|
179
|
+
def apply_param_overrides(types, slots, rbs_method)
|
|
180
|
+
override_map = RbsExtended.param_type_override_map(rbs_method)
|
|
181
|
+
return if override_map.empty?
|
|
182
|
+
|
|
183
|
+
slots.each do |slot|
|
|
184
|
+
next if slot.name.nil?
|
|
185
|
+
|
|
186
|
+
override = override_map[slot.name]
|
|
187
|
+
next if override.nil?
|
|
188
|
+
|
|
189
|
+
types[slot.name] = override
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
168
193
|
def collect_translated_types(method_types, slot)
|
|
169
194
|
rbs_types = method_types.flat_map do |mt|
|
|
170
195
|
t = rbs_type_for_slot(mt.type, slot)
|
|
@@ -1029,6 +1029,8 @@ module Rigor
|
|
|
1029
1029
|
# the effect's `negative?` flag. Shared between
|
|
1030
1030
|
# predicate-if-* and assert-if-* application paths.
|
|
1031
1031
|
def narrow_for_effect(current, effect, environment)
|
|
1032
|
+
return effect.refinement_type if effect.respond_to?(:refinement?) && effect.refinement?
|
|
1033
|
+
|
|
1032
1034
|
if effect.negative?
|
|
1033
1035
|
narrow_not_class(current, effect.class_name, exact: false, environment: environment)
|
|
1034
1036
|
else
|
|
@@ -894,6 +894,8 @@ module Rigor
|
|
|
894
894
|
end
|
|
895
895
|
|
|
896
896
|
def narrow_for_assert_effect(current_type, effect, environment)
|
|
897
|
+
return effect.refinement_type if effect.refinement?
|
|
898
|
+
|
|
897
899
|
if effect.negative?
|
|
898
900
|
Narrowing.narrow_not_class(current_type, effect.class_name, exact: false, environment: environment)
|
|
899
901
|
else
|