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.
- checksums.yaml +4 -4
- data/README.md +40 -13
- data/lib/rigor/analysis/fact_store.rb +15 -3
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +387 -12
- data/lib/rigor/analysis/worker_session.rb +327 -0
- data/lib/rigor/builtins/imported_refinements.rb +6 -2
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +40 -7
- data/lib/rigor/cli.rb +52 -2
- data/lib/rigor/configuration.rb +131 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/lockfile_resolver.rb +125 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
- data/lib/rigor/environment/rbs_loader.rb +194 -6
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment.rb +78 -6
- data/lib/rigor/inference/acceptance.rb +35 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
- data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
- data/lib/rigor/inference/expression_typer.rb +12 -2
- data/lib/rigor/inference/macro_block_self_type.rb +96 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
- data/lib/rigor/inference/method_dispatcher.rb +128 -3
- data/lib/rigor/inference/method_parameter_binder.rb +21 -11
- data/lib/rigor/inference/narrowing.rb +127 -8
- data/lib/rigor/inference/synthetic_method.rb +86 -0
- data/lib/rigor/inference/synthetic_method_index.rb +82 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
- data/lib/rigor/plugin/blueprint.rb +60 -0
- data/lib/rigor/plugin/loader.rb +3 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
- data/lib/rigor/plugin/macro/external_file.rb +143 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
- data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
- data/lib/rigor/plugin/macro.rb +31 -0
- data/lib/rigor/plugin/manifest.rb +78 -7
- data/lib/rigor/plugin/registry.rb +32 -2
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/bot.rb +6 -3
- data/lib/rigor/type/combinator.rb +12 -1
- data/lib/rigor/type/integer_range.rb +7 -7
- data/lib/rigor/type/refined.rb +18 -12
- data/lib/rigor/type/top.rb +4 -3
- data/lib/rigor/type_node/generic.rb +7 -1
- data/lib/rigor/type_node/identifier.rb +9 -1
- data/lib/rigor/type_node/string_literal.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +5 -2
- data/sig/rigor/plugin/blueprint.rbs +7 -0
- data/sig/rigor/plugin/manifest.rbs +1 -1
- data/sig/rigor/plugin/registry.rbs +14 -1
- data/sig/rigor.rbs +35 -2
- 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
|
|
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
|
|
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
|
|
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]
|
|
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
|
|
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
|
|
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
|