rigortype 0.1.4 → 0.1.6

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -56
  3. data/lib/rigor/analysis/buffer_binding.rb +36 -0
  4. data/lib/rigor/analysis/check_rules.rb +11 -1
  5. data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
  6. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
  8. data/lib/rigor/analysis/fact_store.rb +15 -3
  9. data/lib/rigor/analysis/project_scan.rb +39 -0
  10. data/lib/rigor/analysis/result.rb +11 -3
  11. data/lib/rigor/analysis/run_stats.rb +193 -0
  12. data/lib/rigor/analysis/runner.rb +681 -19
  13. data/lib/rigor/analysis/worker_session.rb +339 -0
  14. data/lib/rigor/builtins/hkt_builtins.rb +342 -0
  15. data/lib/rigor/builtins/imported_refinements.rb +6 -2
  16. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  17. data/lib/rigor/builtins/static_return_refinements.rb +120 -0
  18. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  19. data/lib/rigor/cache/store.rb +72 -9
  20. data/lib/rigor/cli/lsp_command.rb +129 -0
  21. data/lib/rigor/cli/type_of_command.rb +44 -5
  22. data/lib/rigor/cli.rb +122 -10
  23. data/lib/rigor/configuration.rb +168 -7
  24. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  25. data/lib/rigor/environment/class_registry.rb +12 -3
  26. data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
  27. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  28. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  29. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  30. data/lib/rigor/environment/rbs_loader.rb +238 -7
  31. data/lib/rigor/environment/reflection.rb +152 -0
  32. data/lib/rigor/environment/reporters.rb +40 -0
  33. data/lib/rigor/environment.rb +179 -10
  34. data/lib/rigor/inference/acceptance.rb +83 -4
  35. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  36. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  37. data/lib/rigor/inference/expression_typer.rb +59 -2
  38. data/lib/rigor/inference/hkt_body.rb +171 -0
  39. data/lib/rigor/inference/hkt_body_parser.rb +363 -0
  40. data/lib/rigor/inference/hkt_reducer.rb +256 -0
  41. data/lib/rigor/inference/hkt_registry.rb +223 -0
  42. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  43. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
  44. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  45. data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
  46. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +126 -31
  47. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
  48. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
  49. data/lib/rigor/inference/method_dispatcher.rb +282 -6
  50. data/lib/rigor/inference/method_parameter_binder.rb +21 -11
  51. data/lib/rigor/inference/narrowing.rb +127 -8
  52. data/lib/rigor/inference/project_patched_methods.rb +70 -0
  53. data/lib/rigor/inference/project_patched_scanner.rb +210 -0
  54. data/lib/rigor/inference/scope_indexer.rb +156 -12
  55. data/lib/rigor/inference/statement_evaluator.rb +106 -6
  56. data/lib/rigor/inference/synthetic_method.rb +86 -0
  57. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  58. data/lib/rigor/inference/synthetic_method_scanner.rb +599 -0
  59. data/lib/rigor/language_server/buffer_table.rb +63 -0
  60. data/lib/rigor/language_server/completion_provider.rb +438 -0
  61. data/lib/rigor/language_server/debouncer.rb +86 -0
  62. data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
  63. data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
  64. data/lib/rigor/language_server/folding_range_provider.rb +75 -0
  65. data/lib/rigor/language_server/hover_provider.rb +74 -0
  66. data/lib/rigor/language_server/hover_renderer.rb +312 -0
  67. data/lib/rigor/language_server/loop.rb +71 -0
  68. data/lib/rigor/language_server/project_context.rb +145 -0
  69. data/lib/rigor/language_server/selection_range_provider.rb +93 -0
  70. data/lib/rigor/language_server/server.rb +384 -0
  71. data/lib/rigor/language_server/signature_help_provider.rb +249 -0
  72. data/lib/rigor/language_server/synchronized_writer.rb +28 -0
  73. data/lib/rigor/language_server/uri.rb +40 -0
  74. data/lib/rigor/language_server.rb +29 -0
  75. data/lib/rigor/plugin/base.rb +63 -0
  76. data/lib/rigor/plugin/blueprint.rb +60 -0
  77. data/lib/rigor/plugin/loader.rb +3 -1
  78. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  79. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  80. data/lib/rigor/plugin/macro/heredoc_template.rb +315 -0
  81. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  82. data/lib/rigor/plugin/macro.rb +31 -0
  83. data/lib/rigor/plugin/manifest.rb +127 -9
  84. data/lib/rigor/plugin/registry.rb +51 -2
  85. data/lib/rigor/plugin.rb +1 -0
  86. data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
  87. data/lib/rigor/rbs_extended.rb +82 -2
  88. data/lib/rigor/sig_gen/generator.rb +12 -3
  89. data/lib/rigor/trinary.rb +15 -11
  90. data/lib/rigor/type/app.rb +107 -0
  91. data/lib/rigor/type/bot.rb +6 -3
  92. data/lib/rigor/type/combinator.rb +12 -1
  93. data/lib/rigor/type/integer_range.rb +7 -7
  94. data/lib/rigor/type/refined.rb +18 -12
  95. data/lib/rigor/type/top.rb +4 -3
  96. data/lib/rigor/type.rb +1 -0
  97. data/lib/rigor/type_node/generic.rb +7 -1
  98. data/lib/rigor/type_node/identifier.rb +9 -1
  99. data/lib/rigor/type_node/string_literal.rb +4 -1
  100. data/lib/rigor/version.rb +1 -1
  101. data/sig/rigor/environment.rbs +11 -4
  102. data/sig/rigor/inference.rbs +2 -0
  103. data/sig/rigor/plugin/blueprint.rbs +7 -0
  104. data/sig/rigor/plugin/manifest.rbs +1 -1
  105. data/sig/rigor/plugin/registry.rbs +14 -1
  106. data/sig/rigor.rbs +37 -2
  107. metadata +92 -1
