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
|
@@ -693,14 +693,16 @@ module Rigor
|
|
|
693
693
|
return nil if method_def.nil? || method_def == true
|
|
694
694
|
return nil unless method_def.method_types.size == 1
|
|
695
695
|
|
|
696
|
-
|
|
696
|
+
param_overrides = Rigor::RbsExtended.param_type_override_map(method_def)
|
|
697
|
+
mismatch = first_argument_mismatch(method_def.method_types.first, call_node, scope, param_overrides)
|
|
697
698
|
return nil if mismatch.nil?
|
|
698
699
|
|
|
699
700
|
build_argument_type_diagnostic(path, call_node, class_name, mismatch)
|
|
700
701
|
end
|
|
701
702
|
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
702
703
|
|
|
703
|
-
|
|
704
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
705
|
+
def first_argument_mismatch(method_type, call_node, scope, param_overrides)
|
|
704
706
|
function = method_type.type
|
|
705
707
|
return nil unless argument_check_eligible?(function)
|
|
706
708
|
|
|
@@ -710,7 +712,12 @@ module Rigor
|
|
|
710
712
|
param = params[index]
|
|
711
713
|
next if param.nil? # arity mismatch is the wrong-arity rule's concern.
|
|
712
714
|
|
|
713
|
-
|
|
715
|
+
# `rigor:v1:param: <name> <refinement>` annotations
|
|
716
|
+
# tighten the RBS-declared parameter type. The
|
|
717
|
+
# override is the authoritative contract when
|
|
718
|
+
# present; otherwise we translate the RBS type as
|
|
719
|
+
# before.
|
|
720
|
+
param_type = param_overrides[param.name] || translate_param_type(param.type, scope.environment)
|
|
714
721
|
next if param_type.is_a?(Type::Dynamic) || param_type.is_a?(Type::Top)
|
|
715
722
|
|
|
716
723
|
arg_type = scope.type_of(arg)
|
|
@@ -721,6 +728,7 @@ module Rigor
|
|
|
721
728
|
end
|
|
722
729
|
nil
|
|
723
730
|
end
|
|
731
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
|
|
724
732
|
|
|
725
733
|
def argument_check_eligible?(function)
|
|
726
734
|
# See `arity_eligible?`: `UntypedFunction` lacks
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
3
5
|
require_relative "../type"
|
|
4
6
|
|
|
5
7
|
module Rigor
|
|
@@ -13,7 +15,7 @@ module Rigor
|
|
|
13
15
|
# `non-empty-array`, …) to the Rigor type each name denotes.
|
|
14
16
|
# The registry is the single integration point for:
|
|
15
17
|
#
|
|
16
|
-
# - The
|
|
18
|
+
# - The `rigor:v1:return:` RBS::Extended directive
|
|
17
19
|
# ([`Rigor::RbsExtended.read_return_type_override`](../rbs_extended.rb)),
|
|
18
20
|
# which overrides a method's RBS-declared return type
|
|
19
21
|
# with a refinement carrier.
|
|
@@ -28,12 +30,19 @@ module Rigor
|
|
|
28
30
|
# decide whether to fall back to the RBS-declared type or
|
|
29
31
|
# raise a parse error.
|
|
30
32
|
#
|
|
31
|
-
# The
|
|
32
|
-
#
|
|
33
|
-
# `non-empty-
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
33
|
+
# The registry covers two surfaces:
|
|
34
|
+
#
|
|
35
|
+
# - **No-argument refinement names** (`non-empty-string`,
|
|
36
|
+
# `non-zero-int`, `lowercase-string`, …) live in `REGISTRY`
|
|
37
|
+
# and resolve through `lookup(name)`.
|
|
38
|
+
# - **Parameterised refinement payloads** (`non-empty-array[Integer]`,
|
|
39
|
+
# `non-empty-hash[Symbol, Integer]`, `int<5, 10>`) are
|
|
40
|
+
# accepted by `parse(payload)`. The full grammar is documented
|
|
41
|
+
# on `Parser`. The two surfaces share `REGISTRY` for the
|
|
42
|
+
# no-arg head names; the parameterised head names live in
|
|
43
|
+
# `PARAMETERISED_TYPE_BUILDERS` (square-bracket form, type
|
|
44
|
+
# args) and `PARAMETERISED_INT_BUILDERS` (angle-bracket form,
|
|
45
|
+
# integer bounds).
|
|
37
46
|
module ImportedRefinements
|
|
38
47
|
REGISTRY = {
|
|
39
48
|
"non-empty-string" => -> { Type::Combinator.non_empty_string },
|
|
@@ -43,10 +52,51 @@ module Rigor
|
|
|
43
52
|
"positive-int" => -> { Type::Combinator.positive_int },
|
|
44
53
|
"non-negative-int" => -> { Type::Combinator.non_negative_int },
|
|
45
54
|
"negative-int" => -> { Type::Combinator.negative_int },
|
|
46
|
-
"non-positive-int" => -> { Type::Combinator.non_positive_int }
|
|
55
|
+
"non-positive-int" => -> { Type::Combinator.non_positive_int },
|
|
56
|
+
"lowercase-string" => -> { Type::Combinator.lowercase_string },
|
|
57
|
+
"uppercase-string" => -> { Type::Combinator.uppercase_string },
|
|
58
|
+
"numeric-string" => -> { Type::Combinator.numeric_string },
|
|
59
|
+
"decimal-int-string" => -> { Type::Combinator.decimal_int_string },
|
|
60
|
+
"octal-int-string" => -> { Type::Combinator.octal_int_string },
|
|
61
|
+
"hex-int-string" => -> { Type::Combinator.hex_int_string },
|
|
62
|
+
"non-empty-lowercase-string" => -> { Type::Combinator.non_empty_lowercase_string },
|
|
63
|
+
"non-empty-uppercase-string" => -> { Type::Combinator.non_empty_uppercase_string }
|
|
47
64
|
}.freeze
|
|
48
65
|
private_constant :REGISTRY
|
|
49
66
|
|
|
67
|
+
# `name[T]` / `name[K, V]` — type-arg parameterised
|
|
68
|
+
# refinements. Each builder takes an `Array<Rigor::Type>`
|
|
69
|
+
# and returns a `Rigor::Type` (or `nil` on arity / shape
|
|
70
|
+
# mismatch so the caller surfaces a parse failure).
|
|
71
|
+
PARAMETERISED_TYPE_BUILDERS = {
|
|
72
|
+
"non-empty-array" => lambda { |args|
|
|
73
|
+
return nil unless args.size == 1
|
|
74
|
+
|
|
75
|
+
Type::Combinator.non_empty_array(args.first)
|
|
76
|
+
},
|
|
77
|
+
"non-empty-hash" => lambda { |args|
|
|
78
|
+
return nil unless args.size == 2
|
|
79
|
+
|
|
80
|
+
Type::Combinator.non_empty_hash(args[0], args[1])
|
|
81
|
+
}
|
|
82
|
+
}.freeze
|
|
83
|
+
private_constant :PARAMETERISED_TYPE_BUILDERS
|
|
84
|
+
|
|
85
|
+
# `name<min, max>` — integer-bound parameterised
|
|
86
|
+
# refinements. Each builder takes an `Array<Integer>` and
|
|
87
|
+
# returns a `Rigor::Type` (or `nil`). Bounds are signed
|
|
88
|
+
# integer literals; `min` MUST be ≤ `max` for the carrier
|
|
89
|
+
# to construct successfully (`Type::IntegerRange` enforces
|
|
90
|
+
# the invariant).
|
|
91
|
+
PARAMETERISED_INT_BUILDERS = {
|
|
92
|
+
"int" => lambda { |bounds|
|
|
93
|
+
return nil unless bounds.size == 2
|
|
94
|
+
|
|
95
|
+
Type::Combinator.integer_range(bounds[0], bounds[1])
|
|
96
|
+
}
|
|
97
|
+
}.freeze
|
|
98
|
+
private_constant :PARAMETERISED_INT_BUILDERS
|
|
99
|
+
|
|
50
100
|
module_function
|
|
51
101
|
|
|
52
102
|
# @param name [String] kebab-case refinement name.
|
|
@@ -57,13 +107,145 @@ module Rigor
|
|
|
57
107
|
builder&.call
|
|
58
108
|
end
|
|
59
109
|
|
|
110
|
+
# @param payload [String] the trailing payload of a
|
|
111
|
+
# `rigor:v1:return:` (or sibling) directive. Accepts
|
|
112
|
+
# the bare-name forms `lookup` already handles plus the
|
|
113
|
+
# parameterised forms documented on {Parser}.
|
|
114
|
+
# @return [Rigor::Type, nil] the resolved refinement
|
|
115
|
+
# carrier, or `nil` when the payload is unparseable or
|
|
116
|
+
# names a refinement / class not in the registry.
|
|
117
|
+
def parse(payload)
|
|
118
|
+
Parser.new(payload.to_s).parse
|
|
119
|
+
end
|
|
120
|
+
|
|
60
121
|
def known?(name)
|
|
61
|
-
REGISTRY.key?(name.to_s)
|
|
122
|
+
REGISTRY.key?(name.to_s) ||
|
|
123
|
+
PARAMETERISED_TYPE_BUILDERS.key?(name.to_s) ||
|
|
124
|
+
PARAMETERISED_INT_BUILDERS.key?(name.to_s)
|
|
62
125
|
end
|
|
63
126
|
|
|
64
127
|
def known_names
|
|
65
|
-
REGISTRY.keys
|
|
128
|
+
REGISTRY.keys + PARAMETERISED_TYPE_BUILDERS.keys + PARAMETERISED_INT_BUILDERS.keys
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Recursive-descent parser for the refinement-payload
|
|
132
|
+
# grammar:
|
|
133
|
+
#
|
|
134
|
+
# type := simple_name | parametric
|
|
135
|
+
# simple_name := /[a-z][a-z0-9-]*/
|
|
136
|
+
# parametric := simple_name '[' type_arg_list ']'
|
|
137
|
+
# | simple_name '<' int_bound_list '>'
|
|
138
|
+
# type_arg_list := type_arg (',' type_arg)*
|
|
139
|
+
# type_arg := type | class_name
|
|
140
|
+
# class_name := /[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*/
|
|
141
|
+
# int_bound_list := signed_int (',' signed_int)*
|
|
142
|
+
# signed_int := /-?\d+/
|
|
143
|
+
#
|
|
144
|
+
# Whitespace between tokens is ignored. The parser fails
|
|
145
|
+
# soft (returns `nil` from `parse`) on any deviation so the
|
|
146
|
+
# `RBS::Extended` directive site can fall back to the
|
|
147
|
+
# RBS-declared type rather than crash on a typo.
|
|
148
|
+
class Parser
|
|
149
|
+
def initialize(input)
|
|
150
|
+
@scanner = StringScanner.new(input.strip)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def parse
|
|
154
|
+
type = parse_type
|
|
155
|
+
return nil if type.nil?
|
|
156
|
+
return nil unless @scanner.eos?
|
|
157
|
+
|
|
158
|
+
type
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
private
|
|
162
|
+
|
|
163
|
+
SIMPLE_NAME = /[a-z][a-z0-9-]*/
|
|
164
|
+
CLASS_NAME = /[A-Z][A-Za-z0-9_]*(?:::[A-Z][A-Za-z0-9_]*)*/
|
|
165
|
+
SIGNED_INT = /-?\d+/
|
|
166
|
+
private_constant :SIMPLE_NAME, :CLASS_NAME, :SIGNED_INT
|
|
167
|
+
|
|
168
|
+
def parse_type
|
|
169
|
+
name = @scanner.scan(SIMPLE_NAME)
|
|
170
|
+
return nil if name.nil?
|
|
171
|
+
|
|
172
|
+
case @scanner.peek(1)
|
|
173
|
+
when "[" then parse_parametric_type_args(name)
|
|
174
|
+
when "<" then parse_parametric_int_bounds(name)
|
|
175
|
+
else ImportedRefinements.lookup(name)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def parse_parametric_type_args(name)
|
|
180
|
+
builder = PARAMETERISED_TYPE_BUILDERS[name]
|
|
181
|
+
return nil if builder.nil?
|
|
182
|
+
|
|
183
|
+
@scanner.getch # consume '['
|
|
184
|
+
args = parse_type_arg_list
|
|
185
|
+
return nil if args.nil?
|
|
186
|
+
return nil unless @scanner.getch == "]"
|
|
187
|
+
|
|
188
|
+
builder.call(args)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def parse_parametric_int_bounds(name)
|
|
192
|
+
builder = PARAMETERISED_INT_BUILDERS[name]
|
|
193
|
+
return nil if builder.nil?
|
|
194
|
+
|
|
195
|
+
@scanner.getch # consume '<'
|
|
196
|
+
bounds = parse_int_bound_list
|
|
197
|
+
return nil if bounds.nil?
|
|
198
|
+
return nil unless @scanner.getch == ">"
|
|
199
|
+
|
|
200
|
+
builder.call(bounds)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def parse_type_arg_list
|
|
204
|
+
collect_separated_list { parse_type_arg }
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def parse_int_bound_list
|
|
208
|
+
collect_separated_list { parse_int_bound }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def collect_separated_list
|
|
212
|
+
items = []
|
|
213
|
+
loop do
|
|
214
|
+
skip_ws
|
|
215
|
+
item = yield
|
|
216
|
+
return nil if item.nil?
|
|
217
|
+
|
|
218
|
+
items << item
|
|
219
|
+
skip_ws
|
|
220
|
+
break unless @scanner.peek(1) == ","
|
|
221
|
+
|
|
222
|
+
@scanner.getch # consume ','
|
|
223
|
+
end
|
|
224
|
+
items
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def parse_type_arg
|
|
228
|
+
skip_ws
|
|
229
|
+
if (class_name = @scanner.scan(CLASS_NAME))
|
|
230
|
+
Type::Combinator.nominal_of(class_name)
|
|
231
|
+
else
|
|
232
|
+
parse_type
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def parse_int_bound
|
|
237
|
+
skip_ws
|
|
238
|
+
literal = @scanner.scan(SIGNED_INT)
|
|
239
|
+
return nil if literal.nil?
|
|
240
|
+
|
|
241
|
+
Integer(literal)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def skip_ws
|
|
245
|
+
@scanner.skip(/\s+/)
|
|
246
|
+
end
|
|
66
247
|
end
|
|
248
|
+
private_constant :Parser
|
|
67
249
|
end
|
|
68
250
|
end
|
|
69
251
|
end
|
|
@@ -47,7 +47,18 @@ module Rigor
|
|
|
47
47
|
if other_type.is_a?(Type::Dynamic)
|
|
48
48
|
return Type::AcceptsResult.yes(mode: mode, reasons: "gradual: Dynamic[T] passes any boundary")
|
|
49
49
|
end
|
|
50
|
+
|
|
51
|
+
# Structural equality short-circuit. Two identical carriers
|
|
52
|
+
# describe the same value set, so they always accept each
|
|
53
|
+
# other. This is sound for any mode and covers cases where
|
|
54
|
+
# neither side has a per-class rule for the other's exact
|
|
55
|
+
# carrier kind (the canonical example is
|
|
56
|
+
# `Intersection.accepts(Intersection)`, where the disjunction
|
|
57
|
+
# rule below would otherwise reject equal-but-narrow LHSes).
|
|
58
|
+
return Type::AcceptsResult.yes(mode: mode, reasons: "structural equality") if self_type == other_type
|
|
59
|
+
|
|
50
60
|
return accepts_union_other(self_type, other_type, mode) if other_type.is_a?(Type::Union)
|
|
61
|
+
return accepts_intersection_other(self_type, other_type, mode) if other_type.is_a?(Type::Intersection)
|
|
51
62
|
|
|
52
63
|
accepts_one(self_type, other_type, mode)
|
|
53
64
|
end
|
|
@@ -66,6 +77,8 @@ module Rigor
|
|
|
66
77
|
Type::Constant => :accepts_constant,
|
|
67
78
|
Type::IntegerRange => :accepts_integer_range,
|
|
68
79
|
Type::Difference => :accepts_difference,
|
|
80
|
+
Type::Refined => :accepts_refined,
|
|
81
|
+
Type::Intersection => :accepts_intersection,
|
|
69
82
|
Type::Tuple => :accepts_tuple,
|
|
70
83
|
Type::HashShape => :accepts_hash_shape
|
|
71
84
|
}.freeze
|
|
@@ -128,6 +141,27 @@ module Rigor
|
|
|
128
141
|
end
|
|
129
142
|
end
|
|
130
143
|
|
|
144
|
+
# self.accepts(Intersection[Y, Z]) iff self accepts at least
|
|
145
|
+
# one Y_i. Disjunction across members because the intersection
|
|
146
|
+
# is the meet of its members' value sets, so containment in
|
|
147
|
+
# any one member implies containment of the whole
|
|
148
|
+
# intersection. Symmetric counterpart to
|
|
149
|
+
# `accepts_union_other`.
|
|
150
|
+
def accepts_intersection_other(self_type, intersection, mode)
|
|
151
|
+
results = intersection.members.map { |m| accepts(self_type, m, mode: mode) }
|
|
152
|
+
|
|
153
|
+
if results.any?(&:yes?)
|
|
154
|
+
Type::AcceptsResult.yes(mode: mode, reasons: "self accepts an intersection member")
|
|
155
|
+
elsif results.any?(&:maybe?)
|
|
156
|
+
Type::AcceptsResult.maybe(
|
|
157
|
+
mode: mode,
|
|
158
|
+
reasons: "self could not be proven to accept any intersection member"
|
|
159
|
+
)
|
|
160
|
+
else
|
|
161
|
+
Type::AcceptsResult.no(mode: mode, reasons: "self rejects every intersection member")
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
131
165
|
# self.accepts(Union[Y, Z]) iff self accepts every Y_i. Strict
|
|
132
166
|
# AND across members: any "no" turns the whole result no, any
|
|
133
167
|
# "maybe" without a "no" gives maybe, all "yes" gives yes.
|
|
@@ -186,20 +220,40 @@ module Rigor
|
|
|
186
220
|
# - Singleton: never (wrong value kind).
|
|
187
221
|
def accepts_nominal(self_type, other_type, mode)
|
|
188
222
|
case other_type
|
|
189
|
-
when Type::Nominal
|
|
190
|
-
|
|
191
|
-
when Type::
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
223
|
+
when Type::Nominal then accepts_nominal_from_nominal(self_type, other_type, mode)
|
|
224
|
+
when Type::Constant then accepts_nominal_from_constant(self_type, other_type, mode)
|
|
225
|
+
when Type::Singleton then accepts_nominal_from_singleton(self_type, other_type, mode)
|
|
226
|
+
when Type::IntegerRange then accepts_nominal_from_integer_range(self_type, other_type, mode)
|
|
227
|
+
else accepts_nominal_from_shape(self_type, other_type, mode)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Tail of `accepts_nominal` that handles structural shape
|
|
232
|
+
# carriers (`Tuple` / `HashShape`) and refinement carriers
|
|
233
|
+
# (`Difference` / `Refined`). Each branch projects the
|
|
234
|
+
# other-side carrier to the nominal layer it sits above
|
|
235
|
+
# and re-runs acceptance — soundness follows because the
|
|
236
|
+
# carrier's value set is contained in the projected
|
|
237
|
+
# nominal's value set.
|
|
238
|
+
def accepts_nominal_from_shape(self_type, other_type, mode)
|
|
239
|
+
case other_type
|
|
197
240
|
when Type::Tuple
|
|
198
241
|
accepts(self_type, project_tuple_to_nominal(other_type), mode: mode)
|
|
199
242
|
.with_reason("projected Tuple to Nominal[Array]")
|
|
200
243
|
when Type::HashShape
|
|
201
244
|
accepts(self_type, project_hash_shape_to_nominal(other_type), mode: mode)
|
|
202
245
|
.with_reason("projected HashShape to Nominal[Hash]")
|
|
246
|
+
when Type::Difference, Type::Refined
|
|
247
|
+
# A refinement carrier's value set is a subset of its
|
|
248
|
+
# base. So if `self` (Nominal) accepts the base, it
|
|
249
|
+
# also accepts the refinement; if it rejects the
|
|
250
|
+
# base, it cannot accept any subset of it. Forward
|
|
251
|
+
# through to the base nominal so the standard subtype
|
|
252
|
+
# check applies. The recursion is bounded because
|
|
253
|
+
# every refinement carrier's `base` is closer to the
|
|
254
|
+
# nominal layer.
|
|
255
|
+
accepts(self_type, other_type.base, mode: mode)
|
|
256
|
+
.with_reason("projected #{other_type.class.name.split('::').last} to its base")
|
|
203
257
|
else
|
|
204
258
|
Type::AcceptsResult.no(
|
|
205
259
|
mode: mode,
|
|
@@ -486,10 +540,125 @@ module Rigor
|
|
|
486
540
|
when Type::Constant
|
|
487
541
|
!(removed.is_a?(Type::Constant) && removed.value == other_type.value)
|
|
488
542
|
when Type::Difference
|
|
489
|
-
# `Difference[A,
|
|
490
|
-
#
|
|
491
|
-
#
|
|
492
|
-
|
|
543
|
+
# `Difference[A, R].accepts(Difference[B, R])`: the
|
|
544
|
+
# other carrier already excludes `R` at its difference
|
|
545
|
+
# layer, so the disjointness is exhibited regardless of
|
|
546
|
+
# how `B` (its base) relates to `R`. We do NOT recurse
|
|
547
|
+
# into `other_type.base` because that would always fail
|
|
548
|
+
# (a Nominal base contains the removed value).
|
|
549
|
+
other_type.removed == removed
|
|
550
|
+
when Type::Intersection
|
|
551
|
+
# Disjointness is monotonic over Intersection: if any
|
|
552
|
+
# member is provably disjoint from `removed`, the meet
|
|
553
|
+
# is too.
|
|
554
|
+
other_type.members.any? { |m| provably_disjoint_from_removed?(m, removed) }
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# `Refined[base, predicate]` accepts another type X when
|
|
559
|
+
# the base accepts the *base* of X *and* X is provably
|
|
560
|
+
# contained in the predicate's value set. The base
|
|
561
|
+
# check is delegated to `accepts(self.base, X.base)`
|
|
562
|
+
# so handlers like `accepts_nominal` see Nominal-vs-
|
|
563
|
+
# Nominal and return their normal answer (the inner
|
|
564
|
+
# `accepts_nominal` does not register `Refined` /
|
|
565
|
+
# `Difference` as direct other-shapes — projecting to
|
|
566
|
+
# the base is what makes the comparison meaningful).
|
|
567
|
+
#
|
|
568
|
+
# Provability rules in gradual mode (the conservative
|
|
569
|
+
# analogue of `accepts_difference`):
|
|
570
|
+
#
|
|
571
|
+
# - X is a `Refined` with the *same* predicate_id —
|
|
572
|
+
# exact predicate match, accept.
|
|
573
|
+
# - X is a `Constant` whose value the predicate's
|
|
574
|
+
# recogniser accepts — the value is statically
|
|
575
|
+
# contained, accept. A recognised non-match is `:no`.
|
|
576
|
+
# - Anything else (Nominal, Union, IntegerRange,
|
|
577
|
+
# Difference) — predicate-subset cannot be proven
|
|
578
|
+
# without a runtime test, so reject under gradual
|
|
579
|
+
# mode rather than degrade to `:maybe`. Mirrors the
|
|
580
|
+
# `accepts_difference` policy.
|
|
581
|
+
def accepts_refined(self_type, other_type, mode)
|
|
582
|
+
case other_type
|
|
583
|
+
when Type::Refined then accepts_refined_from_refined(self_type, other_type, mode)
|
|
584
|
+
when Type::Constant then accepts_refined_from_constant(self_type, other_type, mode)
|
|
585
|
+
else accepts_refined_other_shape(self_type, other_type, mode)
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def accepts_refined_from_refined(self_type, other_type, mode)
|
|
590
|
+
base_result = accepts(self_type.base, other_type.base, mode: mode)
|
|
591
|
+
return base_result if base_result.no?
|
|
592
|
+
|
|
593
|
+
if other_type.predicate_id == self_type.predicate_id
|
|
594
|
+
base_result.with_reason("matching predicate :#{self_type.predicate_id}")
|
|
595
|
+
else
|
|
596
|
+
Type::AcceptsResult.no(
|
|
597
|
+
mode: mode,
|
|
598
|
+
reasons: "predicate mismatch: :#{self_type.predicate_id} vs :#{other_type.predicate_id}"
|
|
599
|
+
)
|
|
600
|
+
end
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def accepts_refined_from_constant(self_type, constant, mode)
|
|
604
|
+
base_result = accepts(self_type.base, constant, mode: mode)
|
|
605
|
+
return base_result if base_result.no?
|
|
606
|
+
|
|
607
|
+
case self_type.matches?(constant.value)
|
|
608
|
+
when true
|
|
609
|
+
base_result.with_reason("Constant value satisfies :#{self_type.predicate_id}")
|
|
610
|
+
when false
|
|
611
|
+
Type::AcceptsResult.no(
|
|
612
|
+
mode: mode,
|
|
613
|
+
reasons: "Constant value fails :#{self_type.predicate_id}"
|
|
614
|
+
)
|
|
615
|
+
else
|
|
616
|
+
Type::AcceptsResult.maybe(
|
|
617
|
+
mode: mode,
|
|
618
|
+
reasons: "predicate :#{self_type.predicate_id} not in registry"
|
|
619
|
+
)
|
|
620
|
+
end
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
def accepts_refined_other_shape(self_type, other_type, mode)
|
|
624
|
+
base_result = accepts(self_type.base, other_type, mode: mode)
|
|
625
|
+
return base_result if base_result.no?
|
|
626
|
+
|
|
627
|
+
Type::AcceptsResult.no(
|
|
628
|
+
mode: mode,
|
|
629
|
+
reasons: "#{self_type.describe} cannot prove #{other_type.class} satisfies " \
|
|
630
|
+
":#{self_type.predicate_id}"
|
|
631
|
+
)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
# `Intersection[M1, M2, …]` accepts X iff *every* member
|
|
635
|
+
# accepts X — the meet of value sets is contained iff the
|
|
636
|
+
# candidate is contained in each. Conjunctive combine: any
|
|
637
|
+
# `:no` makes the result `:no`, any `:maybe` without a
|
|
638
|
+
# `:no` makes the result `:maybe`, all `:yes` makes the
|
|
639
|
+
# result `:yes`. The 0-member case is unreachable because
|
|
640
|
+
# `Combinator.intersection` collapses empty intersections
|
|
641
|
+
# to `Top`.
|
|
642
|
+
def accepts_intersection(self_type, other_type, mode)
|
|
643
|
+
per_member = self_type.members.map { |m| accepts(m, other_type, mode: mode) }
|
|
644
|
+
|
|
645
|
+
if per_member.any?(&:no?)
|
|
646
|
+
return Type::AcceptsResult.no(
|
|
647
|
+
mode: mode,
|
|
648
|
+
reasons: "an intersection member rejected #{other_type.class}"
|
|
649
|
+
)
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
if per_member.any?(&:maybe?)
|
|
653
|
+
Type::AcceptsResult.maybe(
|
|
654
|
+
mode: mode,
|
|
655
|
+
reasons: "an intersection member could not be proven accepted"
|
|
656
|
+
)
|
|
657
|
+
else
|
|
658
|
+
Type::AcceptsResult.yes(
|
|
659
|
+
mode: mode,
|
|
660
|
+
reasons: "every intersection member accepted #{other_type.class}"
|
|
661
|
+
)
|
|
493
662
|
end
|
|
494
663
|
end
|
|
495
664
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Hash` catalog. Singleton — load once, consult during dispatch.
|
|
9
|
+
#
|
|
10
|
+
# Hash mirrors Array's mutation pattern: nearly every iteration
|
|
11
|
+
# method yields through `rb_hash_foreach` plus a per-pair static
|
|
12
|
+
# callback (`each_value_i`, `keep_if_i`, …), and the C-body
|
|
13
|
+
# classifier does not follow into the callback so it lands as
|
|
14
|
+
# `:leaf` despite being block-dependent. The blocklist below
|
|
15
|
+
# captures every false-positive `:leaf` we have spotted in the
|
|
16
|
+
# generated YAML — bias toward conservatism so a missed fold is
|
|
17
|
+
# acceptable but a folded mutator/yielder is not.
|
|
18
|
+
HASH_CATALOG = MethodCatalog.new(
|
|
19
|
+
path: File.expand_path(
|
|
20
|
+
"../../../../data/builtins/ruby_core/hash.yml",
|
|
21
|
+
__dir__
|
|
22
|
+
),
|
|
23
|
+
mutating_selectors: {
|
|
24
|
+
"Hash" => Set[
|
|
25
|
+
# Block-dependent iteration — yields via `rb_hash_foreach`
|
|
26
|
+
# plus a per-pair callback that the regex classifier does
|
|
27
|
+
# not follow:
|
|
28
|
+
:each, :each_pair, :each_key, :each_value,
|
|
29
|
+
:select, :filter, :reject,
|
|
30
|
+
:transform_values,
|
|
31
|
+
# Block-dependent merge — `rb_hash_merge` delegates into
|
|
32
|
+
# `rb_hash_update`, which yields per conflict when a block
|
|
33
|
+
# is given:
|
|
34
|
+
:merge
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Range` catalog. Singleton — load once, consult during
|
|
9
|
+
# dispatch.
|
|
10
|
+
#
|
|
11
|
+
# Range is largely immutable: `begin`, `end`, and `excl` are
|
|
12
|
+
# set at construction by `range_initialize` and never mutated
|
|
13
|
+
# afterwards. The blocklist below therefore stays small. The
|
|
14
|
+
# entries we DO need are the iteration methods whose C body
|
|
15
|
+
# routes through a helper the block/yield regex does not
|
|
16
|
+
# recognise, so the classifier mis-flags them as `:leaf`
|
|
17
|
+
# despite yielding to a block.
|
|
18
|
+
RANGE_CATALOG = MethodCatalog.new(
|
|
19
|
+
path: File.expand_path(
|
|
20
|
+
"../../../../data/builtins/ruby_core/range.yml",
|
|
21
|
+
__dir__
|
|
22
|
+
),
|
|
23
|
+
mutating_selectors: {
|
|
24
|
+
"Range" => Set[
|
|
25
|
+
# `range_initialize` / `range_initialize_copy` write
|
|
26
|
+
# `begin`/`end`/`excl` slots on the receiver; classed
|
|
27
|
+
# `:leaf` because the writes go through the struct
|
|
28
|
+
# accessor not `rb_check_frozen`. Blocked for symmetry
|
|
29
|
+
# with String / Array.
|
|
30
|
+
:initialize, :initialize_copy,
|
|
31
|
+
# `range_reverse_each` yields to its block via
|
|
32
|
+
# `range_each_func` -> caller's block; the regex
|
|
33
|
+
# classifier follows direct `rb_yield*` calls only.
|
|
34
|
+
:reverse_each,
|
|
35
|
+
# `range_percent_step` returns an Enumerator unless a
|
|
36
|
+
# block is supplied, in which case it yields. Treated
|
|
37
|
+
# as block-dependent so the fold tier never invokes it
|
|
38
|
+
# against a literal Range and tries to materialise an
|
|
39
|
+
# Enumerator into a Constant.
|
|
40
|
+
:%
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Set` catalog. Singleton — load once, consult during dispatch.
|
|
9
|
+
#
|
|
10
|
+
# Set was rewritten in C and folded into CRuby for Ruby 3.2+;
|
|
11
|
+
# the reference branch (`ruby_4_0`) ships the implementation in
|
|
12
|
+
# `references/ruby/set.c` with `Init_Set` registering every
|
|
13
|
+
# method directly. There is no `set.rb` prelude — the trailing
|
|
14
|
+
# `rb_provide("set.rb")` makes `require "set"` a no-op against
|
|
15
|
+
# the built-in.
|
|
16
|
+
#
|
|
17
|
+
# The blocklist below catches the catalog `:leaf` entries the
|
|
18
|
+
# C-body classifier mis-attributes. Set's iteration helpers
|
|
19
|
+
# (`set_iter`, `RETURN_SIZED_ENUMERATOR`) and its identity-
|
|
20
|
+
# mode and reset paths drive into helpers the regex classifier
|
|
21
|
+
# does not yet recognise as block-yielding or mutating.
|
|
22
|
+
SET_CATALOG = MethodCatalog.new(
|
|
23
|
+
path: File.expand_path(
|
|
24
|
+
"../../../../data/builtins/ruby_core/set.yml",
|
|
25
|
+
__dir__
|
|
26
|
+
),
|
|
27
|
+
mutating_selectors: {
|
|
28
|
+
"Set" => Set[
|
|
29
|
+
# Indirect mutators classified `:leaf` because the C
|
|
30
|
+
# classifier did not follow the helper functions:
|
|
31
|
+
#
|
|
32
|
+
# - `initialize_copy` calls `set_copy` to overwrite the
|
|
33
|
+
# receiver's table.
|
|
34
|
+
# - `compare_by_identity` swaps the internal hash type
|
|
35
|
+
# via `set_reset_table_with_type`.
|
|
36
|
+
# - `reset` rebuilds the internal table to dedup after
|
|
37
|
+
# element mutation.
|
|
38
|
+
:initialize_copy, :compare_by_identity, :reset,
|
|
39
|
+
# Block-dependent methods classified `:leaf` because the
|
|
40
|
+
# C body uses `set_iter` / `RETURN_SIZED_ENUMERATOR`
|
|
41
|
+
# rather than calling `rb_yield` directly:
|
|
42
|
+
:each, :classify, :divide,
|
|
43
|
+
# `disjoint?` delegates into `set_i_intersect`, which
|
|
44
|
+
# for non-Set enumerables uses `rb_funcall(other,
|
|
45
|
+
# :any?, ...)` — that is user-redefinable dispatch the
|
|
46
|
+
# classifier missed because the call site is in a
|
|
47
|
+
# sibling function.
|
|
48
|
+
:disjoint?
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|