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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(libraries: [], signature_paths: [], cache_store: nil) ⇒ 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.



117
118
119
120
121
122
123
124
125
126
# File 'lib/rigor/environment/rbs_loader.rb', line 117

def initialize(libraries: [], signature_paths: [], cache_store: nil)
  @libraries = libraries.map(&:to_s).freeze
  @signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
  @cache_store = cache_store
  @state = { env: nil, builder: nil }
  @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.



98
99
100
# File 'lib/rigor/environment/rbs_loader.rb', line 98

def cache_store
  @cache_store
end

#librariesObject (readonly)

Returns the value of attribute libraries.



98
99
100
# File 'lib/rigor/environment/rbs_loader.rb', line 98

def libraries
  @libraries
end

#signature_pathsObject (readonly)

Returns the value of attribute signature_paths.



98
99
100
# File 'lib/rigor/environment/rbs_loader.rb', line 98

def signature_paths
  @signature_paths
end

Class Method Details

.build_env_for(libraries:, signature_paths:) ⇒ 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.



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/rigor/environment/rbs_loader.rb', line 59

def build_env_for(libraries:, signature_paths:)
  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
  RBS::Environment.from_loader(rbs_loader).resolve_type_names
end

.defaultObject



34
35
36
# File 'lib/rigor/environment/rbs_loader.rb', line 34

def default
  @default ||= new.freeze
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.



42
43
44
# File 'lib/rigor/environment/rbs_loader.rb', line 42

def reset_default!
  @default = nil
end

.vendored_gem_sig_pathsObject



89
90
91
92
93
94
95
# File 'lib/rigor/environment/rbs_loader.rb', line 89

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.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/rigor/environment/rbs_loader.rb', line 178

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)


139
140
141
142
143
144
145
146
147
148
# File 'lib/rigor/environment/rbs_loader.rb', line 139

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



307
308
309
# File 'lib/rigor/environment/rbs_loader.rb', line 307

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.



295
296
297
298
299
300
301
302
303
304
305
# File 'lib/rigor/environment/rbs_loader.rb', line 295

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.



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

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.



356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/rigor/environment/rbs_loader.rb', line 356

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_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).



329
330
331
332
333
334
335
336
337
338
# File 'lib/rigor/environment/rbs_loader.rb', line 329

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?.



154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/rigor/environment/rbs_loader.rb', line 154

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).



211
212
213
214
215
216
217
218
219
220
# File 'lib/rigor/environment/rbs_loader.rb', line 211

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)


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

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.



388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/rigor/environment/rbs_loader.rb', line 388

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

#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.



414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'lib/rigor/environment/rbs_loader.rb', line 414

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).



248
249
250
251
252
253
254
255
256
257
# File 'lib/rigor/environment/rbs_loader.rb', line 248

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.



269
270
271
272
273
274
# File 'lib/rigor/environment/rbs_loader.rb', line 269

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

  definition.methods[method_name.to_sym]
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.



226
227
228
# File 'lib/rigor/environment/rbs_loader.rb', line 226

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.



260
261
262
# File 'lib/rigor/environment/rbs_loader.rb', line 260

def uncached_singleton_definition(class_name)
  build_singleton_definition(class_name)
end