rigortype 0.0.8 → 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 (42) 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/runner.rb +19 -3
  11. data/lib/rigor/builtins/imported_refinements.rb +6 -1
  12. data/lib/rigor/cache/rbs_class_ancestor_table.rb +63 -0
  13. data/lib/rigor/cache/rbs_class_type_param_names.rb +60 -0
  14. data/lib/rigor/cache/rbs_constant_table.rb +15 -51
  15. data/lib/rigor/cache/rbs_descriptor.rb +53 -0
  16. data/lib/rigor/cache/rbs_environment.rb +52 -0
  17. data/lib/rigor/cache/rbs_environment_marshal_patch.rb +40 -0
  18. data/lib/rigor/cache/rbs_known_class_names.rb +43 -0
  19. data/lib/rigor/cache/store.rb +79 -15
  20. data/lib/rigor/cli.rb +36 -4
  21. data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
  22. data/lib/rigor/environment/rbs_loader.rb +137 -25
  23. data/lib/rigor/environment.rb +11 -2
  24. data/lib/rigor/flow_contribution.rb +128 -0
  25. data/lib/rigor/inference/builtins/encoding_catalog.rb +67 -0
  26. data/lib/rigor/inference/builtins/exception_catalog.rb +92 -0
  27. data/lib/rigor/inference/builtins/proc_catalog.rb +122 -0
  28. data/lib/rigor/inference/builtins/random_catalog.rb +58 -0
  29. data/lib/rigor/inference/builtins/re_catalog.rb +81 -0
  30. data/lib/rigor/inference/builtins/struct_catalog.rb +55 -0
  31. data/lib/rigor/inference/expression_typer.rb +26 -1
  32. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +16 -1
  33. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +87 -0
  34. data/lib/rigor/inference/method_dispatcher.rb +2 -0
  35. data/lib/rigor/inference/narrowing.rb +29 -14
  36. data/lib/rigor/rbs_extended.rb +55 -0
  37. data/lib/rigor/type/combinator.rb +72 -0
  38. data/lib/rigor/type/refined.rb +50 -2
  39. data/lib/rigor/version.rb +1 -1
  40. data/lib/rigor.rb +6 -0
  41. data/sig/rigor.rbs +3 -1
  42. metadata +21 -1
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest"
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
- # - `gems`: the `rbs` gem (with the locked version) so a gem
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 = build_descriptor(loader)
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
- loader.constant_names.each_with_object({}) do |name, table|
48
- translated = loader.constant_type(name)
49
- table[name] = translated unless translated.nil?
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.
50
40
  end
41
+ table
51
42
  end
52
43
 
53
- def self.rbs_gem_entry
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,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
@@ -30,10 +30,27 @@ module Rigor
30
30
 
31
31
  def initialize(root:)
32
32
  @root = root.to_s.dup.freeze
33
+ @hits = 0
34
+ @misses = 0
35
+ @writes = 0
36
+ @by_producer = Hash.new { |h, k| h[k] = { hits: 0, misses: 0, writes: 0 } }
33
37
  end
34
38
 
35
39
  attr_reader :root
36
40
 
41
+ # Returns a frozen snapshot of this Store's per-run hit / miss /
42
+ # write counters. The bookkeeping is in-memory only — every new
43
+ # `Store.new` starts at zero — so the counters reflect activity
44
+ # against this specific instance rather than the on-disk cache
45
+ # state. Disk-level state is reported separately by
46
+ # {.disk_inventory}.
47
+ #
48
+ # @return [Hash] `{ hits:, misses:, writes:, by_producer: { id => { hits:, misses:, writes: } } }`
49
+ def stats
50
+ per_producer = @by_producer.transform_values { |counts| counts.dup.freeze }.freeze
51
+ { hits: @hits, misses: @misses, writes: @writes, by_producer: per_producer }.freeze
52
+ end
53
+
37
54
  # Walks the on-disk cache rooted at `root` and reports a
38
55
  # producer-level inventory. Used by `rigor check --cache-stats`
