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
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Environment
5
+ # Open item O4 Layer 3 slice 3 — graceful-degradation
6
+ # coverage report.
7
+ #
8
+ # When the user has a `Gemfile.lock` (via slice 1) and rigor
9
+ # has resolved its target-project RBS sources (DEFAULT_LIBRARIES,
10
+ # `data/vendored_gem_sigs/`, slice-1 bundle-shipped `sig/`,
11
+ # slice-2 `rbs_collection.lock.yaml` paths), this module
12
+ # classifies each locked gem by RBS provenance and surfaces
13
+ # the "no RBS available" set so the run-start diagnostic in
14
+ # {Rigor::Analysis::Runner} can suggest `rbs collection
15
+ # install` or `dependencies.source_inference:` for the
16
+ # uncovered gems.
17
+ #
18
+ # The classification is a pure function over the inputs
19
+ # (`locked_gems`, two arrays of resolved sig paths). It does
20
+ # NOT touch the filesystem on its own — the caller passes in
21
+ # what discovery returned.
22
+ module RbsCoverageReport
23
+ # Frozen result row.
24
+ #
25
+ # `source` is a Symbol naming where RBS for this gem
26
+ # resolves; `:missing` means none of the four resolution
27
+ # paths covered it.
28
+ Coverage = Data.define(:gem_name, :version, :source) do
29
+ def initialize(gem_name:, version:, source:)
30
+ super(
31
+ gem_name: -gem_name.to_s,
32
+ version: -version.to_s,
33
+ source: source
34
+ )
35
+ end
36
+ end
37
+
38
+ # Names of gems whose RBS ships under
39
+ # `data/vendored_gem_sigs/`. Kept in sync with the
40
+ # vendored-stubs directory listing; when a new gem is
41
+ # vendored, add its name here too. (The set is small
42
+ # enough that hard-coding is acceptable; a directory walk
43
+ # at every call would add stat-cost to no benefit.)
44
+ VENDORED_GEM_NAMES = Set[
45
+ "bcrypt", "idn-ruby", "mysql2", "nokogiri", "pg", "redis"
46
+ ].freeze
47
+
48
+ # @param locked_gems [Hash{String => LockfileResolver::LockedGem}]
49
+ # The lockfile-resolved gem set. Empty hash → no
50
+ # coverage analysis to do.
51
+ # @param default_libraries [Array<String>] gem names rigor
52
+ # auto-loads through `RBS::EnvironmentLoader#add(library:)`.
53
+ # Pass `Rigor::Environment::DEFAULT_LIBRARIES` from callers
54
+ # running in a project context.
55
+ # @param bundle_sig_paths [Array<Pathname, String>] the
56
+ # discovered `<bundle>/.../gems/<name>-<ver>/sig` paths
57
+ # from {BundleSigDiscovery.discover}.
58
+ # @param rbs_collection_paths [Array<Pathname, String>] the
59
+ # discovered `<collection>/<name>/<version>/` paths from
60
+ # {RbsCollectionDiscovery.discover}.
61
+ # @return [Array<Coverage>] one row per locked gem; sorted
62
+ # by gem name for deterministic output.
63
+ def self.classify(locked_gems:, default_libraries:,
64
+ bundle_sig_paths:, rbs_collection_paths:)
65
+ default_set = default_libraries.to_set
66
+ bundle_names = extract_gem_names_from_bundle_paths(bundle_sig_paths)
67
+ collection_names = extract_gem_names_from_collection_paths(rbs_collection_paths)
68
+
69
+ locked_gems.each_value.map do |locked|
70
+ name = locked.name
71
+ source = if default_set.include?(name)
72
+ :default_library
73
+ elsif VENDORED_GEM_NAMES.include?(name)
74
+ :vendored_gem_sig
75
+ elsif bundle_names.include?(name)
76
+ :bundle_sig
77
+ elsif collection_names.include?(name)
78
+ :rbs_collection
79
+ else
80
+ :missing
81
+ end
82
+ Coverage.new(gem_name: name, version: locked.version, source: source)
83
+ end.sort_by(&:gem_name)
84
+ end
85
+
86
+ # Convenience accessor for the run-start diagnostic.
87
+ # Filters {classify} down to `:missing` rows.
88
+ def self.missing(coverage_rows)
89
+ coverage_rows.select { |row| row.source == :missing }
90
+ end
91
+
92
+ def self.extract_gem_names_from_bundle_paths(paths)
93
+ paths.each_with_object(Set.new) do |path, set|
94
+ pathname = path.is_a?(Pathname) ? path : Pathname.new(path)
95
+ set << BundleSigDiscovery.gem_name_from_sig_path(pathname)
96
+ end
97
+ end
98
+ private_class_method :extract_gem_names_from_bundle_paths
99
+
100
+ def self.extract_gem_names_from_collection_paths(paths)
101
+ # `RbsCollectionDiscovery.discover` returns
102
+ # `<collection_root>/<name>/<version>/` so the parent
103
+ # basename is the gem name.
104
+ paths.each_with_object(Set.new) do |path, set|
105
+ pathname = path.is_a?(Pathname) ? path : Pathname.new(path)
106
+ set << pathname.parent.basename.to_s
107
+ end
108
+ end
109
+ private_class_method :extract_gem_names_from_collection_paths
110
+ end
111
+ end
112
+ end
@@ -49,6 +49,13 @@ module Rigor
49
49
  # miss without holding a loader instance, and the
