Class: Rigor::Environment::RbsLoader

Inherits:
Object
  • Object
show all
Defined in:
lib/rigor/environment/rbs_loader.rb

Overview

Loads RBS class declarations and method definitions from disk and exposes them to the inference engine in a small, stable surface.

Slice 4 phase 1 only enabled the RBS core signatures shipped with the ‘rbs` gem (`Object`, `Integer`, `String`, `Array`, …). Phase 2a adds opt-in stdlib library loading (`pathname`, `json`, `tempfile`, …) and arbitrary-directory signature loading (typically the project’s local ‘sig/` tree). Both are off by default on `RbsLoader.default` so the core-only fast path stays cheap; project-aware loading is opted into through for_project or by constructing a custom loader.

The default instance is shared across the process: building the core RBS environment costs hundreds of milliseconds and the data is read-only. The shared instance is frozen, but holds a mutable state hash for lazy memoization of the heavy ‘RBS::Environment` and `RBS::DefinitionBuilder` – the user-visible API stays purely functional.

See docs/internal-spec/inference-engine.md for the binding contract. rubocop:disable Metrics/ClassLength

Constant Summary collapse

SYNTHETIC_NAMESPACE_BUFFER =

Buffer name stamped on the ‘module` declarations synthesized by synthesize_missing_namespaces. Re-read off the built env by #synthesized_namespaces so the analysis layer can surface an `:info` diagnostic naming the project’s malformed-RBS namespaces — robust across the marshalled env cache, since the sentinel rides along on each synthetic declaration’s location.

"(rigor: synthesized namespaces)"
SYNTHETIC_STUB_BUFFER =

Buffer name stamped on the stub ‘class` / `module` declarations synthesized by stub_missing_referenced_types for types the project’s RBS references but no loaded signature declares. #synthesized_stub_types reads them back off the built env (so the answer survives the marshalled env cache), and #synthesized_type_names folds them together with the namespace stubs into the set MethodDispatcher resolves to ‘Dynamic` (no false `call.undefined-method`).

"(rigor: synthesized stub types)"
MAX_STUB_PASSES =

ADR-5 robustness, second tier. A project ‘signature_paths:` RBS that references a type no loaded signature declares —`def x: () -> DRb::DRbServer` when the `drb` RBS is not available, or a stale reference to its own removed `Textbringer::EditorError` — makes `RBS::DefinitionBuilder#build_instance` raise `NoTypeFoundError`, and (per RBS’s all-or-nothing per-class build) that single unresolved reference takes down EVERY method on the class, not just the one signature. Observed on shugo/textbringer: one ‘DRb::DRbServer` reference left the whole `Textbringer::Commands` module — including its 186-call-site `define_command` DSL — resolving as `Dynamic`.

We synthesize an empty stub for each such referenced-but- undeclared type so the rest of the class builds. A leaf type is stubbed as ‘class`, its enclosing namespaces as `module`. Stubbed types carry no methods, so a call against a value of a stubbed type would otherwise mis-fire `call.undefined-method`; MethodDispatcher consults #synthesized_type_names and resolves such calls to `Dynamic` instead (the same no-false-positive contract as the dependency-source tier).

Detection re-uses RBS’s own builder (correct by construction): build every PROJECT class and read the missing name out of the raised error. Bounded to ‘signature_paths` classes (stdlib / vendored RBS is well-formed) and to MAX_STUB_PASSES iterations — a fresh stub can expose a deeper reference the first build error hid, but empty stubs reference nothing, so the fixpoint converges quickly.

5

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: []) ⇒ RbsLoader

Returns a new instance of RbsLoader.

Parameters:

  • libraries (Array<String, Symbol>) (defaults to: [])

    stdlib library names to load on top of core (e.g., ‘[“pathname”, “json”]`). Empty by default. Each entry MUST correspond to a directory under the `rbs` gem’s ‘stdlib/` tree; unknown names are silently dropped on environment build (the underlying `RBS::EnvironmentLoader` raises and we fail-soft).

  • signature_paths (Array<String, Pathname>) (defaults to: [])

    additional directories of ‘.rbs` files to load (typically the project’s ‘sig/` tree). Non-existent or non-directory paths are filtered out at build time so the loader stays robust to fixtures and bare repositories.

  • cache_store (Rigor::Cache::Store, nil) (defaults to: nil)

    the persistent cache the loader consults for translated constant lookups (and, in later v0.0.9 slices, other Marshal-clean reflection artefacts). Pass ‘nil` (the default) to skip the cache entirely; the runner threads its own Store through here when caching is enabled.

  • virtual_rbs (Array<[String, String]>) (defaults to: [])

    ADR-32 WD4 —‘[virtual_filename, rbs_source]` pairs synthesised from project source by a plugin’s ‘Manifest#source_rbs_synthesizer`. Merged into the env after `signature_paths:` and the vendored stubs. Pass `[]` (the default) when no synthesizer-emitting plugin is loaded.



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/rigor/environment/rbs_loader.rb', line 353