@@ -76,6 +76,21 @@ module Rigor
76
76
  return nil unless receiver.is_a?(Type::BoundMethod)
77
77
  return nil unless backward_method?(method_name)
78
78
 
79
+ # `Method#curry` is treated as identity on the carrier
80
+ # — `<bound>.curry` keeps the same
81
+ # `(receiver_type, method_name)` so a subsequent
82
+ # `<curried>.call` still routes through the recursive
83
+ # dispatch below. This is correct for the dominant
84
+ # no-arg form (`.curry.call`); partially-applied
85
+ # forms (`.curry(n).call(a)`) lose precision and fall
86
+ # through to RBS via the trailing
87
+ # `Type::Combinator.untyped`. A faithful
88
+ # `Type::CurriedBoundMethod(receiver_type,
89
+ # method_name, accumulated_args)` carrier is reserved
90
+ # for a future slice when concrete user demand
91
+ # surfaces.
92
+ return receiver if method_name == :curry
93
+
79
94
  MethodDispatcher.dispatch(
80
95
  receiver_type: receiver.receiver_type,
81
96
  method_name: receiver.method_name,
@@ -92,7 +107,9 @@ module Rigor
92
107
  # commonly used as a case-equality predicate, so we
93
108
  # do NOT fold through it (the case/when narrowing path
94
109
  # already special-cases `===` for branch typing).
95
- BACKWARD_METHOD_NAMES = %i[call []].freeze
110
+ # `Method#curry` rides through as identity (see the
111
+ # comment in `try_backward`).
112
+ BACKWARD_METHOD_NAMES = %i[call [] curry].freeze
96
113
  private_constant :BACKWARD_METHOD_NAMES
97
114
 
98
115
  def backward_method?(method_name)
@@ -3,6 +3,7 @@
3
3
  require_relative "../../type"
4
4
  require_relative "../acceptance"
5
5
  require_relative "../rbs_type_translator"
6
+ require_relative "receiver_affinity"
6
7
 
7
8
  module Rigor
8
9
  module Inference
@@ -26,7 +27,7 @@ module Rigor
26
27
  # `Array#[](Range) -> Array[Elem]?` overload for a Range
27
28
  # argument. (Surfaced during v0.1.1 self-analysis; see the
28
29
  # "Interface-strictness on overload selection" item in
29
- # `docs/MILESTONES.md`.)
30
+ # `docs/ROADMAP.md`.)
30
31
  # 3. **Pass 2 — gradual fall-back.** If no fully strict overload
31
32
  # matches, accept the first arity-and-gradual-accept match
32
33
  # (the v0.1.1 behaviour). Alias / Interface / Intersection
@@ -44,6 +45,33 @@ module Rigor
44
45
  module OverloadSelector
45
46
  module_function
46
47
 
48
+ # Canonical RBS-core aliases shipped by `core/builtin.rbs`
49
+ # whose body is `<Nominal> | _DuckType`. Matching an
50
+ # overload against an Integer literal should pick the
51
+ # `(int) -> Array[Elem]` body over the `(string) -> String`
52
+ # body because Integer satisfies `int`'s strict arm and
53
+ # not `string`'s. The translator collapses both aliases
54
+ # to `Dynamic[Top]` (interfaces are not structurally
55
+ # matched yet), so a dedicated pass 1.5 between strict
56
+ # and gradual consults this map to pick the alias whose
57
+ # strict arm matches.
58
+ #
59
+ # Symbol keys are the alias names as they appear under
60
+ # `RBS::Types::Alias#name.to_s` (the `name` is a
61
+ # `TypeName` whose `to_s` includes the `::` prefix).
62
+ # Values are an Array of class names whose Nominal[..]
63
+ # form is the alias's strict-arm matcher.
64
+ ALIAS_STRICT_NOMINALS = {
65
+ "::int" => ["Integer"],
66
+ "::string" => ["String"],
67
+ "::interned" => %w[Symbol String],
68
+ "::io" => ["IO"],
69
+ "::encoding" => %w[Encoding String],
70
+ "::path" => ["String"],
71
+ "::boolean" => %w[TrueClass FalseClass]
72
+ }.freeze
73
+ private_constant :ALIAS_STRICT_NOMINALS
74
+
47
75
  # @param method_definition [RBS::Definition::Method]
48
76
  # @param arg_types [Array<Rigor::Type>] caller-provided types in
49
77
  # positional order. Empty when there are no arguments.
@@ -77,36 +105,19 @@ module Rigor
77
105
  # compatibility.
78
106
  param_overrides = RbsExtended.param_type_override_map(method_definition, environment: environment)
79
107
 
80
- # Pass 1: prefer overloads whose param types stay strict —
81
- # no translator-induced `Dynamic[Top]` from Alias /
82
- # Interface / Intersection. The pass is skipped
83
- # entirely when any arg is `Dynamic[Top]` (literally
84
- # `untyped`), because gradual acceptance against an
85
- # untyped arg accepts every param indiscriminately and
86
- # would let pass 1 lock in an arbitrary strict overload
87
- # (e.g. `Regexp#=~(nil) -> nil` over the
88
- # `(::interned?) -> Integer?` overload). Pass 2 falls
89
- # back to the original gradual matcher so overloads
90
- # that legitimately rely on duck-typed params still
91
- # resolve when nothing stricter applies.
92
- match = find_matching_overload(
93
- overloads,
94
- arg_types: arg_types,
95
- self_type: self_type,
96
- instance_type: instance_type,
97
- type_vars: type_vars,
98
- block_required: block_required,
99
- param_overrides: param_overrides,
100
- strict: true
101
- ) || find_matching_overload(
102
- overloads,
103
- arg_types: arg_types,
104
- self_type: self_type,
105
- instance_type: instance_type,
106
- type_vars: type_vars,
107
- block_required: block_required,
108
- param_overrides: param_overrides,
109
- strict: false
108
+ # Pre-sort: demote overloads whose param class is a
109
+ # disjoint sibling of the receiver class (e.g.
110
+ # `Integer#+(BigDecimal) -> BigDecimal` from the
111
+ # `bigdecimal` RBS reopen). Honors the coerce
112
+ # convention so `5 + ?` for unknown `?` resolves to
113
+ # the receiver-class-preserving arm rather than an
114
+ # arbitrary sibling-class arm that only wins by
115
+ # overload-list position.
116
+ overloads = ReceiverAffinity.reorder(overloads, self_type: self_type, environment: environment)
117
+
118
+ match = run_selection_passes(
119
+ overloads, arg_types: arg_types, self_type: self_type, instance_type: instance_type,
120
+ type_vars: type_vars, block_required: block_required, param_overrides: param_overrides
110
121
  )
111
122
  return match if match
112
123
  return overloads.find { |mt| overload_has_block?(mt) } if block_required
@@ -121,6 +132,30 @@ module Rigor
121
132
  class << self
122
133
  private
123
134
 
135
+ # Three-pass overload search:
136
+ # - Pass 1 (strict): skipped when any arg is
137
+ # `Dynamic[Top]`, because gradual acceptance against
138
+ # an untyped arg accepts every param indiscriminately
139
+ # and would let pass 1 lock in an arbitrary strict
140
+ # overload (e.g. `Regexp#=~(nil) -> nil` over the
141
+ # `(::interned?) -> Integer?` overload).
142
+ # - Pass 1.5 (alias-resolved): consults each `RBS::Types::Alias`'s
143
+ # strict arm so e.g. `Array#*(int)` wins over the
144
+ # `Array#*(string) -> String` overload for Integer args.
145
+ # - Pass 2 (gradual): the original gradual matcher so
146
+ # overloads that legitimately rely on duck-typed
147
+ # params still resolve when nothing stricter applies.
148
+ def run_selection_passes(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:,
149
+ param_overrides:)
150
+ shared = {
151
+ arg_types: arg_types, self_type: self_type, instance_type: instance_type,
152
+ type_vars: type_vars, block_required: block_required, param_overrides: param_overrides
153
+ }
154
+ find_matching_overload(overloads, **shared, strict: true) ||
155
+ find_matching_overload_via_aliases(overloads, arg_types: arg_types, block_required: block_required) ||
156
+ find_matching_overload(overloads, **shared, strict: false)
157
+ end
158
+
124
159
  # rubocop:disable Metrics/ParameterLists
125
160
  def find_matching_overload(overloads, arg_types:, self_type:, instance_type:, type_vars:, block_required:,
126
161
  param_overrides:, strict:)
@@ -150,6 +185,66 @@ module Rigor
150
185
  type.is_a?(Type::Dynamic) && type.static_facet.is_a?(Type::Top)
151
186
  end
152
187
 
188
+ # Pass 1.5: for arity-compatible overloads whose every
189
+ # positional param is either a strict nominal OR a
190
+ # well-known core alias (`int` / `string` / `interned`
191
+ # / etc.), check the arg against the alias's STRICT
192
+ # arm. An Integer literal arg matches `int` here but
193
+ # not `string`, so `Array#*(int)` wins over the
194
+ # `Array#*(string) -> String` overload — even though
195
+ # both translate to `Dynamic[Top]` at the param level.
196
+ # Only fires when EVERY positional param has a known
197
+ # alias-or-strict shape; otherwise gradual matching
198
+ # takes over.
199
+ def find_matching_overload_via_aliases(overloads, arg_types:, block_required:)
200
+ overloads.find do |method_type|
201
+ next false if block_required && !OverloadSelector.overload_has_block?(method_type)
202
+
203
+ fun = method_type.type
204
+ next false unless arity_compatible?(fun, arg_types.size)
205
+
206
+ params = positional_params_for(fun, arg_types.size)
207
+ next false unless params.size == arg_types.size
208
+
209
+ params.zip(arg_types).all? { |param, arg| alias_param_accepts?(param.type, arg) }
210
+ end
211
+ end
212
+
213
+ # Checks the param's RBS type against an arg using
214
+ # alias-strict-arm matching. Optional / Union wrappers
215
+ # are flattened; alias resolution is one level deep
216
+ # (the canonical core aliases all have non-alias
217
+ # strict arms).
218
+ def alias_param_accepts?(rbs_type, arg)
219
+ nominal_names = strict_nominal_names_for(rbs_type)
220
+ return false if nominal_names.nil? || nominal_names.empty?
221
+
222
+ nominal_names.any? do |class_name|
223
+ result = Type::Combinator.nominal_of(class_name).accepts(arg, mode: :gradual)
224
+ result.yes? || result.maybe?
225
+ end
226
+ end
227
+
228
+ # Returns the candidate class names a param's RBS type
229
+ # accepts under alias-resolved strict matching, or nil
230
+ # when the shape cannot be reduced to a closed set of
231
+ # nominals (e.g. an Interface or an unrecognised alias).
232
+ def strict_nominal_names_for(rbs_type)
233
+ case rbs_type
234
+ when RBS::Types::ClassInstance
235
+ [rbs_type.name.to_s.delete_prefix("::")]
236
+ when RBS::Types::Alias
237
+ ALIAS_STRICT_NOMINALS[rbs_type.name.to_s]
238
+ when RBS::Types::Optional
239
+ strict_nominal_names_for(rbs_type.type)
240
+ when RBS::Types::Union
241
+ parts = rbs_type.types.map { |t| strict_nominal_names_for(t) }
242
+ return nil if parts.any?(&:nil?)
243
+
244
+ parts.flatten
245
+ end
246
+ end
247
+
153
248
  # Returns true when every positional param the call
154
249
  # site engages translates to a non-`Dynamic[Top]`
155
250
  # carrier. Alias / Interface / Intersection RBS types
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ module MethodDispatcher
8
+ # Stable-sort an overload list so that "receiver-affinity"
9
+ # arms come first. An overload is receiver-affinity-matching
10
+ # when every positional param's class equals `self_type`'s
11
+ # class name OR is one of its proper RBS ancestors. The
12
+ # canonical case the helper exists for: when `bigdecimal`'s
13
+ # stdlib RBS reopens `Integer#+` at the FRONT of the
14
+ # overload list with `(BigDecimal) -> BigDecimal`, that
15
+ # disjoint-sibling arm would win every dispatch for
16
+ # `Integer#+(?)` by overload-list position alone, returning
17
+ # a spurious `BigDecimal` for plain integer arithmetic.
18
+ # Demoting the arm honours the coerce convention: when the
19
+ # arg type is unknown or itself an Integer, the
20
+ # receiver-preserving `(Integer) -> Integer` arm should win.
21
+ #
22
+ # No-op when (a) the environment can't answer
23
+ # `class_ordering` (nil env), or (b) the receiver isn't a
24
+ # nominal / singleton carrying a class name. The partition
25
+ # is stable, so within each bucket the RBS-declared order
26
+ # is preserved.
27
+ module ReceiverAffinity
28
+ module_function
29
+
30
+ def reorder(overloads, self_type:, environment:)
31
+ return overloads if environment.nil?
32
+
33
+ self_class_name = self_type_class_name(self_type)
34
+ return overloads if self_class_name.nil?
35
+
36
+ affinity, other = overloads.partition do |mt|
37
+ overload_param_classes_in_ancestry?(mt, self_class_name, environment)
38
+ end
39
+ affinity + other
40
+ end
41
+
42
+ class << self
43
+ private
44
+
45
+ def self_type_class_name(self_type)
46
+ case self_type
47
+ when Type::Nominal, Type::Singleton then self_type.class_name
48
+ end
49
+ end
50
+
51
+ def overload_param_classes_in_ancestry?(method_type, self_class_name, environment)
52
+ fun = method_type.type
53
+ params = fun.required_positionals + fun.optional_positionals + fun.trailing_positionals
54
+ return false if params.empty?
55
+
56
+ params.all? { |param| param_class_in_ancestry?(param.type, self_class_name, environment) }
57
+ end
58
+
59
+ # Walks Optional and Union one level so `(Numeric?)` and
60
+ # `(Integer | Float)` still classify when every branch
61
+ # sits in the ancestry. Non-`ClassInstance` shapes
62
+ # (Alias / Interface / Intersection / type variables)
63
+ # don't carry a clean class identity and therefore
64
+ # disqualify the overload from the affinity bucket.
65
+ def param_class_in_ancestry?(rbs_type, self_class_name, environment)
66
+ case rbs_type
67
+ when RBS::Types::ClassInstance
68
+ class_in_ancestry?(rbs_type.name.to_s.delete_prefix("::"), self_class_name, environment)
69
+ when RBS::Types::Optional
70
+ param_class_in_ancestry?(rbs_type.type, self_class_name, environment)
71
+ when RBS::Types::Union
72
+ rbs_type.types.all? { |t| param_class_in_ancestry?(t, self_class_name, environment) }
73
+ else
74
+ false
75
+ end
76
+ end
77
+
78
+ def class_in_ancestry?(param_class_name, self_class_name, environment)
79
+ return true if param_class_name == self_class_name
80
+
81
+ environment.class_ordering(self_class_name, param_class_name) == :subclass
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -155,13 +155,13 @@ module Rigor
155
155
  # tier ahead of RBS sees the more precise carrier so
156
156
  # downstream narrowing (`if size > 0; …`) actually has a
157
157
  # range to intersect with.
158
- SIZE_RETURNING_NOMINALS = {
159
- "Array" => %i[size length count],
160
- "String" => %i[length size bytesize],
161
- "Hash" => %i[size length count],
162
- "Set" => %i[size length count],
163
- "Range" => %i[size length count]
164
- }.freeze
158
+ SIZE_RETURNING_NOMINALS = Ractor.make_shareable({
159
+ "Array" => %i[size length count],
160
+ "String" => %i[length size bytesize],
161
+ "Hash" => %i[size length count],
162
+ "Set" => %i[size length count],
163
+ "Range" => %i[size length count]
164
+ })
165
165
  private_constant :SIZE_RETURNING_NOMINALS
166
166
 
167
167
  # When the difference removes the empty value of the
@@ -323,39 +323,45 @@ module Rigor
323
323
  # `dispatch_nominal_size` so size-returning calls on
324
324
  # a `Refined[String, *]` still tighten to
325
325
  # `non_negative_int`.
326
- REFINED_STRING_PROJECTIONS = {
327
- %i[lowercase downcase] => :refined_self,
328
- %i[lowercase upcase] => :uppercase_string,
329
- %i[uppercase upcase] => :refined_self,
330
- %i[uppercase downcase] => :lowercase_string,
331
- %i[numeric downcase] => :refined_self,
332
- %i[numeric upcase] => :refined_self,
333
- # Digit-only strings are case-invariant; the prefix
334
- # letters in `0o…` / `0x…` are accepted by the
335
- # predicate in either case so the predicate-subset
336
- # is preserved across `#downcase` / `#upcase` even
337
- # though the value-set element changes.
338
- %i[decimal_int downcase] => :refined_self,
339
- %i[decimal_int upcase] => :refined_self,
340
- %i[octal_int downcase] => :refined_self,
341
- %i[octal_int upcase] => :refined_self,
342
- %i[hex_int downcase] => :refined_self,
343
- %i[hex_int upcase] => :refined_self,
344
- # v0.1.1 Track 1 slice 2 — `to_i` / `to_int` on a
345
- # known digit-only string. `decimal-int-string`
346
- # (`/\A\d+\z/`) and `numeric-string` (Rigor's
347
- # numeric-string predicate, ASCII digits) are
348
- # predicates over digit-only strings, so the parse
349
- # is total over the carrier domain and the result
350
- # is always `>= 0`. `non-negative-int` is the
351
- # tightest carrier that captures both the lower
352
- # bound and the integer-ness without inventing a
353
- # narrower carrier.
354
- %i[decimal_int to_i] => :non_negative_int,
355
- %i[decimal_int to_int] => :non_negative_int,
356
- %i[numeric to_i] => :non_negative_int,
357
- %i[numeric to_int] => :non_negative_int
358
- }.freeze
326
+ # ADR-15 Phase 4b.x — `Ractor.make_shareable` (not `.freeze`)
327
+ # because the keys are two-element Symbol arrays whose
328
+ # inner arrays are unfrozen under shallow `.freeze`.
329
+ # Surfaced on Discourse via `Ractor::IsolationError` when
330
+ # the dispatch loop's `REFINED_STRING_PROJECTIONS[[id, sym]]`
331
+ # lookup ran from a worker Ractor.
332
+ REFINED_STRING_PROJECTIONS = Ractor.make_shareable({
333
+ %i[lowercase downcase] => :refined_self,
334
+ %i[lowercase upcase] => :uppercase_string,
335
+ %i[uppercase upcase] => :refined_self,
336
+ %i[uppercase downcase] => :lowercase_string,
337
+ %i[numeric downcase] => :refined_self,
338
+ %i[numeric upcase] => :refined_self,
339
+ # Digit-only strings are case-invariant; the prefix
340
+ # letters in `0o…` / `0x…` are accepted by the
341
+ # predicate in either case so the predicate-subset
342
+ # is preserved across `#downcase` / `#upcase` even
343
+ # though the value-set element changes.
344
+ %i[decimal_int downcase] => :refined_self,
345
+ %i[decimal_int upcase] => :refined_self,
346
+ %i[octal_int downcase] => :refined_self,
347
+ %i[octal_int upcase] => :refined_self,
348
+ %i[hex_int downcase] => :refined_self,
349
+ %i[hex_int upcase] => :refined_self,
350
+ # v0.1.1 Track 1 slice 2 — `to_i` / `to_int` on a
351
+ # known digit-only string. `decimal-int-string`
352
+ # (`/\A\d+\z/`) and `numeric-string` (Rigor's
353
+ # numeric-string predicate, ASCII digits) are
354
+ # predicates over digit-only strings, so the parse
355
+ # is total over the carrier domain and the result
356
+ # is always `>= 0`. `non-negative-int` is the
357
+ # tightest carrier that captures both the lower
358
+ # bound and the integer-ness without inventing a
359
+ # narrower carrier.
360
+ %i[decimal_int to_i] => :non_negative_int,
361
+ %i[decimal_int to_int] => :non_negative_int,
362
+ %i[numeric to_i] => :non_negative_int,
363
+ %i[numeric to_int] => :non_negative_int
364
+ })
359
365
  private_constant :REFINED_STRING_PROJECTIONS
360
366
 
361
367
  def dispatch_refined(refined, method_name, args)