50
50
  # instance-side {#build_env} delegates here so the
51
51
  # implementation stays single-rooted.
52
+ #
53
+ # Vendored gem stubs (`data/vendored_gem_sigs/<gem>/`) are
54
+ # loaded on top of `signature_paths` so the per-gem RBS
55
+ # bundled with Rigor itself is in scope for every analysis
56
+ # run. The gem stubs are intentionally read-only and
57
+ # appended LAST so user-supplied `signature_paths` win on
58
+ # name conflicts.
52
59
  def build_env_for(libraries:, signature_paths:)
53
60
  rbs_loader = RBS::EnvironmentLoader.new
54
61
  libraries.each do |library|
@@ -60,8 +67,32 @@ module Rigor
60
67
  path = Pathname(path) unless path.is_a?(Pathname)
61
68
  rbs_loader.add(path: path) if path.directory?
62
69
  end
70
+ vendored_gem_sig_paths.each do |path|
71
+ rbs_loader.add(path: path) if path.directory?
72
+ end
63
73
  RBS::Environment.from_loader(rbs_loader).resolve_type_names
64
74
  end
75
+
76
+ # Per-gem `data/vendored_gem_sigs/<gem>/` directories that
77
+ # ship with Rigor. Each subdirectory is one gem's RBS surface
78
+ # (the `<gem>.rbs` file is the typical content; `LICENSE.upstream`
79
+ # records provenance). Coverage is deliberately scoped to the
80
+ # native-extension and "everywhere in Rails" gems whose absence
81
+ # dominated `call.undefined-method` noise in the real-world
82
+ # survey at `docs/notes/20260515-real-world-rails-survey.md`.
83
+ VENDORED_GEM_SIGS_ROOT = File.expand_path(
84
+ "../../../data/vendored_gem_sigs",
85
+ __dir__
86
+ )
87
+ private_constant :VENDORED_GEM_SIGS_ROOT
88
+
89
+ def vendored_gem_sig_paths
90
+ return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
91
+
92
+ Dir.children(VENDORED_GEM_SIGS_ROOT).map do |gem_dir|
93
+ Pathname(File.join(VENDORED_GEM_SIGS_ROOT, gem_dir))
94
+ end
95
+ end
65
96
  end
66
97
 
67
98
  attr_reader :libraries, :signature_paths, :cache_store
@@ -122,6 +153,7 @@ module Rigor
122
153
  # it never recurses back through {#class_known?}.
123
154
  def each_known_class_name
124
155
  return enum_for(:each_known_class_name) unless block_given?
156
+ return if env.nil?
125
157
 
126
158
  env.class_decls.each_key { |rbs_name| yield rbs_name.to_s }
127
159
  env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s }
@@ -133,6 +165,38 @@ module Rigor
133
165
  # v0.0.9 cache `Cache::Descriptor` regression did.
134
166
  end
135
167
 
