rigortype 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +195 -21
  3. data/data/builtins/ruby_core/encoding.yml +210 -0
  4. data/data/builtins/ruby_core/exception.yml +641 -0
  5. data/data/builtins/ruby_core/numeric.yml +3 -2
  6. data/data/builtins/ruby_core/proc.yml +731 -0
  7. data/data/builtins/ruby_core/random.yml +166 -0
  8. data/data/builtins/ruby_core/re.yml +689 -0
  9. data/data/builtins/ruby_core/struct.yml +449 -0
  10. data/lib/rigor/analysis/diagnostic.rb +28 -2
  11. data/lib/rigor/analysis/runner.rb +19 -3
  12. data/lib/rigor/builtins/imported_refinements.rb +6 -1
  13. data/lib/rigor/cache/descriptor.rb +278 -0
  14. data/lib/rigor/cache/rbs_class_ancestor_table.rb +63 -0
  15. data/lib/rigor/cache/rbs_class_type_param_names.rb +60 -0
  16. data/lib/rigor/cache/rbs_constant_table.rb +47 -0
  17. data/lib/rigor/cache/rbs_descriptor.rb +53 -0
  18. data/lib/rigor/cache/rbs_environment.rb +52 -0
  19. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +40 -0
  20. data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
  21. data/lib/rigor/cache/store.rb +325 -0
  22. data/lib/rigor/cli.rb +88 -7
  23. data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
  24. data/lib/rigor/environment/rbs_loader.rb +148 -25
  25. data/lib/rigor/environment.rb +11 -2
  26. data/lib/rigor/flow_contribution.rb +128 -0
  27. data/lib/rigor/inference/builtins/encoding_catalog.rb +67 -0
  28. data/lib/rigor/inference/builtins/exception_catalog.rb +92 -0
  29. data/lib/rigor/inference/builtins/proc_catalog.rb +122 -0
  30. data/lib/rigor/inference/builtins/random_catalog.rb +58 -0
  31. data/lib/rigor/inference/builtins/re_catalog.rb +81 -0
  32. data/lib/rigor/inference/builtins/struct_catalog.rb +55 -0
  33. data/lib/rigor/inference/expression_typer.rb +26 -1
  34. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
  35. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +87 -0
  36. data/lib/rigor/inference/method_dispatcher.rb +2 -0
  37. data/lib/rigor/inference/narrowing.rb +29 -14
  38. data/lib/rigor/rbs_extended.rb +55 -0
  39. data/lib/rigor/type/combinator.rb +72 -0
  40. data/lib/rigor/type/refined.rb +50 -2
  41. data/lib/rigor/version.rb +1 -1
  42. data/lib/rigor.rb +9 -0
  43. data/sig/rigor.rbs +3 -1
  44. metadata +24 -1
