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.



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

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.



67
68
69
# File 'lib/rigor/environment/rbs_loader.rb', line 67

def cache_store
  @cache_store
end

#librariesObject (readonly)

Returns the value of attribute libraries.



67
68
69
# File 'lib/rigor/environment/rbs_loader.rb', line 67

def libraries
  @libraries
end

#signature_pathsObject (readonly)

Returns the value of attribute signature_paths.



67
68
69
# File 'lib/rigor/environment/rbs_loader.rb', line 67

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.



52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/rigor/environment/rbs_loader.rb', line 52

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

Instance Method Details

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


108
109
110
111
112
113
114
115
116
117
# File 'lib/rigor/environment/rbs_loader.rb', line 108

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



206
207
208
# File 'lib/rigor/environment/rbs_loader.rb', line 206

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.



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

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.



215
216
217
218
219
# File 'lib/rigor/environment/rbs_loader.rb', line 215

def constant_names
  env.constant_decls.keys.map(&:to_s)
rescue StandardError
  []
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.



252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/rigor/environment/rbs_loader.rb', line 252

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



226
227
228
229
230
231
232
233
234
# File 'lib/rigor/environment/rbs_loader.rb', line 226

def each_constant_decl
  return enum_for(:each_constant_decl) unless block_given?

  env.constant_decls.each do |rbs_name, entry|
    yield rbs_name.to_s, entry
  end
rescue StandardError
  # fail-soft: a broken 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?.



123
124
125
126
127
128
129
130
# File 'lib/rigor/environment/rbs_loader.rb', line 123

def each_known_class_name
  return enum_for(:each_known_class_name) unless block_given?

  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 StandardError
  # fail-soft: a broken environment yields no names.
end

#instance_definition(class_name) ⇒ RBS::Definition?

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

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



136
137
138
139
140
141
# File 'lib/rigor/environment/rbs_loader.rb', line 136

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] = build_instance_definition(class_name)
end

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

Returns:

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


144
145
146
147
148
149
# File 'lib/rigor/environment/rbs_loader.rb', line 144

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

  definition.methods[method_name.to_sym]
end

#singleton_definition(class_name) ⇒ RBS::Definition?

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

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



156
157
158
159
160
161
# File 'lib/rigor/environment/rbs_loader.rb', line 156

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] = build_singleton_definition(class_name)
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.



168
169
170
171
172
173
# File 'lib/rigor/environment/rbs_loader.rb', line 168

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

  definition.methods[method_name.to_sym]
end