def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: [])
  @libraries = libraries.map(&:to_s).freeze
  @signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
  @cache_store = cache_store
  @virtual_rbs = virtual_rbs.map { |name, content| [name.to_s.dup.freeze, content.to_s.dup.freeze].freeze }.freeze
  # Per-loader memoization bucket. Held as a single
  # mutable Hash so the loader instance itself can be
  # `.freeze`d (per ADR-15 reflection-facade contract)
  # without losing the lazy-memo behaviour. Slot names
  # currently consulted: `:env`, `:env_loaded`,
  # `:env_build_warned`, `:builder`, `:reflection`,
  # `:instance_definitions_table`,
  # `:singleton_definitions_table`. Constructed via
  # `Hash.new` (NOT a `{ ... }` literal) so Rigor's
  # `HashShape` narrowing doesn't infer a fixed key set
  # from the initial state and fold post-initial slot
  # reads (e.g. `@state[:env_loaded]`) to a constant
  # `nil`.
  @state = Hash.new # rubocop:disable Style/EmptyLiteral
  @instance_definition_cache = {}
  @singleton_definition_cache = {}
  @class_known_cache = {}
  @hierarchy = RbsHierarchy.new(self)
end

Instance Attribute Details

#cache_storeObject (readonly)

Returns the value of attribute cache_store.



327
328
329
# File 'lib/rigor/environment/rbs_loader.rb', line 327

def cache_store
  @cache_store
end

#librariesObject (readonly)

Returns the value of attribute libraries.



327
328
329
# File 'lib/rigor/environment/rbs_loader.rb', line 327

def libraries
  @libraries
end

#signature_pathsObject (readonly)

Returns the value of attribute signature_paths.



327
328
329
# File 'lib/rigor/environment/rbs_loader.rb', line 327

def signature_paths
  @signature_paths
end

#virtual_rbsObject (readonly)

Returns the value of attribute virtual_rbs.



327
328
329
# File 'lib/rigor/environment/rbs_loader.rb', line 327

def virtual_rbs
  @virtual_rbs
end

Class Method Details

.add_virtual_rbs(env, virtual_rbs) ⇒ Object

ADR-32 WD4 — merge synthesised-from-source RBS strings into the freshly-built environment. Each entry is a ‘[virtual_filename, rbs_source]` pair. `virtual_filename` is purely for diagnostic provenance (RBS parse errors cite it) — it is not a real file path. Per WD6 the synthesizer-emit path is responsible for catching its own parse errors and returning `nil` rather than garbage; this method assumes its input is parseable and only rescues `RBS::ParsingError` as a fail-soft.



270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/rigor/environment/rbs_loader.rb', line 270

def add_virtual_rbs(env, virtual_rbs)
  return if virtual_rbs.nil? || virtual_rbs.empty?

  virtual_rbs.each do |filename, content|
    next if content.nil? || content.empty?

    buffer = ::RBS::Buffer.new(name: filename.to_s, content: content.to_s)
    _, directives, decls = ::RBS::Parser.parse_signature(buffer)
    source = ::RBS::Source::RBS.new(buffer, directives || [], decls || [])
    env.add_source(source)
  rescue ::RBS::BaseError
    # WD6 fail-soft: a single broken virtual RBS contribution
    # does not pull the whole env down. The plugin layer
    # records a `source-rbs-synthesis-failed` info diagnostic
    # in slice 2; here we just skip the entry.
  end
end

.append_stub_declarations(base_env, missing) ⇒ Object

Adds empty stub declarations for the missing referenced types (and any enclosing namespace they need) to the pre-resolve env, tagged with SYNTHETIC_STUB_BUFFER. A name that is a prefix of another name is declared ‘module` (it is a namespace); a leaf is declared `class` (referenced types appear in instance position far more often than as mixins).



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/rigor/environment/rbs_loader.rb', line 244