168
+ # Returns a frozen `Hash<String, String>` mapping each loaded
169
+ # class / module name (top-level prefixed) to the file path of
170
+ # its FIRST declaration's RBS source. Used by
171
+ # {Rigor::Analysis::RunStats} to attribute the type universe
172
+ # between "project sig/" (paths under the configured
173
+ # `signature_paths`) and "bundled" (everything else — RBS
174
+ # core, stdlib libraries, gem-bundled RBS). Each value is a
175
+ # frozen `String` so the whole result is `Ractor.shareable?`
176
+ # — the Phase 4b worker pool ships a snapshot back to the
177
+ # coordinator on the first `:prepare` message.
178
+ def class_decl_paths
179
+ return {}.freeze if env.nil?
180
+
181
+ result = {}
182
+ env.class_decls.each do |rbs_name, entry|
183
+ decl = entry.primary_decl
184
+ next if decl.nil?
185
+
186
+ location = decl.location
187
+ next if location.nil?
188
+
189
+ buffer = location.buffer
190
+ name = buffer.respond_to?(:name) ? buffer.name : nil
191
+ next if name.nil?
192
+
193
+ result[rbs_name.to_s.dup.freeze] = name.to_s.dup.freeze
194
+ end
195
+ result.freeze
196
+ rescue ::RBS::BaseError
197
+ {}.freeze
198
+ end
199
+
136
200
  # @return [RBS::Definition, nil] the resolved instance definition
137
201
  # for `class_name`, or nil when the class is unknown or its
138
202
  # definition cannot be built (RBS may raise on broken hierarchies;
@@ -250,6 +314,8 @@ module Rigor
250
314
  # materialises the constant-type table; ordinary callers
251
315
  # should keep using {#constant_type} for point lookups.
252
316
  def constant_names
317
+ return [] if env.nil?
318
+
253
319
  env.constant_decls.keys.map(&:to_s)
254
320
  rescue ::RBS::BaseError
255
321
  []
@@ -262,6 +328,7 @@ module Rigor
262
328
  # back into the cache when `cache_store` is set).
263
329
  def each_constant_decl
264
330
  return enum_for(:each_constant_decl) unless block_given?
331
+ return if env.nil?
265
332
 
266
333
  env.constant_decls.each do |rbs_name, entry|
267
334
  yield rbs_name.to_s, entry
@@ -299,26 +366,109 @@ module Rigor
299
366
  nil
300
367
  end
301
368
 
369
+ # ADR-15 Phase 4b.x — eagerly drives every cached
370
+ # producer so a subsequent worker Ractor can serve all
371
+ # of its RBS queries from the Marshal blob on disk
372
+ # without ever calling `RBS::EnvironmentLoader.new`.
373
+ # The loader path that calls `EnvironmentLoader.new`
374
+ # transitively reads a chain of non-`Ractor.shareable?`
375
+ # module constants
376
+ # (`RBS::EnvironmentLoader::DEFAULT_CORE_ROOT`,
377
+ # `RBS::Repository::DEFAULT_STDLIB_ROOT`,
378
+ # `Gem::Requirement::DefaultRequirement`, …) and trips
379
+ # `Ractor::IsolationError`. Pre-warming the cache on
380
+ # the main Ractor and letting workers consult ONLY the
381
+ # Marshal-loaded blob sidesteps the whole chain.
382
+ #
383
+ # No-op when `cache_store` is nil — without a Store the
384
+ # worker has no choice but to build env via the loader,
385
+ # so the caller MUST ensure pool mode runs with caching
386
+ # enabled. Returns `self` so the call chains cleanly
387
+ # from the `Runner` pre-spawn hook.
388
+ def prewarm
389
+ return self if cache_store.nil?
390
+
391
+ env
392
+ known_class_names_set
393
+ constant_type_table
394
+ type_param_names_table
395
+ ancestor_names_table
396
+ instance_definitions_table
397
+ singleton_definitions_table
398
+ self
399
+ end
400
+
401
+ # ADR-15 Phase 2b — return the loader's read-only
402
+ # query surface as a frozen, `Ractor.shareable?`
403
+ # {Reflection} value object. Built lazily on first
404
+ # access; the loader memoises so repeated calls return
405
+ # the same instance.
406
+ #
407
+ # The Reflection consumes the loader's already-warmed
408
+ # cache producers (or, when no `cache_store` is set,
409
+ # eagerly walks the env). Once constructed, the
410
+ # Reflection carries the derived tables independently
411
+ # and never re-consults the loader — making it safe to
412
+ # share across Ractors while the loader stays per-
413
+ # process / per-Ractor for write-path operations.
414
+ def reflection
415
+ @state[:reflection] ||= begin
416
+ require_relative "reflection"
417
+ Environment::Reflection.new(
418
+ known_class_names: known_class_names_set,
419
+ instance_definitions: instance_definitions_table,
420
+ singleton_definitions: singleton_definitions_table,
421
+ type_param_names: type_param_names_table,
422
+ constant_types: constant_type_table,
423
+ ancestor_names: ancestor_names_table
424
+ )
425
+ end
426
+ end
427
+
302
428
  private
