rigortype 0.0.4 → 0.0.5
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 +215 -134
- data/data/builtins/ruby_core/comparable.yml +87 -0
- data/data/builtins/ruby_core/complex.yml +505 -0
- data/data/builtins/ruby_core/date.yml +1737 -0
- data/data/builtins/ruby_core/enumerable.yml +557 -0
- data/data/builtins/ruby_core/file.yml +9 -0
- data/data/builtins/ruby_core/rational.yml +365 -0
- data/data/builtins/ruby_core/string.yml +9 -0
- data/data/builtins/ruby_core/time.yml +6 -4
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/inference/builtins/comparable_catalog.rb +27 -0
- data/lib/rigor/inference/builtins/complex_catalog.rb +41 -0
- data/lib/rigor/inference/builtins/date_catalog.rb +98 -0
- data/lib/rigor/inference/builtins/enumerable_catalog.rb +27 -0
- data/lib/rigor/inference/builtins/rational_catalog.rb +38 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +126 -12
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +99 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +95 -0
- data/lib/rigor/inference/method_dispatcher.rb +3 -1
- data/lib/rigor/inference/narrowing.rb +210 -1
- data/lib/rigor/inference/scope_indexer.rb +87 -11
- data/lib/rigor/inference/statement_evaluator.rb +5 -1
- data/lib/rigor/rbs_extended.rb +11 -6
- data/lib/rigor/type/integer_range.rb +4 -2
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +4 -6
- data/sig/rigor/inference.rbs +2 -1
- data/sig/rigor/type.rbs +41 -41
- metadata +12 -1
|
@@ -8,6 +8,11 @@ require_relative "../builtins/hash_catalog"
|
|
|
8
8
|
require_relative "../builtins/range_catalog"
|
|
9
9
|
require_relative "../builtins/set_catalog"
|
|
10
10
|
require_relative "../builtins/time_catalog"
|
|
11
|
+
require_relative "../builtins/date_catalog"
|
|
12
|
+
require_relative "../builtins/comparable_catalog"
|
|
13
|
+
require_relative "../builtins/enumerable_catalog"
|
|
14
|
+
require_relative "../builtins/rational_catalog"
|
|
15
|
+
require_relative "../builtins/complex_catalog"
|
|
11
16
|
|
|
12
17
|
module Rigor
|
|
13
18
|
module Inference
|
|
@@ -109,9 +114,14 @@ module Rigor
|
|
|
109
114
|
arg_sets = args.map { |a| numeric_set_of(a) }
|
|
110
115
|
return nil if arg_sets.any?(&:nil?)
|
|
111
116
|
|
|
112
|
-
|
|
117
|
+
dispatch_by_arity(receiver_set, method_name, arg_sets)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def dispatch_by_arity(receiver_set, method_name, arg_sets)
|
|
121
|
+
case arg_sets.size
|
|
113
122
|
when 0 then try_fold_unary(receiver_set, method_name)
|
|
114
123
|
when 1 then try_fold_binary(receiver_set, method_name, arg_sets.first)
|
|
124
|
+
when 2 then try_fold_ternary(receiver_set, method_name, arg_sets)
|
|
115
125
|
end
|
|
116
126
|
end
|
|
117
127
|
|
|
@@ -220,6 +230,38 @@ module Rigor
|
|
|
220
230
|
build_constant_type(results, source: receiver_values + arg_values)
|
|
221
231
|
end
|
|
222
232
|
|
|
233
|
+
# 2-arg fold dispatch. Used by `Comparable#between?(min, max)`,
|
|
234
|
+
# `Comparable#clamp(min, max)`, and `Integer#pow(exp, mod)` —
|
|
235
|
+
# methods the catalog classifies `:leaf` but that the prior
|
|
236
|
+
# 0/1-arg switch could not reach. Range receivers/args are
|
|
237
|
+
# held back: a precise 2-arg range fold (e.g.
|
|
238
|
+
# `int<0,10>.between?(0, 10)` → `Constant[true]`) is a
|
|
239
|
+
# follow-up; for now any IntegerRange operand bails to the
|
|
240
|
+
# RBS tier.
|
|
241
|
+
def try_fold_ternary(receiver_set, method_name, arg_sets)
|
|
242
|
+
return nil if receiver_set.is_a?(Type::IntegerRange)
|
|
243
|
+
return nil if arg_sets.any?(Type::IntegerRange)
|
|
244
|
+
|
|
245
|
+
try_fold_ternary_set(receiver_set, method_name, arg_sets)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def try_fold_ternary_set(receiver_values, method_name, arg_sets)
|
|
249
|
+
total = receiver_values.size * arg_sets[0].size * arg_sets[1].size
|
|
250
|
+
return nil if total > UNION_FOLD_INPUT_LIMIT
|
|
251
|
+
return nil unless receiver_values.all? { |rv| ternary_method_allowed?(rv, method_name) }
|
|
252
|
+
|
|
253
|
+
results = ternary_cartesian(receiver_values, method_name, arg_sets)
|
|
254
|
+
build_constant_type(results, source: receiver_values + arg_sets.flatten)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def ternary_cartesian(receiver_values, method_name, arg_sets)
|
|
258
|
+
receiver_values.flat_map do |rv|
|
|
259
|
+
arg_sets[0].flat_map do |av0|
|
|
260
|
+
arg_sets[1].flat_map { |av1| invoke_ternary(rv, method_name, av0, av1) || [] }
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
223
265
|
def unary_method_allowed?(receiver_value, method_name)
|
|
224
266
|
unary_ops_for(receiver_value).include?(method_name) ||
|
|
225
267
|
catalog_allows?(receiver_value, method_name)
|
|
@@ -230,6 +272,13 @@ module Rigor
|
|
|
230
272
|
catalog_allows?(receiver_value, method_name)
|
|
231
273
|
end
|
|
232
274
|
|
|
275
|
+
# 2-arg methods have no hand-rolled allow list; the catalog
|
|
276
|
+
# is the sole gate. Adding a per-class arity-2 set is reserved
|
|
277
|
+
# for future cases that need it.
|
|
278
|
+
def ternary_method_allowed?(receiver_value, method_name)
|
|
279
|
+
catalog_allows?(receiver_value, method_name)
|
|
280
|
+
end
|
|
281
|
+
|
|
233
282
|
# Builds a Constant or Union[Constant…] from a flat list of
|
|
234
283
|
# Ruby values. When the deduped set exceeds
|
|
235
284
|
# `UNION_FOLD_OUTPUT_LIMIT` and every result is an Integer,
|
|
@@ -598,6 +647,19 @@ module Rigor
|
|
|
598
647
|
nil
|
|
599
648
|
end
|
|
600
649
|
|
|
650
|
+
# Returns `[value]` on success, `nil` to signal "skip this triple".
|
|
651
|
+
# Mirrors `invoke_binary` but for the 2-argument shape; the wrap
|
|
652
|
+
# convention lets callers `flat_map` without losing
|
|
653
|
+
# legitimate `false`/`nil` folds.
|
|
654
|
+
def invoke_ternary(receiver_value, method_name, av0, av1)
|
|
655
|
+
return nil unless ternary_method_allowed?(receiver_value, method_name)
|
|
656
|
+
|
|
657
|
+
result = receiver_value.public_send(method_name, av0, av1)
|
|
658
|
+
foldable_constant_value?(result) ? [result] : nil
|
|
659
|
+
rescue StandardError
|
|
660
|
+
nil
|
|
661
|
+
end
|
|
662
|
+
|
|
601
663
|
# Returns `[value]` on success, `nil` to signal "skip". See
|
|
602
664
|
# `invoke_binary` for why we wrap.
|
|
603
665
|
def invoke_unary(receiver_value, method_name)
|
|
@@ -623,11 +685,54 @@ module Rigor
|
|
|
623
685
|
# implementation does not call back into user-redefinable
|
|
624
686
|
# Ruby methods, so executing them on a literal Integer/Float
|
|
625
687
|
# is safe regardless of monkey-patching.
|
|
688
|
+
#
|
|
689
|
+
# Resolution order:
|
|
690
|
+
#
|
|
691
|
+
# 1. Primary class catalog (e.g. NumericCatalog for an
|
|
692
|
+
# Integer receiver). When the catalog has an entry —
|
|
693
|
+
# even one classified `:dispatch` — that answer wins.
|
|
694
|
+
# The class's direct `rb_define_method` registration is
|
|
695
|
+
# authoritative; we MUST NOT fall through to a module
|
|
696
|
+
# catalog and risk over-folding.
|
|
697
|
+
# 2. Module catalogs (Comparable, Enumerable, …) that the
|
|
698
|
+
# receiver's class includes by ancestry. Reached only
|
|
699
|
+
# when the primary catalog has NO entry for the method
|
|
700
|
+
# — typically because the method is inherited purely
|
|
701
|
+
# through `include Comparable` / `include Enumerable`
|
|
702
|
+
# (e.g. `Integer#between?` / `Integer#clamp` are not in
|
|
703
|
+
# numeric.yml because the Init block does not
|
|
704
|
+
# `rb_define_method` them on Integer).
|
|
626
705
|
def catalog_allows?(receiver_value, method_name)
|
|
627
706
|
catalog, class_name = catalog_for(receiver_value)
|
|
628
|
-
return
|
|
707
|
+
return catalog.safe_for_folding?(class_name, method_name) if catalog&.method_entry(class_name, method_name)
|
|
629
708
|
|
|
630
|
-
|
|
709
|
+
module_catalogs_for(receiver_value).any? do |mod_catalog, mod_name|
|
|
710
|
+
mod_catalog.method_entry(mod_name, method_name) &&
|
|
711
|
+
mod_catalog.safe_for_folding?(mod_name, method_name)
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# `(Module, catalog, class_name)` triples consulted as a
|
|
716
|
+
# fallthrough when the primary class catalog has no entry.
|
|
717
|
+
# Each triple's Module is matched against the receiver
|
|
718
|
+
# class's ancestor chain at lookup time; the catalog
|
|
719
|
+
# corresponds to the module-mode YAML at
|
|
720
|
+
# `data/builtins/ruby_core/<topic>.yml`.
|
|
721
|
+
MODULE_CATALOGS = [
|
|
722
|
+
[Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
|
|
723
|
+
[Enumerable, Builtins::ENUMERABLE_CATALOG, "Enumerable"]
|
|
724
|
+
].freeze
|
|
725
|
+
private_constant :MODULE_CATALOGS
|
|
726
|
+
|
|
727
|
+
# Returns the `(catalog, class_name)` pairs for every
|
|
728
|
+
# registered module that is in the receiver's ancestor
|
|
729
|
+
# chain. The receiver's class's `Module#ancestors` is
|
|
730
|
+
# cached by Ruby; the `Set` membership check is cheap.
|
|
731
|
+
def module_catalogs_for(receiver_value)
|
|
732
|
+
ancestors = Set.new(receiver_value.class.ancestors)
|
|
733
|
+
MODULE_CATALOGS.filter_map do |mod, catalog, class_name|
|
|
734
|
+
[catalog, class_name] if ancestors.include?(mod)
|
|
735
|
+
end
|
|
631
736
|
end
|
|
632
737
|
|
|
633
738
|
# `(catalog, class_name)` per receiver class. The class_name
|
|
@@ -637,16 +742,25 @@ module Rigor
|
|
|
637
742
|
# before any base-class fallback would, and adding a new
|
|
638
743
|
# class is a one-line addition rather than another `when`
|
|
639
744
|
# arm on a growing case statement.
|
|
745
|
+
# Subclass-before-superclass ordering: `DateTime < Date`,
|
|
746
|
+
# so the `DateTime` row MUST come before the `Date` row.
|
|
747
|
+
# Otherwise a `DateTime` receiver would match the `Date`
|
|
748
|
+
# arm first and the catalog would consult the Date entry
|
|
749
|
+
# in `DATE_CATALOG` for the wrong class.
|
|
640
750
|
CATALOG_BY_CLASS = [
|
|
641
|
-
[Integer,
|
|
642
|
-
[Float,
|
|
643
|
-
[String,
|
|
644
|
-
[Symbol,
|
|
645
|
-
[Array,
|
|
646
|
-
[Hash,
|
|
647
|
-
[Range,
|
|
648
|
-
[::Set,
|
|
649
|
-
[Time,
|
|
751
|
+
[Integer, [Builtins::NumericCatalog, "Integer"]],
|
|
752
|
+
[Float, [Builtins::NumericCatalog, "Float"]],
|
|
753
|
+
[String, [Builtins::STRING_CATALOG, "String"]],
|
|
754
|
+
[Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
|
|
755
|
+
[Array, [Builtins::ARRAY_CATALOG, "Array"]],
|
|
756
|
+
[Hash, [Builtins::HASH_CATALOG, "Hash"]],
|
|
757
|
+
[Range, [Builtins::RANGE_CATALOG, "Range"]],
|
|
758
|
+
[::Set, [Builtins::SET_CATALOG, "Set"]],
|
|
759
|
+
[Time, [Builtins::TIME_CATALOG, "Time"]],
|
|
760
|
+
[DateTime, [Builtins::DATE_CATALOG, "DateTime"]],
|
|
761
|
+
[Date, [Builtins::DATE_CATALOG, "Date"]],
|
|
762
|
+
[Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
|
|
763
|
+
[Complex, [Builtins::COMPLEX_CATALOG, "Complex"]]
|
|
650
764
|
].freeze
|
|
651
765
|
private_constant :CATALOG_BY_CLASS
|
|
652
766
|
|
|
@@ -32,14 +32,20 @@ module Rigor
|
|
|
32
32
|
|
|
33
33
|
# @return [Array<Rigor::Type>, nil] block-param types, or
|
|
34
34
|
# nil to fall through to the next tier.
|
|
35
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
35
36
|
def block_param_types(receiver:, method_name:, args:)
|
|
36
37
|
case method_name
|
|
37
38
|
when :times then times_block_params(receiver)
|
|
38
39
|
when :upto then upto_block_params(receiver, args.first)
|
|
39
40
|
when :downto then downto_block_params(receiver, args.first)
|
|
40
41
|
when :each_with_index then each_with_index_block_params(receiver)
|
|
42
|
+
when :each_with_object then each_with_object_block_params(receiver, args.first)
|
|
43
|
+
when :inject, :reduce then inject_block_params(receiver, args)
|
|
44
|
+
when :group_by, :partition then single_element_block_params(receiver)
|
|
45
|
+
when :each_slice, :each_cons then slice_block_params(receiver)
|
|
41
46
|
end
|
|
42
47
|
end
|
|
48
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
43
49
|
|
|
44
50
|
def times_block_params(receiver)
|
|
45
51
|
return nil unless integer_rooted?(receiver)
|
|
@@ -88,6 +94,99 @@ module Rigor
|
|
|
88
94
|
[element, Type::Combinator.non_negative_int]
|
|
89
95
|
end
|
|
90
96
|
|
|
97
|
+
# `each_with_object(memo) { |elem, memo_inner| … }` yields
|
|
98
|
+
# `(element, memo)` where `memo` is the second argument's
|
|
99
|
+
# type (passed by reference and threaded across iterations
|
|
100
|
+
# at runtime — Rigor reflects that by binding the block's
|
|
101
|
+
# second parameter to whatever the call site supplied).
|
|
102
|
+
# When the call has no memo argument the dispatcher
|
|
103
|
+
# declines so the user's RBS / overload selector decides.
|
|
104
|
+
def each_with_object_block_params(receiver, memo_arg)
|
|
105
|
+
return nil if memo_arg.nil?
|
|
106
|
+
|
|
107
|
+
element = element_type_of(receiver)
|
|
108
|
+
return nil if element.nil?
|
|
109
|
+
|
|
110
|
+
[element, memo_arg]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# `inject(seed) { |memo, elem| … }` and `reduce` accept
|
|
114
|
+
# three call shapes:
|
|
115
|
+
#
|
|
116
|
+
# - `(seed) { |memo, elem| … }` — block params `[seed, element]`.
|
|
117
|
+
# The memo's static type is the seed's; we cannot prove the
|
|
118
|
+
# block return type here without round-tripping through the
|
|
119
|
+
# block analyser, so the binding is the seed's type and
|
|
120
|
+
# downstream inference widens as needed.
|
|
121
|
+
# - `() { |memo, elem| … }` — the first iteration uses the
|
|
122
|
+
# first element as the memo, so `[element, element]` is the
|
|
123
|
+
# sound binding.
|
|
124
|
+
# - `(seed, :sym)` / `(:sym)` — Symbol method-name forms have
|
|
125
|
+
# no block. `inject` with a Symbol final arg is recognised
|
|
126
|
+
# and declined (returns nil) so the dispatcher does not
|
|
127
|
+
# pretend a block existed.
|
|
128
|
+
def inject_block_params(receiver, args)
|
|
129
|
+
element = element_type_of(receiver)
|
|
130
|
+
return nil if element.nil?
|
|
131
|
+
|
|
132
|
+
case args.size
|
|
133
|
+
when 0
|
|
134
|
+
[element, element]
|
|
135
|
+
when 1
|
|
136
|
+
seed = args.first
|
|
137
|
+
return nil if symbol_constant?(seed)
|
|
138
|
+
|
|
139
|
+
[seed, element]
|
|
140
|
+
when 2
|
|
141
|
+
# `inject(seed, :sym)` — Symbol-call form, no block.
|
|
142
|
+
return nil if symbol_constant?(args[1])
|
|
143
|
+
|
|
144
|
+
[args[0], element]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def symbol_constant?(type)
|
|
149
|
+
type.is_a?(Type::Constant) && type.value.is_a?(Symbol)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Element-yielding Enumerable methods covered as a v0.0.5
|
|
153
|
+
# placeholder. RBS already binds the block parameter
|
|
154
|
+
# correctly for plain `Array[T]` / `Set[T]` / `Range[T]`
|
|
155
|
+
# receivers via generic substitution; this tier exists so
|
|
156
|
+
# Tuple- and HashShape-shaped receivers reach the block
|
|
157
|
+
# body with the precise per-position element union /
|
|
158
|
+
# `Tuple[K, V]` pair rather than the projected
|
|
159
|
+
# `Array[union]` / `Hash[K, V]` widening.
|
|
160
|
+
#
|
|
161
|
+
# NOTE (v0.0.5): the per-method coverage here (group_by,
|
|
162
|
+
# partition, each_slice, each_cons) is intentionally
|
|
163
|
+
# narrow. The longer-term direction is to move
|
|
164
|
+
# Enumerable-aware projections into a plugin tier modelled
|
|
165
|
+
# after PHPStan's extension API (ADR-2). The placeholders
|
|
166
|
+
# below stay until the plugin surface is in place; once it
|
|
167
|
+
# ships, this dispatcher loses these arms and the
|
|
168
|
+
# equivalent rules move into a built-in plugin loaded at
|
|
169
|
+
# boot.
|
|
170
|
+
def single_element_block_params(receiver)
|
|
171
|
+
element = element_type_of(receiver)
|
|
172
|
+
return nil if element.nil?
|
|
173
|
+
|
|
174
|
+
[element]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# `each_slice(n) { |slice| … }` and `each_cons(n) { |window| … }`
|
|
178
|
+
# both yield an `Array[element]` once per iteration. The
|
|
179
|
+
# tier ignores the slice-size argument (a Constant<Integer>
|
|
180
|
+
# `n` could in principle bound the slice's length, but a
|
|
181
|
+
# tighter Tuple-of-`n` carrier is reserved for the plugin
|
|
182
|
+
# tier per the NOTE above).
|
|
183
|
+
def slice_block_params(receiver)
|
|
184
|
+
element = element_type_of(receiver)
|
|
185
|
+
return nil if element.nil?
|
|
186
|
+
|
|
187
|
+
[Type::Combinator.nominal_of("Array", type_args: [element])]
|
|
188
|
+
end
|
|
189
|
+
|
|
91
190
|
ELEMENT_BY_NOMINAL = {
|
|
92
191
|
"Array" => :nominal_unary_element,
|
|
93
192
|
"Set" => :nominal_unary_element,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Kernel intrinsic shape-folding — precision tier for the
|
|
9
|
+
# `Kernel` module-functions whose return type is a function
|
|
10
|
+
# of the argument's *shape*, not just its class.
|
|
11
|
+
#
|
|
12
|
+
# Today the only catalogued intrinsic is `Kernel#Array`. The
|
|
13
|
+
# default RBS sig is `Array(untyped) -> Array[untyped]`, which
|
|
14
|
+
# collapses to `Array[Dynamic[top]]` for every caller. This
|
|
15
|
+
# tier short-circuits with a precise answer when the argument's
|
|
16
|
+
# type lattice tells us what the result element type MUST be:
|
|
17
|
+
#
|
|
18
|
+
# Array(Constant[nil]) -> Array[bot] # `[]`
|
|
19
|
+
# Array(Nominal["Array",[E]]) -> Array[E] # already an Array
|
|
20
|
+
# Array(Tuple[T1,T2,…]) -> Array[T1|T2|…]
|
|
21
|
+
# Array(Union[A,B,…]) -> distribute, then unify
|
|
22
|
+
# Array(other Nominal[T]) -> Array[Nominal[T]]
|
|
23
|
+
#
|
|
24
|
+
# For receiver shapes we cannot prove (`Top`, `Dynamic`, …)
|
|
25
|
+
# the tier returns nil and the RBS tier answers with the
|
|
26
|
+
# generic `Array[untyped]` envelope.
|
|
27
|
+
#
|
|
28
|
+
# See `docs/type-specification/value-lattice.md` for the
|
|
29
|
+
# union-distribution contract this tier mirrors.
|
|
30
|
+
module KernelDispatch
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
34
|
+
return nil unless method_name == :Array
|
|
35
|
+
return nil if args.length != 1
|
|
36
|
+
return nil if receiver.nil?
|
|
37
|
+
|
|
38
|
+
arg = args.first
|
|
39
|
+
element = element_type_of(arg)
|
|
40
|
+
return nil if element.nil?
|
|
41
|
+
|
|
42
|
+
Type::Combinator.nominal_of("Array", type_args: [element])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Computes the element type the argument contributes to the
|
|
46
|
+
# `Array(arg)` result, mirroring Ruby's coercion contract:
|
|
47
|
+
#
|
|
48
|
+
# - `nil` becomes `[]` (element type Bot — the empty array
|
|
49
|
+
# contributes no inhabitants).
|
|
50
|
+
# - An existing `Array[E]` is returned as-is, so its element
|
|
51
|
+
# type is `E`.
|
|
52
|
+
# - A `Tuple[T1, T2, …]` is materialised as `Array[T1|T2|…]`
|
|
53
|
+
# (every tuple inhabitant is a tuple, hence Array-like).
|
|
54
|
+
# - Any other value `v` becomes `[v]`, so the element type
|
|
55
|
+
# is the value's own type.
|
|
56
|
+
#
|
|
57
|
+
# Returns nil for receiver shapes the tier cannot prove
|
|
58
|
+
# (Top, Dynamic, Bot in pre-coercion position) so the
|
|
59
|
+
# caller falls back to the RBS-tier envelope.
|
|
60
|
+
def element_type_of(type)
|
|
61
|
+
case type
|
|
62
|
+
when Type::Union
|
|
63
|
+
distribute_over_union(type)
|
|
64
|
+
when Type::Constant
|
|
65
|
+
type.value.nil? ? Type::Combinator.bot : type
|
|
66
|
+
when Type::Nominal
|
|
67
|
+
array_element_or_self(type)
|
|
68
|
+
when Type::Tuple
|
|
69
|
+
tuple_element_union(type)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def distribute_over_union(union)
|
|
74
|
+
contributions = union.members.map { |member| element_type_of(member) }
|
|
75
|
+
return nil if contributions.any?(&:nil?)
|
|
76
|
+
|
|
77
|
+
Type::Combinator.union(*contributions)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def array_element_or_self(nominal)
|
|
81
|
+
return nominal unless nominal.class_name == "Array"
|
|
82
|
+
return Type::Combinator.untyped if nominal.type_args.empty?
|
|
83
|
+
|
|
84
|
+
Type::Combinator.union(*nominal.type_args)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def tuple_element_union(tuple)
|
|
88
|
+
return Type::Combinator.bot if tuple.elements.empty?
|
|
89
|
+
|
|
90
|
+
Type::Combinator.union(*tuple.elements)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -6,6 +6,7 @@ require_relative "method_dispatcher/shape_dispatch"
|
|
|
6
6
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
7
7
|
require_relative "method_dispatcher/iterator_dispatch"
|
|
8
8
|
require_relative "method_dispatcher/file_folding"
|
|
9
|
+
require_relative "method_dispatcher/kernel_dispatch"
|
|
9
10
|
|
|
10
11
|
module Rigor
|
|
11
12
|
module Inference
|
|
@@ -93,7 +94,8 @@ module Rigor
|
|
|
93
94
|
|
|
94
95
|
ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
95
96
|
ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
96
|
-
FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types)
|
|
97
|
+
FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
98
|
+
KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types)
|
|
97
99
|
end
|
|
98
100
|
|
|
99
101
|
def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
@@ -241,6 +241,55 @@ module Rigor
|
|
|
241
241
|
narrow_class_dispatch(type, class_name, context)
|
|
242
242
|
end
|
|
243
243
|
|
|
244
|
+
# Negation pair for `assert_value is ~refinement` /
|
|
245
|
+
# `predicate-if-* … is ~refinement` directives. Computes
|
|
246
|
+
# the complement of `refinement` within the current
|
|
247
|
+
# local's domain `current_type`.
|
|
248
|
+
#
|
|
249
|
+
# Carrier-by-carrier rules:
|
|
250
|
+
#
|
|
251
|
+
# - `Difference[base, Constant[v]]`. Complement of
|
|
252
|
+
# `base \ {v}` within `current_type`. Walk the current
|
|
253
|
+
# type's union members, keep each part disjoint from
|
|
254
|
+
# `base`, and add the removed-value Constant once when
|
|
255
|
+
# any current member covers it. `assert s is
|
|
256
|
+
# ~non-empty-string` over `s: String | nil` narrows to
|
|
257
|
+
# `Constant[""] | NilClass`.
|
|
258
|
+
# - `IntegerRange[a, b]` (v0.0.5+ slice). Complement is
|
|
259
|
+
# the two open halves `int<min, a-1>` and
|
|
260
|
+
# `int<b+1, max>`, each intersected with the
|
|
261
|
+
# integer-domain parts of `current_type`. Non-integer
|
|
262
|
+
# parts (nil, String, …) of a Union receiver survive
|
|
263
|
+
# unchanged. `assert n is ~int<5, 10>` over `n:
|
|
264
|
+
# Integer | nil` narrows to `int<min, 4> | int<11,
|
|
265
|
+
# max> | NilClass`.
|
|
266
|
+
# - `Type::Intersection[M1, M2, …]` (v0.0.5+ slice). De
|
|
267
|
+
# Morgan: `D \ (M1 ∩ M2) = (D \ M1) ∪ (D \ M2)`. Each
|
|
268
|
+
# member's complement is computed independently within
|
|
269
|
+
# `current_type` and the results are unioned. Members
|
|
270
|
+
# the algebra cannot complement (Refined, non-Constant
|
|
271
|
+
# Difference, …) contribute `current_type` itself, so
|
|
272
|
+
# the union widens the answer to `current_type` —
|
|
273
|
+
# sound but imprecise.
|
|
274
|
+
# - `Refined[base, predicate]`. Predicate complements are
|
|
275
|
+
# not reducible to a finite carrier without a richer
|
|
276
|
+
# shape (e.g. `~lowercase-string` is "uppercase OR
|
|
277
|
+
# mixed-case"); `current_type` is returned unchanged.
|
|
278
|
+
def narrow_not_refinement(current_type, refinement_type)
|
|
279
|
+
case refinement_type
|
|
280
|
+
when Type::Difference
|
|
281
|
+
return current_type unless refinement_type.removed.is_a?(Type::Constant)
|
|
282
|
+
|
|
283
|
+
complement_difference(current_type, refinement_type)
|
|
284
|
+
when Type::IntegerRange
|
|
285
|
+
complement_integer_range(current_type, refinement_type)
|
|
286
|
+
when Type::Intersection
|
|
287
|
+
complement_intersection(current_type, refinement_type)
|
|
288
|
+
else
|
|
289
|
+
current_type
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
244
293
|
# Public predicate analyser. Returns `[truthy_scope, falsey_scope]`,
|
|
245
294
|
# always; when no narrowing rule matches the predicate node both
|
|
246
295
|
# entries are the receiver scope unchanged.
|
|
@@ -321,6 +370,162 @@ module Rigor
|
|
|
321
370
|
class << self
|
|
322
371
|
private
|
|
323
372
|
|
|
373
|
+
# Complement of `Difference[base, Constant[v]]` within
|
|
374
|
+
# `current_type`. Walks the current type's union members,
|
|
375
|
+
# keeps each member disjoint from `base` (those values
|
|
376
|
+
# were never in the refinement to begin with), and adds
|
|
377
|
+
# the removed-value `Constant[v]` exactly once when any
|
|
378
|
+
# current member covers it. Members that are fully
|
|
379
|
+
# contained in the refinement (i.e. inside `base` and
|
|
380
|
+
# NOT equal to the removed value) are dropped — they are
|
|
381
|
+
# exactly the values the negation excludes.
|
|
382
|
+
def complement_difference(current_type, difference)
|
|
383
|
+
base = difference.base
|
|
384
|
+
removed = difference.removed
|
|
385
|
+
parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
|
|
386
|
+
|
|
387
|
+
survivors = []
|
|
388
|
+
add_removed = false
|
|
389
|
+
parts.each do |part|
|
|
390
|
+
if base_disjoint?(base, part)
|
|
391
|
+
survivors << part
|
|
392
|
+
elsif part_covers_constant?(part, removed)
|
|
393
|
+
add_removed = true
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
survivors << removed if add_removed
|
|
397
|
+
|
|
398
|
+
return current_type if survivors.empty?
|
|
399
|
+
|
|
400
|
+
Type::Combinator.union(*survivors)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def base_disjoint?(base, part)
|
|
404
|
+
base.accepts(part, mode: :gradual).no?
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def part_covers_constant?(part, constant)
|
|
408
|
+
result = part.accepts(constant, mode: :gradual)
|
|
409
|
+
result.yes? || result.maybe?
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Complement of an `IntegerRange[a, b]` within
|
|
413
|
+
# `current_type`. Splits the range complement into the
|
|
414
|
+
# two open halves `int<min, a-1>` and `int<b+1, max>`
|
|
415
|
+
# (skipping a half when its bound is infinity), then
|
|
416
|
+
# intersects each half with the integer-domain parts of
|
|
417
|
+
# `current_type`. Non-integer parts of a Union receiver
|
|
418
|
+
# (nil, String, …) survive unchanged.
|
|
419
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
420
|
+
def complement_integer_range(current_type, range)
|
|
421
|
+
halves = integer_range_complement_halves(range)
|
|
422
|
+
parts = current_type.is_a?(Type::Union) ? current_type.members : [current_type]
|
|
423
|
+
|
|
424
|
+
survivors = []
|
|
425
|
+
parts.each do |part|
|
|
426
|
+
if integer_member?(part)
|
|
427
|
+
halves.each do |half|
|
|
428
|
+
meet = intersect_integer_part(part, half)
|
|
429
|
+
survivors << meet unless meet.nil? || meet.is_a?(Type::Bot)
|
|
430
|
+
end
|
|
431
|
+
else
|
|
432
|
+
survivors << part
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
return current_type if survivors.empty?
|
|
437
|
+
|
|
438
|
+
Type::Combinator.union(*survivors)
|
|
439
|
+
end
|
|
440
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
441
|
+
|
|
442
|
+
# Returns the two open halves of an IntegerRange's
|
|
443
|
+
# complement: the left half `int<-∞, a-1>` (when `a` is
|
|
444
|
+
# finite) and the right half `int<b+1, ∞>` (when `b` is
|
|
445
|
+
# finite). Universal ranges (both bounds infinite) yield
|
|
446
|
+
# an empty array — the complement is empty.
|
|
447
|
+
def integer_range_complement_halves(range)
|
|
448
|
+
halves = []
|
|
449
|
+
left_max = range.min
|
|
450
|
+
right_min = range.max
|
|
451
|
+
|
|
452
|
+
if left_max.is_a?(Integer)
|
|
453
|
+
halves << Type::Combinator.integer_range(Type::IntegerRange::NEG_INFINITY, left_max - 1)
|
|
454
|
+
end
|
|
455
|
+
if right_min.is_a?(Integer)
|
|
456
|
+
halves << Type::Combinator.integer_range(right_min + 1, Type::IntegerRange::POS_INFINITY)
|
|
457
|
+
end
|
|
458
|
+
halves
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def integer_member?(part)
|
|
462
|
+
case part
|
|
463
|
+
when Type::Constant then part.value.is_a?(Integer)
|
|
464
|
+
when Type::IntegerRange then true
|
|
465
|
+
when Type::Nominal then part.class_name == "Integer"
|
|
466
|
+
else false
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Intersect an integer-domain part with a complement
|
|
471
|
+
# half-range. For a Nominal[Integer] receiver the meet
|
|
472
|
+
# is the half itself; for an existing IntegerRange the
|
|
473
|
+
# meet narrows both bounds; for a Constant[Integer] the
|
|
474
|
+
# meet is the constant when the half covers it,
|
|
475
|
+
# otherwise nil.
|
|
476
|
+
def intersect_integer_part(part, half)
|
|
477
|
+
case part
|
|
478
|
+
when Type::Nominal
|
|
479
|
+
half
|
|
480
|
+
when Type::IntegerRange
|
|
481
|
+
integer_range_meet(part, half)
|
|
482
|
+
when Type::Constant
|
|
483
|
+
half.covers?(part.value) ? part : nil
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def integer_range_meet(left, right)
|
|
488
|
+
low = numeric_to_bound([integer_bound_value(left.min), integer_bound_value(right.min)].max)
|
|
489
|
+
high = numeric_to_bound([integer_bound_value(left.max), integer_bound_value(right.max)].min)
|
|
490
|
+
return nil if integer_range_disjoint?(low, high)
|
|
491
|
+
|
|
492
|
+
Type::Combinator.integer_range(low, high)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def integer_bound_value(bound)
|
|
496
|
+
return -Float::INFINITY if bound == Type::IntegerRange::NEG_INFINITY
|
|
497
|
+
return Float::INFINITY if bound == Type::IntegerRange::POS_INFINITY
|
|
498
|
+
|
|
499
|
+
bound
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def numeric_to_bound(value)
|
|
503
|
+
return Type::IntegerRange::NEG_INFINITY if value == -Float::INFINITY
|
|
504
|
+
return Type::IntegerRange::POS_INFINITY if value == Float::INFINITY
|
|
505
|
+
|
|
506
|
+
value.to_i
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def integer_range_disjoint?(low, high)
|
|
510
|
+
return false if low == Type::IntegerRange::NEG_INFINITY
|
|
511
|
+
return false if high == Type::IntegerRange::POS_INFINITY
|
|
512
|
+
|
|
513
|
+
low > high
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# De Morgan: `D \ (M1 ∩ M2 ∩ …) = (D \ M1) ∪ (D \ M2) ∪
|
|
517
|
+
# …`. Each member's complement is computed independently
|
|
518
|
+
# within `current_type` and the results are unioned.
|
|
519
|
+
# Members the algebra cannot complement contribute
|
|
520
|
+
# `current_type` itself, so the union widens to
|
|
521
|
+
# `current_type` overall — sound but imprecise.
|
|
522
|
+
def complement_intersection(current_type, intersection)
|
|
523
|
+
per_member = intersection.members.map do |member|
|
|
524
|
+
Narrowing.narrow_not_refinement(current_type, member)
|
|
525
|
+
end
|
|
526
|
+
Type::Combinator.union(*per_member)
|
|
527
|
+
end
|
|
528
|
+
|
|
324
529
|
def falsey_value?(value)
|
|
325
530
|
value.nil? || value == false
|
|
326
531
|
end
|
|
@@ -1029,7 +1234,11 @@ module Rigor
|
|
|
1029
1234
|
# the effect's `negative?` flag. Shared between
|
|
1030
1235
|
# predicate-if-* and assert-if-* application paths.
|
|
1031
1236
|
def narrow_for_effect(current, effect, environment)
|
|
1032
|
-
|
|
1237
|
+
if effect.respond_to?(:refinement?) && effect.refinement?
|
|
1238
|
+
return narrow_not_refinement(current, effect.refinement_type) if effect.negative?
|
|
1239
|
+
|
|
1240
|
+
return effect.refinement_type
|
|
1241
|
+
end
|
|
1033
1242
|
|
|
1034
1243
|
if effect.negative?
|
|
1035
1244
|
narrow_not_class(current, effect.class_name, exact: false, environment: environment)
|