def append_stub_declarations(base_env, missing)
  names = missing.to_set
  missing.each do |name|
    parts = name.split("::")
    (1...parts.length).each { |i| names << parts[0, i].join("::") }
  end
  source = names.sort_by { |n| n.count(":") }.map do |name|
    keyword = names.any? { |other| other != name && other.start_with?("#{name}::") } ? "module" : "class"
    "#{keyword} #{name}\nend\n"
  end.join
  buffer = ::RBS::Buffer.new(name: SYNTHETIC_STUB_BUFFER, content: source)
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
  base_env.add_source(::RBS::Source::RBS.new(buffer, directives || [], decls || []))
rescue ::RBS::BaseError
  nil
end

.build_env_for(libraries:, signature_paths:, virtual_rbs: []) ⇒ Object

Builds an ‘RBS::Environment` from explicit `libraries` and `signature_paths`. Stateless surface so the v0.0.9 Cache::RbsEnvironment producer can build an env on cache miss without holding a loader instance, and the instance-side #build_env delegates here so the implementation stays single-rooted.

Vendored gem stubs (‘data/vendored_gem_sigs/<gem>/`) are loaded on top of `signature_paths` so the per-gem RBS bundled with Rigor itself is in scope for every analysis run. The gem stubs are intentionally read-only and appended LAST so user-supplied `signature_paths` win on name conflicts.



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/rigor/environment/rbs_loader.rb', line 77

def build_env_for(libraries:, signature_paths:, virtual_rbs: [])
  rbs_loader = RBS::EnvironmentLoader.new
  libraries.each do |library|
    next unless rbs_loader.has_library?(library: library, version: nil)

    rbs_loader.add(library: library, version: nil)
  end
  signature_paths.each do |path|
    path = Pathname(path) unless path.is_a?(Pathname)
    rbs_loader.add(path: path) if path.directory?
  end
  vendored_gem_sig_paths.each do |path|
    rbs_loader.add(path: path) if path.directory?
  end
  env = RBS::Environment.from_loader(rbs_loader)
  add_virtual_rbs(env, virtual_rbs)
  synthesize_missing_namespaces(env)
  resolved = env.resolve_type_names
  stub_missing_referenced_types(env, resolved, project_sig_files(signature_paths))
end

.collect_missing_namespaces(env) ⇒ Object

Returns the ‘::`-stripped names of every enclosing namespace that some declaration references but no declaration defines, shallowest-first so the synthesized source declares `Foo` before `Foo::Bar`.



176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/rigor/environment/rbs_loader.rb', line 176

def collect_missing_namespaces(env)
  declared = env.class_decls.keys.to_set
  missing = {}
  env.class_decls.each_key do |type_name|
    path = type_name.namespace.path
    path.each_index do |i|
      prefix = path[0..i]
      full = ::RBS::TypeName.parse("::#{prefix.join('::')}")
      missing[prefix.join("::")] = prefix.length unless declared.include?(full)
    end
  end
  missing.sort_by { |_name, depth| depth }.map(&:first)
end

.defaultObject



52
53
54
# File 'lib/rigor/environment/rbs_loader.rb', line 52

def default
  @default ||= new.freeze
end

.project_entry?(entry, project_files) ⇒ Boolean

True when a ‘class_decls` entry was declared in one of the project’s own signature files (by declaration location), so the sweep skips the bundled stdlib / vendored universe.

Returns:

  • (Boolean)


229
230
231
232
233
234
235
236
# File 'lib/rigor/environment/rbs_loader.rb', line 229

def project_entry?(entry, project_files)
  decl = entry.respond_to?(:primary_decl) ? entry.primary_decl : nil
  location = decl&.location
  buffer_name = location&.buffer&.name
  return false unless buffer_name

  project_files.include?(File.expand_path(buffer_name.to_s))
end

.project_sig_files(signature_paths) ⇒ Object

The absolute paths of every ‘.rbs` file under the project’s ‘signature_paths:` (NOT vendored / stdlib RBS — those are well-formed, so attempting to build them would only waste time). Used to scope the referenced-type build sweep.



194
195
196
197
198
199
200
201
# File 'lib/rigor/environment/rbs_loader.rb', line 194

def project_sig_files(signature_paths)
  signature_paths.flat_map do |path|
    path = Pathname(path) unless path.is_a?(Pathname)
    next [] unless path.directory?

    Dir.glob(path.join("**", "*.rbs")).map { |p| File.expand_path(p) }
  end.to_set