303
429
 
304
430
  def constant_type_table
305
431
  @constant_type_table ||= begin
306
432
  require_relative "../cache/rbs_constant_table"
307
- Cache::RbsConstantTable.fetch(loader: self, store: cache_store)
433
+ fetch_or_compute_producer(Cache::RbsConstantTable)
308
434
  end
309
435
  end
310
436
 
311
437
  def known_class_names_set
312
438
  @known_class_names_set ||= begin
313
439
  require_relative "../cache/rbs_known_class_names"
314
- Cache::RbsKnownClassNames.fetch(loader: self, store: cache_store)
440
+ fetch_or_compute_producer(Cache::RbsKnownClassNames)
315
441
  end
316
442
  end
317
443
 
318
444
  def type_param_names_table
319
445
  @type_param_names_table ||= begin
320
446
  require_relative "../cache/rbs_class_type_param_names"
321
- Cache::RbsClassTypeParamNames.fetch(loader: self, store: cache_store)
447
+ fetch_or_compute_producer(Cache::RbsClassTypeParamNames)
448
+ end
449
+ end
450
+
451
+ # ADR-15 Phase 2b — the `Reflection` build path
452
+ # consumes these tables even when `cache_store` is nil
453
+ # (e.g. tests that build a `Reflection` without a
454
+ # persistent cache). The helper routes through the
455
+ # producer's `.fetch` when a store IS available, and
456
+ # falls back to the producer's `.compute` otherwise.
457
+ def fetch_or_compute_producer(producer)
458
+ return producer.fetch(loader: self, store: cache_store) if cache_store
459
+
460
+ producer.send(:compute, self)
461
+ end
462
+
463
+ # ADR-15 Phase 2b — `Hash<String, Array<String>>` of
464
+ # normalised ancestor chains per class. Consumes the
465
+ # existing `RbsClassAncestorTable` producer when
466
+ # `cache_store` is set; falls back to the producer's
467
+ # `compute` otherwise. Used by {#reflection}.
468
+ def ancestor_names_table
469
+ @ancestor_names_table ||= begin
470
+ require_relative "../cache/rbs_class_ancestor_table"
471
+ fetch_or_compute_producer(Cache::RbsClassAncestorTable)
322
472
  end
323
473
  end
324
474
 
@@ -332,6 +482,8 @@ module Rigor
332
482
  end
333
483
 
334
484
  def translate_constant_decl(rbs_name)
485
+ return nil if env.nil?
486
+
335
487
  entry = env.constant_decls[rbs_name]
336
488
  return nil unless entry
337
489
 
@@ -339,8 +491,41 @@ module Rigor
339
491
  translated unless translated.is_a?(Type::Bot)
340
492
  end
341
493
 
494
+ # The RBS environment for this loader. Memoised both on
495
+ # success AND on failure: when the env build raises
496
+ # (typically `RBS::DuplicatedDeclarationError` because a
497
+ # `signature_paths:` entry redeclares a constant or class
498
+ # already shipped by stdlib RBS), retrying on every
499
+ # subsequent `env` call would re-parse and re-resolve the
500
+ # whole sig set per AST node touched during analysis,
501
+ # multiplying per-file analysis cost by ~100x. Failures
502
+ # short-circuit to `nil` here and are surfaced to the user
503
+ # via `warn_about_env_build_failure_once` so the broken
504
+ # `signature_paths:` entry is identifiable.
342
505
  def env
343
- @state[:env] ||= cache_store ? cached_env : build_env
506
+ return @state[:env] if @state[:env_loaded]
507
+
508
+ @state[:env_loaded] = true
509
+ @state[:env] = cache_store ? cached_env : build_env
510
+ rescue ::RBS::BaseError => e
511
+ warn_about_env_build_failure_once(e)
512
+ @state[:env] = nil
513
+ end
514
+
515
+ def warn_about_env_build_failure_once(error)
516
+ return if @state[:env_build_warned]
517
+
518
+ @state[:env_build_warned] = true
519
+ first_line = error.message.to_s.lines.first.to_s.strip
520
+ warn(
521
+ "rigor: RBS environment build failed: #{error.class}: #{first_line}\n " \
522
+ "Likely cause: a `signature_paths:` entry redeclares a constant or class\n " \
523
+ "already shipped by Rigor's bundled RBS (Ruby core / stdlib / gem-bundled\n " \
524
+ "RBS / `data/vendored_gem_sigs/`). Rigor will continue analyzing with no\n " \
525
+ "RBS env in scope, so most type-of queries will return `Dynamic[top]` and\n " \
526
+ "most rule diagnostics will not fire. Remove the conflicting `.rbs` from\n " \
527
+ "your `signature_paths:` to restore type coverage."
528
+ )
344
529
  end
