rigortype 0.0.9 → 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 +40 -2
- data/lib/rigor/analysis/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +183 -4
- 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.rb +9 -3
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +110 -6
- data/lib/rigor/environment/rbs_loader.rb +89 -13
- 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 +84 -5
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +95 -9
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +105 -130
- 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 +57 -9
- data/lib/rigor/reflection.rb +2 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +7 -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
- metadata +18 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class FlowContribution
|
|
5
|
+
# Tagged element flattening of a {FlowContribution} bundle —
|
|
6
|
+
# the analyzer-internal representation [ADR-2 § "Flow
|
|
7
|
+
# Contribution Bundle"](../../../docs/adr/2-extension-api.md)
|
|
8
|
+
# routes through the {Merger}.
|
|
9
|
+
#
|
|
10
|
+
# The flattening is **mechanical, deterministic, and round-
|
|
11
|
+
# trippable** with the bundle: every non-empty slot expands
|
|
12
|
+
# into one or more elements keyed by `(target, edge, kind)`,
|
|
13
|
+
# and an array of elements rebuilds an equivalent bundle when
|
|
14
|
+
# routed through `Merger.merge`.
|
|
15
|
+
#
|
|
16
|
+
# Plugin authors should not depend on the element shape — the
|
|
17
|
+
# bundle is the public contract; the element list is the
|
|
18
|
+
# implementation surface the merge policy operates over.
|
|
19
|
+
ELEMENT_VALID_EDGES = %i[normal truthy falsey post_return exceptional].freeze
|
|
20
|
+
ELEMENT_VALID_KINDS = %i[
|
|
21
|
+
return_type
|
|
22
|
+
truthy_fact
|
|
23
|
+
falsey_fact
|
|
24
|
+
post_return_fact
|
|
25
|
+
mutation
|
|
26
|
+
invalidation
|
|
27
|
+
exception
|
|
28
|
+
role
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
Element = Data.define(:target, :edge, :kind, :payload, :provenance) do
|
|
32
|
+
def initialize(target:, edge:, kind:, payload:, provenance:)
|
|
33
|
+
unless ELEMENT_VALID_EDGES.include?(edge)
|
|
34
|
+
raise ArgumentError,
|
|
35
|
+
"FlowContribution::Element edge must be one of " \
|
|
36
|
+
"#{ELEMENT_VALID_EDGES.inspect}, got #{edge.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
unless ELEMENT_VALID_KINDS.include?(kind)
|
|
40
|
+
raise ArgumentError,
|
|
41
|
+
"FlowContribution::Element kind must be one of " \
|
|
42
|
+
"#{ELEMENT_VALID_KINDS.inspect}, got #{kind.inspect}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def merge_key
|
|
49
|
+
[target, edge, kind]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class FlowContribution
|
|
5
|
+
# Canonical slot payload for the four edge-aware fact slots
|
|
6
|
+
# (`truthy_facts`, `falsey_facts`, `post_return_facts`, plus
|
|
7
|
+
# the equivalent under `mutations` / `invalidations` /
|
|
8
|
+
# `role_conformance` once those carriers grow Fact-shaped
|
|
9
|
+
# variants).
|
|
10
|
+
#
|
|
11
|
+
# ADR-7 § "Slice 4-A" pins this object as the **single
|
|
12
|
+
# canonical translation target** for the four parallel
|
|
13
|
+
# contribution carriers the engine has carried so far:
|
|
14
|
+
#
|
|
15
|
+
# 1. Built-in narrowing rules' direct fact emission
|
|
16
|
+
# (Inference::Narrowing#predicate_scopes).
|
|
17
|
+
# 2. RBS::Extended `predicate-if-*` directives
|
|
18
|
+
# (`Rigor::RbsExtended::PredicateEffect`).
|
|
19
|
+
# 3. RBS::Extended `assert*` directives
|
|
20
|
+
# (`Rigor::RbsExtended::AssertEffect`).
|
|
21
|
+
# 4. Future plugin contributions (slice 5 emission protocol).
|
|
22
|
+
#
|
|
23
|
+
# Each of those four carriers translates to / from Fact at
|
|
24
|
+
# its boundary; downstream of {Rigor::FlowContribution#to_element_list}
|
|
25
|
+
# and {Rigor::FlowContribution::Merger.merge}, every slot
|
|
26
|
+
# payload is a Fact (or a value that the merger compares by
|
|
27
|
+
# equality and never inspects). The typed `RbsExtended::*Effect`
|
|
28
|
+
# carriers stay internal to the parser side — they hold the
|
|
29
|
+
# source-text shape, but lose their identity at the
|
|
30
|
+
# `read_flow_contribution` boundary.
|
|
31
|
+
#
|
|
32
|
+
# ## Field set
|
|
33
|
+
#
|
|
34
|
+
# - `target_kind`: `:parameter` (call-site argument) or
|
|
35
|
+
# `:self` (receiver). Future slices may extend the set
|
|
36
|
+
# (`:local`, `:ivar`, `:result`); the merger is agnostic
|
|
37
|
+
# to the concrete kinds and only requires equality.
|
|
38
|
+
# - `target_name`: a `Symbol`. For `:parameter` it's the
|
|
39
|
+
# declared parameter name. For `:self` it is the literal
|
|
40
|
+
# `:self` symbol so the field stays non-nil and the merge
|
|
41
|
+
# key is well-defined.
|
|
42
|
+
# - `type`: a `Rigor::Type::*` (Nominal, Refined,
|
|
43
|
+
# IntegerRange, Difference, …) the fact narrows the
|
|
44
|
+
# target toward (when `negative` is false) or away from
|
|
45
|
+
# (when `negative` is true).
|
|
46
|
+
# - `negative`: `true` for the `~T` negation form
|
|
47
|
+
# (`predicate-if-true x is ~Integer`), `false` for the
|
|
48
|
+
# plain positive form. Mirrors the `negative` field on
|
|
49
|
+
# `PredicateEffect` / `AssertEffect`.
|
|
50
|
+
#
|
|
51
|
+
# The `target` accessor returns `:self` for self-targeted
|
|
52
|
+
# facts and `[:parameter, name]` otherwise — that's the
|
|
53
|
+
# value {Element#target} keys on, so two facts that narrow
|
|
54
|
+
# the same parameter from different contribution sources
|
|
55
|
+
# land in the same merge bucket.
|
|
56
|
+
FACT_VALID_TARGET_KINDS = %i[parameter self].freeze
|
|
57
|
+
|
|
58
|
+
Fact = Data.define(:target_kind, :target_name, :type, :negative) do
|
|
59
|
+
def initialize(target_kind:, target_name:, type:, negative: false)
|
|
60
|
+
unless FACT_VALID_TARGET_KINDS.include?(target_kind)
|
|
61
|
+
raise ArgumentError,
|
|
62
|
+
"FlowContribution::Fact target_kind must be one of " \
|
|
63
|
+
"#{FACT_VALID_TARGET_KINDS.inspect}, got #{target_kind.inspect}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
unless target_name.is_a?(Symbol)
|
|
67
|
+
raise ArgumentError,
|
|
68
|
+
"FlowContribution::Fact target_name must be a Symbol, got #{target_name.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Composite target identifier the merger keys on. `:self`
|
|
75
|
+
# for self-targeted facts; otherwise `[:parameter, name]`
|
|
76
|
+
# so two contributions that narrow the same parameter
|
|
77
|
+
# (regardless of source family) land in the same merge
|
|
78
|
+
# bucket.
|
|
79
|
+
def target
|
|
80
|
+
target_kind == :self ? :self : [target_kind, target_name]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def negative?
|
|
84
|
+
negative == true
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class FlowContribution
|
|
5
|
+
# Result of folding any number of {FlowContribution} bundles
|
|
6
|
+
# through {Merger.merge}. Surfaces the merged content slot-by-
|
|
7
|
+
# slot, the ordered list of contributing provenances, and the
|
|
8
|
+
# {Conflict} list collected along the way.
|
|
9
|
+
#
|
|
10
|
+
# The merge result is a sibling shape of {FlowContribution} —
|
|
11
|
+
# the analyzer reads from it to drive narrowing / dispatch /
|
|
12
|
+
# diagnostics, and the formatter reads from it to surface
|
|
13
|
+
# plugin / RBS::Extended provenance. The shape is derived per
|
|
14
|
+
# ADR-2 § "Plugin Contribution Merging"; see
|
|
15
|
+
# [`docs/internal-spec/flow-contribution-merger.md`](../../../docs/internal-spec/flow-contribution-merger.md)
|
|
16
|
+
# for the slice-3 normative description.
|
|
17
|
+
class MergeResult
|
|
18
|
+
attr_reader :return_type, :truthy_facts, :falsey_facts, :post_return_facts,
|
|
19
|
+
:mutations, :invalidations, :exceptional, :role_conformance,
|
|
20
|
+
:provenances, :conflicts
|
|
21
|
+
|
|
22
|
+
# rubocop:disable Metrics/ParameterLists
|
|
23
|
+
def initialize(return_type: nil, truthy_facts: [], falsey_facts: [],
|
|
24
|
+
post_return_facts: [], mutations: [], invalidations: [],
|
|
25
|
+
exceptional: nil, role_conformance: [],
|
|
26
|
+
provenances: [], conflicts: [])
|
|
27
|
+
# rubocop:enable Metrics/ParameterLists
|
|
28
|
+
@return_type = return_type
|
|
29
|
+
@truthy_facts = truthy_facts.dup.freeze
|
|
30
|
+
@falsey_facts = falsey_facts.dup.freeze
|
|
31
|
+
@post_return_facts = post_return_facts.dup.freeze
|
|
32
|
+
@mutations = mutations.dup.freeze
|
|
33
|
+
@invalidations = invalidations.dup.freeze
|
|
34
|
+
@exceptional = exceptional
|
|
35
|
+
@role_conformance = role_conformance.dup.freeze
|
|
36
|
+
@provenances = provenances.dup.freeze
|
|
37
|
+
@conflicts = conflicts.dup.freeze
|
|
38
|
+
freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def conflict?
|
|
42
|
+
!@conflicts.empty?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def empty? # rubocop:disable Metrics/CyclomaticComplexity
|
|
46
|
+
@return_type.nil? && @truthy_facts.empty? && @falsey_facts.empty? &&
|
|
47
|
+
@post_return_facts.empty? && @mutations.empty? && @invalidations.empty? &&
|
|
48
|
+
@exceptional.nil? && @role_conformance.empty?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_h
|
|
52
|
+
{
|
|
53
|
+
"return_type" => return_type,
|
|
54
|
+
"truthy_facts" => truthy_facts,
|
|
55
|
+
"falsey_facts" => falsey_facts,
|
|
56
|
+
"post_return_facts" => post_return_facts,
|
|
57
|
+
"mutations" => mutations,
|
|
58
|
+
"invalidations" => invalidations,
|
|
59
|
+
"exceptional" => exceptional,
|
|
60
|
+
"role_conformance" => role_conformance,
|
|
61
|
+
"provenances" => provenances.map { |p| p.respond_to?(:to_h) ? p.to_h : p },
|
|
62
|
+
"conflicts" => conflicts.map(&:to_h)
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -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?
|