rigortype 0.1.4 → 0.1.5

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +40 -13
  3. data/lib/rigor/analysis/fact_store.rb +15 -3
  4. data/lib/rigor/analysis/result.rb +11 -3
  5. data/lib/rigor/analysis/run_stats.rb +193 -0
  6. data/lib/rigor/analysis/runner.rb +387 -12
  7. data/lib/rigor/analysis/worker_session.rb +327 -0
  8. data/lib/rigor/builtins/imported_refinements.rb +6 -2
  9. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  10. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  11. data/lib/rigor/cache/store.rb +40 -7
  12. data/lib/rigor/cli.rb +52 -2
  13. data/lib/rigor/configuration.rb +131 -6
  14. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  15. data/lib/rigor/environment/class_registry.rb +12 -3
  16. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  17. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  18. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  19. data/lib/rigor/environment/rbs_loader.rb +194 -6
  20. data/lib/rigor/environment/reflection.rb +152 -0
  21. data/lib/rigor/environment.rb +78 -6
  22. data/lib/rigor/inference/acceptance.rb +35 -1
  23. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  24. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  25. data/lib/rigor/inference/expression_typer.rb +12 -2
  26. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  27. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
  28. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  29. data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
  30. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -1
  31. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
  32. data/lib/rigor/inference/method_dispatcher.rb +128 -3
  33. data/lib/rigor/inference/method_parameter_binder.rb +21 -11
  34. data/lib/rigor/inference/narrowing.rb +127 -8
  35. data/lib/rigor/inference/synthetic_method.rb +86 -0
  36. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  37. data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
  38. data/lib/rigor/plugin/blueprint.rb +60 -0
  39. data/lib/rigor/plugin/loader.rb +3 -1
  40. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  41. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  42. data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
  43. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  44. data/lib/rigor/plugin/macro.rb +31 -0
  45. data/lib/rigor/plugin/manifest.rb +78 -7
  46. data/lib/rigor/plugin/registry.rb +32 -2
  47. data/lib/rigor/plugin.rb +1 -0
  48. data/lib/rigor/trinary.rb +15 -11
  49. data/lib/rigor/type/bot.rb +6 -3
  50. data/lib/rigor/type/combinator.rb +12 -1
  51. data/lib/rigor/type/integer_range.rb +7 -7
  52. data/lib/rigor/type/refined.rb +18 -12
  53. data/lib/rigor/type/top.rb +4 -3
  54. data/lib/rigor/type_node/generic.rb +7 -1
  55. data/lib/rigor/type_node/identifier.rb +9 -1
  56. data/lib/rigor/type_node/string_literal.rb +4 -1
  57. data/lib/rigor/version.rb +1 -1
  58. data/sig/rigor/environment.rbs +5 -2
  59. data/sig/rigor/plugin/blueprint.rbs +7 -0
  60. data/sig/rigor/plugin/manifest.rbs +1 -1
  61. data/sig/rigor/plugin/registry.rbs +14 -1
  62. data/sig/rigor.rbs +35 -2
  63. metadata +39 -1
@@ -2,6 +2,12 @@
2
2
 
3
3
  require_relative "environment/class_registry"
4
4
  require_relative "environment/rbs_loader"
5
+ require_relative "environment/reflection"
6
+ require_relative "environment/bundle_sig_discovery"
7
+ require_relative "environment/lockfile_resolver"
8
+ require_relative "environment/rbs_collection_discovery"
9
+ require_relative "environment/rbs_coverage_report"
10
+ require_relative "inference/synthetic_method_index"
5
11
  require_relative "type_node/name_scope"
6
12
  require_relative "type_node/resolver_chain"
7
13
 
@@ -40,11 +46,19 @@ module Rigor
40
46
  pathname optparse json yaml fileutils tempfile tmpdir
41
47
  stringio forwardable digest securerandom
42
48
  uri logger date
