rigortype 0.0.9 → 0.1.1
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 +45 -2
- data/data/builtins/ruby_core/array.yml +6 -6
- data/data/builtins/ruby_core/hash.yml +1 -1
- data/data/builtins/ruby_core/io.yml +3 -3
- data/data/builtins/ruby_core/numeric.yml +1 -1
- data/data/builtins/ruby_core/pathname.yml +100 -100
- data/data/builtins/ruby_core/proc.yml +1 -1
- data/data/builtins/ruby_core/time.yml +3 -3
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +269 -7
- data/lib/rigor/builtins/regex_refinement.rb +104 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
- data/lib/rigor/cache/rbs_class_type_param_names.rb +1 -1
- data/lib/rigor/cache/rbs_constant_table.rb +2 -2
- data/lib/rigor/cache/rbs_descriptor.rb +2 -0
- data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
- data/lib/rigor/cache/store.rb +2 -0
- data/lib/rigor/cli/type_of_command.rb +3 -3
- data/lib/rigor/cli/type_scan_command.rb +4 -4
- data/lib/rigor/cli.rb +20 -7
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +286 -15
- data/lib/rigor/environment/rbs_loader.rb +89 -13
- data/lib/rigor/environment.rb +12 -4
- data/lib/rigor/flow_contribution/conflict.rb +81 -0
- data/lib/rigor/flow_contribution/element.rb +53 -0
- data/lib/rigor/flow_contribution/fact.rb +88 -0
- data/lib/rigor/flow_contribution/merge_result.rb +67 -0
- data/lib/rigor/flow_contribution/merger.rb +275 -0
- data/lib/rigor/flow_contribution.rb +51 -0
- data/lib/rigor/inference/block_parameter_binder.rb +15 -0
- data/lib/rigor/inference/expression_typer.rb +87 -6
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +136 -9
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
- data/lib/rigor/inference/method_dispatcher.rb +50 -1
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +246 -127
- data/lib/rigor/inference/scope_indexer.rb +124 -16
- data/lib/rigor/inference/statement_evaluator.rb +406 -37
- data/lib/rigor/plugin/access_denied_error.rb +24 -0
- data/lib/rigor/plugin/base.rb +284 -0
- data/lib/rigor/plugin/fact_store.rb +92 -0
- data/lib/rigor/plugin/io_boundary.rb +102 -0
- data/lib/rigor/plugin/load_error.rb +35 -0
- data/lib/rigor/plugin/loader.rb +307 -0
- data/lib/rigor/plugin/manifest.rb +203 -0
- data/lib/rigor/plugin/registry.rb +50 -0
- data/lib/rigor/plugin/services.rb +77 -0
- data/lib/rigor/plugin/trust_policy.rb +99 -0
- data/lib/rigor/plugin.rb +62 -0
- data/lib/rigor/rbs_extended.rb +57 -9
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/trinary.rb +1 -1
- data/lib/rigor/type/integer_range.rb +6 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +7 -0
- data/sig/rigor/environment.rbs +10 -3
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +1 -0
- data/sig/rigor/type.rbs +7 -0
- data/sig/rigor.rbs +8 -2
- metadata +20 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class FlowContribution
|
|
5
|
+
# Composes any number of {FlowContribution} bundles into a
|
|
6
|
+
# single {MergeResult} per ADR-2 § "Plugin Contribution
|
|
7
|
+
# Merging". The merger is the **single point of integration**
|
|
8
|
+
# the analyzer uses to combine contributions from built-in
|
|
9
|
+
# narrowing rules, `RBS::Extended` annotations, and plugins;
|
|
10
|
+
# slice 4 routes the existing internal narrowing through it
|
|
11
|
+
# and slice 6 wires plugin-side cache producers around it.
|
|
12
|
+
#
|
|
13
|
+
# ## Authority tiers
|
|
14
|
+
#
|
|
15
|
+
# - Tier 0: `:builtin` — Core Ruby semantics and accepted RBS
|
|
16
|
+
# contracts. Authoritative; lower tiers may not contradict.
|
|
17
|
+
# - Tier 1: `:rbs_extended` (`RBS::Extended` directive bundles,
|
|
18
|
+
# v0.0.9 group D reference impl) and `:generated` (generated
|
|
19
|
+
# signatures / metadata).
|
|
20
|
+
# - Tier 2: `:plugin` and `plugin.<id>` source families.
|
|
21
|
+
# - Tier 3: anything else — treated as the lowest tier.
|
|
22
|
+
#
|
|
23
|
+
# Within a tier, contributions are merged in deterministic
|
|
24
|
+
# order: provenance-supplied `plugin_id` alphabetical (nil
|
|
25
|
+
# plugin ids sort first to keep `:rbs_extended` / `:generated`
|
|
26
|
+
# pre-plugin contributions stable), then by their original
|
|
27
|
+
# input position as the final tie-break.
|
|
28
|
+
#
|
|
29
|
+
# ## Composition rules (ADR-2)
|
|
30
|
+
#
|
|
31
|
+
# - `:return_type` — Intersect via `Type::Combinator.intersection`;
|
|
32
|
+
# collapse to bot raises `:return_type_collapse`.
|
|
33
|
+
# - `:truthy_fact` / `:falsey_fact` / `:post_return_fact` —
|
|
34
|
+
# Edge-local; accumulate while deduping by payload equality.
|
|
35
|
+
# - `:mutation` / `:invalidation` / `:role` — Union; dedupe by
|
|
36
|
+
# equality.
|
|
37
|
+
# - `:exception` — Single-valued. Two non-`nil` non-equal
|
|
38
|
+
# exceptional effects raise `:exceptional_disagreement`.
|
|
39
|
+
#
|
|
40
|
+
# ## Cross-tier contradictions
|
|
41
|
+
#
|
|
42
|
+
# Lower tiers may refine higher tiers but must not weaken them.
|
|
43
|
+
# Slice 3 surfaces contradictions through `:lower_tier_contradiction`
|
|
44
|
+
# when:
|
|
45
|
+
#
|
|
46
|
+
# - the higher tier already pinned a `return_type` and a lower
|
|
47
|
+
# tier's intersection collapses to `bot`;
|
|
48
|
+
# - the higher tier set `exceptional` to a non-`nil` value and a
|
|
49
|
+
# lower tier disagrees.
|
|
50
|
+
#
|
|
51
|
+
# In every conflict case the result keeps the higher-tier value
|
|
52
|
+
# for that slot, records a {Conflict} with both provenances, and
|
|
53
|
+
# continues processing the remaining slots / contributions.
|
|
54
|
+
module Merger # rubocop:disable Metrics/ModuleLength
|
|
55
|
+
AUTHORITY_TIERS = {
|
|
56
|
+
builtin: 0,
|
|
57
|
+
rbs_extended: 1,
|
|
58
|
+
generated: 1
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
module_function
|
|
62
|
+
|
|
63
|
+
# @param contributions [Array<FlowContribution>]
|
|
64
|
+
# @return [MergeResult]
|
|
65
|
+
def merge(contributions)
|
|
66
|
+
contributions = Array(contributions)
|
|
67
|
+
return MergeResult.new if contributions.empty?
|
|
68
|
+
|
|
69
|
+
ordered = order_contributions(contributions)
|
|
70
|
+
state = MergeState.new
|
|
71
|
+
ordered.each do |contribution|
|
|
72
|
+
tier = tier_for(contribution.provenance)
|
|
73
|
+
fold_into(state, contribution, tier)
|
|
74
|
+
end
|
|
75
|
+
state.to_result
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def tier_for(provenance)
|
|
79
|
+
family = provenance.respond_to?(:source_family) ? provenance.source_family : nil
|
|
80
|
+
return AUTHORITY_TIERS[family] if AUTHORITY_TIERS.key?(family)
|
|
81
|
+
return 2 if family == :plugin || family.to_s.start_with?("plugin.")
|
|
82
|
+
|
|
83
|
+
3
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class << self
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def order_contributions(contributions)
|
|
90
|
+
# Stable sort: tier ascending, then provenance plugin_id
|
|
91
|
+
# alphabetical (nil first), then original input position.
|
|
92
|
+
contributions.each_with_index
|
|
93
|
+
.sort_by { |c, i| [tier_for(c.provenance), plugin_id_key(c.provenance), i] }
|
|
94
|
+
.map { |c, _| c }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def plugin_id_key(provenance)
|
|
98
|
+
id = provenance.respond_to?(:plugin_id) ? provenance.plugin_id : nil
|
|
99
|
+
[id.nil? ? 0 : 1, id.to_s]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def fold_into(state, contribution, tier)
|
|
103
|
+
state.add_provenance(contribution.provenance)
|
|
104
|
+
fold_return_type(state, contribution, tier)
|
|
105
|
+
fold_facts(state, contribution)
|
|
106
|
+
fold_effects(state, contribution)
|
|
107
|
+
fold_exceptional(state, contribution, tier)
|
|
108
|
+
fold_role_conformance(state, contribution)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def fold_return_type(state, contribution, tier) # rubocop:disable Metrics/AbcSize
|
|
112
|
+
incoming = contribution.return_type
|
|
113
|
+
return if incoming.nil?
|
|
114
|
+
|
|
115
|
+
if state.return_type.nil?
|
|
116
|
+
state.return_type = incoming
|
|
117
|
+
state.return_type_tier = tier
|
|
118
|
+
state.return_type_provenance = contribution.provenance
|
|
119
|
+
return
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
if intersection_empty?(state.return_type, incoming)
|
|
123
|
+
reason = tier > state.return_type_tier ? :lower_tier_contradiction : :return_type_collapse
|
|
124
|
+
state.conflicts << build_conflict(
|
|
125
|
+
target: :return,
|
|
126
|
+
edge: :normal,
|
|
127
|
+
kind: :return_type,
|
|
128
|
+
reason: reason,
|
|
129
|
+
provenances: [state.return_type_provenance, contribution.provenance],
|
|
130
|
+
message: return_type_conflict_message(reason, state.return_type, incoming)
|
|
131
|
+
)
|
|
132
|
+
return
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
state.return_type = Rigor::Type::Combinator.intersection(state.return_type, incoming)
|
|
136
|
+
state.return_type_tier = [state.return_type_tier, tier].min
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Two types' intersection collapses to bot when neither
|
|
140
|
+
# accepts the other under gradual mode — i.e. the value
|
|
141
|
+
# domains are disjoint. `Rigor::Type::Combinator.intersection`
|
|
142
|
+
# itself does not collapse incompatible nominals (`String ∩
|
|
143
|
+
# Integer` builds a structurally-empty `Intersection`
|
|
144
|
+
# carrier), so the merger checks the disjointness condition
|
|
145
|
+
# directly via the `accepts` trinary.
|
|
146
|
+
def intersection_empty?(lhs, rhs)
|
|
147
|
+
return false if lhs.equal?(rhs)
|
|
148
|
+
|
|
149
|
+
lhs_no = lhs.accepts(rhs).no?
|
|
150
|
+
rhs_no = rhs.accepts(lhs).no?
|
|
151
|
+
lhs_no && rhs_no
|
|
152
|
+
rescue StandardError
|
|
153
|
+
false
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def fold_facts(state, contribution)
|
|
157
|
+
accumulate(state.truthy_facts, contribution.truthy_facts)
|
|
158
|
+
accumulate(state.falsey_facts, contribution.falsey_facts)
|
|
159
|
+
accumulate(state.post_return_facts, contribution.post_return_facts)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def fold_effects(state, contribution)
|
|
163
|
+
accumulate(state.mutations, contribution.mutations)
|
|
164
|
+
accumulate(state.invalidations, contribution.invalidations)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def fold_exceptional(state, contribution, tier)
|
|
168
|
+
incoming = contribution.exceptional
|
|
169
|
+
return if incoming.nil?
|
|
170
|
+
|
|
171
|
+
if state.exceptional.nil?
|
|
172
|
+
state.exceptional = incoming
|
|
173
|
+
state.exceptional_tier = tier
|
|
174
|
+
state.exceptional_provenance = contribution.provenance
|
|
175
|
+
return
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
return if state.exceptional == incoming
|
|
179
|
+
|
|
180
|
+
reason = tier > state.exceptional_tier ? :lower_tier_contradiction : :exceptional_disagreement
|
|
181
|
+
state.conflicts << build_conflict(
|
|
182
|
+
target: :raise,
|
|
183
|
+
edge: :exceptional,
|
|
184
|
+
kind: :exception,
|
|
185
|
+
reason: reason,
|
|
186
|
+
provenances: [state.exceptional_provenance, contribution.provenance],
|
|
187
|
+
message: "exceptional effect disagreement: #{state.exceptional.inspect} vs #{incoming.inspect}"
|
|
188
|
+
)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def fold_role_conformance(state, contribution)
|
|
192
|
+
accumulate(state.role_conformance, contribution.role_conformance)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def accumulate(target, incoming)
|
|
196
|
+
Array(incoming).each do |item|
|
|
197
|
+
target << item unless target.include?(item)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def build_conflict(target:, edge:, kind:, reason:, provenances:, message:) # rubocop:disable Metrics/ParameterLists
|
|
202
|
+
Conflict.new(target: target, edge: edge, kind: kind, reason: reason,
|
|
203
|
+
provenances: provenances, message: message)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def return_type_conflict_message(reason, lhs, rhs)
|
|
207
|
+
case reason
|
|
208
|
+
when :return_type_collapse
|
|
209
|
+
"return-type intersection collapses to bot: #{describe(lhs)} vs #{describe(rhs)}"
|
|
210
|
+
when :lower_tier_contradiction
|
|
211
|
+
"lower-tier return-type #{describe(rhs)} contradicts higher-tier proof #{describe(lhs)}"
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def describe(type)
|
|
216
|
+
if type.respond_to?(:describe)
|
|
217
|
+
type.describe(:short)
|
|
218
|
+
else
|
|
219
|
+
type.inspect
|
|
220
|
+
end
|
|
221
|
+
rescue StandardError
|
|
222
|
+
type.inspect
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Internal accumulator carried through a single merge call.
|
|
227
|
+
# Not part of the public API; folds into a {MergeResult} at
|
|
228
|
+
# the end via {#to_result}.
|
|
229
|
+
class MergeState
|
|
230
|
+
attr_accessor :return_type, :return_type_tier, :return_type_provenance,
|
|
231
|
+
:exceptional, :exceptional_tier, :exceptional_provenance
|
|
232
|
+
attr_reader :truthy_facts, :falsey_facts, :post_return_facts,
|
|
233
|
+
:mutations, :invalidations, :role_conformance,
|
|
234
|
+
:provenances, :conflicts
|
|
235
|
+
|
|
236
|
+
def initialize
|
|
237
|
+
@return_type = nil
|
|
238
|
+
@return_type_tier = nil
|
|
239
|
+
@return_type_provenance = nil
|
|
240
|
+
@truthy_facts = []
|
|
241
|
+
@falsey_facts = []
|
|
242
|
+
@post_return_facts = []
|
|
243
|
+
@mutations = []
|
|
244
|
+
@invalidations = []
|
|
245
|
+
@exceptional = nil
|
|
246
|
+
@exceptional_tier = nil
|
|
247
|
+
@exceptional_provenance = nil
|
|
248
|
+
@role_conformance = []
|
|
249
|
+
@provenances = []
|
|
250
|
+
@conflicts = []
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def add_provenance(provenance)
|
|
254
|
+
@provenances << provenance
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def to_result
|
|
258
|
+
MergeResult.new(
|
|
259
|
+
return_type: @return_type,
|
|
260
|
+
truthy_facts: @truthy_facts,
|
|
261
|
+
falsey_facts: @falsey_facts,
|
|
262
|
+
post_return_facts: @post_return_facts,
|
|
263
|
+
mutations: @mutations,
|
|
264
|
+
invalidations: @invalidations,
|
|
265
|
+
exceptional: @exceptional,
|
|
266
|
+
role_conformance: @role_conformance,
|
|
267
|
+
provenances: @provenances,
|
|
268
|
+
conflicts: @conflicts
|
|
269
|
+
)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
private_constant :MergeState
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "flow_contribution/element"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
# The public packaging of a flow contribution at a single call edge.
|
|
5
7
|
# Plugins, `RBS::Extended` annotations, and built-in narrowing rules
|
|
@@ -101,6 +103,40 @@ module Rigor
|
|
|
101
103
|
end
|
|
102
104
|
end
|
|
103
105
|
|
|
106
|
+
# Flattens this bundle into a tagged element list keyed by
|
|
107
|
+
# `(target, edge, kind)`. The flattening is mechanical and
|
|
108
|
+
# round-trippable through {Merger.merge}: feeding the result
|
|
109
|
+
# back through the merger produces an equivalent bundle.
|
|
110
|
+
#
|
|
111
|
+
# Layout:
|
|
112
|
+
#
|
|
113
|
+
# | slot | edge | kind | target |
|
|
114
|
+
# | --------------------|---------------|---------------------|-------------------------|
|
|
115
|
+
# | return_type | normal | return_type | :return |
|
|
116
|
+
# | truthy_facts | truthy | truthy_fact | (per-fact target) |
|
|
117
|
+
# | falsey_facts | falsey | falsey_fact | (per-fact target) |
|
|
118
|
+
# | post_return_facts | post_return | post_return_fact | (per-fact target) |
|
|
119
|
+
# | mutations | normal | mutation | (per-mutation target) |
|
|
120
|
+
# | invalidations | normal | invalidation | (per-fact target) |
|
|
121
|
+
# | exceptional | exceptional | exception | :raise |
|
|
122
|
+
# | role_conformance | normal | role | (per-role target) |
|
|
123
|
+
#
|
|
124
|
+
# @return [Array<Element>]
|
|
125
|
+
def to_element_list # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
126
|
+
elements = []
|
|
127
|
+
elements << element_for(:return, :normal, :return_type, return_type) unless return_type.nil?
|
|
128
|
+
Array(truthy_facts).each { |fact| elements << element_for(fact_target(fact), :truthy, :truthy_fact, fact) }
|
|
129
|
+
Array(falsey_facts).each { |fact| elements << element_for(fact_target(fact), :falsey, :falsey_fact, fact) }
|
|
130
|
+
Array(post_return_facts).each do |fact|
|
|
131
|
+
elements << element_for(fact_target(fact), :post_return, :post_return_fact, fact)
|
|
132
|
+
end
|
|
133
|
+
Array(mutations).each { |m| elements << element_for(fact_target(m), :normal, :mutation, m) }
|
|
134
|
+
Array(invalidations).each { |i| elements << element_for(fact_target(i), :normal, :invalidation, i) }
|
|
135
|
+
elements << element_for(:raise, :exceptional, :exception, exceptional) unless exceptional.nil?
|
|
136
|
+
Array(role_conformance).each { |r| elements << element_for(fact_target(r), :normal, :role, r) }
|
|
137
|
+
elements.freeze
|
|
138
|
+
end
|
|
139
|
+
|
|
104
140
|
def ==(other)
|
|
105
141
|
other.is_a?(FlowContribution) && to_h == other.to_h
|
|
106
142
|
end
|
|
@@ -112,6 +148,21 @@ module Rigor
|
|
|
112
148
|
|
|
113
149
|
private
|
|
114
150
|
|
|
151
|
+
def element_for(target, edge, kind, payload)
|
|
152
|
+
Element.new(target: target, edge: edge, kind: kind, payload: payload, provenance: provenance)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Best-effort target extraction. Payloads that expose a
|
|
156
|
+
# `#target` accessor (typed-fact carriers, mutation effects)
|
|
157
|
+
# provide their own; everything else falls through with the
|
|
158
|
+
# payload itself as the merge key, which keeps deduplication
|
|
159
|
+
# well-defined for opaque entries.
|
|
160
|
+
def fact_target(payload)
|
|
161
|
+
return payload.target if payload.respond_to?(:target)
|
|
162
|
+
|
|
163
|
+
payload
|
|
164
|
+
end
|
|
165
|
+
|
|
115
166
|
def freeze_collection(value)
|
|
116
167
|
return nil if value.nil?
|
|
117
168
|
|
|
@@ -34,6 +34,12 @@ module Rigor
|
|
|
34
34
|
# `[1, 2, 3].each { _1 + _2 }` sees `_1`/`_2` typed identically
|
|
35
35
|
# to their explicit `|x, y|` counterparts.
|
|
36
36
|
#
|
|
37
|
+
# The `it` implicit parameter (Ruby 3.4+) is bound from
|
|
38
|
+
# `Prism::ItParametersNode`. It is the single-argument cousin of
|
|
39
|
+
# `_1`: the binder produces `{ it: expected_param_types[0] }` so
|
|
40
|
+
# the body's `Prism::ItLocalVariableReadNode` lookup sees the same
|
|
41
|
+
# type as the explicit `|x|` form would.
|
|
42
|
+
#
|
|
37
43
|
# Block-local declarations after `;` (e.g., `|x; y, z|`) are
|
|
38
44
|
# still skipped — they are explicitly block-local, so the outer
|
|
39
45
|
# scope MUST NOT observe them and the binder leaves them unbound.
|
|
@@ -65,6 +71,8 @@ module Rigor
|
|
|
65
71
|
case params_root
|
|
66
72
|
when Prism::NumberedParametersNode
|
|
67
73
|
bind_numbered_parameters(params_root)
|
|
74
|
+
when Prism::ItParametersNode
|
|
75
|
+
bind_it_parameter
|
|
68
76
|
when Prism::BlockParametersNode
|
|
69
77
|
bind_block_parameters(params_root)
|
|
70
78
|
else
|
|
@@ -88,6 +96,13 @@ module Rigor
|
|
|
88
96
|
bindings
|
|
89
97
|
end
|
|
90
98
|
|
|
99
|
+
# `{ it.foo }` — Ruby 3.4 `it` is a single-argument implicit
|
|
100
|
+
# parameter. Always binds the symbol `:it`; `ItLocalVariableReadNode`
|
|
101
|
+
# in the body looks the binding up by name.
|
|
102
|
+
def bind_it_parameter
|
|
103
|
+
{ it: positional_type_at(0) }
|
|
104
|
+
end
|
|
105
|
+
|
|
91
106
|
def bind_block_parameters(params_root)
|
|
92
107
|
params_node = params_root.parameters
|
|
93
108
|
return {} if params_node.nil?
|
|
@@ -65,6 +65,7 @@ module Rigor
|
|
|
65
65
|
Prism::NilNode => :type_of_nil,
|
|
66
66
|
# Locals
|
|
67
67
|
Prism::LocalVariableReadNode => :local_read,
|
|
68
|
+
Prism::ItLocalVariableReadNode => :it_read,
|
|
68
69
|
Prism::LocalVariableWriteNode => :type_of_assignment_write,
|
|
69
70
|
# Containers and pass-throughs
|
|
70
71
|
Prism::ArrayNode => :array_type_for,
|
|
@@ -171,9 +172,11 @@ module Rigor
|
|
|
171
172
|
Prism::WhileNode => :type_of_loop,
|
|
172
173
|
Prism::UntilNode => :type_of_loop,
|
|
173
174
|
Prism::ForNode => :type_of_dynamic_top,
|
|
174
|
-
Prism::DefinedNode => :
|
|
175
|
-
Prism::
|
|
176
|
-
Prism::
|
|
175
|
+
Prism::DefinedNode => :type_of_defined,
|
|
176
|
+
Prism::NumberedReferenceReadNode => :type_of_string_or_nil,
|
|
177
|
+
Prism::BackReferenceReadNode => :type_of_string_or_nil,
|
|
178
|
+
Prism::MatchPredicateNode => :type_of_match_predicate,
|
|
179
|
+
Prism::MatchRequiredNode => :type_of_match_required,
|
|
177
180
|
Prism::MatchWriteNode => :type_of_dynamic_top,
|
|
178
181
|
# Literal containers
|
|
179
182
|
Prism::LambdaNode => :type_of_lambda,
|
|
@@ -298,6 +301,45 @@ module Rigor
|
|
|
298
301
|
dynamic_top
|
|
299
302
|
end
|
|
300
303
|
|
|
304
|
+
# `defined?(expr)` returns `String | nil` per Ruby semantics —
|
|
305
|
+
# a description of the expression's category (`"local-variable"`,
|
|
306
|
+
# `"method"`, ...) when defined, or `nil` when not. The argument
|
|
307
|
+
# is not evaluated (it is statically inspected by the runtime),
|
|
308
|
+
# so the typer does not recurse into it.
|
|
309
|
+
def type_of_defined(_node)
|
|
310
|
+
Type::Combinator.union(
|
|
311
|
+
Type::Combinator.nominal_of("String"),
|
|
312
|
+
Type::Combinator.constant_of(nil)
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# `$1`, `$&`, `$'`, `$+`, `$\`` — the regex back-reference and
|
|
317
|
+
# numbered-capture globals each carry `String | nil`. They share
|
|
318
|
+
# the typer because the typing rule is identical regardless of
|
|
319
|
+
# which back-reference shape Prism emitted.
|
|
320
|
+
def type_of_string_or_nil(_node)
|
|
321
|
+
Type::Combinator.union(
|
|
322
|
+
Type::Combinator.nominal_of("String"),
|
|
323
|
+
Type::Combinator.constant_of(nil)
|
|
324
|
+
)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# `expr in pattern` — pattern-match predicate. Returns `true`
|
|
328
|
+
# when the pattern matches, `false` otherwise.
|
|
329
|
+
def type_of_match_predicate(_node)
|
|
330
|
+
Type::Combinator.union(
|
|
331
|
+
Type::Combinator.constant_of(true),
|
|
332
|
+
Type::Combinator.constant_of(false)
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# `expr => pattern` — one-line pattern-match assertion. Raises
|
|
337
|
+
# `NoMatchingPatternError` on mismatch; on success the expression
|
|
338
|
+
# itself evaluates to `nil`.
|
|
339
|
+
def type_of_match_required(_node)
|
|
340
|
+
Type::Combinator.constant_of(nil)
|
|
341
|
+
end
|
|
342
|
+
|
|
301
343
|
# The expression `Foo` evaluates to the *class object* `Foo`, not
|
|
302
344
|
# an instance. From Slice 4 phase 2b on we therefore type a
|
|
303
345
|
# bare-constant reference as `Singleton[Foo]`; method dispatch on
|
|
@@ -723,9 +765,38 @@ module Rigor
|
|
|
723
765
|
def type_of_range(node)
|
|
724
766
|
left_static, left = static_range_endpoint(node.left)
|
|
725
767
|
right_static, right = static_range_endpoint(node.right)
|
|
726
|
-
return Type::Combinator.
|
|
768
|
+
return Type::Combinator.constant_of(Range.new(left, right, node.exclude_end?)) if left_static && right_static
|
|
727
769
|
|
|
728
|
-
|
|
770
|
+
nominal_range_for_endpoints(node.left, node.right)
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
# Derives `Nominal[Range, [T]]` from the endpoint expression
|
|
774
|
+
# types when at least one endpoint is statically typeable. The
|
|
775
|
+
# element parameter is the union of the endpoint types (lifted
|
|
776
|
+
# from `Constant<v>` to `Nominal<v.class>` so the carrier matches
|
|
777
|
+
# what `Range#each` would yield). Falls back to bare
|
|
778
|
+
# `Nominal[Range]` when no endpoint contributes a typable shape.
|
|
779
|
+
def nominal_range_for_endpoints(left_node, right_node)
|
|
780
|
+
endpoints = [left_node, right_node].compact.map { |n| range_endpoint_element_type(n) }
|
|
781
|
+
endpoints.reject! { |t| t.equal?(Type::Combinator.untyped) }
|
|
782
|
+
return Type::Combinator.nominal_of("Range") if endpoints.empty?
|
|
783
|
+
|
|
784
|
+
Type::Combinator.nominal_of("Range", type_args: [Type::Combinator.union(*endpoints)])
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def range_endpoint_element_type(node)
|
|
788
|
+
type = type_of(node)
|
|
789
|
+
case type
|
|
790
|
+
when Type::Constant
|
|
791
|
+
value = type.value
|
|
792
|
+
return Type::Combinator.untyped if value.nil?
|
|
793
|
+
|
|
794
|
+
Type::Combinator.nominal_of(value.class.name)
|
|
795
|
+
when Type::IntegerRange
|
|
796
|
+
Type::Combinator.nominal_of("Integer")
|
|
797
|
+
else
|
|
798
|
+
type
|
|
799
|
+
end
|
|
729
800
|
end
|
|
730
801
|
|
|
731
802
|
# v0.0.7 — non-interpolated regex literals lift to
|
|
@@ -746,6 +817,7 @@ module Rigor
|
|
|
746
817
|
def static_range_endpoint(node)
|
|
747
818
|
return [true, nil] if node.nil?
|
|
748
819
|
return [true, node.value] if node.is_a?(Prism::IntegerNode)
|
|
820
|
+
return [true, node.unescaped] if node.is_a?(Prism::StringNode) && node.respond_to?(:unescaped)
|
|
749
821
|
|
|
750
822
|
[false, nil]
|
|
751
823
|
end
|
|
@@ -805,6 +877,13 @@ module Rigor
|
|
|
805
877
|
scope.local(node.name) || dynamic_top
|
|
806
878
|
end
|
|
807
879
|
|
|
880
|
+
# `it` (Ruby 3.4) — `ItLocalVariableReadNode` carries no `name`
|
|
881
|
+
# field; the implicit name is always `:it`, matching the binding
|
|
882
|
+
# `BlockParameterBinder` installs for `Prism::ItParametersNode`.
|
|
883
|
+
def it_read(_node)
|
|
884
|
+
scope.local(:it) || dynamic_top
|
|
885
|
+
end
|
|
886
|
+
|
|
808
887
|
# Slice 5 phase 1 upgrades array literals to `Tuple[T1..Tn]`
|
|
809
888
|
# when every element is a non-splat value. Splatted entries
|
|
810
889
|
# (`[*xs, 1]`) preserve the Slice 4 phase 2d behavior: we union
|
|
@@ -902,7 +981,9 @@ module Rigor
|
|
|
902
981
|
method_name: node.name,
|
|
903
982
|
arg_types: arg_types,
|
|
904
983
|
block_type: block_type,
|
|
905
|
-
environment: scope.environment
|
|
984
|
+
environment: scope.environment,
|
|
985
|
+
call_node: node,
|
|
986
|
+
scope: scope
|
|
906
987
|
)
|
|
907
988
|
return result if result
|
|
908
989
|
|
|
@@ -42,14 +42,45 @@ module Rigor
|
|
|
42
42
|
}.freeze
|
|
43
43
|
private_constant :NUMERIC_CONSTRUCTORS
|
|
44
44
|
|
|
45
|
+
# `Kernel#Integer(s)` predicate-aware refinement set
|
|
46
|
+
# (v0.1.1 Track 1 slice 2b). Both `decimal-int-string` and
|
|
47
|
+
# `numeric-string` describe digit-only ASCII strings, so
|
|
48
|
+
# `Integer(s)` is total over the carrier domain and the
|
|
49
|
+
# result is `>= 0`. The default `base: 10` invocation
|
|
50
|
+
# accepts the same shape `String#to_i` does for these
|
|
51
|
+
# predicates; the `Integer(s, base)` overload is left for
|
|
52
|
+
# a later slice.
|
|
53
|
+
INTEGER_REFINEMENT_PREDICATES = Set[:decimal_int, :numeric].freeze
|
|
54
|
+
private_constant :INTEGER_REFINEMENT_PREDICATES
|
|
55
|
+
|
|
45
56
|
def try_dispatch(receiver:, method_name:, args:)
|
|
46
57
|
return nil if receiver.nil?
|
|
47
58
|
return try_array(args) if method_name == :Array
|
|
48
59
|
return try_numeric_constructor(method_name, args) if NUMERIC_CONSTRUCTORS.key?(method_name)
|
|
60
|
+
return try_integer_from_refinement(args) if method_name == :Integer
|
|
49
61
|
|
|
50
62
|
nil
|
|
51
63
|
end
|
|
52
64
|
|
|
65
|
+
# `Kernel#Integer(s)` over a `Refined[String, predicate]`
|
|
66
|
+
# whose predicate is in {INTEGER_REFINEMENT_PREDICATES}.
|
|
67
|
+
# Mirrors the `String#to_i` projection in `ShapeDispatch`
|
|
68
|
+
# (v0.1.1 slice 2a) — the result is always
|
|
69
|
+
# `non-negative-int`. Returns nil for any other arg shape
|
|
70
|
+
# so the RBS tier handles the generic `Integer(arg)` case.
|
|
71
|
+
def try_integer_from_refinement(args)
|
|
72
|
+
return nil unless args.size == 1
|
|
73
|
+
|
|
74
|
+
arg = args.first
|
|
75
|
+
return nil unless arg.is_a?(Type::Refined)
|
|
76
|
+
|
|
77
|
+
base = arg.base
|
|
78
|
+
return nil unless base.is_a?(Type::Nominal) && base.class_name == "String"
|
|
79
|
+
return nil unless INTEGER_REFINEMENT_PREDICATES.include?(arg.predicate_id)
|
|
80
|
+
|
|
81
|
+
Type::Combinator.non_negative_int
|
|
82
|
+
end
|
|
83
|
+
|
|
53
84
|
def try_array(args)
|
|
54
85
|
return nil if args.length != 1
|
|
55
86
|
|