end

.reset_default!Object

Used by tests to discard the cached default loader; production code MUST NOT call this. The shared loader holds a several-MB RBS::Environment, so dropping it during a normal run wastes the cost of rebuilding it.



60
61
62
# File 'lib/rigor/environment/rbs_loader.rb', line 60

def reset_default!
  @default = nil
end

.stub_missing_referenced_types(base_env, resolved, project_files) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/rigor/environment/rbs_loader.rb', line 130

def stub_missing_referenced_types(base_env, resolved, project_files)
  return resolved if project_files.empty?

  MAX_STUB_PASSES.times do
    missing = unresolved_referenced_types(resolved, project_files)
    break if missing.empty?

    append_stub_declarations(base_env, missing)
    resolved = base_env.resolve_type_names
  end
  resolved
end

.synthesize_missing_namespaces(env) ⇒ Object

Robustness (ADR-5): a project whose RBS declares qualified names (‘class Foo::Bar`) without ever declaring the enclosing namespace (`module Foo`) is invalid by upstream RBS rules —`RBS::DefinitionBuilder#build_instance` raises `NoTypeFoundError: Could not find ::Foo`, which the loader’s fail-soft rescue turns into a silent dispatch miss (every method on every such class degrades to ‘Dynamic`). This is a common authoring mistake (e.g. shugo/textbringer ships a `sig/` that `rbs validate` itself rejects). Rather than let an otherwise-usable signature set contribute nothing, synthesize an empty `module` declaration for each undeclared enclosing namespace so the definitions build. We only ever add names that are absent — a genuinely-declared namespace (module or class, here or in a loaded gem) is left untouched.



157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/rigor/environment/rbs_loader.rb', line 157

def synthesize_missing_namespaces(env)
  missing = collect_missing_namespaces(env)
  return if missing.empty?

  source = missing.map { |name| "module #{name}\nend\n" }.join
  buffer = ::RBS::Buffer.new(name: SYNTHETIC_NAMESPACE_BUFFER, content: source)
  _, directives, decls = ::RBS::Parser.parse_signature(buffer)
  env.add_source(::RBS::Source::RBS.new(buffer, directives || [], decls || []))
rescue ::RBS::BaseError
  # Fail-soft: synthesis is an opportunistic uplift, never a
  # hard requirement. A parse failure here just leaves the env
  # as it was (dispatch misses on the affected classes).
  nil
end

.unresolved_referenced_types(env, project_files) ⇒ Object

Builds every project class (instance + singleton side) and returns the ‘::`-stripped names of the types whose absence raised `NoTypeFoundError`. Only the FIRST missing reference per class surfaces per build, which is why the caller loops.



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/rigor/environment/rbs_loader.rb', line 207

def unresolved_referenced_types(env, project_files)
  builder = ::RBS::DefinitionBuilder.new(env: env)
  missing = []
  env.class_decls.each do |type_name, entry|
    next unless project_entry?(entry, project_files)

    %i[build_instance build_singleton].each do |build|
      builder.public_send(build, type_name)
    rescue ::RBS::NoTypeFoundError => e
      name = e.message[/Could not find (\S+)/, 1]
      missing << name.sub(/\A::/, "") if name
    rescue ::RBS::BaseError
      # Other build failures (duplicate decl, mixin cycle, ...)
      # are not ours to repair here — leave them fail-soft.
    end
  end
  missing.uniq
end

.vendored_gem_namesObject

Gem names whose RBS ships under ‘data/vendored_gem_sigs/<gem>/`. The directory walk is the source of truth (the `README.md` sibling is not a gem and is excluded). Callers building the RBS env use this set to drop the matching `rbs collection install` directory before it double-declares against the vendored copy — the same hazard `DEFAULT_LIBRARIES` creates for stdlib-extracted gems. See `RbsCollectionDiscovery`’s ‘skip_gem_names:`.



318
319
320
321
322
323
324
# File 'lib/rigor/environment/rbs_loader.rb', line 318

def vendored_gem_names
  return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)

  Dir.children(VENDORED_GEM_SIGS_ROOT).reject do |child|
    File.file?(File.join(VENDORED_GEM_SIGS_ROOT, child))
  end
end

.vendored_gem_sig_pathsObject



301
302
303
304
305
306
307
# File 'lib/rigor/environment/rbs_loader.rb', line 301

def vendored_gem_sig_paths
  return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)

  Dir.children(VENDORED_GEM_SIGS_ROOT).map do |gem_dir|
    Pathname(File.join(VENDORED_GEM_SIGS_ROOT, gem_dir))
  end
end

Instance Method Details

#class_decl_pathsObject

Returns a frozen ‘Hash<String, String>` mapping each loaded class / module name (top-level prefixed) to the file path of its FIRST declaration’s RBS source. Used by Analysis::RunStats to attribute the type universe between “project sig/” (paths under the configured ‘signature_paths`) and “bundled” (everything else — RBS core, stdlib libraries, gem-bundled RBS). Each value is a frozen `String` so the whole result is `Ractor.shareable?` — the Phase 4b worker pool ships a snapshot back to the coordinator on the first `:prepare` message.



514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
# File 'lib/rigor/environment/rbs_loader.rb', line 514

def class_decl_paths
  return {}.freeze if env.nil?

  result = {}
  env.class_decls.each do |rbs_name, entry|
    decl = entry.primary_decl
    next if decl.nil?

    location = decl.location
    next if location.nil?

    buffer = location.buffer
    name = buffer.respond_to?(:name) ? buffer.name : nil
    next if name.nil?

    result[rbs_name.to_s.dup.freeze] = name.to_s.dup.freeze
  end
  result.freeze
rescue ::RBS::BaseError
  {}.freeze
end

#class_known?(name) ⇒ Boolean

Returns true when an RBS class or module declaration with the given name is loaded. Accepts unprefixed or top-level-prefixed names (“Integer” or “::Integer”). Memoized per-name (positive and negative results both cache).

When ‘cache_store` is set, the loader fetches the entire set of known class / module / alias names once (per process) through Cache::RbsKnownClassNames.fetch and answers `class_known?` from the in-memory Set. Cold runs pay a single env walk and persist the result; warm runs (and a separate loader sharing the same Store) skip the env walk entirely.

Returns:

  • (Boolean)


423
424
425
426
427
428
429
430
431
432
# File 'lib/rigor/environment/rbs_loader.rb', line 423

def class_known?(name)
  key = name.to_s
  return @class_known_cache[key] if @class_known_cache.key?(key)

  @class_known_cache[key] = if cache_store
                              cached_class_known(name)
                            else
                              compute_class_known(name)
                            end
end

#class_ordering(lhs, rhs) ⇒ Object



643
644
645
# File 'lib/rigor/environment/rbs_loader.rb', line 643

def class_ordering(lhs, rhs)
  @hierarchy.class_ordering(lhs, rhs)
end

#class_type_param_names(class_name) ⇒ Object

Slice 4 phase 2d. Returns the class’s declared type-parameter names as Symbols (e.g., ‘[:Elem]` for `Array`, `[:K, :V]` for `Hash`). Used by the dispatcher to build the substitution map from receiver `type_args` into the method’s return type. The instance definition is the canonical source because singleton methods (e.g., ‘Array.new`) parameterize over the same `Elem` as instance methods.

Returns an empty array for non-generic classes and for unknown names (the loader stays fail-soft). NOTE: in the ‘rbs` gem, `RBS::Definition#type_params` returns `Array<Symbol>` directly, not the AST `TypeParam` object (those live on the AST level).

When ‘cache_store` is set, the loader fetches the entire type-parameter-name table once (per process) through Cache::RbsClassTypeParamNames.fetch and answers point lookups from it. Cold runs build the table once and persist it; warm runs (and a separate loader sharing the same Store) skip the env walk entirely.



631
632
633
634
635
636
637
638
639
640
641
# File 'lib/rigor/environment/rbs_loader.rb', line 631

def class_type_param_names(class_name)
  if cache_store
    key = class_name.to_s.delete_prefix("::")
    return type_param_names_table.fetch(key, []).dup
  end

  definition = instance_definition(class_name)
  return [] unless definition

  definition.type_params.dup
end

#constant_namesArray<String>

Returns every RBS-declared constant name (top-level prefixed, e.g., ‘“::Math::PI”`) currently loaded into the environment. Used by the cache producer that materialises the constant-type table; ordinary callers should keep using #constant_type for point lookups.

Returns:

  • (Array<String>)

    every RBS-declared constant name (top-level prefixed, e.g., ‘“::Math::PI”`) currently loaded into the environment. Used by the cache producer that materialises the constant-type table; ordinary callers should keep using #constant_type for point lookups.



652
653
654
655
656
657
658
# File 'lib/rigor/environment/rbs_loader.rb', line 652

def constant_names
  return [] if env.nil?

  env.constant_decls.keys.map(&:to_s)
rescue ::RBS::BaseError
  []
end

#constant_type(name) ⇒ Object

Slice A constant-value lookup. Returns the translated ‘Rigor::Type` for a non-class constant declaration (`BUCKETS: Array`, `DEFAULT_PATH: String`, …) or `nil` when no constant entry exists for `name` in the loaded RBS environment. Callers MUST treat the return value as authoritative when present and as “unknown” when nil; the loader does NOT consult the class declarations here — class objects are still resolved through #class_known? and `Environment#singleton_for_name`.

When ‘cache_store` is set, the loader fetches the entire translated constant table once (per process) through Cache::RbsConstantTable.fetch and answers point lookups from it. Cold runs pay the translation cost up-front and write the result to disk; warm runs skip the translation entirely and pay only a `Marshal.load` of the table.



692
693
694
695
696
697
698
699
700
701
702
703
# File 'lib/rigor/environment/rbs_loader.rb', line 692

def constant_type(name)
  rbs_name = parse_type_name(name)
  return nil unless rbs_name

  if cache_store
    constant_type_table[rbs_name.to_s]
  else
    translate_constant_decl(rbs_name)
  end
rescue ::RBS::BaseError
  nil
end

#each_class_decl_annotationObject

ADR-20 slice 2e — iterates over every ‘%a…` annotation attached to a class- or module-level declaration in the loaded RBS environment, yielding `(annotation_string, source_location)` pairs. Used by Inference::HktRegistry.scan_rbs_loader to find `rigor:v1:hkt_register` / `rigor:v1:hkt_define` directives in user-authored overlays and merge them into the per-`Environment` HKT registry. Yields nothing when the env failed to build (fail-soft, same shape as #each_known_class_name).



484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
# File 'lib/rigor/environment/rbs_loader.rb', line 484

def each_class_decl_annotation
  return enum_for(:each_class_decl_annotation) unless block_given?
  return if env.nil?

  env.class_decls.each_value do |entry|
    entry.each_decl do |decl|
      next unless decl.respond_to?(:annotations)

      decl.annotations.each { |a| yield a.string, a.location }
    end
  end
rescue ::RBS::BaseError, ::Ractor::IsolationError
  # fail-soft: matches each_known_class_name's policy.
  # Ractor::IsolationError surfaces when the scan is
  # invoked from a non-main Ractor pool worker before
  # ADR-15's full deep-freeze migration completes — the
  # worker falls back to the base (builtins-only)
  # registry rather than crashing.
end

#each_constant_declObject

Yields ‘(name, entry)` for every RBS constant declaration currently loaded into the environment. The cache producer uses this to materialise the constant-type table without going back through #constant_type (which would recurse back into the cache when `cache_store` is set).



665
666
667
668
669
670
671
672
673
674
# File 'lib/rigor/environment/rbs_loader.rb', line 665

def each_constant_decl
  return enum_for(:each_constant_decl) unless block_given?
  return if env.nil?

  env.constant_decls.each do |rbs_name, entry|
    yield rbs_name.to_s, entry
  end
rescue ::RBS::BaseError
  # fail-soft: a broken RBS environment yields no entries.
end

#each_known_class_nameObject

Yields every known class / module / alias name (top-level prefixed) currently loaded into the environment. The cache producer that materialises the known-name set uses this so it never recurses back through #class_known?.



460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/rigor/environment/rbs_loader.rb', line 460

def each_known_class_name
  return enum_for(:each_known_class_name) unless block_given?
  return if env.nil?

  env.class_decls.each_key { |rbs_name| yield rbs_name.to_s }
  env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s }
rescue ::RBS::BaseError
  # fail-soft: a broken RBS environment yields no names.
  # Analyzer-internal errors (NameError, NoMethodError,
  # LoadError) are NOT swallowed — those are bugs and
  # must surface so they don't hide silently the way the
  # v0.0.9 cache `Cache::Descriptor` regression did.
end

#instance_definition(class_name) ⇒ RBS::Definition?

When ‘cache_store` is set, the loader fetches the per-class definition through Cache::RbsInstanceDefinitions.fetch so subsequent runs (and other loaders sharing the same Store) skip the `RBS::DefinitionBuilder.build_instance` step. In-memory `@instance_definition_cache` keeps the per-process short-circuit on top.

Returns:

  • (RBS::Definition, nil)

    the resolved instance definition for ‘class_name`, or nil when the class is unknown or its definition cannot be built (RBS may raise on broken hierarchies; we fail-soft and return nil so the caller can fall back).



547
548
549
550
551
552
553
554
555
556
# File 'lib/rigor/environment/rbs_loader.rb', line 547

def instance_definition(class_name)
  key = class_name.to_s
  return @instance_definition_cache[key] if @instance_definition_cache.key?(key)

  @instance_definition_cache[key] = if cache_store
                                      cached_instance_definition(class_name)
                                    else
                                      build_instance_definition(class_name)
                                    end
end

#instance_method(class_name:, method_name:) ⇒ RBS::Definition::Method?

Returns:

  • (RBS::Definition::Method, nil)


567
568
569
570
571
572
# File 'lib/rigor/environment/rbs_loader.rb', line 567

def instance_method(class_name:, method_name:)
  definition = instance_definition(class_name)
  return nil unless definition

  definition.methods[method_name.to_sym]
end

#prewarmObject

ADR-15 Phase 4b.x — eagerly drives every cached producer so a subsequent worker Ractor can serve all of its RBS queries from the Marshal blob on disk without ever calling ‘RBS::EnvironmentLoader.new`. The loader path that calls `EnvironmentLoader.new` transitively reads a chain of non-`Ractor.shareable?` module constants (`RBS::EnvironmentLoader::DEFAULT_CORE_ROOT`, `RBS::Repository::DEFAULT_STDLIB_ROOT`, `Gem::Requirement::DefaultRequirement`, …) and trips `Ractor::IsolationError`. Pre-warming the cache on the main Ractor and letting workers consult ONLY the Marshal-loaded blob sidesteps the whole chain.

No-op when ‘cache_store` is nil — without a Store the worker has no choice but to build env via the loader, so the caller MUST ensure pool mode runs with caching enabled. Returns `self` so the call chains cleanly from the `Runner` pre-spawn hook.



724
725
726
727
728
729
730
731
732
733
734
735
# File 'lib/rigor/environment/rbs_loader.rb', line 724

def prewarm
  return self if cache_store.nil?

  env
  known_class_names_set
  constant_type_table
  type_param_names_table
  ancestor_names_table
  instance_definitions_table
  singleton_definitions_table
  self
end

#rbs_module?(name) ⇒ Boolean

Returns true when the named RBS declaration is a Module (‘RBS::AST::Declarations::Module`) rather than a Class. The `user_class_fallback_receiver` tier consults this to route `Nominal.some_kernel_method` (where M is a module mixin like `PP::ObjectMixin`) through the `Nominal` fallback, because every concrete includer of M sees Kernel / Object instance methods as part of its own ancestor chain.

Returns false for classes, for unknown names, and when the RBS environment failed to build (fail-soft).

Returns:

  • (Boolean)


444
445
446
447
448
449
450
451
452
453
454
# File 'lib/rigor/environment/rbs_loader.rb', line 444

def rbs_module?(name)
  return false if env.nil?

  rbs_name = parse_type_name(name)
  return false if rbs_name.nil?

  entry = env.class_decls[rbs_name]
  entry.is_a?(::RBS::Environment::ModuleEntry)
rescue ::RBS::BaseError
  false
end

#reflectionObject

ADR-15 Phase 2b — return the loader’s read-only query surface as a frozen, ‘Ractor.shareable?` Rigor::Environment::Reflection value object. Built lazily on first access; the loader memoises so repeated calls return the same instance.

The Reflection consumes the loader’s already-warmed cache producers (or, when no ‘cache_store` is set, eagerly walks the env). Once constructed, the Reflection carries the derived tables independently and never re-consults the loader — making it safe to share across Ractors while the loader stays per- process / per-Ractor for write-path operations.



750
751
752
753
754
755
756
757
758
759
760
761
762
# File 'lib/rigor/environment/rbs_loader.rb', line 750

def reflection
  @state[:reflection] ||= begin
    require_relative "reflection"
    Environment::Reflection.new(
      known_class_names: known_class_names_set,
      instance_definitions: instance_definitions_table,
      singleton_definitions: singleton_definitions_table,
      type_param_names: type_param_names_table,
      constant_types: constant_type_table,
      ancestor_names: ancestor_names_table
    )
  end
end

#singleton_definition(class_name) ⇒ RBS::Definition?

When ‘cache_store` is set, the loader fetches the per-class singleton definition through Cache::RbsSingletonDefinitions.fetch; the same caching discipline as #instance_definition.

Returns:

  • (RBS::Definition, nil)

    the resolved singleton (class object) definition for ‘class_name`. The methods on this definition are the *class methods* of `class_name`, including those inherited from `Class` and `Module` for class types. Returns nil for unknown names and on RBS build errors (fail-soft).



584
585
586
587
588
589
590
591
592
593
# File 'lib/rigor/environment/rbs_loader.rb', line 584

def singleton_definition(class_name)
  key = class_name.to_s
  return @singleton_definition_cache[key] if @singleton_definition_cache.key?(key)

  @singleton_definition_cache[key] = if cache_store
                                       cached_singleton_definition(class_name)
                                     else
                                       build_singleton_definition(class_name)
                                     end
end

#singleton_method(class_name:, method_name:) ⇒ RBS::Definition::Method?

Returns the class method on ‘class_name`. For example, `singleton_method(class_name: “Integer”, method_name: :sqrt)` returns the definition for `Integer.sqrt`, while `singleton_method(class_name: “Foo”, method_name: :new)` returns Class#new for any class type.

Returns:

  • (RBS::Definition::Method, nil)

    the class method on ‘class_name`. For example, `singleton_method(class_name: “Integer”, method_name: :sqrt)` returns the definition for `Integer.sqrt`, while `singleton_method(class_name: “Foo”, method_name: :new)` returns Class#new for any class type.



605
606
607
608
609
610
# File 'lib/rigor/environment/rbs_loader.rb', line 605

def singleton_method(class_name:, method_name:)
  definition = singleton_definition(class_name)
  return nil unless definition

  definition.methods[method_name.to_sym]
end

#synthesized_namespacesObject

The enclosing namespaces synthesize_missing_namespaces had to invent because the project’s ‘signature_paths:` RBS declared qualified names (`class Foo::Bar`) without ever declaring `Foo`. Recovered by scanning the built env for class/module entries whose every declaration originated from the synthetic buffer, so the answer survives the marshalled-env cache (where no build-time collector would). Returns `::`-stripped names, shallowest-first. Empty for a well-formed sig set (the common case) and whenever the env failed to build.



387
388
389
# File 'lib/rigor/environment/rbs_loader.rb', line 387

def synthesized_namespaces
  names_synthesized_in(SYNTHETIC_NAMESPACE_BUFFER)
end

#synthesized_stub_typesObject

The referenced-but-undeclared types stub_missing_referenced_types stubbed so the project classes that mention them could build (e.g. an unavailable ‘DRb::DRbServer`, or a stale `Textbringer::EditorError`). Recovered off the built env like #synthesized_namespaces, so it survives the marshalled-env cache.



