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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -22
  3. data/data/builtins/ruby_core/encoding.yml +210 -0
  4. data/data/builtins/ruby_core/exception.yml +641 -0
  5. data/data/builtins/ruby_core/numeric.yml +3 -2
  6. data/data/builtins/ruby_core/proc.yml +731 -0
  7. data/data/builtins/ruby_core/random.yml +166 -0
  8. data/data/builtins/ruby_core/re.yml +689 -0
  9. data/data/builtins/ruby_core/struct.yml +449 -0
  10. data/lib/rigor/analysis/check_rules.rb +228 -40
  11. data/lib/rigor/analysis/diagnostic.rb +15 -1
  12. data/lib/rigor/analysis/runner.rb +199 -4
  13. data/lib/rigor/builtins/imported_refinements.rb +6 -1
  14. data/lib/rigor/cache/rbs_class_ancestor_table.rb +63 -0
  15. data/lib/rigor/cache/rbs_class_type_param_names.rb +60 -0
  16. data/lib/rigor/cache/rbs_constant_table.rb +15 -51
  17. data/lib/rigor/cache/rbs_descriptor.rb +55 -0
  18. data/lib/rigor/cache/rbs_environment.rb +52 -0
  19. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +40 -0
  20. data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
  21. data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
  22. data/lib/rigor/cache/store.rb +81 -15
  23. data/lib/rigor/cli.rb +45 -7
  24. data/lib/rigor/configuration/severity_profile.rb +109 -0
  25. data/lib/rigor/configuration.rb +110 -6
  26. data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
  27. data/lib/rigor/environment/rbs_loader.rb +220 -32
  28. data/lib/rigor/environment.rb +11 -2
  29. data/lib/rigor/flow_contribution/conflict.rb +81 -0
  30. data/lib/rigor/flow_contribution/element.rb +53 -0
  31. data/lib/rigor/flow_contribution/fact.rb +88 -0
  32. data/lib/rigor/flow_contribution/merge_result.rb +67 -0
  33. data/lib/rigor/flow_contribution/merger.rb +275 -0
  34. data/lib/rigor/flow_contribution.rb +179 -0
  35. data/lib/rigor/inference/block_parameter_binder.rb +15 -0
  36. data/lib/rigor/inference/builtins/encoding_catalog.rb +67 -0
  37. data/lib/rigor/inference/builtins/exception_catalog.rb +92 -0
  38. data/lib/rigor/inference/builtins/proc_catalog.rb +122 -0
  39. data/lib/rigor/inference/builtins/random_catalog.rb +58 -0
  40. data/lib/rigor/inference/builtins/re_catalog.rb +81 -0
  41. data/lib/rigor/inference/builtins/struct_catalog.rb +55 -0
  42. data/lib/rigor/inference/expression_typer.rb +110 -6
  43. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
  44. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +173 -0
  45. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
  46. data/lib/rigor/inference/method_dispatcher.rb +2 -0
  47. data/lib/rigor/inference/multi_target_binder.rb +2 -0
  48. data/lib/rigor/inference/narrowing.rb +134 -144
  49. data/lib/rigor/inference/scope_indexer.rb +75 -1
  50. data/lib/rigor/inference/statement_evaluator.rb +380 -40
  51. data/lib/rigor/plugin/access_denied_error.rb +24 -0
  52. data/lib/rigor/plugin/base.rb +241 -0
  53. data/lib/rigor/plugin/io_boundary.rb +102 -0
  54. data/lib/rigor/plugin/load_error.rb +23 -0
  55. data/lib/rigor/plugin/loader.rb +191 -0
  56. data/lib/rigor/plugin/manifest.rb +134 -0
  57. data/lib/rigor/plugin/registry.rb +50 -0
  58. data/lib/rigor/plugin/services.rb +65 -0
  59. data/lib/rigor/plugin/trust_policy.rb +99 -0
  60. data/lib/rigor/plugin.rb +61 -0
  61. data/lib/rigor/rbs_extended.rb +103 -0
  62. data/lib/rigor/reflection.rb +2 -2
  63. data/lib/rigor/type/combinator.rb +72 -0
  64. data/lib/rigor/type/refined.rb +50 -2
  65. data/lib/rigor/version.rb +1 -1
  66. data/lib/rigor.rb +13 -0
  67. data/sig/rigor/environment.rbs +7 -1
  68. data/sig/rigor/inference.rbs +1 -0
  69. data/sig/rigor/rbs_extended.rbs +2 -0
  70. data/sig/rigor/scope.rbs +1 -0
  71. data/sig/rigor/type.rbs +7 -0
  72. data/sig/rigor.rbs +3 -1
  73. 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