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.
- checksums.yaml +4 -4
- data/README.md +195 -21
- data/data/builtins/ruby_core/encoding.yml +210 -0
- data/data/builtins/ruby_core/exception.yml +641 -0
- data/data/builtins/ruby_core/numeric.yml +3 -2
- data/data/builtins/ruby_core/proc.yml +731 -0
- data/data/builtins/ruby_core/random.yml +166 -0
- data/data/builtins/ruby_core/re.yml +689 -0
- data/data/builtins/ruby_core/struct.yml +449 -0
- data/lib/rigor/analysis/diagnostic.rb +28 -2
- data/lib/rigor/analysis/runner.rb +19 -3
- data/lib/rigor/builtins/imported_refinements.rb +6 -1
- data/lib/rigor/cache/descriptor.rb +278 -0
- data/lib/rigor/cache/rbs_class_ancestor_table.rb +63 -0
- data/lib/rigor/cache/rbs_class_type_param_names.rb +60 -0
- data/lib/rigor/cache/rbs_constant_table.rb +47 -0
- data/lib/rigor/cache/rbs_descriptor.rb +53 -0
- data/lib/rigor/cache/rbs_environment.rb +52 -0
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +40 -0
- data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
- data/lib/rigor/cache/store.rb +325 -0
- data/lib/rigor/cli.rb +88 -7
- data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
- data/lib/rigor/environment/rbs_loader.rb +148 -25
- data/lib/rigor/environment.rb +11 -2
- data/lib/rigor/flow_contribution.rb +128 -0
- data/lib/rigor/inference/builtins/encoding_catalog.rb +67 -0
- data/lib/rigor/inference/builtins/exception_catalog.rb +92 -0
- data/lib/rigor/inference/builtins/proc_catalog.rb +122 -0
- data/lib/rigor/inference/builtins/random_catalog.rb +58 -0
- data/lib/rigor/inference/builtins/re_catalog.rb +81 -0
- data/lib/rigor/inference/builtins/struct_catalog.rb +55 -0
- data/lib/rigor/inference/expression_typer.rb +26 -1
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +29 -14
- data/lib/rigor/rbs_extended.rb +55 -0
- data/lib/rigor/type/combinator.rb +72 -0
- data/lib/rigor/type/refined.rb +50 -2
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +9 -0
- data/sig/rigor.rbs +3 -1
- 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
|