397
398
399
# File 'lib/rigor/environment/rbs_loader.rb', line 397

def synthesized_stub_types
  names_synthesized_in(SYNTHETIC_STUB_BUFFER)
end

#synthesized_type_namesObject

Every type name Rigor invented to make an otherwise-inert / unbuildable project signature set resolve — both the namespace stubs and the referenced-type stubs. MethodDispatcher resolves a call whose receiver is one of these (and that no real signature answered) to ‘Dynamic`, so the empty stub never mis-fires `call.undefined-method`. Memoised; empty (and cheap) for the common well-formed sig set.



408
409
410
# File 'lib/rigor/environment/rbs_loader.rb', line 408

def synthesized_type_names
  @state[:synthesized_type_names] ||= (synthesized_namespaces + synthesized_stub_types).to_set
end

#uncached_instance_definition(class_name) ⇒ Object

Public uncached accessor used by the cache producer (Cache::RbsInstanceDefinitions). Avoids the ‘private_method_called` round-trip a `loader.send(…)` callsite would require.



562
563
564
# File 'lib/rigor/environment/rbs_loader.rb', line 562

def uncached_instance_definition(class_name)
  build_instance_definition(class_name)
end

#uncached_singleton_definition(class_name) ⇒ Object

Public uncached accessor used by the cache producer.



596
597
598
# File 'lib/rigor/environment/rbs_loader.rb', line 596

def uncached_singleton_definition(class_name)
  build_singleton_definition(class_name)
end