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.
- 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/runner.rb +19 -3
- 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 +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 +79 -15
- data/lib/rigor/cli.rb +36 -4
- data/lib/rigor/environment/rbs_hierarchy.rb +18 -5
- data/lib/rigor/environment/rbs_loader.rb +137 -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 +6 -0
- data/sig/rigor.rbs +3 -1
- metadata +21 -1
|
@@ -42,9 +42,29 @@ module Rigor
|
|
|
42
42
|
def reset_default!
|
|
43
43
|
@default = nil
|
|
44
44
|
end
|
|
45
|
+
|
|
46
|
+
# Builds an `RBS::Environment` from explicit `libraries` and
|
|
47
|
+
# `signature_paths`. Stateless surface so the v0.0.9
|
|
48
|
+
# {Cache::RbsEnvironment} producer can build an env on cache
|
|
49
|
+
# miss without holding a loader instance, and the
|
|
50
|
+
# instance-side {#build_env} delegates here so the
|
|
51
|
+
# implementation stays single-rooted.
|
|
52
|
+
def build_env_for(libraries:, signature_paths:)
|
|
53
|
+
rbs_loader = RBS::EnvironmentLoader.new
|
|
54
|
+
libraries.each do |library|
|
|
55
|
+
next unless rbs_loader.has_library?(library: library, version: nil)
|
|
56
|
+
|
|
57
|
+
rbs_loader.add(library: library, version: nil)
|
|
58
|
+
end
|
|
59
|
+
signature_paths.each do |path|
|
|
60
|
+
path = Pathname(path) unless path.is_a?(Pathname)
|
|
61
|
+
rbs_loader.add(path: path) if path.directory?
|
|
62
|
+
end
|
|
63
|
+
RBS::Environment.from_loader(rbs_loader).resolve_type_names
|
|
64
|
+
end
|
|
45
65
|
end
|
|
46
66
|
|
|
47
|
-
attr_reader :libraries, :signature_paths
|
|
67
|
+
attr_reader :libraries, :signature_paths, :cache_store
|
|
48
68
|
|
|
49
69
|
# @param libraries [Array<String, Symbol>] stdlib library names to
|
|
50
70
|
# load on top of core (e.g., `["pathname", "json"]`). Empty by
|
|
@@ -57,9 +77,16 @@ module Rigor
|
|
|
57
77
|
# `sig/` tree). Non-existent or non-directory paths are filtered
|
|
58
78
|
# out at build time so the loader stays robust to fixtures and
|
|
59
79
|
# bare repositories.
|
|
60
|
-
|
|
80
|
+
# @param cache_store [Rigor::Cache::Store, nil] the persistent
|
|
81
|
+
# cache the loader consults for translated constant lookups
|
|
82
|
+
# (and, in later v0.0.9 slices, other Marshal-clean
|
|
83
|
+
# reflection artefacts). Pass `nil` (the default) to skip
|
|
84
|
+
# the cache entirely; the runner threads its own Store
|
|
85
|
+
# through here when caching is enabled.
|
|
86
|
+
def initialize(libraries: [], signature_paths: [], cache_store: nil)
|
|
61
87
|
@libraries = libraries.map(&:to_s).freeze
|
|
62
88
|
@signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
|
|
89
|
+
@cache_store = cache_store
|
|
63
90
|
@state = { env: nil, builder: nil }
|
|
64
91
|
@instance_definition_cache = {}
|
|
65
92
|
@singleton_definition_cache = {}
|
|
@@ -71,11 +98,35 @@ module Rigor
|
|
|
71
98
|
# name is loaded. Accepts unprefixed or top-level-prefixed names
|
|
72
99
|
# ("Integer" or "::Integer"). Memoized per-name (positive and
|
|
73
100
|
# negative results both cache).
|
|
101
|
+
#
|
|
102
|
+
# When `cache_store` is set, the loader fetches the entire set of
|
|
103
|
+
# known class / module / alias names once (per process) through
|
|
104
|
+
# {Cache::RbsKnownClassNames.fetch} and answers `class_known?`
|
|
105
|
+
# from the in-memory Set. Cold runs pay a single env walk and
|
|
106
|
+
# persist the result; warm runs (and a separate loader sharing
|
|
107
|
+
# the same Store) skip the env walk entirely.
|
|
74
108
|
def class_known?(name)
|
|
75
109
|
key = name.to_s
|
|
76
110
|
return @class_known_cache[key] if @class_known_cache.key?(key)
|
|
77
111
|
|
|
78
|
-
@class_known_cache[key] =
|
|
112
|
+
@class_known_cache[key] = if cache_store
|
|
113
|
+
cached_class_known(name)
|
|
114
|
+
else
|
|
115
|
+
compute_class_known(name)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Yields every known class / module / alias name (top-level
|
|
120
|
+
# prefixed) currently loaded into the environment. The cache
|
|
121
|
+
# producer that materialises the known-name set uses this so
|
|
122
|
+
# it never recurses back through {#class_known?}.
|
|
123
|
+
def each_known_class_name
|
|
124
|
+
return enum_for(:each_known_class_name) unless block_given?
|
|
125
|
+
|
|
126
|
+
env.class_decls.each_key { |rbs_name| yield rbs_name.to_s }
|
|
127
|
+
env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s }
|
|
128
|
+
rescue StandardError
|
|
129
|
+
# fail-soft: a broken environment yields no names.
|
|
79
130
|
end
|
|
80
131
|
|
|
81
132
|
# @return [RBS::Definition, nil] the resolved instance definition
|
|
@@ -133,7 +184,19 @@ module Rigor
|
|
|
133
184
|
# names (the loader stays fail-soft). NOTE: in the `rbs` gem,
|
|
134
185
|
# `RBS::Definition#type_params` returns `Array<Symbol>` directly,
|
|
135
186
|
# not the AST `TypeParam` object (those live on the AST level).
|
|
187
|
+
#
|
|
188
|
+
# When `cache_store` is set, the loader fetches the entire
|
|
189
|
+
# type-parameter-name table once (per process) through
|
|
190
|
+
# {Cache::RbsClassTypeParamNames.fetch} and answers point
|
|
191
|
+
# lookups from it. Cold runs build the table once and persist
|
|
192
|
+
# it; warm runs (and a separate loader sharing the same Store)
|
|
193
|
+
# skip the env walk entirely.
|
|
136
194
|
def class_type_param_names(class_name)
|
|
195
|
+
if cache_store
|
|
196
|
+
key = class_name.to_s.delete_prefix("::")
|
|
197
|
+
return type_param_names_table.fetch(key, []).dup
|
|
198
|
+
end
|
|
199
|
+
|
|
137
200
|
definition = instance_definition(class_name)
|
|
138
201
|
return [] unless definition
|
|
139
202
|
|
|
@@ -155,6 +218,21 @@ module Rigor
|
|
|
155
218
|
[]
|
|
156
219
|
end
|
|
157
220
|
|
|
221
|
+
# Yields `(name, entry)` for every RBS constant declaration
|
|
222
|
+
# currently loaded into the environment. The cache producer
|
|
223
|
+
# uses this to materialise the constant-type table without
|
|
224
|
+
# going back through {#constant_type} (which would recurse
|
|
225
|
+
# back into the cache when `cache_store` is set).
|
|
226
|
+
def each_constant_decl
|
|
227
|
+
return enum_for(:each_constant_decl) unless block_given?
|
|
228
|
+
|
|
229
|
+
env.constant_decls.each do |rbs_name, entry|
|
|
230
|
+
yield rbs_name.to_s, entry
|
|
231
|
+
end
|
|
232
|
+
rescue StandardError
|
|
233
|
+
# fail-soft: a broken environment yields no entries.
|
|
234
|
+
end
|
|
235
|
+
|
|
158
236
|
# Slice A constant-value lookup. Returns the translated
|
|
159
237
|
# `Rigor::Type` for a non-class constant declaration
|
|
160
238
|
# (`BUCKETS: Array[Symbol]`, `DEFAULT_PATH: String`, ...) or
|
|
@@ -164,23 +242,73 @@ module Rigor
|
|
|
164
242
|
# nil; the loader does NOT consult the class declarations
|
|
165
243
|
# here — class objects are still resolved through
|
|
166
244
|
# {#class_known?} and `Environment#singleton_for_name`.
|
|
245
|
+
#
|
|
246
|
+
# When `cache_store` is set, the loader fetches the entire
|
|
247
|
+
# translated constant table once (per process) through
|
|
248
|
+
# {Cache::RbsConstantTable.fetch} and answers point lookups
|
|
249
|
+
# from it. Cold runs pay the translation cost up-front and
|
|
250
|
+
# write the result to disk; warm runs skip the translation
|
|
251
|
+
# entirely and pay only a `Marshal.load` of the table.
|
|
167
252
|
def constant_type(name)
|
|
168
253
|
rbs_name = parse_type_name(name)
|
|
169
254
|
return nil unless rbs_name
|
|
170
255
|
|
|
256
|
+
if cache_store
|
|
257
|
+
constant_type_table[rbs_name.to_s]
|
|
258
|
+
else
|
|
259
|
+
translate_constant_decl(rbs_name)
|
|
260
|
+
end
|
|
261
|
+
rescue StandardError
|
|
262
|
+
nil
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
private
|
|
266
|
+
|
|
267
|
+
def constant_type_table
|
|
268
|
+
@constant_type_table ||= begin
|
|
269
|
+
require_relative "../cache/rbs_constant_table"
|
|
270
|
+
Cache::RbsConstantTable.fetch(loader: self, store: cache_store)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def known_class_names_set
|
|
275
|
+
@known_class_names_set ||= begin
|
|
276
|
+
require_relative "../cache/rbs_known_class_names"
|
|
277
|
+
Cache::RbsKnownClassNames.fetch(loader: self, store: cache_store)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def type_param_names_table
|
|
282
|
+
@type_param_names_table ||= begin
|
|
283
|
+
require_relative "../cache/rbs_class_type_param_names"
|
|
284
|
+
Cache::RbsClassTypeParamNames.fetch(loader: self, store: cache_store)
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def cached_class_known(name)
|
|
289
|
+
rbs_name = parse_type_name(name)
|
|
290
|
+
return false unless rbs_name
|
|
291
|
+
|
|
292
|
+
known_class_names_set.include?(rbs_name.to_s)
|
|
293
|
+
rescue StandardError
|
|
294
|
+
false
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def translate_constant_decl(rbs_name)
|
|
171
298
|
entry = env.constant_decls[rbs_name]
|
|
172
299
|
return nil unless entry
|
|
173
300
|
|
|
174
301
|
translated = Inference::RbsTypeTranslator.translate(entry.decl.type)
|
|
175
302
|
translated unless translated.is_a?(Type::Bot)
|
|
176
|
-
rescue StandardError
|
|
177
|
-
nil
|
|
178
303
|
end
|
|
179
304
|
|
|
180
|
-
private
|
|
181
|
-
|
|
182
305
|
def env
|
|
183
|
-
@state[:env] ||= build_env
|
|
306
|
+
@state[:env] ||= cache_store ? cached_env : build_env
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def cached_env
|
|
310
|
+
require_relative "../cache/rbs_environment"
|
|
311
|
+
Cache::RbsEnvironment.fetch(loader: self, store: cache_store)
|
|
184
312
|
end
|
|
185
313
|
|
|
186
314
|
def builder
|
|
@@ -188,23 +316,7 @@ module Rigor
|
|
|
188
316
|
end
|
|
189
317
|
|
|
190
318
|
def build_env
|
|
191
|
-
|
|
192
|
-
@libraries.each do |library|
|
|
193
|
-
# Phase 2a deliberately fails-soft on unknown stdlib libraries
|
|
194
|
-
# so a stale `.rigor.yml` (or future config plumbing) does not
|
|
195
|
-
# take down the whole analyzer. Phase 2b will surface this
|
|
196
|
-
# through diagnostics once the configuration layer can name
|
|
197
|
-
# the offending source. The unknown-library check happens at
|
|
198
|
-
# `from_loader` time, not at `add` time, so we have to gate
|
|
199
|
-
# ahead of `add`.
|
|
200
|
-
next unless rbs_loader.has_library?(library: library, version: nil)
|
|
201
|
-
|
|
202
|
-
rbs_loader.add(library: library, version: nil)
|
|
203
|
-
end
|
|
204
|
-
@signature_paths.each do |path|
|
|
205
|
-
rbs_loader.add(path: path) if path.directory?
|
|
206
|
-
end
|
|
207
|
-
RBS::Environment.from_loader(rbs_loader).resolve_type_names
|
|
319
|
+
self.class.build_env_for(libraries: @libraries, signature_paths: @signature_paths)
|
|
208
320
|
end
|
|
209
321
|
|
|
210
322
|
def build_instance_definition(class_name)
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -76,11 +76,20 @@ module Rigor
|
|
|
76
76
|
# list of `sig/`-style directories. When `nil` (the default),
|
|
77
77
|
# the canonical project layout `<root>/sig` is used if it
|
|
78
78
|
# exists, otherwise no signature path is loaded.
|
|
79
|
+
# @param cache_store [Rigor::Cache::Store, nil] persistent cache
|
|
80
|
+
# threaded into the underlying {Environment::RbsLoader} so
|
|
81
|
+
# constant lookups (and, in later v0.0.9 slices, other
|
|
82
|
+
# reflection artefacts) consult the cache. Pass `nil` (the
|
|
83
|
+
# default) to skip caching for this environment.
|
|
79
84
|
# @return [Rigor::Environment]
|
|
80
|
-
def for_project(root: Dir.pwd, libraries: [], signature_paths: nil)
|
|
85
|
+
def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil)
|
|
81
86
|
resolved_paths = signature_paths || default_signature_paths(root)
|
|
82
87
|
merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
|
|
83
|
-
loader = RbsLoader.new(
|
|
88
|
+
loader = RbsLoader.new(
|
|
89
|
+
libraries: merged_libraries,
|
|
90
|
+
signature_paths: resolved_paths,
|
|
91
|
+
cache_store: cache_store
|
|
92
|
+
)
|
|
84
93
|
new(rbs_loader: loader)
|
|
85
94
|
end
|
|
86
95
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
# The public packaging of a flow contribution at a single call edge.
|
|
5
|
+
# Plugins, `RBS::Extended` annotations, and built-in narrowing rules
|
|
6
|
+
# all hand the analyzer this same bundle shape; the inference engine
|
|
7
|
+
# merges contributions through the policy described in
|
|
8
|
+
# [ADR-2 § "Plugin Contribution Merging"](../../docs/adr/2-extension-api.md)
|
|
9
|
+
# rather than letting any one source override another silently.
|
|
10
|
+
#
|
|
11
|
+
# Eight content slots plus a {Provenance} block. A slot left as `nil`
|
|
12
|
+
# (or, for collection-shaped slots, an empty collection) means the
|
|
13
|
+
# contribution does not assert anything in that dimension; the merge
|
|
14
|
+
# policy treats it as absent.
|
|
15
|
+
#
|
|
16
|
+
# The struct is the only shape plugin authors need to learn. Richer
|
|
17
|
+
# or more permissive shapes are not part of the first public
|
|
18
|
+
# contract — see ADR-2 § "Flow Contribution Bundle" for the binding
|
|
19
|
+
# definition.
|
|
20
|
+
#
|
|
21
|
+
# The element-list flattening (`to_element_list`) ADR-2 mentions is
|
|
22
|
+
# intentionally not implemented yet: it is the analyzer-internal
|
|
23
|
+
# bookkeeping behind the merge policy and will land alongside the
|
|
24
|
+
# plugin contribution merger in v0.1.0. Plugin authors should not
|
|
25
|
+
# rely on it.
|
|
26
|
+
class FlowContribution
|
|
27
|
+
# Provenance carries the metadata every contribution needs for
|
|
28
|
+
# diagnostic attribution and cache invalidation. `source_family`
|
|
29
|
+
# mirrors {Rigor::Analysis::Diagnostic::DEFAULT_SOURCE_FAMILY};
|
|
30
|
+
# `descriptor` is the {Rigor::Cache::Descriptor} this
|
|
31
|
+
# contribution attaches to (or `nil` when the contribution does
|
|
32
|
+
# not need its own cache slice).
|
|
33
|
+
Provenance = Data.define(:source_family, :plugin_id, :node, :descriptor) do
|
|
34
|
+
def self.builtin
|
|
35
|
+
new(source_family: :builtin, plugin_id: nil, node: nil, descriptor: nil)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
SLOT_NAMES = %i[
|
|
40
|
+
return_type
|
|
41
|
+
truthy_facts
|
|
42
|
+
falsey_facts
|
|
43
|
+
post_return_facts
|
|
44
|
+
mutations
|
|
45
|
+
invalidations
|
|
46
|
+
exceptional
|
|
47
|
+
role_conformance
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
attr_reader(*SLOT_NAMES, :provenance)
|
|
51
|
+
|
|
52
|
+
# @param return_type [Object, nil] normal-edge return type. Use
|
|
53
|
+
# `nil` when the contribution does not refine the return type
|
|
54
|
+
# selected from the RBS contract.
|
|
55
|
+
# @param truthy_facts [Array, nil] facts that hold only on the
|
|
56
|
+
# truthy control-flow edge. Edge-local: a truthy-edge fact does
|
|
57
|
+
# NOT imply its falsey-edge complement (ADR-2 § "Plugin
|
|
58
|
+
# Contribution Merging").
|
|
59
|
+
# @param falsey_facts [Array, nil] dual of `truthy_facts`.
|
|
60
|
+
# @param post_return_facts [Array, nil] facts that hold after the
|
|
61
|
+
# call returns normally on every edge — the carrier for
|
|
62
|
+
# assertion-style contributions.
|
|
63
|
+
# @param mutations [Array, nil] receiver and argument mutation
|
|
64
|
+
# effects.
|
|
65
|
+
# @param invalidations [Array, nil] targeted fact invalidations
|
|
66
|
+
# beyond what mutation effects already imply.
|
|
67
|
+
# @param exceptional [Object, nil] non-returning, raising, or
|
|
68
|
+
# unreachable effect.
|
|
69
|
+
# @param role_conformance [Array, nil] capability-role conformance
|
|
70
|
+
# facts the contribution provides.
|
|
71
|
+
# @param provenance [Provenance] source-family, plugin-id, node,
|
|
72
|
+
# and cache-descriptor metadata. Defaults to `Provenance.builtin`.
|
|
73
|
+
# rubocop:disable Metrics/ParameterLists
|
|
74
|
+
def initialize(return_type: nil, truthy_facts: nil, falsey_facts: nil,
|
|
75
|
+
post_return_facts: nil, mutations: nil, invalidations: nil,
|
|
76
|
+
exceptional: nil, role_conformance: nil,
|
|
77
|
+
provenance: Provenance.builtin)
|
|
78
|
+
# rubocop:enable Metrics/ParameterLists
|
|
79
|
+
@return_type = return_type
|
|
80
|
+
@truthy_facts = freeze_collection(truthy_facts)
|
|
81
|
+
@falsey_facts = freeze_collection(falsey_facts)
|
|
82
|
+
@post_return_facts = freeze_collection(post_return_facts)
|
|
83
|
+
@mutations = freeze_collection(mutations)
|
|
84
|
+
@invalidations = freeze_collection(invalidations)
|
|
85
|
+
@exceptional = exceptional
|
|
86
|
+
@role_conformance = freeze_collection(role_conformance)
|
|
87
|
+
@provenance = provenance
|
|
88
|
+
freeze
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @return [Boolean] true when every content slot is unset (nil or
|
|
92
|
+
# an empty collection). Provenance does not count toward
|
|
93
|
+
# emptiness — an empty bundle still carries source attribution.
|
|
94
|
+
def empty?
|
|
95
|
+
SLOT_NAMES.all? { |slot| slot_empty?(public_send(slot)) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def to_h
|
|
99
|
+
SLOT_NAMES.each_with_object(provenance: provenance.to_h) do |slot, acc|
|
|
100
|
+
acc[slot] = public_send(slot)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def ==(other)
|
|
105
|
+
other.is_a?(FlowContribution) && to_h == other.to_h
|
|
106
|
+
end
|
|
107
|
+
alias eql? ==
|
|
108
|
+
|
|
109
|
+
def hash
|
|
110
|
+
to_h.hash
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def freeze_collection(value)
|
|
116
|
+
return nil if value.nil?
|
|
117
|
+
|
|
118
|
+
value.dup.freeze
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def slot_empty?(value)
|
|
122
|
+
return true if value.nil?
|
|
123
|
+
return value.empty? if value.respond_to?(:empty?)
|
|
124
|
+
|
|
125
|
+
false
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Encoding` catalog. Singleton — load once, consult during
|
|
9
|
+
# dispatch.
|
|
10
|
+
#
|
|
11
|
+
# Encoding instances are deep-frozen value objects: once
|
|
12
|
+
# registered, their `name` / `dummy?` / `ascii_compatible?`
|
|
13
|
+
# slots never change and the C bodies for the per-instance
|
|
14
|
+
# methods are pure. The C-body classifier therefore lands
|
|
15
|
+
# every instance method as `:leaf` correctly.
|
|
16
|
+
#
|
|
17
|
+
# The blocklist focuses on the *singleton* surface where the
|
|
18
|
+
# hidden state is the process-wide encoding registry. Every
|
|
19
|
+
# method classified `:leaf` on the singleton actually reads
|
|
20
|
+
# (or, for the setters, writes) a global, so a hypothetical
|
|
21
|
+
# `Constant<Encoding>`-class receiver MUST NOT fold them
|
|
22
|
+
# against the analyzer process's registry — what UTF-8's
|
|
23
|
+
# alias list is in the analyzer is not necessarily what it
|
|
24
|
+
# is in the analysed program.
|
|
25
|
+
ENCODING_CATALOG = MethodCatalog.new(
|
|
26
|
+
path: File.expand_path(
|
|
27
|
+
"../../../../data/builtins/ruby_core/encoding.yml",
|
|
28
|
+
__dir__
|
|
29
|
+
),
|
|
30
|
+
mutating_selectors: {
|
|
31
|
+
"Encoding" => Set[
|
|
32
|
+
# Defence-in-depth: mirrors range_catalog.rb /
|
|
33
|
+
# complex_catalog.rb. Encoding does not currently
|
|
34
|
+
# expose a public `initialize_copy` (Encoding objects
|
|
35
|
+
# are deep-frozen and #dup is a no-op), but the
|
|
36
|
+
# convention keeps the door closed against future
|
|
37
|
+
# CRuby changes that would leak a copy-mutator.
|
|
38
|
+
:initialize_copy,
|
|
39
|
+
:hash,
|
|
40
|
+
:eql?,
|
|
41
|
+
# `Encoding.find(name)` walks the global encoding
|
|
42
|
+
# registry. Pure with respect to its argument but
|
|
43
|
+
# the registry itself can drift (load-order, locale,
|
|
44
|
+
# process-wide `default_external=` calls), so a
|
|
45
|
+
# constant-fold would lock in the analyzer's view.
|
|
46
|
+
:find,
|
|
47
|
+
# `Encoding.list` / `Encoding.aliases` /
|
|
48
|
+
# `Encoding.name_list` enumerate the same global
|
|
49
|
+
# registry. Same reasoning as `find` — the values
|
|
50
|
+
# are not guaranteed to match the analysed program's
|
|
51
|
+
# registry.
|
|
52
|
+
:list,
|
|
53
|
+
:aliases,
|
|
54
|
+
:name_list,
|
|
55
|
+
# Global-default mutators. `MethodCatalog#blocked?`
|
|
56
|
+
# only auto-blocks `!`-suffixed selectors, so we MUST
|
|
57
|
+
# list these explicitly: each writes the process-wide
|
|
58
|
+
# default-encoding slot read by `default_external` /
|
|
59
|
+
# `default_internal`.
|
|
60
|
+
:default_external=,
|
|
61
|
+
:default_internal=
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Exception` catalog. Singleton — load once, consult during
|
|
9
|
+
# dispatch.
|
|
10
|
+
#
|
|
11
|
+
# Exception is the base of every Ruby error class (RuntimeError,
|
|
12
|
+
# StandardError, KeyError, …). The Init_Exception block in
|
|
13
|
+
# `references/ruby/error.c` registers the entire hierarchy in
|
|
14
|
+
# one pass, so the YAML carries 27 classes — but only the base
|
|
15
|
+
# `Exception` row is wired into `CATALOG_BY_CLASS` for v0.0.5.
|
|
16
|
+
# A `RuntimeError` receiver hits the Exception arm via
|
|
17
|
+
# `is_a?(Exception)` and the catalog answers with the base-class
|
|
18
|
+
# entries; subclass-specific methods (`KeyError#receiver`,
|
|
19
|
+
# `NameError#name`, …) intentionally miss the lookup until a
|
|
20
|
+
# later slice routes per-subclass class_names.
|
|
21
|
+
#
|
|
22
|
+
# The catalog tier here is *defence in depth* — every base
|
|
23
|
+
# method that could plausibly fold has been weighed against the
|
|
24
|
+
# robustness principle (strict on returns) and either left
|
|
25
|
+
# `:dispatch` / `:mutates_self` (in which case the catalog
|
|
26
|
+
# already declines) or blocklisted because the static classifier
|
|
27
|
+
# missed an indirect side effect. The remaining `:leaf` method
|
|
28
|
+
# that DOES fold is `#cause`, a pure accessor.
|
|
29
|
+
EXCEPTION_CATALOG = MethodCatalog.new(
|
|
30
|
+
path: File.expand_path(
|
|
31
|
+
"../../../../data/builtins/ruby_core/exception.yml",
|
|
32
|
+
__dir__
|
|
33
|
+
),
|
|
34
|
+
mutating_selectors: {
|
|
35
|
+
"Exception" => Set[
|
|
36
|
+
# `exc_initialize` writes `mesg` / `backtrace` ivars on
|
|
37
|
+
# self via `rb_ivar_set` — the C-body classifier missed
|
|
38
|
+
# the indirect mutator because the helpers are not in
|
|
39
|
+
# its regex. Blocklisted so a hypothetical future
|
|
40
|
+
# `Constant<Exception>` carrier cannot fold an aliasing
|
|
41
|
+
# constructor through the catalog.
|
|
42
|
+
:initialize,
|
|
43
|
+
# `exc_exception` either returns self (no-arg) or calls
|
|
44
|
+
# `rb_obj_clone` + `exc_initialize_internal` on the
|
|
45
|
+
# clone — the clone branch mutates fresh state through
|
|
46
|
+
# the same indirect helpers as `:initialize`. Conservative
|
|
47
|
+
# blocklist; the cost is one folded no-arg call.
|
|
48
|
+
:exception,
|
|
49
|
+
# `exc_detailed_message` formats with platform / locale
|
|
50
|
+
# data (highlight markers depend on `$stderr.tty?` via
|
|
51
|
+
# the keyword arg default and `rb_io_tty_p`). Folding
|
|
52
|
+
# would freeze a value that depends on the calling
|
|
53
|
+
# process's stderr state.
|
|
54
|
+
:detailed_message,
|
|
55
|
+
# `exc_backtrace` reads the captured frame list, which
|
|
56
|
+
# depends on where the exception was raised — context
|
|
57
|
+
# the static fold tier cannot reproduce.
|
|
58
|
+
:backtrace,
|
|
59
|
+
# Same rationale as `:backtrace`; `Thread::Backtrace::Location`
|
|
60
|
+
# objects are runtime artefacts.
|
|
61
|
+
:backtrace_locations,
|
|
62
|
+
# `exc_set_backtrace` mutates the @backtrace ivar via
|
|
63
|
+
# `rb_ivar_set` — another indirect mutator the classifier
|
|
64
|
+
# missed.
|
|
65
|
+
:set_backtrace,
|
|
66
|
+
# `initialize_copy` is blocklisted by convention so a
|
|
67
|
+
# hypothetical future `Constant<Exception>` carrier
|
|
68
|
+
# cannot fold an aliasing copy through the catalog.
|
|
69
|
+
:initialize_copy,
|
|
70
|
+
# Defensive entries for the universal mutation surface.
|
|
71
|
+
# Object-identity hashing on a constant carrier is fine,
|
|
72
|
+
# but `eql?` on Exception delegates to `==` (dispatch);
|
|
73
|
+
# blocking both keeps the constant-fold tier honest.
|
|
74
|
+
:hash,
|
|
75
|
+
:eql?
|
|
76
|
+
],
|
|
77
|
+
# `Exception.to_tty?` (singleton) calls
|
|
78
|
+
# `rb_io_tty_p($stderr)`; its return depends on the
|
|
79
|
+
# process's stderr state at runtime, never on compile-time
|
|
80
|
+
# arguments. The catalog tier today only consults
|
|
81
|
+
# `mutating_selectors` for instance-receiver dispatches via
|
|
82
|
+
# `CATALOG_BY_CLASS`, so this row is documentation-grade —
|
|
83
|
+
# it records the soundness rationale for any future slice
|
|
84
|
+
# that wires the singleton path through the catalog.
|
|
85
|
+
"Exception.singleton" => Set[
|
|
86
|
+
:to_tty?
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "method_catalog"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
module Builtins
|
|
8
|
+
# `Proc` / `Method` / `UnboundMethod` catalog. Singleton —
|
|
9
|
+
# load once, consult during dispatch.
|
|
10
|
+
#
|
|
11
|
+
# The three callable carriers are imported together because
|
|
12
|
+
# `Init_Proc` registers them in a single C init block. They
|
|
13
|
+
# share the same fundamental hazard at the catalog tier:
|
|
14
|
+
# most of their public methods invoke the wrapped Ruby code
|
|
15
|
+
# (the proc body, the bound method's receiver, …) and that
|
|
16
|
+
# code can do anything — read mutable state, call I/O, return
|
|
17
|
+
# different values on successive calls. The static C-body
|
|
18
|
+
# classifier marks these `:leaf` because the C functions
|
|
19
|
+
# themselves do not call `rb_funcall*` / `rb_yield` directly
|
|
20
|
+
# (they delegate through the VM's optimised call paths and
|
|
21
|
+
# method-entry table), but folding any of them at compile
|
|
22
|
+
# time would freeze a value the runtime never actually
|
|
23
|
+
# produces twice.
|
|
24
|
+
#
|
|
25
|
+
# The blocklist below errs aggressively on the side of
|
|
26
|
+
# caution: a hypothetical future `Constant<Proc>` /
|
|
27
|
+
# `Constant<Method>` / `Constant<UnboundMethod>` carrier
|
|
28
|
+
# would have very little to gain from these folds and a
|
|
29
|
+
# great deal to lose if user code ran behind the analyzer's
|
|
30
|
+
# back. Reflective readers (`#arity`, `#parameters`,
|
|
31
|
+
# `#source_location`, `#name`, `#owner`, `#receiver`) remain
|
|
32
|
+
# foldable; the RBS tier still resolves return types for
|
|
33
|
+
# the blocklisted methods so callers do not lose precision.
|
|
34
|
+
PROC_CATALOG = MethodCatalog.new(
|
|
35
|
+
path: File.expand_path(
|
|
36
|
+
"../../../../data/builtins/ruby_core/proc.yml",
|
|
37
|
+
__dir__
|
|
38
|
+
),
|
|
39
|
+
mutating_selectors: {
|
|
40
|
+
"Proc" => Set[
|
|
41
|
+
# `#call` / `#[]` / `#===` / `#yield` invoke the proc
|
|
42
|
+
# body. The C body routes through
|
|
43
|
+
# `OPTIMIZED_METHOD_TYPE_CALL` (a VM fast path the
|
|
44
|
+
# classifier cannot see into); the proc body can do
|
|
45
|
+
# anything — read globals, mutate captured locals,
|
|
46
|
+
# raise. MUST decline to fold.
|
|
47
|
+
:call,
|
|
48
|
+
:[],
|
|
49
|
+
:===,
|
|
50
|
+
:yield,
|
|
51
|
+
# `#curry` / `#<<` / `#>>` allocate a fresh `Proc`
|
|
52
|
+
# that closes over the receiver (and, for `<<` /
|
|
53
|
+
# `>>`, over the argument). Folding would freeze a
|
|
54
|
+
# specific `Proc` instance whose identity the runtime
|
|
55
|
+
# never actually produces (object_id differs every
|
|
56
|
+
# call), so the catalog tier declines.
|
|
57
|
+
:curry,
|
|
58
|
+
:<<,
|
|
59
|
+
:>>,
|
|
60
|
+
# `#to_proc` returns `self` for `Proc` (cheap), but
|
|
61
|
+
# blocking it keeps the rule shape uniform across the
|
|
62
|
+
# three callable carriers (Method#to_proc allocates a
|
|
63
|
+
# fresh `Proc`).
|
|
64
|
+
:to_proc,
|
|
65
|
+
# Identity-based equality and hashing: `#hash` is
|
|
66
|
+
# derived from the underlying ISeq pointer; `#==` /
|
|
67
|
+
# `#eql?` compare ISeq + binding. Folding to a
|
|
68
|
+
# `Constant<Integer>` / `Constant<bool>` would freeze
|
|
69
|
+
# an answer that depends on memory layout. Defensive.
|
|
70
|
+
:hash,
|
|
71
|
+
:==,
|
|
72
|
+
:eql?,
|
|
73
|
+
# `initialize_copy` is blocklisted by convention so a
|
|
74
|
+
# hypothetical future `Constant<Proc>` carrier cannot
|
|
75
|
+
# fold an aliasing copy through the catalog.
|
|
76
|
+
:initialize_copy
|
|
77
|
+
],
|
|
78
|
+
"Method" => Set[
|
|
79
|
+
# `#call` / `#[]` / `#===` invoke the bound method.
|
|
80
|
+
# Same hazard as `Proc#call`: arbitrary user code,
|
|
81
|
+
# arbitrary side effects.
|
|
82
|
+
:call,
|
|
83
|
+
:[],
|
|
84
|
+
:===,
|
|
85
|
+
# `#curry` / `#<<` / `#>>` allocate a fresh `Proc`
|
|
86
|
+
# that closes over the bound method.
|
|
87
|
+
:curry,
|
|
88
|
+
:<<,
|
|
89
|
+
:>>,
|
|
90
|
+
# `#to_proc` allocates a fresh `Proc` wrapping the
|
|
91
|
+
# bound method — folding would freeze its object_id.
|
|
92
|
+
# The classifier already marks it `:block_dependent`,
|
|
93
|
+
# but the explicit entry keeps the intent obvious.
|
|
94
|
+
:to_proc,
|
|
95
|
+
# `#unbind` allocates a fresh `UnboundMethod` whose
|
|
96
|
+
# identity differs every call.
|
|
97
|
+
:unbind,
|
|
98
|
+
# Identity-based equality and hashing.
|
|
99
|
+
:hash,
|
|
100
|
+
:==,
|
|
101
|
+
:eql?,
|
|
102
|
+
# `initialize_copy` is blocklisted by convention.
|
|
103
|
+
:initialize_copy
|
|
104
|
+
],
|
|
105
|
+
"UnboundMethod" => Set[
|
|
106
|
+
# `#bind` allocates a fresh `Method` whose object_id
|
|
107
|
+
# differs every call; `#bind_call` invokes the bound
|
|
108
|
+
# method (already classified `:block_dependent`).
|
|
109
|
+
:bind,
|
|
110
|
+
:bind_call,
|
|
111
|
+
# Identity-based equality and hashing.
|
|
112
|
+
:hash,
|
|
113
|
+
:==,
|
|
114
|
+
:eql?,
|
|
115
|
+
# `initialize_copy` is blocklisted by convention.
|
|
116
|
+
:initialize_copy
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|