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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -2
  3. data/data/builtins/ruby_core/array.yml +6 -6
  4. data/data/builtins/ruby_core/hash.yml +1 -1
  5. data/data/builtins/ruby_core/io.yml +3 -3
  6. data/data/builtins/ruby_core/numeric.yml +1 -1
  7. data/data/builtins/ruby_core/pathname.yml +100 -100
  8. data/data/builtins/ruby_core/proc.yml +1 -1
  9. data/data/builtins/ruby_core/time.yml +3 -3
  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 +269 -7
  13. data/lib/rigor/builtins/regex_refinement.rb +104 -0
  14. data/lib/rigor/cache/rbs_class_ancestor_table.rb +1 -1
  15. data/lib/rigor/cache/rbs_class_type_param_names.rb +1 -1
  16. data/lib/rigor/cache/rbs_constant_table.rb +2 -2
  17. data/lib/rigor/cache/rbs_descriptor.rb +2 -0
  18. data/lib/rigor/cache/rbs_instance_definitions.rb +79 -0
  19. data/lib/rigor/cache/store.rb +2 -0
  20. data/lib/rigor/cli/type_of_command.rb +3 -3
  21. data/lib/rigor/cli/type_scan_command.rb +4 -4
  22. data/lib/rigor/cli.rb +20 -7
  23. data/lib/rigor/configuration/severity_profile.rb +109 -0
  24. data/lib/rigor/configuration.rb +286 -15
  25. data/lib/rigor/environment/rbs_loader.rb +89 -13
  26. data/lib/rigor/environment.rb +12 -4
  27. data/lib/rigor/flow_contribution/conflict.rb +81 -0
  28. data/lib/rigor/flow_contribution/element.rb +53 -0
  29. data/lib/rigor/flow_contribution/fact.rb +88 -0
  30. data/lib/rigor/flow_contribution/merge_result.rb +67 -0
  31. data/lib/rigor/flow_contribution/merger.rb +275 -0
  32. data/lib/rigor/flow_contribution.rb +51 -0
  33. data/lib/rigor/inference/block_parameter_binder.rb +15 -0
  34. data/lib/rigor/inference/expression_typer.rb +87 -6
  35. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +31 -0
  36. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +136 -9
  37. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
  38. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +68 -2
  39. data/lib/rigor/inference/method_dispatcher.rb +50 -1
  40. data/lib/rigor/inference/multi_target_binder.rb +2 -0
  41. data/lib/rigor/inference/narrowing.rb +246 -127
  42. data/lib/rigor/inference/scope_indexer.rb +124 -16
  43. data/lib/rigor/inference/statement_evaluator.rb +406 -37
  44. data/lib/rigor/plugin/access_denied_error.rb +24 -0
  45. data/lib/rigor/plugin/base.rb +284 -0
  46. data/lib/rigor/plugin/fact_store.rb +92 -0
  47. data/lib/rigor/plugin/io_boundary.rb +102 -0
  48. data/lib/rigor/plugin/load_error.rb +35 -0
  49. data/lib/rigor/plugin/loader.rb +307 -0
  50. data/lib/rigor/plugin/manifest.rb +203 -0
  51. data/lib/rigor/plugin/registry.rb +50 -0
  52. data/lib/rigor/plugin/services.rb +77 -0
  53. data/lib/rigor/plugin/trust_policy.rb +99 -0
  54. data/lib/rigor/plugin.rb +62 -0
  55. data/lib/rigor/rbs_extended.rb +57 -9
  56. data/lib/rigor/reflection.rb +2 -2
  57. data/lib/rigor/trinary.rb +1 -1
  58. data/lib/rigor/type/integer_range.rb +6 -2
  59. data/lib/rigor/version.rb +1 -1
  60. data/lib/rigor.rb +7 -0
  61. data/sig/rigor/environment.rbs +10 -3
  62. data/sig/rigor/inference.rbs +1 -0
  63. data/sig/rigor/rbs_extended.rbs +2 -0
  64. data/sig/rigor/scope.rbs +1 -0
  65. data/sig/rigor/type.rbs +7 -0
  66. data/sig/rigor.rbs +8 -2
  67. 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 => :type_of_dynamic_top,
175
- Prism::MatchPredicateNode => :type_of_dynamic_top,
176
- Prism::MatchRequiredNode => :type_of_dynamic_top,
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.nominal_of(Range) unless left_static && right_static
768
+ return Type::Combinator.constant_of(Range.new(left, right, node.exclude_end?)) if left_static && right_static
727
769
 
728
- Type::Combinator.constant_of(Range.new(left, right, node.exclude_end?))
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