49
+ pp delegate observable abbrev find tsort singleton
50
+ shellwords benchmark base64 did_you_mean
51
+ monitor mutex_m timeout
52
+ open3 erb etc ipaddr bigdecimal bigdecimal-math
53
+ prettyprint random-formatter time open-uri resolv
54
+ csv pstore objspace io-console cgi cgi-escape
55
+ strscan
43
56
  prism rbs
44
57
  ].freeze
45
58
 
46
59
  attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
47
- :rbs_extended_reporter, :boundary_cross_reporter, :name_scope
60
+ :rbs_extended_reporter, :boundary_cross_reporter, :name_scope,
61
+ :synthetic_method_index
48
62
 
49
63
  # @param class_registry [Rigor::Environment::ClassRegistry]
50
64
  # @param rbs_loader [Rigor::Environment::RbsLoader, nil] when nil the
@@ -67,13 +81,15 @@ module Rigor
67
81
  # participates and the dispatcher tier is a no-op.
68
82
  def initialize(class_registry: ClassRegistry.default, rbs_loader: nil,
69
83
  plugin_registry: nil, dependency_source_index: nil,
70
- rbs_extended_reporter: nil, boundary_cross_reporter: nil)
84
+ rbs_extended_reporter: nil, boundary_cross_reporter: nil,
85
+ synthetic_method_index: nil)
71
86
  @class_registry = class_registry
72
87
  @rbs_loader = rbs_loader
73
88
  @plugin_registry = plugin_registry
74
89
  @dependency_source_index = dependency_source_index
75
90
  @rbs_extended_reporter = rbs_extended_reporter
76
91
  @boundary_cross_reporter = boundary_cross_reporter
92
+ @synthetic_method_index = synthetic_method_index || Inference::SyntheticMethodIndex::EMPTY
77
93
  @name_scope = build_name_scope
78
94
  freeze
79
95
  end
@@ -104,14 +120,57 @@ module Rigor
104
120
  # reflection artefacts) consult the cache. Pass `nil` (the
105
121
  # default) to skip caching for this environment.
106
122
  # @return [Rigor::Environment]
107
- def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil, # rubocop:disable Metrics/ParameterLists
123
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
124
+ def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil,
108
125
  plugin_registry: nil, dependency_source_index: nil,
109
- rbs_extended_reporter: nil, boundary_cross_reporter: nil)
126
+ rbs_extended_reporter: nil, boundary_cross_reporter: nil,
127
+ bundler_bundle_path: nil, bundler_auto_detect: false,
128
+ bundler_lockfile: nil,
129
+ rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
130
+ synthetic_method_index: nil)
110
131
  resolved_paths = signature_paths || default_signature_paths(root)
132
+ # O4 MVP — append per-gem `sig/` directories discovered
133
+ # under the target project's bundler install root. Empty
134
+ # array when neither an explicit path nor auto-detection
135
+ # finds a bundle. Order: user `signature_paths:` win first
136
+ # (semantic precedence inside `RbsLoader.build_env_for`);
137
+ # gem-shipped sigs append last so user overrides stay
138
+ # authoritative.
139
+ #
140
+ # O4 Layer 3 — when a Gemfile.lock is available (explicit
141
+ # `bundler_lockfile:` or auto-detected next to the project
142
+ # root), use the locked gem set to filter the discovered
143
+ # `sig/` directories. Stale gems in the bundle install
144
+ # tree (out-of-band installs, version drift after a
145
+ # `bundle update`) are silently dropped so only gems the
146
+ # project actually declares contribute RBS.
147
+ locked = LockfileResolver.locked_gems(
148
+ lockfile_path: bundler_lockfile,
149
+ project_root: root,
150
+ auto_detect: bundler_auto_detect
151
+ )
152
+ gem_sig_paths = BundleSigDiscovery.discover(
153
+ bundle_path: bundler_bundle_path,
154
+ project_root: root,
155
+ auto_detect: bundler_auto_detect,
156
+ locked_gems: locked.empty? ? nil : locked
157
+ ).map(&:to_s)
158
+ # O4 Layer 3 slice 2 — when `rbs collection install`
159
+ # has been run for the target project, parse the
160
+ # resulting `rbs_collection.lock.yaml` and feed each
161
+ # gem's `<collection_path>/<name>/<version>/` directory
162
+ # into `signature_paths:`. Stdlib-typed entries are
163
+ # skipped (already covered by `DEFAULT_LIBRARIES`).
164
+ collection_paths = RbsCollectionDiscovery.discover(
165
+ lockfile_path: rbs_collection_lockfile,
166
+ project_root: root,
167
+ auto_detect: rbs_collection_auto_detect
168
+ ).map(&:to_s)
169
+ loader_signature_paths = resolved_paths + gem_sig_paths + collection_paths
111
170
  merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