@@ -0,0 +1,278 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+
6
+ module Rigor
7
+ module Cache
8
+ # Cache invalidation descriptor — the typed-slot schema fixed by
9
+ # [`docs/design/20260505-cache-slice-taxonomy.md`](../../../docs/design/20260505-cache-slice-taxonomy.md).
10
+ # Pure value object: no I/O, no global state, fully immutable
11
+ # after construction. The storage layer
12
+ # ([`Rigor::Cache::Store`](store.rb), v0.0.8 slice 2) consumes
13
+ # descriptors but does not extend them.
14
+ #
15
+ # The descriptor has four slots (`files`, `gems`, `plugins`,
16
+ # `configs`); every slot is an array of typed entries; an empty
17
+ # array means "no dependency in this slot". Composition unions
18
+ # by key per slot; conflicts on the comparison fields raise
19
+ # {Conflict}.
20
+ #
21
+ # See ADR-2 § "Registration, Configuration, and Caching" for
22
+ # the design rationale and ADR-6 for the storage backend
23
+ # decisions that consume this schema.
24
+ class Descriptor # rubocop:disable Metrics/ClassLength
25
+ # Bumped on incompatible schema changes. The storage layer
26
+ # mixes this into the cache key, so a bump implicitly
27
+ # invalidates every cached value.
28
+ SCHEMA_VERSION = 1
29
+
30
+ # Per-slot entry value objects. Constructors validate enums /
31
+ # required fields and freeze the resulting struct so no caller
32
+ # can mutate after the entry is in a Descriptor.
33
+
34
+ class FileEntry
35
+ VALID_COMPARATORS = %i[digest mtime exists].freeze
36
+
37
+ attr_reader :path, :comparator, :value
38
+
39
+ def initialize(path:, comparator:, value:)
40
+ unless VALID_COMPARATORS.include?(comparator)
41
+ raise ArgumentError,
42
+ "FileEntry comparator must be one of #{VALID_COMPARATORS.inspect}, got #{comparator.inspect}"
43
+ end
44
+
45
+ @path = path.to_s.dup.freeze
46
+ @comparator = comparator
47
+ @value = value.to_s.dup.freeze
48
+ freeze
49
+ end
50
+
51
+ def to_h
52
+ { "path" => path, "comparator" => comparator.to_s, "value" => value }
53
+ end
54
+
55
+ def ==(other)
56
+ other.is_a?(FileEntry) && other.path == path && other.comparator == comparator && other.value == value
57
+ end
58
+ alias eql? ==
59
+
60
+ def hash
61
+ [self.class, path, comparator, value].hash
62
+ end
63
+ end
64
+
65
+ class GemEntry
66
+ attr_reader :name, :requirement, :locked
67
+
68
+ def initialize(name:, requirement:, locked: nil)
69
+ @name = name.to_s.dup.freeze
70
+ @requirement = requirement.to_s.dup.freeze
71
+ @locked = locked.nil? ? nil : locked.to_s.dup.freeze
72
+ freeze
73
+ end
74
+
75
+ def to_h
76
+ { "name" => name, "requirement" => requirement, "locked" => locked }
77
+ end
78
+
79
+ def ==(other)
80
+ other.is_a?(GemEntry) && other.name == name && other.requirement == requirement && other.locked == locked
81
+ end
82
+ alias eql? ==
83
+
84
+ def hash
85
+ [self.class, name, requirement, locked].hash
86
+ end
87
+ end
88
+
89
+ class PluginEntry
90
+ attr_reader :id, :version, :config_hash
91
+
92
+ def initialize(id:, version:, config_hash: nil)
93
+ @id = id.to_s.dup.freeze
94
+ @version = version.to_s.dup.freeze
95
+ @config_hash = config_hash.nil? ? nil : config_hash.to_s.dup.freeze
96
+ freeze
97
+ end
98
+
99
+ def to_h
100
+ { "id" => id, "version" => version, "config_hash" => config_hash }
101
+ end
102
+
103
+ def ==(other)
104
+ other.is_a?(PluginEntry) &&
105
+ other.id == id && other.version == version && other.config_hash == config_hash
106
+ end
107
+ alias eql? ==
108
+
109
+ def hash
110
+ [self.class, id, version, config_hash].hash
111
+ end
112
+ end
113
+
114
+ class ConfigEntry
115
+ attr_reader :key, :value_hash
116
+
117
+ def initialize(key:, value_hash:)
118
+ @key = key.to_s.dup.freeze
119
+ @value_hash = value_hash.to_s.dup.freeze
120
+ freeze
121
+ end
122
+
123
+ def to_h
124
+ { "key" => key, "value_hash" => value_hash }
125
+ end
126
+
127
+ def ==(other)
128
+ other.is_a?(ConfigEntry) && other.key == key && other.value_hash == value_hash
129
+ end
130
+ alias eql? ==
131
+
132
+ def hash
133
+ [self.class, key, value_hash].hash
134
+ end
135
+ end
136
+
137
+ # Raised when {.compose} encounters incompatible entries
138
+ # under the same key (file digest mismatch, gem-locked
139
+ # disagreement, …). Callers handle the exception by
140
+ # invalidating the cache slice rather than choosing one
141
+ # contribution silently.
142
+ class Conflict < StandardError; end
143
+
144
+ attr_reader :files, :gems, :plugins, :configs
145
+
146
+ def initialize(files: [], gems: [], plugins: [], configs: [])
147
+ @files = files.dup.freeze
148
+ @gems = gems.dup.freeze
149
+ @plugins = plugins.dup.freeze
150
+ @configs = configs.dup.freeze
151
+ freeze
152
+ end
153
+
154
+ # File-comparator strictness ordering. `:digest` is strictest
155
+ # (deterministic across machines); `:mtime` is cheaper but
156
+ # local; `:exists` is the weakest signal. When two
157
+ # contributors disagree on the comparator for the same
158
+ # `path`, the stricter one wins.
159
+ COMPARATOR_STRICTNESS = { digest: 2, mtime: 1, exists: 0 }.freeze
160
+ private_constant :COMPARATOR_STRICTNESS
161
+
162
+ # Composes any number of descriptors into a single descriptor
163
+ # whose slots are the union of the inputs' slots. Conflicts
164
+ # raise {Conflict}; idempotent contributions (same key, same
165
+ # value) collapse to a single entry.
166
+ def self.compose(*descriptors)
167
+ return new if descriptors.empty?
168
+
169
+ files = compose_files(descriptors.flat_map(&:files))
170
+ gems = compose_by_key(descriptors.flat_map(&:gems), :name)
171
+ plugins = compose_by_key(descriptors.flat_map(&:plugins), :id)
172
+ configs = compose_by_key(descriptors.flat_map(&:configs), :key)
173
+ new(files: files, gems: gems, plugins: plugins, configs: configs)
174
+ end
175
+
176
+ # @param producer_id [String]
177
+ # @param params [Hash] inputs the producer was called with
178
+ # @return [String] hex SHA-256 cache key for the value
179
+ def cache_key_for(producer_id:, params: {})
180
+ payload = {
181
+ "schema_version" => SCHEMA_VERSION,
182
+ "producer_id" => producer_id.to_s,
183
+ "params" => self.class.canonicalize_value(params),
184
+ "descriptor" => to_canonical_hash
185
+ }
186
+ Digest::SHA256.hexdigest(JSON.generate(payload))
187
+ end
188
+
189
+ # Canonical UTF-8 JSON serialisation. Slots appear in
190
+ # lexicographic order; entries are sorted by their key field
191
+ # so two equivalent descriptors produce identical bytes.
192
+ def to_canonical_bytes
193
+ JSON.generate(to_canonical_hash).b
194
+ end
195
+
196
+ def to_canonical_hash
197
+ {
198
+ "configs" => sort_entries(configs, "key").map(&:to_h),
199
+ "files" => sort_entries(files, "path").map(&:to_h),
200
+ "gems" => sort_entries(gems, "name").map(&:to_h),
201
+ "plugins" => sort_entries(plugins, "id").map(&:to_h)
202
+ }
203
+ end
204
+
205
+ def ==(other)
206
+ other.is_a?(Descriptor) &&
207
+ to_canonical_bytes == other.to_canonical_bytes
208
+ end
209
+ alias eql? ==
210
+
211
+ def hash
212
+ to_canonical_bytes.hash
213
+ end
214
+
215
+ class << self
216
+ # Recursively coerces a Ruby value into a JSON-canonical
217
+ # structure: hash keys are stringified and sorted; arrays
218
+ # preserve order; symbols stringify; everything else is
219
+ # JSON-renderable.
220
+ def canonicalize_value(value)
221
+ case value
222
+ when Hash
223
+ value.to_a.map { |k, v| [k.to_s, canonicalize_value(v)] }.sort_by(&:first).to_h
224
+ when Array
225
+ value.map { |v| canonicalize_value(v) }
226
+ when Symbol
227
+ value.to_s
228
+ else
229
+ value
230
+ end
231
+ end
232
+ end
233
+
234
+ private
235
+
236
+ def sort_entries(entries, key)
237
+ entries.sort_by { |e| e.to_h.fetch(key).to_s }
238
+ end
239
+
240
+ def self.compose_by_key(entries, key)
241
+ grouped = entries.group_by { |e| e.public_send(key) }
242
+ grouped.map do |_k, group|
243
+ unique = group.uniq
244
+ if unique.size == 1
245
+ unique.first
246
+ else
247
+ raise Conflict,
248
+ "cache descriptor conflict on #{key}=#{group.first.public_send(key).inspect}: " \
249
+ "got #{unique.size} incompatible entries"
250
+ end
251
+ end
252
+ end
253
+ private_class_method :compose_by_key
254
+
255
+ def self.compose_files(entries)
256
+ grouped = entries.group_by(&:path)
257
+ grouped.map do |path, group|
258
+ merge_file_group(path, group)
259
+ end
260
+ end
261
+ private_class_method :compose_files
262
+
263
+ def self.merge_file_group(path, group)
264
+ strictest_rank = group.map { |e| COMPARATOR_STRICTNESS.fetch(e.comparator) }.max
265
+ strictest = group.select { |e| COMPARATOR_STRICTNESS.fetch(e.comparator) == strictest_rank }
266
+ values = strictest.map(&:value).uniq
267
+ unless values.size == 1
268
+ raise Conflict,
269
+ "cache descriptor conflict on file=#{path.inspect}: " \
270
+ "got #{values.size} disagreeing values under the stricter comparator"
271
+ end
272
+
273
+ strictest.first
274
+ end
275
+ private_class_method :merge_file_group
276
+ end
277
+ end
278
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rbs_descriptor"
4
+
5
+ module Rigor
6
+ module Cache
7
+ # Cache producer that materialises the RBS-declared ancestor
8
+ # chain of every loaded class / module into a Marshal-clean
9
+ # `Hash<String, Array<String>>`. Ancestor names are top-level-
10
+ # stripped (e.g. `"Integer"` not `"::Integer"`) to match
11
+ # `Environment::RbsHierarchy#normalize_name`.
12
+ #
13
+ # The hierarchy is the substrate behind every `class_ordering`
14
+ # query, which is itself a hot path on the dispatcher (overload
15
+ # selection, narrowing, etc.). Building one ancestor chain
16
+ # requires a full `RBS::DefinitionBuilder#build_instance` over
17
+ # that class — a cold-cost dominated by RBS's own resolution
18
+ # work. Caching the table lets a warm process skip the build
19
+ # entirely and pay only a `Marshal.load` of the resulting
20
+ # hash.
21
+ #
22
+ # Cache descriptor shape is shared with every other cache
23
+ # producer that depends on the RBS environment — see
24
+ # {RbsDescriptor.build}.
25
+ class RbsClassAncestorTable
26
+ PRODUCER_ID = "rbs.class_ancestor_table"
27
+
28
+ # @param loader [Rigor::Environment::RbsLoader]
29
+ # @param store [Rigor::Cache::Store]
30
+ # @return [Hash{String => Array<String>}]
31
+ def self.fetch(loader:, store:)
32
+ descriptor = RbsDescriptor.build(loader)
33
+ store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
34
+ compute(loader)
35
+ end
36
+ end
37
+
38
+ def self.compute(loader)
39
+ table = {}
40
+ loader.each_known_class_name do |name|
41
+ key = name.delete_prefix("::")
42
+ ancestors = ancestors_for(loader, key)
43
+ table[key] = ancestors unless ancestors.nil?
44
+ end
45
+ table
46
+ end
47
+
48
+ def self.ancestors_for(loader, class_name)
49
+ definition = loader.instance_definition(class_name)
50
+ return nil if definition.nil?
51
+
52
+ definition.ancestors.ancestors
53
+ .map { |ancestor| ancestor.name.to_s.delete_prefix("::") }
54
+ .uniq
55
+ .freeze
56
+ rescue StandardError
57
+ nil
58
+ end
59
+
60
+ private_class_method :compute, :ancestors_for
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rbs_descriptor"
4
+
5
+ module Rigor
6
+ module Cache
7
+ # Cache producer that materialises every loaded class's
8
+ # RBS-declared type-parameter names as a Marshal-clean
9
+ # `Hash<String, Array<Symbol>>` keyed by top-level-stripped
10
+ # class name (e.g. `"Array"` → `[:Elem]`, `"Hash"` →
11
+ # `[:K, :V]`). Producer id `"rbs.class_type_param_names"`.
12
+ #
13
+ # The dispatcher reads type-parameter names every time it
14
+ # builds a substitution map from a receiver's `type_args`
15
+ # into a method's return type — it is one of the hottest
16
+ # reflection lookups during analysis. Building one entry
17
+ # requires a full `RBS::DefinitionBuilder#build_instance`
18
+ # over that class, the same expensive operation
19
+ # {RbsClassAncestorTable} caches; the two producers share
20
+ # the build cost when populated together.
21
+ #
22
+ # Cache descriptor shape is shared with every other cache
23
+ # producer that depends on the RBS environment — see
24
+ # {RbsDescriptor.build}.
25
+ class RbsClassTypeParamNames
26
+ PRODUCER_ID = "rbs.class_type_param_names"
27
+
28
+ # @param loader [Rigor::Environment::RbsLoader]
29
+ # @param store [Rigor::Cache::Store]
30
+ # @return [Hash{String => Array<Symbol>}]
31
+ def self.fetch(loader:, store:)
32
+ descriptor = RbsDescriptor.build(loader)
33
+ store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
34
+ compute(loader)
35
+ end
36
+ end
37
+
38
+ def self.compute(loader)
39
+ table = {}
40
+ loader.each_known_class_name do |name|
41
+ key = name.delete_prefix("::")
42
+ params = type_params_for(loader, key)
43
+ table[key] = params unless params.nil?
44
+ end
45
+ table
46
+ end
47
+
48
+ def self.type_params_for(loader, class_name)
49
+ definition = loader.instance_definition(class_name)
50
+ return nil if definition.nil?
51
+
52
+ definition.type_params.dup.freeze
53
+ rescue StandardError
54
+ nil
55
+ end
56
+
57
+ private_class_method :compute, :type_params_for
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rbs_descriptor"
4
+
5
+ module Rigor
6
+ module Cache
7
+ # Cache producer that materialises every RBS-declared constant
8
+ # to its translated `Rigor::Type` form and stores the result as
9
+ # a `Hash<String, Rigor::Type>` keyed by canonical constant name.
10
+ # This is the v0.0.8 first cached producer per ADR-6 § 7; it
11
+ # caches a post-translation artefact so the cache value is
12
+ # `Marshal`-clean (RBS-native objects carry `RBS::Location`,
13
+ # which lacks `_dump_data`).
14
+ #
15
+ # Cache descriptor shape is shared with every other cache
16
+ # producer that depends on the RBS environment — see
17
+ # {RbsDescriptor.build} for the slot definitions.
18
+ class RbsConstantTable
19
+ PRODUCER_ID = "rbs.constant_type_table"
20
+
21
+ # @param loader [Rigor::Environment::RbsLoader]
22
+ # @param store [Rigor::Cache::Store]
23
+ # @return [Hash{String => Rigor::Type}]
24
+ def self.fetch(loader:, store:)
25
+ descriptor = RbsDescriptor.build(loader)
26
+ store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
27
+ compute(loader)
28
+ end
29
+ end
30
+
31
+ def self.compute(loader)
32
+ table = {}
33
+ loader.each_constant_decl do |name, entry|
34
+ translated = Inference::RbsTypeTranslator.translate(entry.decl.type)
35
+ table[name] = translated unless translated.is_a?(Type::Bot)
36
+ rescue StandardError
37
+ # Skip entries whose RBS type fails to translate; the cache
38
+ # stays robust to a broken signature rather than corrupting
39
+ # the whole table.
40
+ end
41
+ table
42
+ end
43
+
44
+ private_class_method :compute
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module Rigor
6
+ module Cache
7
+ # Shared descriptor builder for cache producers that depend on the
8
+ # RBS environment (constant table, known-class set, future
9
+ # Marshal-clean reflection artefacts). Every consumer attaches the
10
+ # same three slots, so factoring the construction here keeps the
11
+ # producers small and ensures invalidation behaves identically
12
+ # across them.
13
+ module RbsDescriptor
14
+ # @param loader [Rigor::Environment::RbsLoader]
15
+ # @return [Rigor::Cache::Descriptor]
16
+ def self.build(loader)
17
+ Descriptor.new(
18
+ gems: [rbs_gem_entry],
19
+ files: file_entries(loader),
20
+ configs: [libraries_entry(loader)]
21
+ )
22
+ end
23
+
24
+ def self.rbs_gem_entry
25
+ Descriptor::GemEntry.new(name: "rbs", requirement: ">= 0", locked: ::RBS::VERSION.to_s)
26
+ end
27
+
28
+ def self.file_entries(loader)
29
+ loader.signature_paths.flat_map do |root|
30
+ next [] unless root.directory?
31
+
32
+ Dir.glob(root.join("**", "*.rbs")).map do |path|
33
+ Descriptor::FileEntry.new(
34
+ path: path,
35
+ comparator: :digest,
36
+ value: Digest::SHA256.file(path).hexdigest
37
+ )
38
+ end
39
+ end
40
+ end
41
+
42
+ def self.libraries_entry(loader)
43
+ sorted = loader.libraries.map(&:to_s).sort
44
+ Descriptor::ConfigEntry.new(
45
+ key: "rbs.libraries",
46
+ value_hash: Digest::SHA256.hexdigest(sorted.join("\n"))
47
+ )
48
+ end
49
+
50
+ private_class_method :rbs_gem_entry, :file_entries, :libraries_entry
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rbs_descriptor"
4
+ require_relative "rbs_environment_marshal_patch"
5
+
6
+ module Rigor
7
+ module Cache
8
+ # Cache producer that materialises the entire
9
+ # `RBS::Environment` (the loader's `build_env` result) and
10
+ # round-trips it through `Marshal` against the patched
11
+ # `RBS::Location` (see {RbsEnvironmentMarshalPatch}).
12
+ #
13
+ # Cold runs pay the full
14
+ # `RBS::EnvironmentLoader#load + RBS::Environment.from_loader
15
+ # + resolve_type_names` cost once; warm runs (and a separate
16
+ # loader sharing the same Store) load the marshalled blob and
17
+ # skip the parse / resolve stages entirely. The
18
+ # `RbsConstantTable`, `RbsKnownClassNames`,
19
+ # `RbsClassAncestorTable`, and `RbsClassTypeParamNames`
20
+ # caches still live alongside this producer — their cached
21
+ # values are reached without re-touching env, but when an
22
+ # uncached lookup happens (`instance_method`,
23
+ # `singleton_method`, …) the env produced here is what
24
+ # answers it.
25
+ #
26
+ # Cache descriptor shape is shared with every other cache
27
+ # producer that depends on the RBS environment — see
28
+ # {RbsDescriptor.build}.
29
+ class RbsEnvironment
30
+ PRODUCER_ID = "rbs.environment"
31
+
32
+ # @param loader [Rigor::Environment::RbsLoader]
33
+ # @param store [Rigor::Cache::Store]
34
+ # @return [::RBS::Environment]
35
+ def self.fetch(loader:, store:)
36
+ descriptor = RbsDescriptor.build(loader)
37
+ store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
38
+ compute(loader)
39
+ end
40
+ end
41
+
42
+ def self.compute(loader)
43
+ Rigor::Environment::RbsLoader.build_env_for(
44
+ libraries: loader.libraries,
45
+ signature_paths: loader.signature_paths
46
+ )
47
+ end
48
+
49
+ private_class_method :compute
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbs"
4
+
5
+ # Adds `_dump` / `_load` to {RBS::Location} so an
6
+ # `RBS::Environment` (and its transitive AST nodes, all of
7
+ # which carry Locations) round-trips through `Marshal`. The
8
+ # rbs gem's C-extension `RBS::Location` ships without the
9
+ # Marshal hooks; until rbs grows them upstream this patch is
10
+ # the minimal monkey-patch the v0.0.9 RBS::Environment cache
11
+ # relies on.
12
+ #
13
+ # Patch policy (purely additive):
14
+ #
15
+ # - `_dump` returns an empty string. The cached env loses
16
+ # per-node source-position info, but Rigor does not consult
17
+ # `RBS::Location` from any analysis code path (every
18
+ # diagnostic uses Prism's own location), so the loss is
19
+ # inert in practice.
20
+ # - `_load` reconstructs a sentinel Location backed by an
21
+ # empty `<cached>` Buffer. Code paths that DID consult
22
+ # Location after a cache hit see a benign zero-range value
23
+ # rather than crashing.
24
+ #
25
+ # Idempotent: the guard checks `method_defined?(:_dump)` so
26
+ # requiring this file twice (or against an upstream rbs that
27
+ # adds Marshal hooks itself) is a no-op.
28
+ module RBS
29
+ class Location
30
+ unless method_defined?(:_dump)
31
+ def _dump(_)
32
+ ""
33
+ end
34
+
35
+ def self._load(_)
36
+ new(buffer: ::RBS::Buffer.new(name: "<cached>", content: ""), start_pos: 0, end_pos: 0)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rbs_descriptor"
4
+
5
+ module Rigor
6
+ module Cache
7
+ # Cache producer that materialises the set of every RBS-declared
8
+ # class / module / alias name (top-level prefixed, e.g.
9
+ # `"::Math"`) currently loaded into the environment. Marshal-
10
+ # clean — the cache value is a `Set<String>`.
11
+ #
12
+ # The set lets `RbsLoader#class_known?` answer point lookups in
13
+ # O(1) without re-parsing the name on each call, and lets a warm
14
+ # process skip the env walk entirely (the env still has to be
15
+ # built to enumerate decls on cold misses; subsequent processes
16
+ # sharing the Store load the set straight from disk).
17
+ #
18
+ # Cache descriptor shape is shared with {RbsConstantTable} via
19
+ # {RbsDescriptor.build}; a single signature change or rbs gem
20
+ # bump invalidates both producers in lockstep.
21
+ class RbsKnownClassNames
22
+ PRODUCER_ID = "rbs.known_class_names"
23
+
24
+ # @param loader [Rigor::Environment::RbsLoader]
25
+ # @param store [Rigor::Cache::Store]
26
+ # @return [Set<String>]
27
+ def self.fetch(loader:, store:)
28
+ descriptor = RbsDescriptor.build(loader)
29
+ store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
30
+ compute(loader)
31
+ end
32
+ end
33
+
34
+ def self.compute(loader)
35
+ names = Set.new
36
+ loader.each_known_class_name { |name| names << name }
37
+ names
38
+ end
39
+
40
+ private_class_method :compute
41
+ end
42
+ end
43
+ end