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
@@ -2,6 +2,17 @@
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/reporters"
7
+ require_relative "environment/hkt_registry_holder"
8
+ require_relative "environment/bundle_sig_discovery"
9
+ require_relative "environment/lockfile_resolver"
10
+ require_relative "environment/rbs_collection_discovery"
11
+ require_relative "environment/rbs_coverage_report"
12
+ require_relative "inference/synthetic_method_index"
13
+ require_relative "inference/project_patched_methods"
14
+ require_relative "inference/hkt_registry"
15
+ require_relative "builtins/hkt_builtins"
5
16
  require_relative "type_node/name_scope"
6
17
  require_relative "type_node/resolver_chain"
7
18
 
@@ -40,11 +51,19 @@ module Rigor
40
51
  pathname optparse json yaml fileutils tempfile tmpdir
41
52
  stringio forwardable digest securerandom
42
53
  uri logger date
54
+ pp delegate observable abbrev find tsort singleton
55
+ shellwords benchmark base64 did_you_mean
56
+ monitor mutex_m timeout
57
+ open3 erb etc ipaddr bigdecimal bigdecimal-math
58
+ prettyprint random-formatter time open-uri resolv
59
+ csv pstore objspace io-console cgi cgi-escape
60
+ strscan
43
61
  prism rbs
44
62
  ].freeze
45
63
 
46
64
  attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
47
- :rbs_extended_reporter, :boundary_cross_reporter, :name_scope
65
+ :reporters, :name_scope,
66
+ :synthetic_method_index, :project_patched_methods
48
67
 
49
68
  # @param class_registry [Rigor::Environment::ClassRegistry]
50
69
  # @param rbs_loader [Rigor::Environment::RbsLoader, nil] when nil the
@@ -65,22 +84,105 @@ module Rigor
65
84
  # sources the dispatcher consults BELOW RBS dispatch.
66
85
  # When nil (the default), no dep-source contribution
67
86
  # participates and the dispatcher tier is a no-op.
68
- def initialize(class_registry: ClassRegistry.default, rbs_loader: nil,
87
+ def initialize(class_registry: ClassRegistry.default, rbs_loader: nil, # rubocop:disable Metrics/ParameterLists
69
88
  plugin_registry: nil, dependency_source_index: nil,
70
- rbs_extended_reporter: nil, boundary_cross_reporter: nil)
89
+ rbs_extended_reporter: nil, boundary_cross_reporter: nil,
90
+ synthetic_method_index: nil, project_patched_methods: nil,
91
+ hkt_registry: nil)
71
92
  @class_registry = class_registry
72
93
  @rbs_loader = rbs_loader
73
94
  @plugin_registry = plugin_registry
74
95
  @dependency_source_index = dependency_source_index
75
- @rbs_extended_reporter = rbs_extended_reporter
76
- @boundary_cross_reporter = boundary_cross_reporter
96
+ # ADR-pending — reporters live in a mutable container so
97
+ # long-lived integrations (LSP `ProjectContext`) can swap
98
+ # them per `Runner.run` without rebuilding the env. The
99
+ # existing `#rbs_extended_reporter` / `#boundary_cross_reporter`
100
+ # accessors below preserve the public lookup shape.
101
+ @reporters = Reporters.new(
102
+ rbs_extended: rbs_extended_reporter,
103
+ boundary_cross: boundary_cross_reporter
104
+ )
105
+ @synthetic_method_index = synthetic_method_index || Inference::SyntheticMethodIndex::EMPTY
106
+ @project_patched_methods = project_patched_methods || Inference::ProjectPatchedMethods::EMPTY
107
+ # ADR-20 slice 2c + 2e — the per-env HKT registry
108
+ # consulted by the reducer when resolving `Type::App`
109
+ # carriers. Defaults to {Inference::HktRegistry::EMPTY};
110
+ # the {.default} / {.for_project} class methods seed it
111
+ # with the bundled builtins (`json::value`, …) plus any
112
+ # `%a{rigor:v1:hkt_register / hkt_define}` annotations
113
+ # the RBS loader exposes. The hkt_registry getter
114
+ # (defined below) MEMOIZES the result of merging the
115
+ # base with the RBS scan so the scan is paid at most
116
+ # once per Environment lifetime — and only when first
117
+ # consulted, leaving fast paths like `rigor check
118
+ # --cache-stats --no-stats` from doing the RBS env
119
+ # build at all.
120
+ @hkt_registry_base = hkt_registry || Inference::HktRegistry::EMPTY
121
+ @hkt_registry_holder = HktRegistryHolder.new
77
122
  @name_scope = build_name_scope