112
171
  loader = RbsLoader.new(
113
172
  libraries: merged_libraries,
114
- signature_paths: resolved_paths,
173
+ signature_paths: loader_signature_paths,
115
174
  cache_store: cache_store
116
175
  )
117
176
  new(
@@ -119,9 +178,11 @@ module Rigor
119
178
  plugin_registry: plugin_registry,
120
179
  dependency_source_index: dependency_source_index,
121
180
  rbs_extended_reporter: rbs_extended_reporter,
122
- boundary_cross_reporter: boundary_cross_reporter
181
+ boundary_cross_reporter: boundary_cross_reporter,
182
+ synthetic_method_index: synthetic_method_index
123
183
  )
124
184
  end
185
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
125
186
 
126
187
  private
127
188
 
@@ -185,6 +246,17 @@ module Rigor
185
246
  class_known_in_rbs?(name)
186
247
  end
187
248
 
249
+ # ADR-15 Phase 2b — returns the loader's read-only,
250
+ # `Ractor.shareable?` query surface as a frozen
251
+ # {Environment::Reflection}. Built lazily on first
252
+ # access; subsequent calls return the same instance.
253
+ # Returns `nil` when the environment carries no RBS
254
+ # loader (test-only `Environment.new` without
255
+ # `rbs_loader:`).
256
+ def reflection
257
+ @rbs_loader&.reflection
258
+ end
259
+
188
260
  # Compares two class/module names using analyzer-owned class data.
189
261
  # Returns `:equal`, `:subclass`, `:superclass`, `:disjoint`, or
190
262
  # `:unknown`. The static registry handles built-ins cheaply; the RBS
@@ -327,10 +327,44 @@ module Rigor
327
327
  )
328
328
  return class_result if class_result.no?
329
329
 
330
- args_result = accepts_nominal_args(self_type, other_type, mode)
330
+ # Parametrized-ancestor projection. When `actual <:= target`
331
+ # holds at the class level but the type-arg arities differ,
332
+ # the actual's parametrization has to be projected into the
333
+ # target's view before the element-wise covariance check.
334
+ # The canonical case is `Hash[K, V] <:= Enumerable[[K, V]]`:
335
+ # Hash carries two type_args, Enumerable carries one, and
336
+ # the inherited parametrization at the Enumerable boundary
337
+ # is `Tuple[K, V]`. RBS encodes this as
338
+ # `include Enumerable[[K, V]]` in `Hash`'s definition.
339
+ projected_other = project_to_target_arity(self_type, other_type) || other_type
340
+ args_result = accepts_nominal_args(self_type, projected_other, mode)
331
341
  combine_results(class_result, args_result, mode)
332
342
  end
333
343
 
344
+ # Returns `other_type` rewritten so its type_args have the
345
+ # same arity as `self_type.type_args`, or `nil` if no
346
+ # projection is known. Today only the Hash → Enumerable
347
+ # projection is hand-rolled; a general RBS-driven
348
+ # implementation that consults `definition.ancestors[i].args`
349
+ # for arbitrary subclass / module-include relations is the
350
+ # principled follow-up.
351
+ def project_to_target_arity(self_type, other_type)
352
+ return nil if self_type.type_args.size == other_type.type_args.size
353
+ return nil if self_type.type_args.empty? || other_type.type_args.empty?
354
+
355
+ if self_type.class_name == "Enumerable" &&
356
+ other_type.class_name == "Hash" &&
357
+ self_type.type_args.size == 1 &&
358
+ other_type.type_args.size == 2
359
+ return Type::Combinator.nominal_of(
360
+ "Hash",
361
+ type_args: [Type::Combinator.tuple_of(*other_type.type_args)]
362
+ )
363
+ end
364
+
365
+ nil
366
+ end
367
+
334
368
  def project_tuple_to_nominal(tuple)