39
56
  # to surface cache size and per-producer entry counts without
@@ -84,21 +101,43 @@ module Rigor
84
101
  # via {Descriptor#cache_key_for}.
85
102
  # @param descriptor [Rigor::Cache::Descriptor] the invalidation
86
103
  # descriptor for the value being cached.
87
- # @yieldreturn the value to cache (must be `Marshal.dump`-able).
104
+ # @param serialize [#call, nil] optional callable that turns the
105
+ # producer's return value into a binary `String`. Defaults to
106
+ # `Marshal.dump(value).b`. Producers whose return values are
107
+ # not `Marshal`-clean (RBS-native objects with `RBS::Location`
108
+ # members, raw `IO`, …) MUST provide a serialiser. The pair
109
+ # `(serialize, deserialize)` MUST round-trip — a producer that
110
+ # reads with one strategy and writes with another corrupts
111
+ # its own cache slice.
112
+ # @param deserialize [#call, nil] optional callable that turns
113
+ # bytes back into the producer's value. Defaults to
114
+ # `Marshal.load`. Any exception (`StandardError`) raised by
115
+ # the deserialiser is treated as a cache miss — the entry is
116
+ # considered corrupt, the producer block reruns, and the
117
+ # next write overwrites it. This is consistent with the
118
+ # fault-tolerance contract for the default `Marshal.load`
119
+ # path.
120
+ # @yieldreturn the value to cache.
88
121
  # @return the cached value (loaded from disk on hit; produced by
89
122
  # the block on miss).
90
- def fetch_or_compute(producer_id:, params:, descriptor:, &block)
123
+ def fetch_or_compute(producer_id:, params:, descriptor:,
124
+ serialize: nil, deserialize: nil, &block)
91
125
  validate_producer_id!(producer_id)
92
126
  ensure_schema_version!
93
127
 
94
128
  key = descriptor.cache_key_for(producer_id: producer_id, params: params)
95
129
  path = entry_path(producer_id, key)
96
130
 
97
- cached = read_entry(path)
98
- return cached.value unless cached.nil?
131
+ cached = read_entry(path, deserialize: deserialize)
132
+ unless cached.nil?
133
+ record(:hits, producer_id)
134
+ return cached.value
135
+ end
99
136
 
137
+ record(:misses, producer_id)
100
138
  value = block.call
101
- write_entry(path, descriptor, value)
139
+ write_entry(path, descriptor, value, serialize: serialize)
140
+ record(:writes, producer_id)
102
141
  value
103
142
  end
104
143
 
@@ -107,6 +146,15 @@ module Rigor
107
146
  Entry = Data.define(:descriptor_bytes, :value)
108
147
  private_constant :Entry
109
148
 
149
+ def record(counter, producer_id)
150
+ case counter
151
+ when :hits then @hits += 1
152
+ when :misses then @misses += 1
153
+ when :writes then @writes += 1
154
+ end
155
+ @by_producer[producer_id][counter] += 1
156
+ end
157
+
110
158
  def validate_producer_id!(producer_id)
111
159
  return if producer_id.is_a?(String) && producer_id.match?(VALID_PRODUCER_ID)
112
160
 
@@ -121,7 +169,7 @@ module Rigor
121
169
  # Reads and validates one entry file. Any failure (missing,
122
170
  # short, bad magic, bad version, bad checksum, unmarshal-able)
123
171
  # returns nil so the caller treats it as a cache miss.
124
- def read_entry(path)
172
+ def read_entry(path, deserialize: nil)
125
173
  return nil unless File.file?(path)
126
174
 
127
175
  bytes = File.binread(path)
@@ -131,8 +179,8 @@ module Rigor
131
179
  descriptor_bytes, value_bytes = parse_body(body)
132
180
  return nil if descriptor_bytes.nil?
133
181
 
134
- value = safe_marshal_load(value_bytes)
135
- return nil if value.equal?(MARSHAL_LOAD_FAILED)
182
+ value = safe_load(value_bytes, deserialize)
183
+ return nil if value.equal?(LOAD_FAILED)
136
184
 
