rigortype 0.0.1 → 0.0.3
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/data/builtins/ruby_core/array.yml +1470 -0
- data/data/builtins/ruby_core/file.yml +501 -0
- data/data/builtins/ruby_core/io.yml +1594 -0
- data/data/builtins/ruby_core/numeric.yml +1809 -0
- data/data/builtins/ruby_core/string.yml +1850 -0
- data/lib/rigor/analysis/check_rules.rb +297 -5
- data/lib/rigor/analysis/diagnostic.rb +13 -2
- data/lib/rigor/analysis/runner.rb +52 -5
- data/lib/rigor/builtins/imported_refinements.rb +69 -0
- data/lib/rigor/cli/type_of_command.rb +11 -5
- data/lib/rigor/cli/type_scan_command.rb +13 -8
- data/lib/rigor/cli.rb +26 -6
- data/lib/rigor/configuration.rb +18 -2
- data/lib/rigor/environment.rb +3 -1
- data/lib/rigor/inference/acceptance.rb +180 -0
- data/lib/rigor/inference/builtins/array_catalog.rb +46 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +90 -0
- data/lib/rigor/inference/builtins/numeric_catalog.rb +93 -0
- data/lib/rigor/inference/builtins/string_catalog.rb +39 -0
- data/lib/rigor/inference/expression_typer.rb +151 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +650 -16
- data/lib/rigor/inference/method_dispatcher/file_folding.rb +144 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +113 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +107 -0
- data/lib/rigor/inference/method_dispatcher.rb +28 -21
- data/lib/rigor/inference/narrowing.rb +471 -10
- data/lib/rigor/inference/scope_indexer.rb +66 -0
- data/lib/rigor/inference/statement_evaluator.rb +305 -2
- data/lib/rigor/rbs_extended.rb +174 -14
- data/lib/rigor/scope.rb +44 -5
- data/lib/rigor/type/combinator.rb +69 -1
- data/lib/rigor/type/difference.rb +155 -0
- data/lib/rigor/type/integer_range.rb +137 -0
- data/lib/rigor/type.rb +2 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/inference.rbs +5 -2
- data/sig/rigor/rbs_extended.rbs +25 -1
- data/sig/rigor/scope.rbs +4 -0
- data/sig/rigor/type.rbs +51 -1
- metadata +15 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# IO / File support — the pure-path-manipulation tier.
|
|
9
|
+
#
|
|
10
|
+
# File and IO carry a lot of side-effecting surface (filesystem
|
|
11
|
+
# reads, descriptor mutations, line iteration) the analyzer
|
|
12
|
+
# cannot fold. Several `File` class methods, however, are
|
|
13
|
+
# functions over their path-string arguments — they do NOT
|
|
14
|
+
# touch the filesystem and do NOT depend on the current
|
|
15
|
+
# working directory.
|
|
16
|
+
#
|
|
17
|
+
# Folding them is platform-sensitive: every recognised method
|
|
18
|
+
# ([:basename, :dirname, :extname, :join, :split,
|
|
19
|
+
# :absolute_path?]) reads `File::SEPARATOR` /
|
|
20
|
+
# `File::ALT_SEPARATOR` and produces different answers on
|
|
21
|
+
# Windows vs POSIX hosts. The Ruby process running the
|
|
22
|
+
# analyzer hosts ONE platform; folding to a `Constant<String>`
|
|
23
|
+
# would silently bake that platform's answer into the
|
|
24
|
+
# analyzer's result and mis-report it on a host with a
|
|
25
|
+
# different separator policy.
|
|
26
|
+
#
|
|
27
|
+
# Default policy (`fold_platform_specific_paths == false`):
|
|
28
|
+
# decline the fold so the RBS tier answers with `Nominal[String]`
|
|
29
|
+
# / `Tuple[Nominal[String], Nominal[String]]` / `bool`. That is
|
|
30
|
+
# the platform-agnostic envelope — every concrete answer the
|
|
31
|
+
# method could legally return on any platform fits inside it.
|
|
32
|
+
# The future `non-empty-string` refinement carrier (see
|
|
33
|
+
# [imported-built-in-types.md](../../../../../docs/type-specification/imported-built-in-types.md))
|
|
34
|
+
# will tighten the basename/dirname/join cases further without
|
|
35
|
+
# leaking platform specifics; today we leave them at the
|
|
36
|
+
# nominal envelope.
|
|
37
|
+
#
|
|
38
|
+
# Opt-in policy (`fold_platform_specific_paths == true`):
|
|
39
|
+
# the analyzer trusts that its host platform matches the
|
|
40
|
+
# callers' deployment target and folds to a precise
|
|
41
|
+
# `Constant<String>`. Single-platform projects (most internal
|
|
42
|
+
# tooling, Rails apps deployed to Linux containers) can
|
|
43
|
+
# enable this in `.rigor.yml`:
|
|
44
|
+
#
|
|
45
|
+
# fold_platform_specific_paths: true
|
|
46
|
+
#
|
|
47
|
+
# The runner reads this on startup (`Rigor::Analysis::Runner`)
|
|
48
|
+
# and writes the flag here. Tests toggle the flag explicitly.
|
|
49
|
+
#
|
|
50
|
+
# See [ADR-5 — robustness principle](../../../../../docs/adr/5-robustness-principle.md):
|
|
51
|
+
# the platform-agnostic default is clause-1 of the principle
|
|
52
|
+
# applied with the constraint that "as strict as can be
|
|
53
|
+
# *correctness-preservingly* proved" excludes Constants whose
|
|
54
|
+
# value is host-specific.
|
|
55
|
+
module FileFolding
|
|
56
|
+
# File class methods that the analyzer can fold *when the
|
|
57
|
+
# fold is platform-safe to perform*. Today every entry is
|
|
58
|
+
# platform-sensitive (every one observes `File::SEPARATOR`
|
|
59
|
+
# or `File::ALT_SEPARATOR`); the gate below requires the
|
|
60
|
+
# opt-in flag for any of them to fire.
|
|
61
|
+
FILE_PURE_CLASS_METHODS = Set[
|
|
62
|
+
:basename,
|
|
63
|
+
:dirname,
|
|
64
|
+
:extname,
|
|
65
|
+
:join,
|
|
66
|
+
:split,
|
|
67
|
+
:absolute_path?
|
|
68
|
+
].freeze
|
|
69
|
+
private_constant :FILE_PURE_CLASS_METHODS
|
|
70
|
+
|
|
71
|
+
# Methods whose result depends on host directory-separator
|
|
72
|
+
# semantics (`/` on POSIX, `/` AND `\` on Windows, drive
|
|
73
|
+
# letters, UNC paths). Folding these would bake the
|
|
74
|
+
# analyzer-host's platform into the inferred type. The opt-
|
|
75
|
+
# in flag below controls whether to do it anyway.
|
|
76
|
+
PLATFORM_DEPENDENT_METHODS = Set[
|
|
77
|
+
:basename, :dirname, :extname, :join, :split, :absolute_path?
|
|
78
|
+
].freeze
|
|
79
|
+
private_constant :PLATFORM_DEPENDENT_METHODS
|
|
80
|
+
|
|
81
|
+
class << self
|
|
82
|
+
# Module-global flag. The runner sets it from
|
|
83
|
+
# `Rigor::Configuration#fold_platform_specific_paths`.
|
|
84
|
+
# Tests toggle it directly.
|
|
85
|
+
attr_accessor :fold_platform_specific_paths
|
|
86
|
+
end
|
|
87
|
+
self.fold_platform_specific_paths = false
|
|
88
|
+
|
|
89
|
+
module_function
|
|
90
|
+
|
|
91
|
+
# @return [Rigor::Type, nil] folded result, or nil to defer
|
|
92
|
+
# to the next dispatcher tier.
|
|
93
|
+
def try_dispatch(receiver:, method_name:, args:)
|
|
94
|
+
return nil unless dispatch_target?(receiver)
|
|
95
|
+
return nil unless FILE_PURE_CLASS_METHODS.include?(method_name)
|
|
96
|
+
return nil if platform_specific_skip?(method_name)
|
|
97
|
+
|
|
98
|
+
string_args = constant_string_args(args)
|
|
99
|
+
return nil if string_args.nil?
|
|
100
|
+
|
|
101
|
+
fold_class_method(method_name, string_args)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def platform_specific_skip?(method_name)
|
|
105
|
+
PLATFORM_DEPENDENT_METHODS.include?(method_name) &&
|
|
106
|
+
!FileFolding.fold_platform_specific_paths
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def dispatch_target?(receiver)
|
|
110
|
+
receiver.is_a?(Type::Singleton) && receiver.class_name == "File"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def constant_string_args(args)
|
|
114
|
+
return [] if args.empty?
|
|
115
|
+
return nil unless args.all? { |arg| constant_string_arg?(arg) }
|
|
116
|
+
|
|
117
|
+
args.map { |arg| arg.value.to_s }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def constant_string_arg?(arg)
|
|
121
|
+
arg.is_a?(Type::Constant) && (arg.value.is_a?(String) || arg.value.is_a?(Symbol))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def fold_class_method(method_name, string_args)
|
|
125
|
+
result = File.public_send(method_name, *string_args)
|
|
126
|
+
wrap_result(result)
|
|
127
|
+
rescue StandardError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def wrap_result(result)
|
|
132
|
+
case result
|
|
133
|
+
when String, true, false
|
|
134
|
+
Type::Combinator.constant_of(result)
|
|
135
|
+
when Array
|
|
136
|
+
return nil unless result.all?(String)
|
|
137
|
+
|
|
138
|
+
Type::Combinator.tuple_of(*result.map { |s| Type::Combinator.constant_of(s) })
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module MethodDispatcher
|
|
8
|
+
# Iterator-style block-parameter typing.
|
|
9
|
+
#
|
|
10
|
+
# Sits ahead of `RbsDispatch.block_param_types` so the precise
|
|
11
|
+
# integer bounds for `Integer#times` / `Integer#upto` /
|
|
12
|
+
# `Integer#downto` reach the block body's parameter binder.
|
|
13
|
+
# Without this tier the RBS signature for `Integer#times`
|
|
14
|
+
# widens the index to `Nominal[Integer]`, dropping every
|
|
15
|
+
# bound the receiver carries.
|
|
16
|
+
#
|
|
17
|
+
# Each rule mirrors Ruby's actual iteration semantics:
|
|
18
|
+
#
|
|
19
|
+
# - `n.times { |i| … }` yields `i ∈ [0, n-1]` when `n > 0`,
|
|
20
|
+
# nothing otherwise. The block-param type is therefore
|
|
21
|
+
# `int<0, n-1>` for a `Constant<Integer>` receiver,
|
|
22
|
+
# `int<0, upper-1>` for a finite `IntegerRange`, and
|
|
23
|
+
# `non_negative_int` for any unbounded-above shape.
|
|
24
|
+
# - `a.upto(b) { |i| … }` yields `i ∈ [a, b]` when `a <= b`.
|
|
25
|
+
# Lower bound from the receiver, upper bound from the
|
|
26
|
+
# argument.
|
|
27
|
+
# - `a.downto(b) { |i| … }` yields the same domain `[b, a]`,
|
|
28
|
+
# just iterated in reverse. Lower bound from the
|
|
29
|
+
# argument, upper bound from the receiver.
|
|
30
|
+
module IteratorDispatch
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# @return [Array<Rigor::Type>, nil] block-param types, or
|
|
34
|
+
# nil to fall through to the next tier.
|
|
35
|
+
def block_param_types(receiver:, method_name:, args:)
|
|
36
|
+
case method_name
|
|
37
|
+
when :times then times_block_params(receiver)
|
|
38
|
+
when :upto then upto_block_params(receiver, args.first)
|
|
39
|
+
when :downto then downto_block_params(receiver, args.first)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def times_block_params(receiver)
|
|
44
|
+
return nil unless integer_rooted?(receiver)
|
|
45
|
+
|
|
46
|
+
upper = upper_bound_of(receiver)
|
|
47
|
+
return [Type::Combinator.non_negative_int] unless upper.is_a?(Integer)
|
|
48
|
+
return [Type::Combinator.non_negative_int] unless upper.positive?
|
|
49
|
+
|
|
50
|
+
[build_index_range(0, upper - 1)]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def upto_block_params(receiver, end_arg)
|
|
54
|
+
return nil unless integer_rooted?(receiver) && integer_rooted?(end_arg)
|
|
55
|
+
|
|
56
|
+
[build_index_range(lower_bound_of(receiver), upper_bound_of(end_arg))]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def downto_block_params(receiver, end_arg)
|
|
60
|
+
return nil unless integer_rooted?(receiver) && integer_rooted?(end_arg)
|
|
61
|
+
|
|
62
|
+
[build_index_range(lower_bound_of(end_arg), upper_bound_of(receiver))]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# `Constant<Integer>`, `IntegerRange`, and `Nominal[Integer]`
|
|
66
|
+
# all participate. Non-integer types (Float, String, …) and
|
|
67
|
+
# `Top`/`Dynamic` decline so the RBS tier answers.
|
|
68
|
+
def integer_rooted?(type)
|
|
69
|
+
case type
|
|
70
|
+
when Type::Constant then type.value.is_a?(Integer)
|
|
71
|
+
when Type::IntegerRange then true
|
|
72
|
+
when Type::Nominal then type.class_name == "Integer" && type.type_args.empty?
|
|
73
|
+
else false
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def lower_bound_of(type)
|
|
78
|
+
case type
|
|
79
|
+
when Type::Constant then type.value
|
|
80
|
+
when Type::IntegerRange then type.min
|
|
81
|
+
when Type::Nominal then Type::IntegerRange::NEG_INFINITY
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def upper_bound_of(type)
|
|
86
|
+
case type
|
|
87
|
+
when Type::Constant then type.value
|
|
88
|
+
when Type::IntegerRange then type.max
|
|
89
|
+
when Type::Nominal then Type::IntegerRange::POS_INFINITY
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Builds a `Constant`/`IntegerRange` from possibly-symbolic
|
|
94
|
+
# bounds. Vacuous ranges (lower > upper, indicating the
|
|
95
|
+
# iterator does not fire) collapse to `non_negative_int` so
|
|
96
|
+
# the body still type-checks against a sensible binding.
|
|
97
|
+
def build_index_range(lower, upper)
|
|
98
|
+
return Type::Combinator.non_negative_int if vacuous_range?(lower, upper)
|
|
99
|
+
return Type::Combinator.constant_of(lower) if lower.is_a?(Integer) && lower == upper
|
|
100
|
+
|
|
101
|
+
Type::Combinator.integer_range(lower, upper)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def vacuous_range?(lower, upper)
|
|
105
|
+
return false if lower == Type::IntegerRange::NEG_INFINITY
|
|
106
|
+
return false if upper == Type::IntegerRange::POS_INFINITY
|
|
107
|
+
|
|
108
|
+
lower > upper
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../type"
|
|
4
|
+
require_relative "../../rbs_extended"
|
|
4
5
|
require_relative "../rbs_type_translator"
|
|
5
6
|
require_relative "overload_selector"
|
|
6
7
|
|
|
@@ -247,6 +248,9 @@ module Rigor
|
|
|
247
248
|
|
|
248
249
|
# rubocop:disable Metrics/ParameterLists
|
|
249
250
|
def translate_return_type(method_definition, class_name:, kind:, args:, type_vars:, block_type:)
|
|
251
|
+
override = RbsExtended.read_return_type_override(method_definition)
|
|
252
|
+
return override if override
|
|
253
|
+
|
|
250
254
|
instance_type = Type::Combinator.nominal_of(class_name)
|
|
251
255
|
self_type =
|
|
252
256
|
case kind
|
|
@@ -93,9 +93,35 @@ module Rigor
|
|
|
93
93
|
case receiver
|
|
94
94
|
when Type::Tuple then dispatch_tuple(receiver, method_name, args)
|
|
95
95
|
when Type::HashShape then dispatch_hash_shape(receiver, method_name, args)
|
|
96
|
+
when Type::Nominal then dispatch_nominal_size(receiver, method_name, args)
|
|
97
|
+
when Type::Difference then dispatch_difference(receiver, method_name, args)
|
|
96
98
|
end
|
|
97
99
|
end
|
|
98
100
|
|
|
101
|
+
# Tightens `Array#size` / `Array#length` / `String#length` /
|
|
102
|
+
# `String#bytesize` / `Hash#size` etc. on a `Nominal` receiver
|
|
103
|
+
# from the RBS-declared `Integer` to `non_negative_int`. The
|
|
104
|
+
# tier ahead of RBS sees the more precise carrier so
|
|
105
|
+
# downstream narrowing (`if size > 0; …`) actually has a
|
|
106
|
+
# range to intersect with.
|
|
107
|
+
SIZE_RETURNING_NOMINALS = {
|
|
108
|
+
"Array" => %i[size length count],
|
|
109
|
+
"String" => %i[length size bytesize],
|
|
110
|
+
"Hash" => %i[size length count],
|
|
111
|
+
"Set" => %i[size length count],
|
|
112
|
+
"Range" => %i[size length count]
|
|
113
|
+
}.freeze
|
|
114
|
+
private_constant :SIZE_RETURNING_NOMINALS
|
|
115
|
+
|
|
116
|
+
# When the difference removes the empty value of the
|
|
117
|
+
# base type (`Constant[""]`, `Constant[0]`, an empty
|
|
118
|
+
# Tuple, an empty HashShape), `size` / `length` /
|
|
119
|
+
# `count` MUST be `positive-int` (the base's
|
|
120
|
+
# non-negative range minus the removed point's `0`),
|
|
121
|
+
# and `empty?` / `zero?` MUST be `Constant[false]`.
|
|
122
|
+
EMPTY_REMOVAL_BASES = %w[String Array Hash Set].freeze
|
|
123
|
+
private_constant :EMPTY_REMOVAL_BASES
|
|
124
|
+
|
|
99
125
|
class << self
|
|
100
126
|
private
|
|
101
127
|
|
|
@@ -113,6 +139,87 @@ module Rigor
|
|
|
113
139
|
send(handler, shape, method_name, args)
|
|
114
140
|
end
|
|
115
141
|
|
|
142
|
+
def dispatch_nominal_size(nominal, method_name, args)
|
|
143
|
+
return nil unless args.empty?
|
|
144
|
+
|
|
145
|
+
selectors = SIZE_RETURNING_NOMINALS[nominal.class_name]
|
|
146
|
+
return nil unless selectors&.include?(method_name)
|
|
147
|
+
|
|
148
|
+
Type::Combinator.non_negative_int
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Refinement-aware projections over a `Difference[base,
|
|
152
|
+
# removed]` receiver. When the removed value is the
|
|
153
|
+
# empty witness of the base (`Constant[""]` for
|
|
154
|
+
# String, `Tuple[]` for Array, `HashShape{}` for Hash,
|
|
155
|
+
# `Constant[0]` for Integer), the catalog tier knows:
|
|
156
|
+
#
|
|
157
|
+
# ns.size # positive-int
|
|
158
|
+
# ns.size == 0 # Constant[false] (via narrowing tier)
|
|
159
|
+
# ns.empty? # Constant[false]
|
|
160
|
+
# nzi.zero? # Constant[false]
|
|
161
|
+
#
|
|
162
|
+
# For any other base method, the difference is opaque
|
|
163
|
+
# to ShapeDispatch — we delegate to the base nominal
|
|
164
|
+
# so the size/length tier still answers the broader
|
|
165
|
+
# `non_negative_int` envelope where applicable.
|
|
166
|
+
def dispatch_difference(difference, method_name, args)
|
|
167
|
+
base = difference.base
|
|
168
|
+
return nil unless base.is_a?(Type::Nominal)
|
|
169
|
+
|
|
170
|
+
if removes_empty_witness?(difference)
|
|
171
|
+
precise = empty_removal_projection(base, method_name, args)
|
|
172
|
+
return precise if precise
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
dispatch_nominal_size(base, method_name, args)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
EMPTY_WITNESS_PREDICATES = {
|
|
179
|
+
"String" => ->(removed) { removed.is_a?(Type::Constant) && removed.value == "" },
|
|
180
|
+
"Integer" => lambda { |removed|
|
|
181
|
+
removed.is_a?(Type::Constant) && removed.value.is_a?(Integer) && removed.value.zero?
|
|
182
|
+
},
|
|
183
|
+
"Array" => ->(removed) { removed.is_a?(Type::Tuple) && removed.elements.empty? },
|
|
184
|
+
"Hash" => ->(removed) { removed.is_a?(Type::HashShape) && removed.pairs.empty? }
|
|
185
|
+
}.freeze
|
|
186
|
+
private_constant :EMPTY_WITNESS_PREDICATES
|
|
187
|
+
|
|
188
|
+
def removes_empty_witness?(difference)
|
|
189
|
+
return false unless difference.base.is_a?(Type::Nominal)
|
|
190
|
+
|
|
191
|
+
predicate = EMPTY_WITNESS_PREDICATES[difference.base.class_name]
|
|
192
|
+
!!(predicate && predicate.call(difference.removed))
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def empty_removal_projection(base, method_name, args)
|
|
196
|
+
return nil unless args.empty?
|
|
197
|
+
|
|
198
|
+
if %i[size length count bytesize].include?(method_name)
|
|
199
|
+
return size_returning_for_empty_removal(base, method_name)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
empty_predicate_projection(base, method_name)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def empty_predicate_projection(base, method_name)
|
|
206
|
+
case method_name
|
|
207
|
+
when :empty?
|
|
208
|
+
base.class_name == "Integer" ? nil : Type::Combinator.constant_of(false)
|
|
209
|
+
when :zero?
|
|
210
|
+
base.class_name == "Integer" ? Type::Combinator.constant_of(false) : nil
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def size_returning_for_empty_removal(base, method_name)
|
|
215
|
+
return nil if base.class_name == "Integer" # Integer has no size method on Difference
|
|
216
|
+
|
|
217
|
+
selectors = SIZE_RETURNING_NOMINALS[base.class_name]
|
|
218
|
+
return nil unless selectors&.include?(method_name)
|
|
219
|
+
|
|
220
|
+
Type::Combinator.positive_int
|
|
221
|
+
end
|
|
222
|
+
|
|
116
223
|
def tuple_first(tuple, _method_name, args)
|
|
117
224
|
return nil unless args.empty?
|
|
118
225
|
return Type::Combinator.constant_of(nil) if tuple.elements.empty?
|
|
@@ -4,6 +4,8 @@ require_relative "../type"
|
|
|
4
4
|
require_relative "method_dispatcher/constant_folding"
|
|
5
5
|
require_relative "method_dispatcher/shape_dispatch"
|
|
6
6
|
require_relative "method_dispatcher/rbs_dispatch"
|
|
7
|
+
require_relative "method_dispatcher/iterator_dispatch"
|
|
8
|
+
require_relative "method_dispatcher/file_folding"
|
|
7
9
|
|
|
8
10
|
module Rigor
|
|
9
11
|
module Inference
|
|
@@ -56,29 +58,12 @@ module Rigor
|
|
|
56
58
|
def dispatch(receiver_type:, method_name:, arg_types:, block_type: nil, environment: nil)
|
|
57
59
|
return nil if receiver_type.nil?
|
|
58
60
|
|
|
59
|
-
|
|
60
|
-
return
|
|
61
|
-
|
|
62
|
-
constant_result = ConstantFolding.try_fold(
|
|
63
|
-
receiver: receiver_type,
|
|
64
|
-
method_name: method_name,
|
|
65
|
-
args: arg_types
|
|
66
|
-
)
|
|
67
|
-
return constant_result if constant_result
|
|
68
|
-
|
|
69
|
-
shape_result = ShapeDispatch.try_dispatch(
|
|
70
|
-
receiver: receiver_type,
|
|
71
|
-
method_name: method_name,
|
|
72
|
-
args: arg_types
|
|
73
|
-
)
|
|
74
|
-
return shape_result if shape_result
|
|
61
|
+
precise = dispatch_precise_tiers(receiver_type, method_name, arg_types)
|
|
62
|
+
return precise if precise
|
|
75
63
|
|
|
76
64
|
rbs_result = RbsDispatch.try_dispatch(
|
|
77
|
-
receiver: receiver_type,
|
|
78
|
-
|
|
79
|
-
args: arg_types,
|
|
80
|
-
environment: environment,
|
|
81
|
-
block_type: block_type
|
|
65
|
+
receiver: receiver_type, method_name: method_name, args: arg_types,
|
|
66
|
+
environment: environment, block_type: block_type
|
|
82
67
|
)
|
|
83
68
|
return rbs_result if rbs_result
|
|
84
69
|
|
|
@@ -96,6 +81,21 @@ module Rigor
|
|
|
96
81
|
try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
97
82
|
end
|
|
98
83
|
|
|
84
|
+
# Runs the precision tiers (constant fold, shape dispatch,
|
|
85
|
+
# file-path fold) in order and returns the first non-nil
|
|
86
|
+
# answer. Each tier owns its own receiver/argument shape
|
|
87
|
+
# checks; a tier that does not recognise the receiver returns
|
|
88
|
+
# nil so the next tier can try. The RBS tier sits below this
|
|
89
|
+
# chain and is invoked by the outer `dispatch` method.
|
|
90
|
+
def dispatch_precise_tiers(receiver_type, method_name, arg_types)
|
|
91
|
+
meta_result = try_meta_introspection(receiver_type, method_name)
|
|
92
|
+
return meta_result if meta_result
|
|
93
|
+
|
|
94
|
+
ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
|
|
95
|
+
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
|
+
end
|
|
98
|
+
|
|
99
99
|
def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
|
|
100
100
|
return nil if environment.nil?
|
|
101
101
|
|
|
@@ -201,6 +201,13 @@ module Rigor
|
|
|
201
201
|
def expected_block_param_types(receiver_type:, method_name:, arg_types:, environment: nil)
|
|
202
202
|
return [] if receiver_type.nil?
|
|
203
203
|
|
|
204
|
+
iterator_result = IteratorDispatch.block_param_types(
|
|
205
|
+
receiver: receiver_type,
|
|
206
|
+
method_name: method_name,
|
|
207
|
+
args: arg_types
|
|
208
|
+
)
|
|
209
|
+
return iterator_result if iterator_result
|
|
210
|
+
|
|
204
211
|
RbsDispatch.block_param_types(
|
|
205
212
|
receiver: receiver_type,
|
|
206
213
|
method_name: method_name,
|