345
530
 
346
531
  def cached_env
@@ -362,7 +547,7 @@ module Rigor
362
547
  def instance_definitions_table
363
548
  @state[:instance_definitions_table] ||= begin
364
549
  require_relative "../cache/rbs_instance_definitions"
365
- Cache::RbsInstanceDefinitions.fetch(loader: self, store: cache_store)
550
+ fetch_or_compute_producer(Cache::RbsInstanceDefinitions)
366
551
  end
367
552
  end
368
553
 
@@ -373,7 +558,7 @@ module Rigor
373
558
  def singleton_definitions_table
374
559
  @state[:singleton_definitions_table] ||= begin
375
560
  require_relative "../cache/rbs_instance_definitions"
376
- Cache::RbsSingletonDefinitions.fetch(loader: self, store: cache_store)
561
+ fetch_or_compute_producer(Cache::RbsSingletonDefinitions)
377
562
  end
378
563
  end
379
564
 
@@ -398,6 +583,7 @@ module Rigor
398
583
  def build_instance_definition(class_name)
399
584
  rbs_name = parse_type_name(class_name)
400
585
  return nil unless rbs_name
586
+ return nil if env.nil?
401
587
  return nil unless env.class_decls.key?(rbs_name)
402
588
 
403
589
  builder.build_instance(rbs_name)
@@ -408,6 +594,7 @@ module Rigor
408
594
  def build_singleton_definition(class_name)
409
595
  rbs_name = parse_type_name(class_name)
410
596
  return nil unless rbs_name
597
+ return nil if env.nil?
411
598
  return nil unless env.class_decls.key?(rbs_name)
412
599
 
413
600
  builder.build_singleton(rbs_name)
@@ -428,6 +615,7 @@ module Rigor
428
615
  def compute_class_known(name)
429
616
  rbs_name = parse_type_name(name)
430
617
  return false unless rbs_name
618
+ return false if env.nil?
431
619
 
432
620
  # `RBS::Environment#class_decls` after `resolve_type_names`