78
123
  freeze
79
124
  end
80
125
 
126
+ # ADR-20 slices 2e + 6 — lazy HKT registry getter.
127
+ # Merge order on first call: builtins (base) ← plugin
128
+ # manifest aggregation ← RBS env scan. Last-write-wins on
129
+ # URI collisions so user-authored `.rbs` overlays beat
130
+ # plugin entries, which beat the bundled JSON_VALUE.
131
+ # Memoised; single-threaded use only (under the Ractor
132
+ # pool path each worker has its own Environment so
133
+ # cross-worker mutation is impossible; the LSP
134
+ # single-publish-at-a-time invariant serialises here).
135
+ def hkt_registry
136
+ @hkt_registry_holder.fetch do
137
+ with_plugin_overlay = if @plugin_registry.respond_to?(:hkt_overlay_registry)
138
+ @hkt_registry_base.merge(@plugin_registry.hkt_overlay_registry)
139
+ else
140
+ @hkt_registry_base
141
+ end
142
+ Inference::HktRegistry.scan_rbs_loader(
143
+ @rbs_loader,
144
+ base: with_plugin_overlay,
145
+ reporter: rbs_extended_reporter
146
+ )
147
+ end
148
+ end
149
+
150
+ # Backwards-compatible reporter accessors — every existing
151
+ # consumer (rbs_extended, method_dispatcher) calls these. The
152
+ # frozen `@reporters` container is mutable for slot reassignment
153
+ # via {#attach_reporters!} below.
154
+ def rbs_extended_reporter
155
+ @reporters.rbs_extended
156
+ end
157
+
158
+ def boundary_cross_reporter
159
+ @reporters.boundary_cross
160
+ end
161
+
162
+ # Replaces the env's per-run reporter slots. Intended for
163
+ # long-lived integrations (LSP `ProjectContext`) that share one
164
+ # Environment instance across many `Runner.run` calls: each call
165
+ # attaches its own fresh reporter pair so per-call diagnostic
166
+ # events stay scoped to that call rather than accumulating
167
+ # across publishes.
168
+ #
169
+ # Single-threaded use only. Concurrent publishes against one
170
+ # Environment must serialise — the LSP `Server` debouncer +
171
+ # synchronized writer already enforces this for the editor
172
+ # path. The Ractor pool path builds a per-worker Environment
173
+ # and does not reach this surface.
174
+ def attach_reporters!(rbs_extended_reporter:, boundary_cross_reporter:)
175
+ @reporters.rbs_extended = rbs_extended_reporter
176
+ @reporters.boundary_cross = boundary_cross_reporter
177
+ nil
178
+ end
179
+
81
180
  class << self
82
181
  def default
83
- @default ||= new(rbs_loader: RbsLoader.default).freeze
182
+ @default ||= new(
183
+ rbs_loader: RbsLoader.default,
184
+ hkt_registry: Builtins::HktBuiltins.registry
185
+ ).freeze
84
186
  end
85
187
 
86
188
  # Builds an Environment that consults the project's local
@@ -104,24 +206,80 @@ module Rigor
104
206
  # reflection artefacts) consult the cache. Pass `nil` (the
105
207
  # default) to skip caching for this environment.
106
208
  # @return [Rigor::Environment]
107
- def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil, # rubocop:disable Metrics/ParameterLists
209
+ # rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
210
+ def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil,
108
211
  plugin_registry: nil, dependency_source_index: nil,
109
- rbs_extended_reporter: nil, boundary_cross_reporter: nil)
212
+ rbs_extended_reporter: nil, boundary_cross_reporter: nil,
213
+ bundler_bundle_path: nil, bundler_auto_detect: false,
214
+ bundler_lockfile: nil,
215
+ rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
216
+ synthetic_method_index: nil, project_patched_methods: nil)
110
217
  resolved_paths = signature_paths || default_signature_paths(root)
