rigortype 0.1.17 → 0.1.19
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 +159 -222
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +25 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +29 -0
- data/lib/rigor/analysis/check_rules/main_pass_collector.rb +54 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +213 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +24 -1
- data/lib/rigor/analysis/check_rules.rb +275 -44
- data/lib/rigor/analysis/diagnostic.rb +8 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +581 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +569 -0
- data/lib/rigor/analysis/runner/project_pre_passes.rb +321 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +46 -0
- data/lib/rigor/analysis/runner.rb +207 -1200
- data/lib/rigor/analysis/worker_session.rb +60 -11
- data/lib/rigor/bleeding_edge.rb +123 -0
- data/lib/rigor/cache/descriptor.rb +86 -8
- data/lib/rigor/cache/incremental_snapshot.rb +10 -4
- data/lib/rigor/cache/rbs_cache_producer.rb +5 -1
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +46 -13
- data/lib/rigor/cli/annotate_command.rb +100 -15
- data/lib/rigor/cli/check_command.rb +708 -0
- data/lib/rigor/cli/ci_detector.rb +94 -0
- data/lib/rigor/cli/diagnostic_formats.rb +345 -0
- data/lib/rigor/cli/plugins_command.rb +2 -4
- data/lib/rigor/cli/plugins_renderer.rb +0 -2
- data/lib/rigor/cli/prism_colorizer.rb +10 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +114 -0
- data/lib/rigor/cli/trace_command.rb +143 -0
- data/lib/rigor/cli/trace_renderer.rb +310 -0
- data/lib/rigor/cli/triage_command.rb +6 -3
- data/lib/rigor/cli/triage_renderer.rb +15 -1
- data/lib/rigor/cli.rb +21 -612
- data/lib/rigor/configuration/severity_profile.rb +13 -1
- data/lib/rigor/configuration.rb +66 -7
- data/lib/rigor/environment/rbs_loader.rb +78 -68
- data/lib/rigor/environment.rb +1 -1
- data/lib/rigor/inference/acceptance.rb +10 -0
- data/lib/rigor/inference/body_fixpoint.rb +89 -0
- data/lib/rigor/inference/budget_trace.rb +29 -2
- data/lib/rigor/inference/expression_typer.rb +1080 -105
- data/lib/rigor/inference/flow_tracer.rb +180 -0
- data/lib/rigor/inference/macro_block_self_type.rb +11 -12
- data/lib/rigor/inference/method_dispatcher/array_to_h_folding.rb +60 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +54 -14
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +33 -1
- data/lib/rigor/inference/method_dispatcher/reduce_folding.rb +281 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +71 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +148 -10
- data/lib/rigor/inference/method_dispatcher.rb +187 -55
- data/lib/rigor/inference/method_parameter_binder.rb +56 -2
- data/lib/rigor/inference/multi_target_binder.rb +46 -3
- data/lib/rigor/inference/mutation_widening.rb +142 -0
- data/lib/rigor/inference/narrowing.rb +330 -37
- data/lib/rigor/inference/scope_indexer.rb +770 -39
- data/lib/rigor/inference/statement_evaluator.rb +998 -68
- data/lib/rigor/inference/synthetic_method_scanner.rb +1 -1
- data/lib/rigor/plugin/additional_initializer.rb +61 -38
- data/lib/rigor/plugin/base.rb +517 -120
- data/lib/rigor/plugin/macro/block_as_method.rb +22 -21
- data/lib/rigor/plugin/macro/nested_class_template.rb +9 -7
- data/lib/rigor/plugin/macro.rb +2 -3
- data/lib/rigor/plugin/manifest.rb +4 -24
- data/lib/rigor/plugin/node_rule_walk.rb +192 -0
- data/lib/rigor/plugin/registry.rb +264 -35
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/conformance_checker.rb +86 -1
- data/lib/rigor/scope/discovery_index.rb +60 -0
- data/lib/rigor/scope.rb +199 -204
- data/lib/rigor/sig_gen/generator.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +6 -6
- data/lib/rigor/source/literals.rb +14 -0
- data/lib/rigor/triage/catalogue.rb +4 -19
- data/lib/rigor/triage.rb +69 -1
- data/lib/rigor/type/combinator.rb +34 -0
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +0 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +13 -29
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +13 -32
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +1 -2
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +27 -90
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +13 -30
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +2 -4
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +90 -51
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -3
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +25 -29
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -2
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +11 -40
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql/type_scanner.rb +2 -2
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +10 -21
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +21 -34
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +11 -18
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +0 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +12 -2
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +37 -31
- data/plugins/rigor-shoulda-matchers/lib/rigor/plugin/shoulda_matchers.rb +3 -23
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +8 -21
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +1 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/absurd_recognizer.rb +8 -29
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog.rb +17 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/sigil_detector.rb +2 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +108 -36
- data/sig/rigor/analysis/fact_store.rbs +3 -0
- data/sig/rigor/environment.rbs +0 -2
- data/sig/rigor/inference/builtins/method_catalog.rbs +1 -1
- data/sig/rigor/inference.rbs +5 -0
- data/sig/rigor/plugin/base.rbs +6 -4
- data/sig/rigor/plugin/manifest.rbs +1 -2
- data/sig/rigor/scope.rbs +50 -29
- data/sig/rigor/source.rbs +1 -0
- data/sig/rigor/type.rbs +1 -0
- data/sig/rigor.rbs +1 -1
- data/skills/rigor-baseline-reduce/references/01-classify.md +27 -0
- data/skills/rigor-ci-setup/SKILL.md +319 -0
- data/skills/rigor-plugin-author/SKILL.md +6 -4
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +22 -17
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +18 -1
- metadata +21 -3
- data/lib/rigor/cache/rbs_instance_definitions.rb +0 -66
- data/lib/rigor/plugin/macro/external_file.rb +0 -143
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../type"
|
|
4
|
+
require_relative "call_context"
|
|
5
|
+
require_relative "iterator_dispatch"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Inference
|
|
9
|
+
module MethodDispatcher
|
|
10
|
+
# Symbol-form `reduce` / `inject` return-type tier.
|
|
11
|
+
#
|
|
12
|
+
# `IteratorDispatch.inject_block_params` deliberately declines the
|
|
13
|
+
# Symbol-call shapes (`(1..n).reduce(1, :*)`, `[1,2,3].reduce(:+)`)
|
|
14
|
+
# because they carry no block to bind parameters for — the decline
|
|
15
|
+
# of *block-param typing* is correct. What that decline leaves on
|
|
16
|
+
# the floor is the *return type*: with no block and no precise tier,
|
|
17
|
+
# the call falls to `Enumerable#reduce`'s RBS overload
|
|
18
|
+
# `(untyped, Symbol) -> untyped`, so the whole fold widens to
|
|
19
|
+
# `Dynamic[top]`.
|
|
20
|
+
#
|
|
21
|
+
# This tier recovers a precise return type for the Symbol-operand
|
|
22
|
+
# forms by dispatching the named operator on the accumulated type:
|
|
23
|
+
#
|
|
24
|
+
# - `(seed, :op)` (2-arg) — operand starts at the seed type `S`;
|
|
25
|
+
# the result type is `dispatch(:op, widen(S) ∪ widen(E), widen(E))`
|
|
26
|
+
# joined with the seed (the seed is returned unchanged when the
|
|
27
|
+
# collection is empty). `E` is the receiver's element type.
|
|
28
|
+
# - `(:op)` (1-arg) — no seed: the first element seeds the memo, so
|
|
29
|
+
# the operand type is `E` and the result is
|
|
30
|
+
# `dispatch(:op, widen(E), widen(E))`. RBS models this overload as
|
|
31
|
+
# `() { (E, E) -> E } -> E` (no nil), so the tier returns the
|
|
32
|
+
# operator result without manufacturing a nil — staying consistent
|
|
33
|
+
# with the declared type and Rigor's false-positive discipline
|
|
34
|
+
# (the empty-collection `nil` runtime case is not modelled by RBS
|
|
35
|
+
# and adding it here would only pressure callers into defensive
|
|
36
|
+
# nil-handling for code that works).
|
|
37
|
+
#
|
|
38
|
+
# The tier is precision-additive: it declines (returns nil, today's
|
|
39
|
+
# `Dynamic[top]` behaviour) for every shape it cannot prove —
|
|
40
|
+
# unknown element type, Dynamic / Top receiver, a non-`Constant`
|
|
41
|
+
# Symbol operand, or an operator the engine cannot dispatch on the
|
|
42
|
+
# widened operand types.
|
|
43
|
+
#
|
|
44
|
+
# `widen_value_pinned` (ADR-55/56) collapses `Constant`/`IntegerRange`
|
|
45
|
+
# operands to their nominal base before dispatch so the result is the
|
|
46
|
+
# operator's nominal return (`Integer`) rather than a constant-folded
|
|
47
|
+
# `Constant[120]` — full constant folding of the reduction is out of
|
|
48
|
+
# scope, the precision target is the carrier (`Integer`), not the value.
|
|
49
|
+
module ReduceFolding
|
|
50
|
+
module_function
|
|
51
|
+
|
|
52
|
+
REDUCE_METHODS = %i[reduce inject].freeze
|
|
53
|
+
private_constant :REDUCE_METHODS
|
|
54
|
+
|
|
55
|
+
# Allow-listed pure, side-effect-free fold operators. Each is
|
|
56
|
+
# safe on the foldable constant operand classes (Integer / Float
|
|
57
|
+
# / Rational); division / modulo by zero is caught by the rescue
|
|
58
|
+
# harness in `execute_constant_reduce` and declines to the
|
|
59
|
+
# nominal fold. `:gcd` / `:lcm` are valid binary Integer methods
|
|
60
|
+
# (`acc.public_send(:gcd, elem)`); `:min` / `:max` are *not* (no
|
|
61
|
+
# `Integer#min`/`#max` — they would raise and decline), so they
|
|
62
|
+
# are deliberately omitted.
|
|
63
|
+
CONSTANT_FOLD_OPERATORS = %i[+ * - / % & | ^ gcd lcm].freeze
|
|
64
|
+
private_constant :CONSTANT_FOLD_OPERATORS
|
|
65
|
+
|
|
66
|
+
# Hard caps (ADR-41 WD4 style). The element count is checked
|
|
67
|
+
# BEFORE the receiver is enumerated, so a `(1..1_000_000)` range
|
|
68
|
+
# never materialises. The bit-length cap rejects factorial-style
|
|
69
|
+
# blow-up (`(1..64).reduce(:*)` is a ~296-bit Integer) so the
|
|
70
|
+
# folded value can never become an analyzer-heavy bignum.
|
|
71
|
+
CONSTANT_FOLD_ELEMENT_CAP = 64
|
|
72
|
+
CONSTANT_FOLD_BIT_CAP = 256
|
|
73
|
+
private_constant :CONSTANT_FOLD_ELEMENT_CAP, :CONSTANT_FOLD_BIT_CAP
|
|
74
|
+
|
|
75
|
+
# @return [Rigor::Type, nil]
|
|
76
|
+
def try_dispatch(context)
|
|
77
|
+
return nil unless REDUCE_METHODS.include?(context.method_name)
|
|
78
|
+
return nil if context.block_type
|
|
79
|
+
|
|
80
|
+
args = context.args
|
|
81
|
+
operator, seed = operator_and_seed(args)
|
|
82
|
+
return nil if operator.nil?
|
|
83
|
+
|
|
84
|
+
# Precision pre-check: when the receiver is a fully-constant
|
|
85
|
+
# collection (a `Constant[Range]` with foldable endpoints or a
|
|
86
|
+
# `Tuple` of `Constant` elements) and the operator is an
|
|
87
|
+
# allow-listed pure op, treat the reduction as a pure function
|
|
88
|
+
# and execute it on the real values, capped. Any decline (size
|
|
89
|
+
# cap, non-constant member, magnitude, exception) falls through
|
|
90
|
+
# to the carrier-precise nominal fold below — byte-identical to
|
|
91
|
+
# pre-fold behaviour.
|
|
92
|
+
folded = try_constant_reduce(context.receiver, operator, seed)
|
|
93
|
+
return folded if folded
|
|
94
|
+
|
|
95
|
+
element = IteratorDispatch.element_type_of(context.receiver)
|
|
96
|
+
return nil if element.nil?
|
|
97
|
+
|
|
98
|
+
fold_result(operator, seed, element, context.environment)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Executes the reduction on real constant values, or returns nil
|
|
102
|
+
# to decline. The caller falls through to the nominal fold on a
|
|
103
|
+
# nil return, so every guard here is precision-additive only.
|
|
104
|
+
#
|
|
105
|
+
# @param seed [Rigor::Type, nil] the optional seed type; only a
|
|
106
|
+
# foldable `Constant` seed is honoured, any other seed declines.
|
|
107
|
+
# @return [Rigor::Type::Constant, nil]
|
|
108
|
+
def try_constant_reduce(receiver, operator, seed)
|
|
109
|
+
return nil unless CONSTANT_FOLD_OPERATORS.include?(operator)
|
|
110
|
+
|
|
111
|
+
members = constant_members(receiver)
|
|
112
|
+
return nil if members.nil?
|
|
113
|
+
|
|
114
|
+
seed_value, seed_present = constant_seed(seed)
|
|
115
|
+
return nil if !seed.nil? && !seed_present
|
|
116
|
+
|
|
117
|
+
execute_constant_reduce(members, operator, seed_value, seed_present)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Extracts the receiver's elements as a Ruby Array of foldable
|
|
121
|
+
# constant values, or nil to decline. Checks the size cap BEFORE
|
|
122
|
+
# enumerating so an unbounded / huge `Constant[Range]` (e.g.
|
|
123
|
+
# `(1..1_000_000)`) never calls `to_a`.
|
|
124
|
+
#
|
|
125
|
+
# @return [Array, nil]
|
|
126
|
+
def constant_members(receiver)
|
|
127
|
+
case receiver
|
|
128
|
+
when Type::Constant
|
|
129
|
+
constant_range_members(receiver.value)
|
|
130
|
+
when Type::Tuple
|
|
131
|
+
tuple_members(receiver.elements)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def constant_range_members(value)
|
|
136
|
+
return nil unless value.is_a?(Range)
|
|
137
|
+
|
|
138
|
+
first = value.begin
|
|
139
|
+
last = value.end
|
|
140
|
+
return nil unless foldable?(first) && foldable?(last)
|
|
141
|
+
# Reject endless / beginless ranges (`(1..)`, `(..5)`).
|
|
142
|
+
return nil if first.nil? || last.nil?
|
|
143
|
+
|
|
144
|
+
# Size BEFORE enumeration — `Range#size` is O(1) for numeric
|
|
145
|
+
# ranges and never materialises the elements.
|
|
146
|
+
size = value.size
|
|
147
|
+
return nil unless size.is_a?(Integer)
|
|
148
|
+
return nil if size > CONSTANT_FOLD_ELEMENT_CAP
|
|
149
|
+
|
|
150
|
+
value.to_a
|
|
151
|
+
rescue StandardError
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def tuple_members(elements)
|
|
156
|
+
return nil if elements.size > CONSTANT_FOLD_ELEMENT_CAP
|
|
157
|
+
return nil unless elements.all? { |e| e.is_a?(Type::Constant) && foldable?(e.value) }
|
|
158
|
+
|
|
159
|
+
elements.map(&:value)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# @return [Array(Object, Boolean)] `[seed_value, present?]`. A nil
|
|
163
|
+
# seed type yields `[nil, false]` (no-seed form). A foldable
|
|
164
|
+
# `Constant` seed yields `[value, true]`. Any other seed yields
|
|
165
|
+
# `[nil, false]` so the caller can decline.
|
|
166
|
+
def constant_seed(seed)
|
|
167
|
+
return [nil, false] if seed.nil?
|
|
168
|
+
return [nil, false] unless seed.is_a?(Type::Constant) && foldable?(seed.value)
|
|
169
|
+
|
|
170
|
+
[seed.value, true]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Runs the actual reduction, guarded by the rescue harness and
|
|
174
|
+
# the magnitude cap. Empty-collection semantics match Ruby:
|
|
175
|
+
# `[].reduce(s, :op) == s` (seed form) and `[].reduce(:op) == nil`
|
|
176
|
+
# (no-seed form → declines so the nominal fold's no-nil contract
|
|
177
|
+
# stands; see `fold_result`).
|
|
178
|
+
#
|
|
179
|
+
# @return [Rigor::Type::Constant, nil]
|
|
180
|
+
def execute_constant_reduce(members, operator, seed_value, seed_present)
|
|
181
|
+
result =
|
|
182
|
+
if seed_present
|
|
183
|
+
members.reduce(seed_value) { |acc, e| acc.public_send(operator, e) }
|
|
184
|
+
else
|
|
185
|
+
# No-seed empty collection reduces to nil; decline so the
|
|
186
|
+
# nominal no-nil contract is preserved rather than folding
|
|
187
|
+
# to `Constant[nil]`.
|
|
188
|
+
return nil if members.empty?
|
|
189
|
+
|
|
190
|
+
members.reduce { |acc, e| acc.public_send(operator, e) }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
return nil unless foldable?(result)
|
|
194
|
+
return nil if magnitude_too_large?(result)
|
|
195
|
+
|
|
196
|
+
Type::Combinator.constant_of(result)
|
|
197
|
+
rescue StandardError
|
|
198
|
+
# Division by zero, type mismatch, or any operator error: decline.
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
FOLDABLE_FOLD_CLASSES = [Integer, Float, Rational].freeze
|
|
203
|
+
private_constant :FOLDABLE_FOLD_CLASSES
|
|
204
|
+
|
|
205
|
+
def foldable?(value)
|
|
206
|
+
FOLDABLE_FOLD_CLASSES.any? { |klass| value.is_a?(klass) }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Rejects bignum blow-up. A folded Integer wider than the bit cap
|
|
210
|
+
# (factorial, repeated multiplication) declines to the nominal
|
|
211
|
+
# `Integer` carrier rather than parking a heavy literal in the
|
|
212
|
+
# type graph. Float / Rational carry no comparable blow-up risk.
|
|
213
|
+
def magnitude_too_large?(result)
|
|
214
|
+
result.is_a?(Integer) && result.bit_length > CONSTANT_FOLD_BIT_CAP
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Splits the call's positional arguments into the operator Symbol
|
|
218
|
+
# and the optional seed. Returns `[nil, nil]` for any non-Symbol-
|
|
219
|
+
# operand shape so `try_dispatch` declines.
|
|
220
|
+
#
|
|
221
|
+
# - `[:op]` -> operator `:op`, no seed
|
|
222
|
+
# - `[seed, :op]` -> operator `:op`, seed `seed`
|
|
223
|
+
def operator_and_seed(args)
|
|
224
|
+
case args.size
|
|
225
|
+
when 1
|
|
226
|
+
sym = symbol_value(args[0])
|
|
227
|
+
sym ? [sym, nil] : [nil, nil]
|
|
228
|
+
when 2
|
|
229
|
+
sym = symbol_value(args[1])
|
|
230
|
+
sym ? [sym, args[0]] : [nil, nil]
|
|
231
|
+
else
|
|
232
|
+
[nil, nil]
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def symbol_value(type)
|
|
237
|
+
return nil unless type.is_a?(Type::Constant)
|
|
238
|
+
|
|
239
|
+
type.value.is_a?(Symbol) ? type.value : nil
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Dispatches the operator on the widened operand types. With a
|
|
243
|
+
# seed the memo type spans `seed | element` (the first iteration's
|
|
244
|
+
# memo is the seed, every later iteration's memo is a previous
|
|
245
|
+
# operator result); without a seed the memo and operand are both
|
|
246
|
+
# the element type.
|
|
247
|
+
def fold_result(operator, seed, element, environment)
|
|
248
|
+
widened_element = Type::Combinator.widen_value_pinned(element)
|
|
249
|
+
memo = if seed.nil?
|
|
250
|
+
widened_element
|
|
251
|
+
else
|
|
252
|
+
Type::Combinator.union(
|
|
253
|
+
Type::Combinator.widen_value_pinned(seed), widened_element
|
|
254
|
+
)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
result = dispatch_operator(memo, operator, widened_element, environment)
|
|
258
|
+
return nil if result.nil?
|
|
259
|
+
|
|
260
|
+
# The seed itself is the result when the collection is empty
|
|
261
|
+
# (`[].reduce(s, :op) == s`), so a 2-arg fold's static type is
|
|
262
|
+
# the operator result joined with the seed. The seed is widened
|
|
263
|
+
# (`Constant[0]` -> `Integer`) for the join so the carrier stays
|
|
264
|
+
# the precision target rather than leaking a value-pinned member
|
|
265
|
+
# (`0 | Integer`) — full constant folding of the fold is out of
|
|
266
|
+
# scope.
|
|
267
|
+
return result if seed.nil?
|
|
268
|
+
|
|
269
|
+
Type::Combinator.union(Type::Combinator.widen_value_pinned(seed), result)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def dispatch_operator(memo, operator, operand, environment)
|
|
273
|
+
MethodDispatcher.dispatch(
|
|
274
|
+
receiver_type: memo, method_name: operator,
|
|
275
|
+
arg_types: [operand], environment: environment
|
|
276
|
+
)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -38,10 +38,81 @@ module Rigor
|
|
|
38
38
|
return nil unless SingletonFolding.receiver?(receiver, "Regexp")
|
|
39
39
|
return fold_escape(args) if REGEXP_ESCAPE_METHODS.include?(method_name)
|
|
40
40
|
return fold_new(args) if method_name == :new
|
|
41
|
+
return fold_last_match(context) if method_name == :last_match
|
|
41
42
|
|
|
42
43
|
nil
|
|
43
44
|
end
|
|
44
45
|
|
|
46
|
+
# `Regexp.last_match` reads the same match-data slot the perlish
|
|
47
|
+
# `$~` global tracks. On a proven-match edge — the truthy branch
|
|
48
|
+
# of `if str =~ /re/`, the surviving path of `unless /re/ =~ s;
|
|
49
|
+
# raise; end`, a `case`/`when` regex arm — the flow engine has
|
|
50
|
+
# already narrowed `$~` to a non-nil `MatchData` (see
|
|
51
|
+
# `Narrowing#regex_match_predicate_scopes`). Upstream RBS types
|
|
52
|
+
# `Regexp.last_match` as `() -> MatchData?` / `(int) -> String?`,
|
|
53
|
+
# so without this consult the call drops back to the nilable
|
|
54
|
+
# return even where the surrounding flow has *proven* the match
|
|
55
|
+
# succeeded — re-introducing the `possible nil receiver` the `$~`
|
|
56
|
+
# narrowing exists to remove the moment the code reaches for the
|
|
57
|
+
# match through the method rather than the global.
|
|
58
|
+
#
|
|
59
|
+
# Mirrors the global's narrowing exactly, so it rides the same
|
|
60
|
+
# `Scope#forget_match_globals` invalidation (an intervening call
|
|
61
|
+
# that can re-run a match clears `$~`, and this consult clears
|
|
62
|
+
# with it):
|
|
63
|
+
#
|
|
64
|
+
# * 0-arg -> `MatchData` (the narrowed `$~`)
|
|
65
|
+
# * `(N)` -> `String` when capture group N is unconditional on
|
|
66
|
+
# every successful match (the same set the `$N`
|
|
67
|
+
# globals narrow), else `String?` (an optional /
|
|
68
|
+
# alternation-reachable group is nil at runtime even
|
|
69
|
+
# on a successful match).
|
|
70
|
+
# * `(:name)` / `(0)` and other forms defer to RBS.
|
|
71
|
+
#
|
|
72
|
+
# Declines (returns nil -> RBS) whenever `$~` is NOT proven
|
|
73
|
+
# non-nil: no match edge established it, or a falsey edge bound it
|
|
74
|
+
# to `Constant[nil]`. Narrowing only on the proven edge keeps the
|
|
75
|
+
# bare `m = Regexp.last_match; m[...]` (no preceding match) firing
|
|
76
|
+
# exactly as today — this never invents a match.
|
|
77
|
+
def fold_last_match(context)
|
|
78
|
+
scope = context.scope
|
|
79
|
+
return nil if scope.nil?
|
|
80
|
+
|
|
81
|
+
match_data = scope.global(:$~)
|
|
82
|
+
return nil unless proven_match?(match_data)
|
|
83
|
+
|
|
84
|
+
args = context.args
|
|
85
|
+
return Type::Combinator.nominal_of("MatchData") if args.empty?
|
|
86
|
+
return nil unless args.size == 1
|
|
87
|
+
|
|
88
|
+
fold_last_match_group(args.first, scope)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Narrowed `$~` is a non-nil `Nominal[MatchData]`. A bare
|
|
92
|
+
# `MatchData?` / `nil` / Dynamic binding is NOT a proven match.
|
|
93
|
+
def proven_match?(match_data)
|
|
94
|
+
match_data.is_a?(Type::Nominal) && match_data.class_name == "MatchData"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# `Regexp.last_match(N)` for a value-pinned positive Integer N.
|
|
98
|
+
# Track the matching `$N` global the predicate edge already
|
|
99
|
+
# narrowed: present (`String`) means the group is unconditional,
|
|
100
|
+
# absent means it is optional / alternation-reachable (nil at
|
|
101
|
+
# runtime on a success), so we surface `String?`. A non-constant
|
|
102
|
+
# or non-positive index defers to RBS.
|
|
103
|
+
def fold_last_match_group(arg, scope)
|
|
104
|
+
return nil unless arg.is_a?(Type::Constant)
|
|
105
|
+
|
|
106
|
+
index = arg.value
|
|
107
|
+
return nil unless index.is_a?(Integer) && index.positive?
|
|
108
|
+
|
|
109
|
+
string_t = Type::Combinator.nominal_of("String")
|
|
110
|
+
group_global = scope.global(:"$#{index}")
|
|
111
|
+
return string_t if group_global.is_a?(Type::Nominal) && group_global.class_name == "String"
|
|
112
|
+
|
|
113
|
+
Type::Combinator.union(string_t, Type::Combinator.constant_of(nil))
|
|
114
|
+
end
|
|
115
|
+
|
|
45
116
|
# `Regexp.escape(str)` / `.quote(str)` — one String arg.
|
|
46
117
|
def fold_escape(args)
|
|
47
118
|
return nil unless args.size == 1
|
|
@@ -98,7 +98,8 @@ module Rigor
|
|
|
98
98
|
uniq: :tuple_uniq,
|
|
99
99
|
index: :tuple_find_index,
|
|
100
100
|
find_index: :tuple_find_index,
|
|
101
|
-
rindex: :tuple_rindex
|
|
101
|
+
rindex: :tuple_rindex,
|
|
102
|
+
flatten: :tuple_flatten
|
|
102
103
|
}.freeze
|
|
103
104
|
|
|
104
105
|
HASH_SHAPE_HANDLERS = {
|
|
@@ -228,15 +229,8 @@ module Rigor
|
|
|
228
229
|
end
|
|
229
230
|
|
|
230
231
|
def dispatch_nominal_size(nominal, method_name, args)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return string_binary if string_binary
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
if nominal.class_name == "Integer" && args.size == 1
|
|
237
|
-
integer_binary = dispatch_integer_binary_from_arg(method_name, args.first)
|
|
238
|
-
return integer_binary if integer_binary
|
|
239
|
-
end
|
|
232
|
+
projection = nominal_projection(nominal, method_name, args)
|
|
233
|
+
return projection if projection
|
|
240
234
|
|
|
241
235
|
return nil unless args.empty?
|
|
242
236
|
|
|
@@ -246,6 +240,106 @@ module Rigor
|
|
|
246
240
|
Type::Combinator.non_negative_int
|
|
247
241
|
end
|
|
248
242
|
|
|
243
|
+
# Arg-/method-driven precision projections for a `Nominal`
|
|
244
|
+
# receiver, consulted ahead of the no-arg size tier. Each
|
|
245
|
+
# branch gates on the class name first so unrelated nominals
|
|
246
|
+
# skip the work. Returns nil when no projection applies.
|
|
247
|
+
def nominal_projection(nominal, method_name, args)
|
|
248
|
+
case nominal.class_name
|
|
249
|
+
when "String"
|
|
250
|
+
dispatch_string_binary_from_arg(method_name, args.first) if args.size == 1
|
|
251
|
+
when "Integer"
|
|
252
|
+
dispatch_integer_binary_from_arg(method_name, args.first) if args.size == 1
|
|
253
|
+
when "Array"
|
|
254
|
+
case method_name
|
|
255
|
+
when :flatten then array_nominal_flatten(nominal, args)
|
|
256
|
+
when :compact then array_nominal_compact(nominal, args)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# `Array[T]#compact` — `compact` removes every `nil` element,
|
|
262
|
+
# so the result element type is `T` with its `nil` constituent
|
|
263
|
+
# stripped (`Array[Node?]#compact` → `Array[Node]`). Mirrors
|
|
264
|
+
# the `Tuple#compact` constant fold for the generic element
|
|
265
|
+
# case. Declines when the receiver carries no type argument
|
|
266
|
+
# (the RBS `Array[untyped]` answer is already maximal) or when
|
|
267
|
+
# `T` has no `nil` constituent to remove (the result equals the
|
|
268
|
+
# receiver, so the RBS tier's answer is already precise).
|
|
269
|
+
def array_nominal_compact(nominal, args)
|
|
270
|
+
return nil unless args.empty?
|
|
271
|
+
|
|
272
|
+
element = nominal.type_args&.first
|
|
273
|
+
return nil if element.nil?
|
|
274
|
+
|
|
275
|
+
stripped = strip_nil_constituent(element)
|
|
276
|
+
return nil if stripped.equal?(element)
|
|
277
|
+
|
|
278
|
+
Type::Combinator.nominal_of("Array", type_args: [stripped])
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Removes the `nil` constituent from a (possibly union) type,
|
|
282
|
+
# returning the same object when there is nothing to remove so
|
|
283
|
+
# callers can detect the no-op cheaply. Kept local to the
|
|
284
|
+
# dispatch tier to avoid a dependency on the narrowing module.
|
|
285
|
+
def strip_nil_constituent(type)
|
|
286
|
+
case type
|
|
287
|
+
when Type::Constant
|
|
288
|
+
type.value.nil? ? Type::Combinator.bot : type
|
|
289
|
+
when Type::Nominal
|
|
290
|
+
type.class_name == "NilClass" ? Type::Combinator.bot : type
|
|
291
|
+
when Type::Union
|
|
292
|
+
kept = type.members.map { |m| strip_nil_constituent(m) }
|
|
293
|
+
return type if kept.zip(type.members).all? { |k, m| k.equal?(m) }
|
|
294
|
+
|
|
295
|
+
Type::Combinator.union(*kept)
|
|
296
|
+
else
|
|
297
|
+
type
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# `Array[T]#flatten` (and `flatten(depth)`). When `T` is a
|
|
302
|
+
# nested `Array[U]` nominal, one flatten level yields the
|
|
303
|
+
# joined inner element type — `Array[Array[U]]#flatten` →
|
|
304
|
+
# `Array[U]`. When `T` is non-nested the result is `Array[T]`
|
|
305
|
+
# unchanged (Ruby returns a copy with the same element type).
|
|
306
|
+
# Multi-level nesting is handled conservatively: each level
|
|
307
|
+
# joins its element types, and a `depth` argument that does
|
|
308
|
+
# not fully resolve the nesting still produces a sound
|
|
309
|
+
# superset. Declines on an `Array` with no type argument
|
|
310
|
+
# (the RBS `Array[untyped]` answer is already as precise as
|
|
311
|
+
# we can be) and on a non-static depth argument.
|
|
312
|
+
def array_nominal_flatten(nominal, args)
|
|
313
|
+
element = nominal.type_args&.first
|
|
314
|
+
return nil if element.nil?
|
|
315
|
+
|
|
316
|
+
depth = tuple_flatten_depth(args)
|
|
317
|
+
return nil if depth == :decline
|
|
318
|
+
|
|
319
|
+
flattened = flatten_nominal_element(element, depth)
|
|
320
|
+
Type::Combinator.nominal_of("Array", type_args: [flattened])
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Resolves the element type of a flattened `Array[element]`.
|
|
324
|
+
# Each `Array[U]` nesting level contributes `U`; the per-level
|
|
325
|
+
# element types are unioned. `depth < 0` recurses without
|
|
326
|
+
# bound; `depth == 0` stops (Ruby's `flatten(0)` is a no-op
|
|
327
|
+
# copy and returns the element unchanged).
|
|
328
|
+
def flatten_nominal_element(element, depth)
|
|
329
|
+
return element if depth.zero?
|
|
330
|
+
return element unless array_nominal?(element)
|
|
331
|
+
|
|
332
|
+
inner = element.type_args.first
|
|
333
|
+
return element if inner.nil?
|
|
334
|
+
|
|
335
|
+
flatten_nominal_element(inner, depth - 1)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def array_nominal?(type)
|
|
339
|
+
type.is_a?(Type::Nominal) && type.class_name == "Array" && !type.type_args.nil? &&
|
|
340
|
+
!type.type_args.empty?
|
|
341
|
+
end
|
|
342
|
+
|
|
249
343
|
# Arg-type-driven String binary projections for any String-typed
|
|
250
344
|
# receiver (including Nominal, Refined, and Difference fallbacks).
|
|
251
345
|
# Called before the no-arg size guard so binary operators are seen.
|
|
@@ -875,6 +969,50 @@ module Rigor
|
|
|
875
969
|
constant_index(tuple, args) { |elements, value| elements.index { |e| e.value == value } }
|
|
876
970
|
end
|
|
877
971
|
|
|
972
|
+
# `tuple.flatten` / `tuple.flatten(depth)` — recursively
|
|
973
|
+
# flattens nested Tuple elements into a single Tuple. With
|
|
974
|
+
# no argument the flatten is unbounded (matching Ruby's
|
|
975
|
+
# `Array#flatten`); a `Constant[Integer]` depth bounds it.
|
|
976
|
+
# Non-Tuple elements (scalars, `Array[T]` nominals, …) pass
|
|
977
|
+
# through unchanged at their level. A non-static depth
|
|
978
|
+
# argument (or a non-Integer one) declines so RBS answers.
|
|
979
|
+
def tuple_flatten(tuple, _method_name, args)
|
|
980
|
+
depth = tuple_flatten_depth(args)
|
|
981
|
+
return nil if depth == :decline
|
|
982
|
+
|
|
983
|
+
Type::Combinator.tuple_of(*flatten_elements(tuple.elements, depth))
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
# Returns the requested flatten depth: `-1` for the no-arg
|
|
987
|
+
# (unbounded) form, the Integer for a `Constant[Integer]`
|
|
988
|
+
# argument, or `:decline` for any non-static / wrong-arity
|
|
989
|
+
# argument shape.
|
|
990
|
+
def tuple_flatten_depth(args)
|
|
991
|
+
return -1 if args.empty?
|
|
992
|
+
return :decline unless args.size == 1
|
|
993
|
+
|
|
994
|
+
arg = args.first
|
|
995
|
+
return arg.value if arg.is_a?(Type::Constant) && arg.value.is_a?(Integer)
|
|
996
|
+
|
|
997
|
+
:decline
|
|
998
|
+
end
|
|
999
|
+
|
|
1000
|
+
# Flattens a list of element types to `depth` levels.
|
|
1001
|
+
# `depth < 0` means unbounded. A Tuple element is spliced
|
|
1002
|
+
# in (recursing with `depth - 1`); everything else passes
|
|
1003
|
+
# through at this level.
|
|
1004
|
+
def flatten_elements(elements, depth)
|
|
1005
|
+
return elements if depth.zero?
|
|
1006
|
+
|
|
1007
|
+
elements.flat_map do |element|
|
|
1008
|
+
if element.is_a?(Type::Tuple)
|
|
1009
|
+
flatten_elements(element.elements, depth - 1)
|
|
1010
|
+
else
|
|
1011
|
+
[element]
|
|
1012
|
+
end
|
|
1013
|
+
end
|
|
1014
|
+
end
|
|
1015
|
+
|
|
878
1016
|
# `rindex(obj)` → the LAST matching index, same decidability gate.
|
|
879
1017
|
def tuple_rindex(tuple, _method_name, args)
|
|
880
1018
|
constant_index(tuple, args) { |elements, value| elements.rindex { |e| e.value == value } }
|