335
369
  if tuple.elements.empty?
336
370
  Type::Combinator.nominal_of(Array)
@@ -30,7 +30,16 @@ module Rigor
30
30
  def initialize(path:, mutating_selectors: {})
31
31
  @path = path
32
32
  @mutating_selectors = mutating_selectors.transform_values(&:freeze).freeze
33
- @catalog = nil
33
+ # ADR-15 Phase 4b.x — eager-load so the instance is
34
+ # safe to `Ractor.make_shareable`. Lazy init via
35
+ # `@catalog ||= load_catalog` would write to a
36
+ # potentially-frozen instance the first time a
37
+ # worker Ractor consults the catalog, raising
38
+ # `FrozenError`. The YAML parse is a once-per-process
39
+ # cost and the catalogs are constructed at module
40
+ # load time anyway, so eager init is free in
41
+ # practice.
42
+ @catalog = load_catalog
34
43
  end
35
44
 
36
45
  def safe_for_folding?(class_name, selector, kind: :instance)
@@ -52,7 +61,7 @@ module Rigor
52
61
  end
53
62
 
54
63
  def reset!
55
- @catalog = nil
64
+ @catalog = load_catalog
56
65
  end
57
66
 
58
67
  private
@@ -72,9 +81,7 @@ module Rigor
72
81
  per_class.include?(selector.to_sym) || per_class.include?(selector_str.to_sym)
73
82
  end
74
83
 
75
- def catalog
76
- @catalog ||= load_catalog
77
- end
84
+ attr_reader :catalog
78
85
 
79
86
  def load_catalog
80
87
  return EMPTY_CATALOG unless File.exist?(@path)
@@ -68,15 +68,21 @@ module Rigor
68
68
  # Used by tests to drop the cached catalog so a different
69
69
  # path or content can be exercised. Production code MUST
70
70
  # NOT call this during normal operation.
71
+ #
72
+ # ADR-15 Phase 4b.x — reset re-loads eagerly so the
73
+ # singleton-class `@catalog` ivar stays populated, and
74
+ # the loaded Hash is deep-shared via `Ractor.make_shareable`
75
+ # so a worker Ractor reading the ivar via `catalog.dig(...)`
76
+ # does not trip `Ractor::IsolationError`. Plain `.freeze`
77
+ # is insufficient: YAML parses to a nested Hash/Array/String
78
+ # graph and only the outer Hash would be frozen.
71
79
  def reset!
72
- @catalog = nil
80
+ @catalog = Ractor.make_shareable(load_catalog)
73
81
  end
74
82
 
75
83
  private
76
84
 
77
- def catalog
78
- @catalog ||= load_catalog
79
- end
85
+ attr_reader :catalog
80
86
 
81
87
  def load_catalog
82
88
  return EMPTY_CATALOG unless File.exist?(CATALOG_PATH)
@@ -87,6 +93,11 @@ module Rigor
87
93
  EMPTY_CATALOG
88
94
  end
89
95
  end
96
+
97
+ # ADR-15 Phase 4b.x — eager-load on the main Ractor at
98
+ # module-load time so worker Ractors only READ the
99
+ # populated singleton-class `@catalog` ivar.
100
+ reset!
90
101
  end
91
102
  end
92
103
  end
@@ -6,6 +6,7 @@ require_relative "../type"
6
6
  require_relative "../ast"
7
7
  require_relative "block_parameter_binder"
8
8
  require_relative "fallback"
9
+ require_relative "macro_block_self_type"
9
10
  require_relative "method_dispatcher"
10
11
 
11
12
  module Rigor
@@ -1194,16 +1195,25 @@ module Rigor
1194
1195
  arg_types: arg_types,