218
+ # O4 MVP — append per-gem `sig/` directories discovered
219
+ # under the target project's bundler install root. Empty
220
+ # array when neither an explicit path nor auto-detection
221
+ # finds a bundle. Order: user `signature_paths:` win first
222
+ # (semantic precedence inside `RbsLoader.build_env_for`);
223
+ # gem-shipped sigs append last so user overrides stay
224
+ # authoritative.
225
+ #
226
+ # O4 Layer 3 — when a Gemfile.lock is available (explicit
227
+ # `bundler_lockfile:` or auto-detected next to the project
228
+ # root), use the locked gem set to filter the discovered
229
+ # `sig/` directories. Stale gems in the bundle install
230
+ # tree (out-of-band installs, version drift after a
231
+ # `bundle update`) are silently dropped so only gems the
232
+ # project actually declares contribute RBS.
233
+ locked = LockfileResolver.locked_gems(
234
+ lockfile_path: bundler_lockfile,
235
+ project_root: root,
236
+ auto_detect: bundler_auto_detect
237
+ )
238
+ gem_sig_paths = BundleSigDiscovery.discover(
239
+ bundle_path: bundler_bundle_path,
240
+ project_root: root,
241
+ auto_detect: bundler_auto_detect,
242
+ locked_gems: locked.empty? ? nil : locked
243
+ ).map(&:to_s)
244
+ # O4 Layer 3 slice 2 — when `rbs collection install`
245
+ # has been run for the target project, parse the
246
+ # resulting `rbs_collection.lock.yaml` and feed each
247
+ # gem's `<collection_path>/<name>/<version>/` directory
248
+ # into `signature_paths:`. Stdlib-typed entries are
249
+ # skipped (already covered by `DEFAULT_LIBRARIES`).
250
+ collection_paths = RbsCollectionDiscovery.discover(
251
+ lockfile_path: rbs_collection_lockfile,
252
+ project_root: root,
253
+ auto_detect: rbs_collection_auto_detect
254
+ ).map(&:to_s)
255
+ loader_signature_paths = resolved_paths + gem_sig_paths + collection_paths
111
256
  merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
112
257
  loader = RbsLoader.new(
113
258
  libraries: merged_libraries,
114
- signature_paths: resolved_paths,
259
+ signature_paths: loader_signature_paths,
115
260
  cache_store: cache_store
116
261
  )
262
+ # ADR-20 slice 2c + 2e — seed hkt_registry with the
263
+ # bundled builtins. The Environment's `#hkt_registry`
264
+ # getter then LAZILY merges in the RBS env scan on
265
+ # first call so fast paths that don't consult HKT
266
+ # (e.g. `rigor check --cache-stats --no-stats`) don't
267
+ # pay the eager env-build cost up front. URI
268
+ # collisions let the user-authored overlay win over
269
+ # the bundled builtin (last-write-wins per ADR-20
270
+ # OQ3 tentative).
117
271
  new(
118
272
  rbs_loader: loader,
119
273
  plugin_registry: plugin_registry,
120
274
  dependency_source_index: dependency_source_index,
121
275
  rbs_extended_reporter: rbs_extended_reporter,
122
- boundary_cross_reporter: boundary_cross_reporter
276
+ boundary_cross_reporter: boundary_cross_reporter,
277
+ synthetic_method_index: synthetic_method_index,
278
+ project_patched_methods: project_patched_methods,
279
+ hkt_registry: Builtins::HktBuiltins.registry
123
280
  )
124
281
  end
282
+ # rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
125
283
 
126
284
  private
127
285
 
@@ -185,6 +343,17 @@ module Rigor
185
343
  class_known_in_rbs?(name)
186
344
  end
187
345
 
346
+ # ADR-15 Phase 2b — returns the loader's read-only,
347
+ # `Ractor.shareable?` query surface as a frozen
348
+ # {Environment::Reflection}. Built lazily on first
349
+ # access; subsequent calls return the same instance.
350
+ # Returns `nil` when the environment carries no RBS
351
+ # loader (test-only `Environment.new` without
352
+ # `rbs_loader:`).
353
+ def reflection
354
+ @rbs_loader&.reflection
355
+ end
356
+
188
357
  # Compares two class/module names using analyzer-owned class data.
189
358
  # Returns `:equal`, `:subclass`, `:superclass`, `:disjoint`, or
190
359
  # `: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)
@@ -412,13 +446,45 @@ module Rigor
412
446
 
413
447
  def accepts_nominal_from_constant(self_type, constant, mode)
414
448
  ruby_class = resolve_class(self_type.class_name)