137
185
  Entry.new(descriptor_bytes, value)
138
186
  end
@@ -164,20 +212,24 @@ module Rigor
164
212
  [descriptor_bytes, value_bytes]
165
213
  end
166
214
 
167
- MARSHAL_LOAD_FAILED = Object.new.freeze
168
- private_constant :MARSHAL_LOAD_FAILED
215
+ LOAD_FAILED = Object.new.freeze
216
+ private_constant :LOAD_FAILED
169
217
 
170
- def safe_marshal_load(bytes)
171
- Marshal.load(bytes) # rubocop:disable Security/MarshalLoad
218
+ def safe_load(bytes, deserialize)
219
+ if deserialize
220
+ deserialize.call(bytes)
221
+ else
222
+ Marshal.load(bytes) # rubocop:disable Security/MarshalLoad
223
+ end
172
224
  rescue StandardError
173
- MARSHAL_LOAD_FAILED
225
+ LOAD_FAILED
174
226
  end
175
227
 
176
- def write_entry(path, descriptor, value)
228
+ def write_entry(path, descriptor, value, serialize: nil)
177
229
  FileUtils.mkdir_p(File.dirname(path))
178
230
 
179
231
  descriptor_bytes = descriptor.to_canonical_bytes
180
- value_bytes = Marshal.dump(value).b
232
+ value_bytes = serialize_value(value, serialize)
181
233
 
182
234
  body = +"".b
183
235
  body << HEADER
@@ -190,6 +242,18 @@ module Rigor
190
242
  atomically_replace(path, body)
191
243
  end
192
244
 
245
+ def serialize_value(value, serialize)
246
+ return Marshal.dump(value).b if serialize.nil?
247
+
248
+ bytes = serialize.call(value)
249
+ unless bytes.is_a?(String)
250
+ raise TypeError,
251
+ "custom serialize must return a String, got #{bytes.class}"
252
+ end
253
+
254
+ bytes.b
255
+ end
256
+
193
257
  def atomically_replace(path, body)
194
258
  File.open(path, File::RDWR | File::CREAT, 0o644) do |lock_fd|
195
259
  lock_fd.flock(File::LOCK_EX)
data/lib/rigor/cli.rb CHANGED
@@ -72,13 +72,19 @@ module Rigor
72
72
 
73
73
  cache_root = ".rigor/cache"
74
74
  handle_clear_cache(cache_root) if options.fetch(:clear_cache)
75
+ cache_store = options.fetch(:no_cache) ? nil : Cache::Store.new(root: cache_root)
75
76
 
76
77
  configuration = Configuration.load(options.fetch(:config))
77
78
  paths = @argv.empty? ? configuration.paths : @argv
78
- result = Analysis::Runner.new(configuration: configuration, explain: options.fetch(:explain)).run(paths)
79
+ runner = Analysis::Runner.new(
80
+ configuration: configuration,
81
+ explain: options.fetch(:explain),
82
+ cache_store: cache_store
83
+ )
84
+ result = runner.run(paths)
79
85
 
80
86
  write_result(result, options.fetch(:format))
81
- write_cache_stats(cache_root) if options.fetch(:cache_stats)
87
+ write_cache_stats(cache_root, runner.cache_store) if options.fetch(:cache_stats)
82
88
  result.success? ? 0 : 1
83
89
  end
84
90
 
@@ -88,7 +94,8 @@ module Rigor
88
94
  format: "text",
89
95
  explain: false,
90
96
  cache_stats: false,
91
- clear_cache: false
97
+ clear_cache: false,
98
+ no_cache: false
92
99
  }
93
100
  parser = OptionParser.new do |opts|
94
101
  opts.banner = "Usage: rigor check [options] [paths]"
@@ -97,6 +104,7 @@ module Rigor
97
104
  opts.on("--explain", "Surface fail-soft fallback events as :info diagnostics") { options[:explain] = true }
98
105
  opts.on("--cache-stats", "Print on-disk cache inventory at end of run") { options[:cache_stats] = true }