1195
1196
  environment: scope.environment
1196
1197
  )
1197
- block_return_for(block_arg, expected)
1198
+ # ADR-16 Tier A: when a registered plugin's `block_as_methods`
1199
+ # entry matches `(receiver_type, call_node.name)`, narrow the
1200
+ # block body's `self_type` to the receiver class's instance
1201
+ # type. The narrowing is `nil` for unmatched calls, leaving
1202
+ # the existing scope contract unchanged.
1203
+ narrowed_self = MacroBlockSelfType.narrow_self_type_for(
1204
+ scope: scope, call_node: call_node, receiver_type: receiver_type
1205
+ )
1206
+ block_return_for(block_arg, expected, narrowed_self_type: narrowed_self)
1198
1207
  rescue StandardError
1199
1208
  nil
1200
1209
  end
1201
1210
 
1202
- def block_return_for(block_arg, expected)
1211
+ def block_return_for(block_arg, expected, narrowed_self_type: nil)
1203
1212
  case block_arg
1204
1213
  when Prism::BlockNode
1205
1214
  bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_arg)
1206
1215
  block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1216
+ block_scope = block_scope.with_self_type(narrowed_self_type) if narrowed_self_type
1207
1217
  type_block_body(block_arg, block_scope)
1208
1218
  when Prism::BlockArgumentNode
1209
1219
  symbol_block_return_type(block_arg, expected)
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../type"
4
+
5
+ module Rigor
6
+ module Inference
7
+ # ADR-16 Tier A — engine hook. Consults every registered
8
+ # plugin manifest's `block_as_methods` entries to decide
9
+ # whether a block call site qualifies for `Scope#self_type`
10
+ # narrowing.
11
+ #
12
+ # The match contract for a class-level DSL like Sinatra's
13
+ # `class MyApp < Sinatra::Base; get '/foo' do ... end; end`:
14
+ #
15
+ # - the call's lexical receiver type is `Singleton[X]`
16
+ # (the implicit-self in a class body, or an explicit
17
+ # `MyApp.get(...)` call);
18
+ # - the underlying class `X` equals or inherits from the
19
+ # entry's `receiver_constraint`;
20
+ # - the call's method name is in the entry's `verbs`.
21
+ #
22
+ # On a match the helper returns the **instance** type of
23
+ # the receiver class (`Nominal[X]`) — the narrowed
24
+ # `self_type` for the block body, matching Sinatra's
25
+ # runtime semantics where `Sinatra::Base#generate_method`
26
+ # turns the block into an instance method of the user's
27
+ # app class.
28
+ #
29
+ # Slice 1b ships the floor only (per ADR-16 § WD13):
30
+ # bare-identifier method lookups inside the block resolve
31
+ # through the inference engine's normal `self_type`-driven
32
+ # path, so methods declared on `Sinatra::Base` (RBS or
33
+ # otherwise) become visible. Precision additions —
34
+ # parameter-typed block params, declared per-verb argument
35
+ # contracts — are ceiling concerns for later slices.
36
+ module MacroBlockSelfType
37
+ module_function
38
+
39
+ # @param scope [Rigor::Scope]
40
+ # @param call_node [Prism::CallNode]
41
+ # @param receiver_type [Rigor::Type, nil]
42
+ # @return [Rigor::Type, nil] the narrowed self-type, or
43
+ # `nil` when no registered entry matches the call shape.
44
+ def narrow_self_type_for(scope:, call_node:, receiver_type:)
45
+ return nil if receiver_type.nil?
46
+
47
+ environment = scope&.environment
48
+ registry = environment&.plugin_registry
49
+ return nil if registry.nil? || registry.empty?
50
+
51
+ receiver_class_name = singleton_receiver_class_name(receiver_type)
52
+ return nil if receiver_class_name.nil?
53
+
54
+ verb = call_node.name
55
+ registry.plugins.each do |plugin|
56
+ plugin.manifest.block_as_methods.each do |entry| # rigor:disable undefined-method
57
+ return instance_type_for(receiver_class_name, environment) if matches?(entry, verb, receiver_class_name,
58
+ environment)
59
+ end
60
+ end
61
+ nil
62
+ end
63
+
64
+ # Tier A's match contract is intentionally narrow:
65
+ # class-level DSL calls (receiver is `Singleton[X]`) only.
66
+ # Instance-receiver calls and DSL forms whose block body
67
+ # binds a different `self` (Concern's `included do`,
68
+ # `instance_eval { ... }`) are handled by later slices
69
+ # (Concern walker, Tier D, etc.) — not Tier A.
70
+ def singleton_receiver_class_name(receiver_type)
71
+ return nil unless receiver_type.is_a?(Type::Singleton)
72
+
73
+ receiver_type.class_name
74
+ end
75
+
76
+ def matches?(entry, verb, receiver_class_name, environment)
77
+ return false unless entry.verbs.include?(verb)
78
+
79
+ receiver_class_inherits_from?(receiver_class_name, entry.receiver_constraint, environment)
80
+ end
81
+
82
+ def receiver_class_inherits_from?(class_name, constraint, environment)
83
+ return true if class_name == constraint
84
+
85
+ ordering = environment.class_ordering(class_name, constraint)
86
+ %i[equal subclass].include?(ordering)
87
+ rescue StandardError
88
+ false
89
+ end
90
+
91
+ def instance_type_for(class_name, environment)
92
+ environment.nominal_for_name(class_name) || Type::Nominal.new(class_name)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -1028,10 +1028,10 @@ module Rigor
1028
1028
  # class's ancestor chain at lookup time; the catalog
