rigortype 0.0.8 → 0.1.0
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 +234 -22
- 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/check_rules.rb +228 -40
- data/lib/rigor/analysis/diagnostic.rb +15 -1
- data/lib/rigor/analysis/runner.rb +199 -4
- data/lib/rigor/builtins/imported_refinements.rb +6 -1
- 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 +15 -51
- data/lib/rigor/cache/rbs_descriptor.rb +55 -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_instance_definitions.rb +79 -0
- data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
- data/lib/rigor/cache/store.rb +81 -15
- data/lib/rigor/cli.rb +45 -7
- data/lib/rigor/configuration/severity_profile.rb +109 -0
- data/lib/rigor/configuration.rb +110 -6
- data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
- data/lib/rigor/environment/rbs_loader.rb +220 -32
- data/lib/rigor/environment.rb +11 -2
- data/lib/rigor/flow_contribution/conflict.rb +81 -0
- data/lib/rigor/flow_contribution/element.rb +53 -0
- data/lib/rigor/flow_contribution/fact.rb +88 -0
- data/lib/rigor/flow_contribution/merge_result.rb +67 -0
- data/lib/rigor/flow_contribution/merger.rb +275 -0
- data/lib/rigor/flow_contribution.rb +179 -0
- data/lib/rigor/inference/block_parameter_binder.rb +15 -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 +110 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +173 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +21 -1
- data/lib/rigor/inference/method_dispatcher.rb +2 -0
- data/lib/rigor/inference/multi_target_binder.rb +2 -0
- data/lib/rigor/inference/narrowing.rb +134 -144
- data/lib/rigor/inference/scope_indexer.rb +75 -1
- data/lib/rigor/inference/statement_evaluator.rb +380 -40
- data/lib/rigor/plugin/access_denied_error.rb +24 -0
- data/lib/rigor/plugin/base.rb +241 -0
- data/lib/rigor/plugin/io_boundary.rb +102 -0
- data/lib/rigor/plugin/load_error.rb +23 -0
- data/lib/rigor/plugin/loader.rb +191 -0
- data/lib/rigor/plugin/manifest.rb +134 -0
- data/lib/rigor/plugin/registry.rb +50 -0
- data/lib/rigor/plugin/services.rb +65 -0
- data/lib/rigor/plugin/trust_policy.rb +99 -0
- data/lib/rigor/plugin.rb +61 -0
- data/lib/rigor/rbs_extended.rb +103 -0
- data/lib/rigor/reflection.rb +2 -2
- 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 +13 -0
- data/sig/rigor/environment.rbs +7 -1
- data/sig/rigor/inference.rbs +1 -0
- data/sig/rigor/rbs_extended.rbs +2 -0
- data/sig/rigor/scope.rbs +1 -0
- data/sig/rigor/type.rbs +7 -0
- data/sig/rigor.rbs +3 -1
- metadata +38 -1
|
@@ -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 ::RBS::BaseError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private_class_method :compute, :type_params_for
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
require_relative "rbs_descriptor"
|
|
4
4
|
|
|
5
5
|
module Rigor
|
|
6
6
|
module Cache
|
|
@@ -12,16 +12,9 @@ module Rigor
|
|
|
12
12
|
# `Marshal`-clean (RBS-native objects carry `RBS::Location`,
|
|
13
13
|
# which lacks `_dump_data`).
|
|
14
14
|
#
|
|
15
|
-
# Cache descriptor
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# upgrade invalidates the table — bundled core + stdlib
|
|
19
|
-
# signatures live inside the gem.
|
|
20
|
-
# - `files`: the digest of every `.rbs` file under the loader's
|
|
21
|
-
# `signature_paths` (project-supplied signatures that the
|
|
22
|
-
# gem's locked version cannot cover).
|
|
23
|
-
# - `configs`: the SHA-256 of the loader's libraries list so
|
|
24
|
-
# adding/removing a stdlib library invalidates.
|
|
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.
|
|
25
18
|
class RbsConstantTable
|
|
26
19
|
PRODUCER_ID = "rbs.constant_type_table"
|
|
27
20
|
|
|
@@ -29,55 +22,26 @@ module Rigor
|
|
|
29
22
|
# @param store [Rigor::Cache::Store]
|
|
30
23
|
# @return [Hash{String => Rigor::Type}]
|
|
31
24
|
def self.fetch(loader:, store:)
|
|
32
|
-
descriptor =
|
|
25
|
+
descriptor = RbsDescriptor.build(loader)
|
|
33
26
|
store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
|
|
34
27
|
compute(loader)
|
|
35
28
|
end
|
|
36
29
|
end
|
|
37
30
|
|
|
38
|
-
def self.build_descriptor(loader)
|
|
39
|
-
Descriptor.new(
|
|
40
|
-
gems: [rbs_gem_entry],
|
|
41
|
-
files: file_entries(loader),
|
|
42
|
-
configs: [libraries_entry(loader)]
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
31
|
def self.compute(loader)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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 ::RBS::BaseError
|
|
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. Analyzer-internal errors propagate.
|
|
50
40
|
end
|
|
41
|
+
table
|
|
51
42
|
end
|
|
52
43
|
|
|
53
|
-
|
|
54
|
-
Descriptor::GemEntry.new(name: "rbs", requirement: ">= 0", locked: ::RBS::VERSION.to_s)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def self.file_entries(loader)
|
|
58
|
-
loader.signature_paths.flat_map do |root|
|
|
59
|
-
next [] unless root.directory?
|
|
60
|
-
|
|
61
|
-
Dir.glob(root.join("**", "*.rbs")).map do |path|
|
|
62
|
-
Descriptor::FileEntry.new(
|
|
63
|
-
path: path,
|
|
64
|
-
comparator: :digest,
|
|
65
|
-
value: Digest::SHA256.file(path).hexdigest
|
|
66
|
-
)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def self.libraries_entry(loader)
|
|
72
|
-
sorted = loader.libraries.map(&:to_s).sort
|
|
73
|
-
Descriptor::ConfigEntry.new(
|
|
74
|
-
key: "rbs.libraries",
|
|
75
|
-
value_hash: Digest::SHA256.hexdigest(sorted.join("\n"))
|
|
76
|
-
)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
private_class_method :build_descriptor, :compute,
|
|
80
|
-
:rbs_gem_entry, :file_entries, :libraries_entry
|
|
44
|
+
private_class_method :compute
|
|
81
45
|
end
|
|
82
46
|
end
|
|
83
47
|
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
require_relative "descriptor"
|
|
6
|
+
|
|
7
|
+
module Rigor
|
|
8
|
+
module Cache
|
|
9
|
+
# Shared descriptor builder for cache producers that depend on the
|
|
10
|
+
# RBS environment (constant table, known-class set, future
|
|
11
|
+
# Marshal-clean reflection artefacts). Every consumer attaches the
|
|
12
|
+
# same three slots, so factoring the construction here keeps the
|
|
13
|
+
# producers small and ensures invalidation behaves identically
|
|
14
|
+
# across them.
|
|
15
|
+
module RbsDescriptor
|
|
16
|
+
# @param loader [Rigor::Environment::RbsLoader]
|
|
17
|
+
# @return [Rigor::Cache::Descriptor]
|
|
18
|
+
def self.build(loader)
|
|
19
|
+
Descriptor.new(
|
|
20
|
+
gems: [rbs_gem_entry],
|
|
21
|
+
files: file_entries(loader),
|
|
22
|
+
configs: [libraries_entry(loader)]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.rbs_gem_entry
|
|
27
|
+
Descriptor::GemEntry.new(name: "rbs", requirement: ">= 0", locked: ::RBS::VERSION.to_s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.file_entries(loader)
|
|
31
|
+
loader.signature_paths.flat_map do |root|
|
|
32
|
+
next [] unless root.directory?
|
|
33
|
+
|
|
34
|
+
Dir.glob(root.join("**", "*.rbs")).map do |path|
|
|
35
|
+
Descriptor::FileEntry.new(
|
|
36
|
+
path: path,
|
|
37
|
+
comparator: :digest,
|
|
38
|
+
value: Digest::SHA256.file(path).hexdigest
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.libraries_entry(loader)
|
|
45
|
+
sorted = loader.libraries.map(&:to_s).sort
|
|
46
|
+
Descriptor::ConfigEntry.new(
|
|
47
|
+
key: "rbs.libraries",
|
|
48
|
+
value_hash: Digest::SHA256.hexdigest(sorted.join("\n"))
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private_class_method :rbs_gem_entry, :file_entries, :libraries_entry
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
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,79 @@
|
|
|
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 full
|
|
9
|
+
# `Hash<String, RBS::Definition>` for instance-side class
|
|
10
|
+
# definitions in the RBS environment, in a single cache
|
|
11
|
+
# entry. Mirrors the {RbsConstantTable} layout.
|
|
12
|
+
#
|
|
13
|
+
# ADR-7 § "Slice 6-D" carry-over and dogfooding feedback:
|
|
14
|
+
# the earlier per-class cache layout (one entry per class,
|
|
15
|
+
# ~1300 files) made warm runs *slower* than `--no-cache`
|
|
16
|
+
# because each `instance_definition` call paid disk-open +
|
|
17
|
+
# `Marshal.load` overhead and the in-memory
|
|
18
|
+
# `RBS::DefinitionBuilder.build_instance` was actually fast
|
|
19
|
+
# given a cached `RBS::Environment`. The single-blob layout
|
|
20
|
+
# collapses that to one `Marshal.load` per process; warm runs
|
|
21
|
+
# now match `--no-cache` timing while preserving the
|
|
22
|
+
# cross-process invalidation story.
|
|
23
|
+
#
|
|
24
|
+
# Marshal-cleanness of `RBS::Definition` is enabled by the
|
|
25
|
+
# v0.0.9 C2 `RBS::Location` patch.
|
|
26
|
+
class RbsInstanceDefinitions
|
|
27
|
+
PRODUCER_ID = "rbs.instance_definitions"
|
|
28
|
+
|
|
29
|
+
# @param loader [Rigor::Environment::RbsLoader]
|
|
30
|
+
# @param store [Rigor::Cache::Store]
|
|
31
|
+
# @return [Hash{String => RBS::Definition}]
|
|
32
|
+
def self.fetch(loader:, store:)
|
|
33
|
+
descriptor = RbsDescriptor.build(loader)
|
|
34
|
+
store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
|
|
35
|
+
compute(loader)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.compute(loader)
|
|
40
|
+
table = {}
|
|
41
|
+
loader.each_known_class_name do |name|
|
|
42
|
+
definition = loader.uncached_instance_definition(name)
|
|
43
|
+
table[name] = definition if definition
|
|
44
|
+
end
|
|
45
|
+
table
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private_class_method :compute
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Singleton-side equivalent of {RbsInstanceDefinitions}.
|
|
52
|
+
# Caches the full `Hash<String, RBS::Definition>` for the
|
|
53
|
+
# singleton class of every RBS-known class.
|
|
54
|
+
class RbsSingletonDefinitions
|
|
55
|
+
PRODUCER_ID = "rbs.singleton_definitions"
|
|
56
|
+
|
|
57
|
+
# @param loader [Rigor::Environment::RbsLoader]
|
|
58
|
+
# @param store [Rigor::Cache::Store]
|
|
59
|
+
# @return [Hash{String => RBS::Definition}]
|
|
60
|
+
def self.fetch(loader:, store:)
|
|
61
|
+
descriptor = RbsDescriptor.build(loader)
|
|
62
|
+
store.fetch_or_compute(producer_id: PRODUCER_ID, params: {}, descriptor: descriptor) do
|
|
63
|
+
compute(loader)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.compute(loader)
|
|
68
|
+
table = {}
|
|
69
|
+
loader.each_known_class_name do |name|
|
|
70
|
+
definition = loader.uncached_singleton_definition(name)
|
|
71
|
+
table[name] = definition if definition
|
|
72
|
+
end
|
|
73
|
+
table
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private_class_method :compute
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
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
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -5,6 +5,8 @@ require "fileutils"
|
|
|
5
5
|
require "json"
|
|
6
6
|
require "securerandom"
|
|
7
7
|
|
|
8
|
+
require_relative "descriptor"
|
|
9
|
+
|
|
8
10
|
module Rigor
|
|
9
11
|
module Cache
|
|
10
12
|
# Filesystem-backed cache store. Schema, layout, file format,
|
|
@@ -30,10 +32,27 @@ module Rigor
|
|
|
30
32
|
|
|
31
33
|
def initialize(root:)
|
|
32
34
|
@root = root.to_s.dup.freeze
|
|
35
|
+
@hits = 0
|
|
36
|
+
@misses = 0
|
|
37
|
+
@writes = 0
|
|
38
|
+
@by_producer = Hash.new { |h, k| h[k] = { hits: 0, misses: 0, writes: 0 } }
|
|
33
39
|
end
|
|
34
40
|
|
|
35
41
|
attr_reader :root
|
|
36
42
|
|
|
43
|
+
# Returns a frozen snapshot of this Store's per-run hit / miss /
|
|
44
|
+
# write counters. The bookkeeping is in-memory only — every new
|
|
45
|
+
# `Store.new` starts at zero — so the counters reflect activity
|
|
46
|
+
# against this specific instance rather than the on-disk cache
|
|
47
|
+
# state. Disk-level state is reported separately by
|
|
48
|
+
# {.disk_inventory}.
|
|
49
|
+
#
|
|
50
|
+
# @return [Hash] `{ hits:, misses:, writes:, by_producer: { id => { hits:, misses:, writes: } } }`
|
|
51
|
+
def stats
|
|
52
|
+
per_producer = @by_producer.transform_values { |counts| counts.dup.freeze }.freeze
|
|
53
|
+
{ hits: @hits, misses: @misses, writes: @writes, by_producer: per_producer }.freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
37
56
|
# Walks the on-disk cache rooted at `root` and reports a
|
|
38
57
|
# producer-level inventory. Used by `rigor check --cache-stats`
|
|
39
58
|
# to surface cache size and per-producer entry counts without
|
|
@@ -84,21 +103,43 @@ module Rigor
|
|
|
84
103
|
# via {Descriptor#cache_key_for}.
|
|
85
104
|
# @param descriptor [Rigor::Cache::Descriptor] the invalidation
|
|
86
105
|
# descriptor for the value being cached.
|
|
87
|
-
# @
|
|
106
|
+
# @param serialize [#call, nil] optional callable that turns the
|
|
107
|
+
# producer's return value into a binary `String`. Defaults to
|
|
108
|
+
# `Marshal.dump(value).b`. Producers whose return values are
|
|
109
|
+
# not `Marshal`-clean (RBS-native objects with `RBS::Location`
|
|
110
|
+
# members, raw `IO`, …) MUST provide a serialiser. The pair
|
|
111
|
+
# `(serialize, deserialize)` MUST round-trip — a producer that
|
|
112
|
+
# reads with one strategy and writes with another corrupts
|
|
113
|
+
# its own cache slice.
|
|
114
|
+
# @param deserialize [#call, nil] optional callable that turns
|
|
115
|
+
# bytes back into the producer's value. Defaults to
|
|
116
|
+
# `Marshal.load`. Any exception (`StandardError`) raised by
|
|
117
|
+
# the deserialiser is treated as a cache miss — the entry is
|
|
118
|
+
# considered corrupt, the producer block reruns, and the
|
|
119
|
+
# next write overwrites it. This is consistent with the
|
|
120
|
+
# fault-tolerance contract for the default `Marshal.load`
|
|
121
|
+
# path.
|
|
122
|
+
# @yieldreturn the value to cache.
|
|
88
123
|
# @return the cached value (loaded from disk on hit; produced by
|
|
89
124
|
# the block on miss).
|
|
90
|
-
def fetch_or_compute(producer_id:, params:, descriptor:,
|
|
125
|
+
def fetch_or_compute(producer_id:, params:, descriptor:,
|
|
126
|
+
serialize: nil, deserialize: nil, &block)
|
|
91
127
|
validate_producer_id!(producer_id)
|
|
92
128
|
ensure_schema_version!
|
|
93
129
|
|
|
94
130
|
key = descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
95
131
|
path = entry_path(producer_id, key)
|
|
96
132
|
|
|
97
|
-
cached = read_entry(path)
|
|
98
|
-
|
|
133
|
+
cached = read_entry(path, deserialize: deserialize)
|
|
134
|
+
unless cached.nil?
|
|
135
|
+
record(:hits, producer_id)
|
|
136
|
+
return cached.value
|
|
137
|
+
end
|
|
99
138
|
|
|
139
|
+
record(:misses, producer_id)
|
|
100
140
|
value = block.call
|
|
101
|
-
write_entry(path, descriptor, value)
|
|
141
|
+
write_entry(path, descriptor, value, serialize: serialize)
|
|
142
|
+
record(:writes, producer_id)
|
|
102
143
|
value
|
|
103
144
|
end
|
|
104
145
|
|
|
@@ -107,6 +148,15 @@ module Rigor
|
|
|
107
148
|
Entry = Data.define(:descriptor_bytes, :value)
|
|
108
149
|
private_constant :Entry
|
|
109
150
|
|
|
151
|
+
def record(counter, producer_id)
|
|
152
|
+
case counter
|
|
153
|
+
when :hits then @hits += 1
|
|
154
|
+
when :misses then @misses += 1
|
|
155
|
+
when :writes then @writes += 1
|
|
156
|
+
end
|
|
157
|
+
@by_producer[producer_id][counter] += 1
|
|
158
|
+
end
|
|
159
|
+
|
|
110
160
|
def validate_producer_id!(producer_id)
|
|
111
161
|
return if producer_id.is_a?(String) && producer_id.match?(VALID_PRODUCER_ID)
|
|
112
162
|
|
|
@@ -121,7 +171,7 @@ module Rigor
|
|
|
121
171
|
# Reads and validates one entry file. Any failure (missing,
|
|
122
172
|
# short, bad magic, bad version, bad checksum, unmarshal-able)
|
|
123
173
|
# returns nil so the caller treats it as a cache miss.
|
|
124
|
-
def read_entry(path)
|
|
174
|
+
def read_entry(path, deserialize: nil)
|
|
125
175
|
return nil unless File.file?(path)
|
|
126
176
|
|
|
127
177
|
bytes = File.binread(path)
|
|
@@ -131,8 +181,8 @@ module Rigor
|
|
|
131
181
|
descriptor_bytes, value_bytes = parse_body(body)
|
|
132
182
|
return nil if descriptor_bytes.nil?
|
|
133
183
|
|
|
134
|
-
value =
|
|
135
|
-
return nil if value.equal?(
|
|
184
|
+
value = safe_load(value_bytes, deserialize)
|
|
185
|
+
return nil if value.equal?(LOAD_FAILED)
|
|
136
186
|
|
|
137
187
|
Entry.new(descriptor_bytes, value)
|
|
138
188
|
end
|
|
@@ -164,20 +214,24 @@ module Rigor
|
|
|
164
214
|
[descriptor_bytes, value_bytes]
|
|
165
215
|
end
|
|
166
216
|
|
|
167
|
-
|
|
168
|
-
private_constant :
|
|
217
|
+
LOAD_FAILED = Object.new.freeze
|
|
218
|
+
private_constant :LOAD_FAILED
|
|
169
219
|
|
|
170
|
-
def
|
|
171
|
-
|
|
220
|
+
def safe_load(bytes, deserialize)
|
|
221
|
+
if deserialize
|
|
222
|
+
deserialize.call(bytes)
|
|
223
|
+
else
|
|
224
|
+
Marshal.load(bytes) # rubocop:disable Security/MarshalLoad
|
|
225
|
+
end
|
|
172
226
|
rescue StandardError
|
|
173
|
-
|
|
227
|
+
LOAD_FAILED
|
|
174
228
|
end
|
|
175
229
|
|
|
176
|
-
def write_entry(path, descriptor, value)
|
|
230
|
+
def write_entry(path, descriptor, value, serialize: nil)
|
|
177
231
|
FileUtils.mkdir_p(File.dirname(path))
|
|
178
232
|
|
|
179
233
|
descriptor_bytes = descriptor.to_canonical_bytes
|
|
180
|
-
value_bytes =
|
|
234
|
+
value_bytes = serialize_value(value, serialize)
|
|
181
235
|
|
|
182
236
|
body = +"".b
|
|
183
237
|
body << HEADER
|
|
@@ -190,6 +244,18 @@ module Rigor
|
|
|
190
244
|
atomically_replace(path, body)
|
|
191
245
|
end
|
|
192
246
|
|
|
247
|
+
def serialize_value(value, serialize)
|
|
248
|
+
return Marshal.dump(value).b if serialize.nil?
|
|
249
|
+
|
|
250
|
+
bytes = serialize.call(value)
|
|
251
|
+
unless bytes.is_a?(String)
|
|
252
|
+
raise TypeError,
|
|
253
|
+
"custom serialize must return a String, got #{bytes.class}"
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
bytes.b
|
|
257
|
+
end
|
|
258
|
+
|
|
193
259
|
def atomically_replace(path, body)
|
|
194
260
|
File.open(path, File::RDWR | File::CREAT, 0o644) do |lock_fd|
|
|
195
261
|
lock_fd.flock(File::LOCK_EX)
|