99
106
  opts.on("--clear-cache", "Remove the .rigor/cache directory before running") { options[:clear_cache] = true }
107
+ opts.on("--no-cache", "Disable the persistent cache for this run") { options[:no_cache] = true }
100
108
  end
101
109
  parser.parse!(@argv)
102
110
  options
@@ -111,13 +119,18 @@ module Rigor
111
119
  end
112
120
  end
113
121
 
114
- def write_cache_stats(cache_root)
122
+ def write_cache_stats(cache_root, runtime_store)
115
123
  inv = Cache::Store.disk_inventory(root: cache_root)
116
124
 
117
125
  @out.puts("")
118
126
  @out.puts("Cache (root: #{inv.fetch(:root)})")
119
127
  schema = inv.fetch(:schema_version)
120
128
  @out.puts(" schema_version: #{schema.nil? ? 'absent' : schema}")
129
+ write_disk_inventory(inv)
130
+ write_runtime_stats(runtime_store) if runtime_store
131
+ end
132
+
133
+ def write_disk_inventory(inv)
121
134
  if inv.fetch(:total_entries).zero?
122
135
  @out.puts(" (empty)")
123
136
  return
@@ -130,6 +143,25 @@ module Rigor
130
143
  end
131
144
  end
132
145
 
146
+ def write_runtime_stats(store)
147
+ stats = store.stats
148
+ hits = stats.fetch(:hits)
149
+ misses = stats.fetch(:misses)
150
+ writes = stats.fetch(:writes)
151
+ @out.puts(" this run: #{hits} #{plural(hits, 'hit')}, " \
152
+ "#{misses} #{plural(misses, 'miss', 'misses')}, " \
153
+ "#{writes} #{plural(writes, 'write')}")
154
+ stats.fetch(:by_producer).each do |id, counts|
155
+ @out.puts(" #{id}: #{counts.fetch(:hits)} #{plural(counts.fetch(:hits), 'hit')}, " \
156
+ "#{counts.fetch(:misses)} #{plural(counts.fetch(:misses), 'miss', 'misses')}, " \
157
+ "#{counts.fetch(:writes)} #{plural(counts.fetch(:writes), 'write')}")
158
+ end
159
+ end
160
+
161
+ def plural(count, singular, plural = "#{singular}s")
162
+ count == 1 ? singular : plural
163
+ end
164
+
133
165
  def format_bytes(bytes)
134
166
  return "#{bytes} B" if bytes < 1024
135
167
  return format("%.1f KiB", bytes / 1024.0) if bytes < 1024 * 1024
@@ -45,15 +45,28 @@ module Rigor
45
45
  key = normalize_name(class_name)
46
46
  return @ancestor_names_cache[key] if @ancestor_names_cache.key?(key)
47
47
 
48
- definition = loader.instance_definition(key)
49
48
  @ancestor_names_cache[key] =
50
- if definition
51
- definition.ancestors.ancestors.map { |ancestor| normalize_name(ancestor.name.to_s) }.uniq.freeze
49
+ if loader.cache_store
50
+ ancestor_table.fetch(key, [].freeze)
52
51
  else
53
- [].freeze
52
+ compute_ancestor_names(key)
54
53
  end
54
+ end
55
+
56
+ def compute_ancestor_names(key)
57
+ definition = loader.instance_definition(key)
58
+ return [].freeze if definition.nil?
59
+
60
+ definition.ancestors.ancestors.map { |ancestor| normalize_name(ancestor.name.to_s) }.uniq.freeze
55
61
  rescue StandardError
56
- @ancestor_names_cache[key] = [].freeze
62
+ [].freeze
63
+ end
64
+
65
+ def ancestor_table
66
+ @ancestor_table ||= begin
67
+ require_relative "../cache/rbs_class_ancestor_table"
68
+ Cache::RbsClassAncestorTable.fetch(loader: loader, store: loader.cache_store)
69
+ end
57
70
  end
58
71
 
59
72
  def normalize_name(name)