1029
1029
  # corresponds to the module-mode YAML at
1030
1030
  # `data/builtins/ruby_core/<topic>.yml`.
1031
- MODULE_CATALOGS = [
1032
- [Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
1033
- [Enumerable, Builtins::ENUMERABLE_CATALOG, "Enumerable"]
1034
- ].freeze
1031
+ MODULE_CATALOGS = Ractor.make_shareable([
1032
+ [Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
1033
+ [Enumerable, Builtins::ENUMERABLE_CATALOG, "Enumerable"]
1034
+ ])
1035
1035
  private_constant :MODULE_CATALOGS
1036
1036
 
1037
1037
  # Returns the `(catalog, class_name)` pairs for every
@@ -1057,31 +1057,31 @@ module Rigor
1057
1057
  # Otherwise a `DateTime` receiver would match the `Date`
1058
1058
  # arm first and the catalog would consult the Date entry
1059
1059
  # in `DATE_CATALOG` for the wrong class.
1060
- CATALOG_BY_CLASS = [
1061
- [Integer, [Builtins::NumericCatalog, "Integer"]],
1062
- [Float, [Builtins::NumericCatalog, "Float"]],
1063
- [String, [Builtins::STRING_CATALOG, "String"]],
1064
- [Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
1065
- [Array, [Builtins::ARRAY_CATALOG, "Array"]],
1066
- [Hash, [Builtins::HASH_CATALOG, "Hash"]],
1067
- [Range, [Builtins::RANGE_CATALOG, "Range"]],
1068
- [::Set, [Builtins::SET_CATALOG, "Set"]],
1069
- [Time, [Builtins::TIME_CATALOG, "Time"]],
1070
- [DateTime, [Builtins::DATE_CATALOG, "DateTime"]],
1071
- [Date, [Builtins::DATE_CATALOG, "Date"]],
1072
- [Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
1073
- [Complex, [Builtins::COMPLEX_CATALOG, "Complex"]],
1074
- [Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]],
1075
- [Random, [Builtins::RANDOM_CATALOG, "Random"]],
1076
- [Struct, [Builtins::STRUCT_CATALOG, "Struct"]],
1077
- [Encoding, [Builtins::ENCODING_CATALOG, "Encoding"]],
1078
- [Regexp, [Builtins::REGEXP_CATALOG, "Regexp"]],
1079
- [MatchData, [Builtins::REGEXP_CATALOG, "MatchData"]],
1080
- [Proc, [Builtins::PROC_CATALOG, "Proc"]],
1081
- [Method, [Builtins::PROC_CATALOG, "Method"]],
1082
- [UnboundMethod, [Builtins::PROC_CATALOG, "UnboundMethod"]],
1083
- [Exception, [Builtins::EXCEPTION_CATALOG, "Exception"]]
1084
- ].freeze
1060
+ CATALOG_BY_CLASS = Ractor.make_shareable([
1061
+ [Integer, [Builtins::NumericCatalog, "Integer"]],
1062
+ [Float, [Builtins::NumericCatalog, "Float"]],
1063
+ [String, [Builtins::STRING_CATALOG, "String"]],
1064
+ [Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
1065
+ [Array, [Builtins::ARRAY_CATALOG, "Array"]],
1066
+ [Hash, [Builtins::HASH_CATALOG, "Hash"]],
1067
+ [Range, [Builtins::RANGE_CATALOG, "Range"]],
1068
+ [::Set, [Builtins::SET_CATALOG, "Set"]],
1069
+ [Time, [Builtins::TIME_CATALOG, "Time"]],
1070
+ [DateTime, [Builtins::DATE_CATALOG, "DateTime"]],
1071
+ [Date, [Builtins::DATE_CATALOG, "Date"]],
1072
+ [Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
1073
+ [Complex, [Builtins::COMPLEX_CATALOG, "Complex"]],
1074
+ [Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]],
1075
+ [Random, [Builtins::RANDOM_CATALOG, "Random"]],
1076
+ [Struct, [Builtins::STRUCT_CATALOG, "Struct"]],
1077
+ [Encoding, [Builtins::ENCODING_CATALOG, "Encoding"]],
1078
+ [Regexp, [Builtins::REGEXP_CATALOG, "Regexp"]],
1079
+ [MatchData, [Builtins::REGEXP_CATALOG, "MatchData"]],
1080
+ [Proc, [Builtins::PROC_CATALOG, "Proc"]],
1081
+ [Method, [Builtins::PROC_CATALOG, "Method"]],
1082
+ [UnboundMethod, [Builtins::PROC_CATALOG, "UnboundMethod"]],
1083
+ [Exception, [Builtins::EXCEPTION_CATALOG, "Exception"]]
1084
+ ])
1085
1085
  private_constant :CATALOG_BY_CLASS
1086
1086
 
1087
1087
  # Returns `[catalog, class_name]` for receivers we have a
@@ -36,10 +36,10 @@ module Rigor
36
36
  # the result into a `Constant<Rational>` / `Constant<Complex>`.
37
37
  # The factory accepts the same shapes as Ruby:
38
38
  # `Rational(a)`, `Rational(a, b)`, `Complex(a)`, `Complex(a, b)`.
39
- NUMERIC_CONSTRUCTORS = {
40
- Rational: ->(*args) { Rational(*args) },
41
- Complex: ->(*args) { Complex(*args) }
42
- }.freeze
39
+ NUMERIC_CONSTRUCTORS = Ractor.make_shareable({
40
+ Rational: Ractor.make_shareable(->(*args) { Rational(*args) }),
41
+ Complex: Ractor.make_shareable(->(*args) { Complex(*args) })
42
+ })
43
43
  private_constant :NUMERIC_CONSTRUCTORS
44
44
 
45
45
  # `Kernel#Integer(s)` predicate-aware refinement set
@@ -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)
@@ -26,7 +26,7 @@ module Rigor
26
26
  # `Array#[](Range) -> Array[Elem]?` overload for a Range
27
27
  # argument. (Surfaced during v0.1.1 self-analysis; see the
28
28
  # "Interface-strictness on overload selection" item in
29
- # `docs/MILESTONES.md`.)
29
+ # `docs/ROADMAP.md`.)
30
30
  # 3. **Pass 2 — gradual fall-back.** If no fully strict overload
31
31
  # matches, accept the first arity-and-gradual-accept match
32
32
  # (the v0.1.1 behaviour). Alias / Interface / Intersection
@@ -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)