433
621
  # holds entries for both classes AND modules; the gem unifies
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ class Environment
5
+ # Frozen, `Ractor.shareable?` read-only RBS query facade.
6
+ # [ADR-15](../../../docs/adr/15-ractor-concurrency.md)
7
+ # Phase 2b extracts the read-only surface of {RbsLoader}
8
+ # into this carrier so future Ractor-isolated workers
9
+ # can share one Reflection across the pool while keeping
10
+ # the per-Ractor mutable accelerator state (per-process
11
+ # memo Hashes) where it belongs.
12
+ #
13
+ # Backing tables (all frozen at construction):
14
+ #
15
+ # - `known_class_names` — `Set<String>` of every
16
+ # class / module / alias name in the loaded RBS
17
+ # environment. Top-level prefixed (`"::Hash"`); plain
18
+ # queries normalise via {#normalise}.
19
+ # - `instance_definitions` —
20
+ # `Hash<String, RBS::Definition>` keyed on
21
+ # `RBS::TypeName#to_s` (top-level prefixed).
22
+ # - `singleton_definitions` — same shape, singleton side.
23
+ # - `type_param_names` —
24
+ # `Hash<String, Array<Symbol>>` of declared type
25
+ # parameters per class.
26
+ # - `constant_types` — `Hash<String, Rigor::Type>` of
27
+ # translated constant declarations.
28
+ # - `ancestor_names` — `Hash<String, Array<String>>` of
29
+ # normalised ancestor chains per class.
30
+ #
31
+ # Each `Reflection` instance is `frozen?` at construction
32
+ # — every cached table is frozen, `self` is frozen.
33
+ # **NOT** `Ractor.shareable?`: the `instance_definitions`
34
+ # / `singleton_definitions` tables hold upstream
35
+ # `RBS::Definition` objects that transitively reference
36
+ # `RBS::Location` (C-extension state that
37
+ # `Ractor.make_shareable` rejects).
38
+ #
39
+ # The Ractor worker pool (ADR-15 Phase 4) sidesteps this
40
+ # by having each worker build ITS OWN `Reflection` from
41
+ # the shared `Cache::Store`. The cross-Ractor sharing
42
+ # point is the Store's on-disk + in-process memo layer,
43
+ # NOT the Reflection itself. Each Reflection is a per-
44
+ # Ractor immutable read-side view; this carrier exists
45
+ # to GUARANTEE the per-worker view never mutates after
46
+ # construction.
47
+ #
48
+ # If a future RBS release makes `RBS::Location`
49
+ # Ractor-shareable, swapping the `freeze` call below for
50
+ # `Ractor.make_shareable(self)` makes the whole carrier
51
+ # cross-Ractor-shareable in one line. Until then, the
52
+ # frozen-read-only contract is the deliverable.
53
+ class Reflection
54
+ attr_reader :known_class_names, :instance_definitions, :singleton_definitions,
55
+ :type_param_names, :constant_types, :ancestor_names
56
+
57
+ def initialize(known_class_names:, instance_definitions:, singleton_definitions:,
58
+ type_param_names:, constant_types:, ancestor_names:)
59
+ @known_class_names = freeze_set(known_class_names)
60
+ @instance_definitions = freeze_hash(instance_definitions)
61
+ @singleton_definitions = freeze_hash(singleton_definitions)
62
+ @type_param_names = freeze_hash(type_param_names)
63
+ @constant_types = freeze_hash(constant_types)
64
+ @ancestor_names = freeze_hash(ancestor_names)
65
+ freeze
66
+ end
67
+
68
+ def class_known?(name)
69
+ @known_class_names.include?(rooted(name))
70
+ end
71
+
72
+ def instance_definition(name)
73
+ @instance_definitions[rooted(name)]
74
+ end
75
+
76
+ def singleton_definition(name)
77
+ @singleton_definitions[rooted(name)]
78
+ end
79
+
80
+ def class_type_param_names(name)
81
+ @type_param_names.fetch(unrooted(name), [])
82
+ end
83
+
84
+ def constant_type(name)
85
+ @constant_types[rooted(name)]
86
+ end
87
+
88
+ # Three-valued `(lhs, rhs)` relation:
89
+ # `:equal` / `:subclass` / `:superclass` / `:disjoint` /
90
+ # `:unknown`. Mirrors {RbsHierarchy#class_ordering}'s
91
+ # contract; the Reflection's frozen ancestor table
92
+ # supports the same queries without any in-process
93
+ # mutation.
94
+ def class_ordering(lhs, rhs)
95
+ lhs = unrooted(lhs)
96
+ rhs = unrooted(rhs)
97
+ return :equal if lhs == rhs
98
+
99
+ lhs_ancestors = @ancestor_names[lhs]
100
+ rhs_ancestors = @ancestor_names[rhs]
101
+ return :unknown if lhs_ancestors.nil? || rhs_ancestors.nil? || lhs_ancestors.empty? || rhs_ancestors.empty?
102
+
103
+ if lhs_ancestors.include?(rhs)
104
+ :subclass
105
+ elsif rhs_ancestors.include?(lhs)
106
+ :superclass
107
+ else
108
+ :disjoint
109
+ end
110
+ end
111
+
112
+ # Yields every known class / module / alias name in
113
+ # the loader's canonical rooted form (`"::Hash"`).
114
+ def each_known_class_name(&)
115
+ @known_class_names.each(&)
116
+ end
117
+
118
+ private
119
+
120
+ # The cached tables use mixed key conventions inherited
121
+ # from the underlying RBS::TypeName surface: the
122
+ # name-set / definition tables / constant table store
123
+ # rooted `"::Foo"` keys; the type-param / ancestor
124
+ # tables store unrooted `"Foo"`. Reflection's queries
125
+ # normalise per-lookup so callers can pass either form.
126
+ def rooted(name)
127
+ s = name.to_s
128
+ s.start_with?("::") ? s : "::#{s}"
129
+ end
130
+
131
+ def unrooted(name)
132
+ name.to_s.delete_prefix("::")
133
+ end
134
+
135
+ def freeze_set(value)
136
+ return value if value.is_a?(Set) && value.frozen?
137
+
138
+ case value
139
+ when Set then value.dup.freeze
140
+ when Array, Hash then Set.new(value).freeze
141
+ else raise ArgumentError, "expected Set / Array / Hash, got #{value.class}"
142
+ end
143
+ end
144
+
145
+ def freeze_hash(value)
146
+ return value if value.frozen?
147
+
148
+ value.dup.freeze
149
+ end
150
+ end
151
+ end
152
+ end