415
- if ruby_class.nil?
416
- return Type::AcceptsResult.maybe(
449
+ return constant_is_a_result(ruby_class, constant, self_type, mode) if ruby_class
450
+
451
+ # The host process may not have required the constant's
452
+ # declared self_type (e.g. `BigDecimal` since Ruby 3.4
453
+ # is no longer a default gem). Fall back to inspecting
454
+ # the value's own class ancestor chain — always loadable
455
+ # because the value already exists. Required for
456
+ # OverloadSelector to reject `Integer#+(BigDecimal) ->
457
+ # BigDecimal` overloads contributed by `bigdecimal`'s
458
+ # RBS reopening when the actual arg is a Constant<Integer>.
459
+ ancestor_names = constant.value.class.ancestors.map(&:name)
460
+ if ancestor_names.include?(self_type.class_name)
461
+ Type::AcceptsResult.yes(
417
462
  mode: mode,
418
- reasons: "class #{self_type.class_name} not loadable; cannot prove from Constant"
463
+ reasons: "Constant value class ancestors include #{self_type.class_name}"
464
+ )
465
+ else
466
+ Type::AcceptsResult.no(
467
+ mode: mode,
468
+ reasons: "Constant value class ancestors exclude #{self_type.class_name}"
469
+ )
470
+ end
471
+ end
472
+
473
+ def subtype_result_via_ancestors(actual_class, target_name, mode)
474
+ if actual_class.ancestors.map(&:name).include?(target_name)
475
+ Type::AcceptsResult.yes(
476
+ mode: mode,
477
+ reasons: "#{actual_class.name} ancestors include #{target_name}"
478
+ )
479
+ else
480
+ Type::AcceptsResult.no(
481
+ mode: mode,
482
+ reasons: "#{actual_class.name} ancestors exclude #{target_name} (target unloadable)"
419
483
  )
420
484
  end
485
+ end
421
486
 
487
+ def constant_is_a_result(ruby_class, constant, self_type, mode)
422
488
  if constant.value.is_a?(ruby_class)
423
489
  Type::AcceptsResult.yes(mode: mode, reasons: "Constant value is_a?(#{self_type.class_name})")
424
490
  else
@@ -760,6 +826,19 @@ module Rigor
760
826
 
761
827
  target_class = resolve_class(target_name)
762
828
  actual_class = resolve_class(actual_name)
829
+ # When only `actual` resolves, we can still rule out
830
+ # `actual <:= target` by inspecting `actual`'s ancestor
831
+ # chain. The canonical case: `target=BigDecimal` is not
832
+ # loadable in the host process (no `require` in rigor's
833
+ # own runtime), but `actual=Integer` IS, and Integer's
834
+ # ancestors do not include `BigDecimal`, so the subtype
835
+ # relation MUST be `:no` rather than the conservative
836
+ # `:maybe`. The reverse asymmetry (target resolves,
837
+ # actual doesn't) does not let us conclude anything —
838
+ # the unloaded `actual` could be an unrelated class or
839
+ # a subclass of `target` we can't see, so we still
840
+ # answer `:maybe` there.
841
+ return subtype_result_via_ancestors(actual_class, target_name, mode) if target_class.nil? && actual_class
763
842
  if target_class.nil? || actual_class.nil?
764
843
  return Type::AcceptsResult.maybe(
765
844
  mode: mode,
@@ -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
@@ -60,6 +61,10 @@ module Rigor
60
61
  Prism::RationalNode => :type_of_literal_value,
61
62
  Prism::SymbolNode => :symbol_type_for,
62
63
  Prism::StringNode => :string_type_for,
64
+ Prism::XStringNode => :type_of_xstring,
65
+ Prism::InterpolatedXStringNode => :type_of_xstring,
66
+ Prism::SourceFileNode => :type_of_source_file,
67
+ Prism::SourceLineNode => :type_of_source_line,
63
68
  Prism::TrueNode => :type_of_true,
64
69
  Prism::FalseNode => :type_of_false,
65
70
  Prism::NilNode => :type_of_nil,
@@ -143,6 +148,9 @@ module Rigor
143
148
  Prism::AliasMethodNode => :type_of_nil_value,
144
149
  Prism::AliasGlobalVariableNode => :type_of_nil_value,
145
150
  Prism::UndefNode => :type_of_nil_value,
151
+ Prism::PostExecutionNode => :type_of_nil_value,
152
+ Prism::ShareableConstantNode => :type_of_shareable_constant,
153
+ Prism::ImplicitNode => :type_of_implicit,
146
154
  Prism::ForwardingSuperNode => :type_of_dynamic_top,
147
155
  Prism::BlockArgumentNode => :type_of_non_value,
148
156
  # Parameters and blocks (non-value positions)
@@ -158,6 +166,7 @@ module Rigor
158
166
  Prism::ForwardingParameterNode => :type_of_non_value,
159
167
  Prism::NoKeywordsParameterNode => :type_of_non_value,
160
168
  Prism::ImplicitRestNode => :type_of_non_value,
169
+ Prism::ItParametersNode => :type_of_non_value,
161
170
  Prism::BlockNode => :type_of_dynamic_top,
162
171
  Prism::SplatNode => :type_of_non_value,
163
172
  # Control flow (Slice 3 phase 1): branch types are unioned, jumps
@@ -887,6 +896,45 @@ module Rigor
887
896
  Type::Combinator.constant_of(unescaped)
888
897
  end
889
898
 
899
+ # Backtick (`cmd`) and `%x{cmd}` invoke Kernel#` and always return a
900
+ # String. Even when the content is statically known, we widen to
901
+ # Nominal[String] because the runtime value depends on the
902
+ # subprocess output, not the source text.
903
+ def type_of_xstring(_node)
904
+ Type::Combinator.nominal_of(String)
905
+ end
906
+
907
+ # __FILE__ is the source file path. Always non-empty when
908
+ # parsing a real file (the path resolver gives the buffer
909
+ # name, which is at minimum `"(stdin)"` / `"-e"` / a real
910
+ # path — never the empty String). Widened to
911
+ # `non-empty-string` instead of `Nominal[String]` so
912
+ # downstream String-emptiness checks know the value cannot
913
+ # be `""`.
914
+ def type_of_source_file(_node)
915
+ Type::Combinator.non_empty_string
916
+ end
917
+
918
+ # __LINE__ is the line of the source literal. Ruby line
919
+ # numbers are 1-indexed, so `__LINE__` is always at least
920
+ # 1 — `positive-int` (Integer in `[1, +Inf)`) is the
921
+ # canonical refinement.
922
+ def type_of_source_line(_node)
923
+ Type::Combinator.positive_int
924
+ end
925
+
926
+ # `# shareable_constant_value:` magic comment wraps the next
927
+ # constant write. Type is the wrapped write's value.
928
+ def type_of_shareable_constant(node)
929
+ type_of(node.write)
930
+ end
931
+
932
+ # `{ x: }` shorthand hash. The implicit value is the call to
933
+ # `x` (or a local read of `x`). Delegate.
934
+ def type_of_implicit(node)
935
+ type_of(node.value)
936
+ end
937
+
890
938
  def local_read(node)
891
939
  scope.local(node.name) || dynamic_top
892
940
  end
@@ -1194,16 +1242,25 @@ module Rigor
1194
1242
  arg_types: arg_types,
1195
1243
  environment: scope.environment
1196
1244
  )
1197
- block_return_for(block_arg, expected)
1245
+ # ADR-16 Tier A: when a registered plugin's `block_as_methods`
1246
+ # entry matches `(receiver_type, call_node.name)`, narrow the
1247
+ # block body's `self_type` to the receiver class's instance
1248
+ # type. The narrowing is `nil` for unmatched calls, leaving
1249
+ # the existing scope contract unchanged.
1250
+ narrowed_self = MacroBlockSelfType.narrow_self_type_for(
1251
+ scope: scope, call_node: call_node, receiver_type: receiver_type
1252
+ )
1253
+ block_return_for(block_arg, expected, narrowed_self_type: narrowed_self)
1198
1254
  rescue StandardError
1199
1255
  nil
1200
1256
  end
1201
1257
 
1202
- def block_return_for(block_arg, expected)
1258
+ def block_return_for(block_arg, expected, narrowed_self_type: nil)
1203
1259
  case block_arg
1204
1260
  when Prism::BlockNode
1205
1261
  bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_arg)
1206
1262
  block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
1263
+ block_scope = block_scope.with_self_type(narrowed_self_type) if narrowed_self_type
1207
1264
  type_block_body(block_arg, block_scope)
1208
1265
  when Prism::BlockArgumentNode
1209
1266
  symbol_block_return_type(block_arg, expected)