rigortype 0.0.8 → 0.1.0
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 +234 -22
- data/data/builtins/ruby_core/encoding.yml +210 -0
- data/data/builtins/ruby_core/exception.yml +641 -0
- data/data/builtins/ruby_core/numeric.yml +3 -2
- data/data/builtins/ruby_core/proc.yml +731 -0
- data/data/builtins/ruby_core/random.yml +166 -0
- data/data/builtins/ruby_core/re.yml +689 -0
- data/data/builtins/ruby_core/struct.yml +449 -0
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +199 -4
- data/lib/rigor/builtins/imported_refinements.rb +6 -1
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +63 -0
- data/lib/rigor/cache/rbs_class_type_param_names.rb +60 -0
- data/lib/rigor/cache/rbs_constant_table.rb +15 -51
- data/lib/rigor/cache/rbs_descriptor.rb +55 -0
- data/lib/rigor/cache/rbs_environment.rb +52 -0
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +40 -0
- data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
- data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
- data/lib/rigor/cache/store.rb +81 -15
- data/lib/rigor/cli.rb +45 -7
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +110 -6
- data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
- data/lib/rigor/environment/rbs_loader.rb +220 -32
- data/lib/rigor/environment.rb +11 -2
- 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 +179 -0
- data/lib/rigor/inference/block_parameter_binder.rb +15 -0
- data/lib/rigor/inference/builtins/encoding_catalog.rb +67 -0
- data/lib/rigor/inference/builtins/exception_catalog.rb +92 -0
- data/lib/rigor/inference/builtins/proc_catalog.rb +122 -0
- data/lib/rigor/inference/builtins/random_catalog.rb +58 -0
- data/lib/rigor/inference/builtins/re_catalog.rb +81 -0
- data/lib/rigor/inference/builtins/struct_catalog.rb +55 -0
- data/lib/rigor/inference/expression_typer.rb +110 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +173 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/method_dispatcher.rb +2 -0
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +134 -144
- data/lib/rigor/inference/scope_indexer.rb +75 -1
- data/lib/rigor/inference/statement_evaluator.rb +380 -40
- data/lib/rigor/plugin/access_denied_error.rb +24 -0
- data/lib/rigor/plugin/base.rb +241 -0
- data/lib/rigor/plugin/io_boundary.rb +102 -0
- data/lib/rigor/plugin/load_error.rb +23 -0
- data/lib/rigor/plugin/loader.rb +191 -0
- data/lib/rigor/plugin/manifest.rb +134 -0
- data/lib/rigor/plugin/registry.rb +50 -0
- data/lib/rigor/plugin/services.rb +65 -0
- data/lib/rigor/plugin/trust_policy.rb +99 -0
- data/lib/rigor/plugin.rb +61 -0
- data/lib/rigor/rbs_extended.rb +103 -0
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/type/combinator.rb +72 -0
- data/lib/rigor/type/refined.rb +50 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +13 -0
- data/sig/rigor/environment.rbs +7 -1
- 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 +3 -1
- metadata +38 -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
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "flow_contribution/element"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
# The public packaging of a flow contribution at a single call edge.
|
|
7
|
+
# Plugins, `RBS::Extended` annotations, and built-in narrowing rules
|
|
8
|
+
# all hand the analyzer this same bundle shape; the inference engine
|
|
9
|
+
# merges contributions through the policy described in
|
|
10
|
+
# [ADR-2 § "Plugin Contribution Merging"](../../docs/adr/2-extension-api.md)
|
|
11
|
+
# rather than letting any one source override another silently.
|
|
12
|
+
#
|
|
13
|
+
# Eight content slots plus a {Provenance} block. A slot left as `nil`
|
|
14
|
+
# (or, for collection-shaped slots, an empty collection) means the
|
|
15
|
+
# contribution does not assert anything in that dimension; the merge
|
|
16
|
+
# policy treats it as absent.
|
|
17
|
+
#
|
|
18
|
+
# The struct is the only shape plugin authors need to learn. Richer
|
|
19
|
+
# or more permissive shapes are not part of the first public
|
|
20
|
+
# contract — see ADR-2 § "Flow Contribution Bundle" for the binding
|
|
21
|
+
# definition.
|
|
22
|
+
#
|
|
23
|
+
# The element-list flattening (`to_element_list`) ADR-2 mentions is
|
|
24
|
+
# intentionally not implemented yet: it is the analyzer-internal
|
|
25
|
+
# bookkeeping behind the merge policy and will land alongside the
|
|
26
|
+
# plugin contribution merger in v0.1.0. Plugin authors should not
|
|
27
|
+
# rely on it.
|
|
28
|
+
class FlowContribution
|
|
29
|
+
# Provenance carries the metadata every contribution needs for
|
|
30
|
+
# diagnostic attribution and cache invalidation. `source_family`
|
|
31
|
+
# mirrors {Rigor::Analysis::Diagnostic::DEFAULT_SOURCE_FAMILY};
|
|
32
|
+
# `descriptor` is the {Rigor::Cache::Descriptor} this
|
|
33
|
+
# contribution attaches to (or `nil` when the contribution does
|
|
34
|
+
# not need its own cache slice).
|
|
35
|
+
Provenance = Data.define(:source_family, :plugin_id, :node, :descriptor) do
|
|
36
|
+
def self.builtin
|
|
37
|
+
new(source_family: :builtin, plugin_id: nil, node: nil, descriptor: nil)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
SLOT_NAMES = %i[
|
|
42
|
+
return_type
|
|
43
|
+
truthy_facts
|
|
44
|
+
falsey_facts
|
|
45
|
+
post_return_facts
|
|
46
|
+
mutations
|
|
47
|
+
invalidations
|
|
48
|
+
exceptional
|
|
49
|
+
role_conformance
|
|
50
|
+
].freeze
|
|
51
|
+
|
|
52
|
+
attr_reader(*SLOT_NAMES, :provenance)
|
|
53
|
+
|
|
54
|
+
# @param return_type [Object, nil] normal-edge return type. Use
|
|
55
|
+
# `nil` when the contribution does not refine the return type
|
|
56
|
+
# selected from the RBS contract.
|
|
57
|
+
# @param truthy_facts [Array, nil] facts that hold only on the
|
|
58
|
+
# truthy control-flow edge. Edge-local: a truthy-edge fact does
|
|
59
|
+
# NOT imply its falsey-edge complement (ADR-2 § "Plugin
|
|
60
|
+
# Contribution Merging").
|
|
61
|
+
# @param falsey_facts [Array, nil] dual of `truthy_facts`.
|
|
62
|
+
# @param post_return_facts [Array, nil] facts that hold after the
|
|
63
|
+
# call returns normally on every edge — the carrier for
|
|
64
|
+
# assertion-style contributions.
|
|
65
|
+
# @param mutations [Array, nil] receiver and argument mutation
|
|
66
|
+
# effects.
|
|
67
|
+
# @param invalidations [Array, nil] targeted fact invalidations
|
|
68
|
+
# beyond what mutation effects already imply.
|
|
69
|
+
# @param exceptional [Object, nil] non-returning, raising, or
|
|
70
|
+
# unreachable effect.
|
|
71
|
+
# @param role_conformance [Array, nil] capability-role conformance
|
|
72
|
+
# facts the contribution provides.
|
|
73
|
+
# @param provenance [Provenance] source-family, plugin-id, node,
|
|
74
|
+
# and cache-descriptor metadata. Defaults to `Provenance.builtin`.
|
|
75
|
+
# rubocop:disable Metrics/ParameterLists
|
|
76
|
+
def initialize(return_type: nil, truthy_facts: nil, falsey_facts: nil,
|
|
77
|
+
post_return_facts: nil, mutations: nil, invalidations: nil,
|
|
78
|
+
exceptional: nil, role_conformance: nil,
|
|
79
|
+
provenance: Provenance.builtin)
|
|
80
|
+
# rubocop:enable Metrics/ParameterLists
|
|
81
|
+
@return_type = return_type
|
|
82
|
+
@truthy_facts = freeze_collection(truthy_facts)
|
|
83
|
+
@falsey_facts = freeze_collection(falsey_facts)
|
|
84
|
+
@post_return_facts = freeze_collection(post_return_facts)
|
|
85
|
+
@mutations = freeze_collection(mutations)
|
|
86
|
+
@invalidations = freeze_collection(invalidations)
|
|
87
|
+
@exceptional = exceptional
|
|
88
|
+
@role_conformance = freeze_collection(role_conformance)
|
|
89
|
+
@provenance = provenance
|
|
90
|
+
freeze
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [Boolean] true when every content slot is unset (nil or
|
|
94
|
+
# an empty collection). Provenance does not count toward
|
|
95
|
+
# emptiness — an empty bundle still carries source attribution.
|
|
96
|
+
def empty?
|
|
97
|
+
SLOT_NAMES.all? { |slot| slot_empty?(public_send(slot)) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def to_h
|
|
101
|
+
SLOT_NAMES.each_with_object(provenance: provenance.to_h) do |slot, acc|
|
|
102
|
+
acc[slot] = public_send(slot)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
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
|
+
|
|
140
|
+
def ==(other)
|
|
141
|
+
other.is_a?(FlowContribution) && to_h == other.to_h
|
|
142
|
+
end
|
|
143
|
+
alias eql? ==
|
|
144
|
+
|
|
145
|
+
def hash
|
|
146
|
+
to_h.hash
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
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
|
+
|
|
166
|
+
def freeze_collection(value)
|
|
167
|
+
return nil if value.nil?
|
|
168
|
+
|
|
169
|
+
value.dup.freeze
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def slot_empty?(value)
|
|
173
|
+
return true if value.nil?
|
|
174
|
+
return value.empty? if value.respond_to?(:empty?)
|
|
175
|
+
|
|
176
|
+
false
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -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?
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Encoding` catalog. Singleton — load once, consult during
|
|
9
|
+
# dispatch.
|
|
10
|
+
#
|
|
11
|
+
# Encoding instances are deep-frozen value objects: once
|
|
12
|
+
# registered, their `name` / `dummy?` / `ascii_compatible?`
|
|
13
|
+
# slots never change and the C bodies for the per-instance
|
|
14
|
+
# methods are pure. The C-body classifier therefore lands
|
|
15
|
+
# every instance method as `:leaf` correctly.
|
|
16
|
+
#
|
|
17
|
+
# The blocklist focuses on the *singleton* surface where the
|
|
18
|
+
# hidden state is the process-wide encoding registry. Every
|
|
19
|
+
# method classified `:leaf` on the singleton actually reads
|
|
20
|
+
# (or, for the setters, writes) a global, so a hypothetical
|
|
21
|
+
# `Constant<Encoding>`-class receiver MUST NOT fold them
|
|
22
|
+
# against the analyzer process's registry — what UTF-8's
|
|
23
|
+
# alias list is in the analyzer is not necessarily what it
|
|
24
|
+
# is in the analysed program.
|
|
25
|
+
ENCODING_CATALOG = MethodCatalog.new(
|
|
26
|
+
path: File.expand_path(
|
|
27
|
+
"../../../../data/builtins/ruby_core/encoding.yml",
|
|
28
|
+
__dir__
|
|
29
|
+
),
|
|
30
|
+
mutating_selectors: {
|
|
31
|
+
"Encoding" => Set[
|
|
32
|
+
# Defence-in-depth: mirrors range_catalog.rb /
|
|
33
|
+
# complex_catalog.rb. Encoding does not currently
|
|
34
|
+
# expose a public `initialize_copy` (Encoding objects
|
|
35
|
+
# are deep-frozen and #dup is a no-op), but the
|
|
36
|
+
# convention keeps the door closed against future
|
|
37
|
+
# CRuby changes that would leak a copy-mutator.
|
|
38
|
+
:initialize_copy,
|
|
39
|
+
:hash,
|
|
40
|
+
:eql?,
|
|
41
|
+
# `Encoding.find(name)` walks the global encoding
|
|
42
|
+
# registry. Pure with respect to its argument but
|
|
43
|
+
# the registry itself can drift (load-order, locale,
|
|
44
|
+
# process-wide `default_external=` calls), so a
|
|
45
|
+
# constant-fold would lock in the analyzer's view.
|
|
46
|
+
:find,
|
|
47
|
+
# `Encoding.list` / `Encoding.aliases` /
|
|
48
|
+
# `Encoding.name_list` enumerate the same global
|
|
49
|
+
# registry. Same reasoning as `find` — the values
|
|
50
|
+
# are not guaranteed to match the analysed program's
|
|
51
|
+
# registry.
|
|
52
|
+
:list,
|
|
53
|
+
:aliases,
|
|
54
|
+
:name_list,
|
|
55
|
+
# Global-default mutators. `MethodCatalog#blocked?`
|
|
56
|
+
# only auto-blocks `!`-suffixed selectors, so we MUST
|
|
57
|
+
# list these explicitly: each writes the process-wide
|
|
58
|
+
# default-encoding slot read by `default_external` /
|
|
59
|
+
# `default_internal`.
|
|
60
|
+
:default_external=,
|
|
61
|
+
:default_internal=
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Exception` catalog. Singleton — load once, consult during
|
|
9
|
+
# dispatch.
|
|
10
|
+
#
|
|
11
|
+
# Exception is the base of every Ruby error class (RuntimeError,
|
|
12
|
+
# StandardError, KeyError, …). The Init_Exception block in
|
|
13
|
+
# `references/ruby/error.c` registers the entire hierarchy in
|
|
14
|
+
# one pass, so the YAML carries 27 classes — but only the base
|
|
15
|
+
# `Exception` row is wired into `CATALOG_BY_CLASS` for v0.0.5.
|
|
16
|
+
# A `RuntimeError` receiver hits the Exception arm via
|
|
17
|
+
# `is_a?(Exception)` and the catalog answers with the base-class
|
|
18
|
+
# entries; subclass-specific methods (`KeyError#receiver`,
|
|
19
|
+
# `NameError#name`, …) intentionally miss the lookup until a
|
|
20
|
+
# later slice routes per-subclass class_names.
|
|
21
|
+
#
|
|
22
|
+
# The catalog tier here is *defence in depth* — every base
|
|
23
|
+
# method that could plausibly fold has been weighed against the
|
|
24
|
+
# robustness principle (strict on returns) and either left
|
|
25
|
+
# `:dispatch` / `:mutates_self` (in which case the catalog
|
|
26
|
+
# already declines) or blocklisted because the static classifier
|
|
27
|
+
# missed an indirect side effect. The remaining `:leaf` method
|
|
28
|
+
# that DOES fold is `#cause`, a pure accessor.
|
|
29
|
+
EXCEPTION_CATALOG = MethodCatalog.new(
|
|
30
|
+
path: File.expand_path(
|
|
31
|
+
"../../../../data/builtins/ruby_core/exception.yml",
|
|
32
|
+
__dir__
|
|
33
|
+
),
|
|
34
|
+
mutating_selectors: {
|
|
35
|
+
"Exception" => Set[
|
|
36
|
+
# `exc_initialize` writes `mesg` / `backtrace` ivars on
|
|
37
|
+
# self via `rb_ivar_set` — the C-body classifier missed
|
|
38
|
+
# the indirect mutator because the helpers are not in
|
|
39
|
+
# its regex. Blocklisted so a hypothetical future
|
|
40
|
+
# `Constant<Exception>` carrier cannot fold an aliasing
|
|
41
|
+
# constructor through the catalog.
|
|
42
|
+
:initialize,
|
|
43
|
+
# `exc_exception` either returns self (no-arg) or calls
|
|
44
|
+
# `rb_obj_clone` + `exc_initialize_internal` on the
|
|
45
|
+
# clone — the clone branch mutates fresh state through
|
|
46
|
+
# the same indirect helpers as `:initialize`. Conservative
|
|
47
|
+
# blocklist; the cost is one folded no-arg call.
|
|
48
|
+
:exception,
|
|
49
|
+
# `exc_detailed_message` formats with platform / locale
|
|
50
|
+
# data (highlight markers depend on `$stderr.tty?` via
|
|
51
|
+
# the keyword arg default and `rb_io_tty_p`). Folding
|
|
52
|
+
# would freeze a value that depends on the calling
|
|
53
|
+
# process's stderr state.
|
|
54
|
+
:detailed_message,
|
|
55
|
+
# `exc_backtrace` reads the captured frame list, which
|
|
56
|
+
# depends on where the exception was raised — context
|
|
57
|
+
# the static fold tier cannot reproduce.
|
|
58
|
+
:backtrace,
|
|
59
|
+
# Same rationale as `:backtrace`; `Thread::Backtrace::Location`
|
|
60
|
+
# objects are runtime artefacts.
|
|
61
|
+
:backtrace_locations,
|
|
62
|
+
# `exc_set_backtrace` mutates the @backtrace ivar via
|
|
63
|
+
# `rb_ivar_set` — another indirect mutator the classifier
|
|
64
|
+
# missed.
|
|
65
|
+
:set_backtrace,
|
|
66
|
+
# `initialize_copy` is blocklisted by convention so a
|
|
67
|
+
# hypothetical future `Constant<Exception>` carrier
|
|
68
|
+
# cannot fold an aliasing copy through the catalog.
|
|
69
|
+
:initialize_copy,
|
|
70
|
+
# Defensive entries for the universal mutation surface.
|
|
71
|
+
# Object-identity hashing on a constant carrier is fine,
|
|
72
|
+
# but `eql?` on Exception delegates to `==` (dispatch);
|
|
73
|
+
# blocking both keeps the constant-fold tier honest.
|
|
74
|
+
:hash,
|
|
75
|
+
:eql?
|
|
76
|
+
],
|
|
77
|
+
# `Exception.to_tty?` (singleton) calls
|
|
78
|
+
# `rb_io_tty_p($stderr)`; its return depends on the
|
|
79
|
+
# process's stderr state at runtime, never on compile-time
|
|
80
|
+
# arguments. The catalog tier today only consults
|
|
81
|
+
# `mutating_selectors` for instance-receiver dispatches via
|
|
82
|
+
# `CATALOG_BY_CLASS`, so this row is documentation-grade —
|
|
83
|
+
# it records the soundness rationale for any future slice
|
|
84
|
+
# that wires the singleton path through the catalog.
|
|
85
|
+
"Exception.singleton" => Set[
|
|
86
|
+
:to_tty?
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|