rigortype 0.1.4 → 0.1.5
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 +40 -13
- data/lib/rigor/analysis/fact_store.rb +15 -3
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +387 -12
- data/lib/rigor/analysis/worker_session.rb +327 -0
- data/lib/rigor/builtins/imported_refinements.rb +6 -2
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +40 -7
- data/lib/rigor/cli.rb +52 -2
- data/lib/rigor/configuration.rb +131 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/lockfile_resolver.rb +125 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
- data/lib/rigor/environment/rbs_loader.rb +194 -6
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment.rb +78 -6
- data/lib/rigor/inference/acceptance.rb +35 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
- data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
- data/lib/rigor/inference/expression_typer.rb +12 -2
- data/lib/rigor/inference/macro_block_self_type.rb +96 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
- data/lib/rigor/inference/method_dispatcher.rb +128 -3
- data/lib/rigor/inference/method_parameter_binder.rb +21 -11
- data/lib/rigor/inference/narrowing.rb +127 -8
- data/lib/rigor/inference/synthetic_method.rb +86 -0
- data/lib/rigor/inference/synthetic_method_index.rb +82 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
- data/lib/rigor/plugin/blueprint.rb +60 -0
- data/lib/rigor/plugin/loader.rb +3 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
- data/lib/rigor/plugin/macro/external_file.rb +143 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
- data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
- data/lib/rigor/plugin/macro.rb +31 -0
- data/lib/rigor/plugin/manifest.rb +78 -7
- data/lib/rigor/plugin/registry.rb +32 -2
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/bot.rb +6 -3
- data/lib/rigor/type/combinator.rb +12 -1
- data/lib/rigor/type/integer_range.rb +7 -7
- data/lib/rigor/type/refined.rb +18 -12
- data/lib/rigor/type/top.rb +4 -3
- data/lib/rigor/type_node/generic.rb +7 -1
- data/lib/rigor/type_node/identifier.rb +9 -1
- data/lib/rigor/type_node/string_literal.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +5 -2
- data/sig/rigor/plugin/blueprint.rbs +7 -0
- data/sig/rigor/plugin/manifest.rbs +1 -1
- data/sig/rigor/plugin/registry.rbs +14 -1
- data/sig/rigor.rbs +35 -2
- metadata +39 -1
data/lib/rigor/environment.rb
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "environment/class_registry"
|
|
4
4
|
require_relative "environment/rbs_loader"
|
|
5
|
+
require_relative "environment/reflection"
|
|
6
|
+
require_relative "environment/bundle_sig_discovery"
|
|
7
|
+
require_relative "environment/lockfile_resolver"
|
|
8
|
+
require_relative "environment/rbs_collection_discovery"
|
|
9
|
+
require_relative "environment/rbs_coverage_report"
|
|
10
|
+
require_relative "inference/synthetic_method_index"
|
|
5
11
|
require_relative "type_node/name_scope"
|
|
6
12
|
require_relative "type_node/resolver_chain"
|
|
7
13
|
|
|
@@ -40,11 +46,19 @@ module Rigor
|
|
|
40
46
|
pathname optparse json yaml fileutils tempfile tmpdir
|
|
41
47
|
stringio forwardable digest securerandom
|
|
42
48
|
uri logger date
|
|
49
|
+
pp delegate observable abbrev find tsort singleton
|
|
50
|
+
shellwords benchmark base64 did_you_mean
|
|
51
|
+
monitor mutex_m timeout
|
|
52
|
+
open3 erb etc ipaddr bigdecimal bigdecimal-math
|
|
53
|
+
prettyprint random-formatter time open-uri resolv
|
|
54
|
+
csv pstore objspace io-console cgi cgi-escape
|
|
55
|
+
strscan
|
|
43
56
|
prism rbs
|
|
44
57
|
].freeze
|
|
45
58
|
|
|
46
59
|
attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
|
|
47
|
-
:rbs_extended_reporter, :boundary_cross_reporter, :name_scope
|
|
60
|
+
:rbs_extended_reporter, :boundary_cross_reporter, :name_scope,
|
|
61
|
+
:synthetic_method_index
|
|
48
62
|
|
|
49
63
|
# @param class_registry [Rigor::Environment::ClassRegistry]
|
|
50
64
|
# @param rbs_loader [Rigor::Environment::RbsLoader, nil] when nil the
|
|
@@ -67,13 +81,15 @@ module Rigor
|
|
|
67
81
|
# participates and the dispatcher tier is a no-op.
|
|
68
82
|
def initialize(class_registry: ClassRegistry.default, rbs_loader: nil,
|
|
69
83
|
plugin_registry: nil, dependency_source_index: nil,
|
|
70
|
-
rbs_extended_reporter: nil, boundary_cross_reporter: nil
|
|
84
|
+
rbs_extended_reporter: nil, boundary_cross_reporter: nil,
|
|
85
|
+
synthetic_method_index: nil)
|
|
71
86
|
@class_registry = class_registry
|
|
72
87
|
@rbs_loader = rbs_loader
|
|
73
88
|
@plugin_registry = plugin_registry
|
|
74
89
|
@dependency_source_index = dependency_source_index
|
|
75
90
|
@rbs_extended_reporter = rbs_extended_reporter
|
|
76
91
|
@boundary_cross_reporter = boundary_cross_reporter
|
|
92
|
+
@synthetic_method_index = synthetic_method_index || Inference::SyntheticMethodIndex::EMPTY
|
|
77
93
|
@name_scope = build_name_scope
|
|
78
94
|
freeze
|
|
79
95
|
end
|
|
@@ -104,14 +120,57 @@ module Rigor
|
|
|
104
120
|
# reflection artefacts) consult the cache. Pass `nil` (the
|
|
105
121
|
# default) to skip caching for this environment.
|
|
106
122
|
# @return [Rigor::Environment]
|
|
107
|
-
|
|
123
|
+
# rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
124
|
+
def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil,
|
|
108
125
|
plugin_registry: nil, dependency_source_index: nil,
|
|
109
|
-
rbs_extended_reporter: nil, boundary_cross_reporter: nil
|
|
126
|
+
rbs_extended_reporter: nil, boundary_cross_reporter: nil,
|
|
127
|
+
bundler_bundle_path: nil, bundler_auto_detect: false,
|
|
128
|
+
bundler_lockfile: nil,
|
|
129
|
+
rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
|
|
130
|
+
synthetic_method_index: nil)
|
|
110
131
|
resolved_paths = signature_paths || default_signature_paths(root)
|
|
132
|
+
# O4 MVP — append per-gem `sig/` directories discovered
|
|
133
|
+
# under the target project's bundler install root. Empty
|
|
134
|
+
# array when neither an explicit path nor auto-detection
|
|
135
|
+
# finds a bundle. Order: user `signature_paths:` win first
|
|
136
|
+
# (semantic precedence inside `RbsLoader.build_env_for`);
|
|
137
|
+
# gem-shipped sigs append last so user overrides stay
|
|
138
|
+
# authoritative.
|
|
139
|
+
#
|
|
140
|
+
# O4 Layer 3 — when a Gemfile.lock is available (explicit
|
|
141
|
+
# `bundler_lockfile:` or auto-detected next to the project
|
|
142
|
+
# root), use the locked gem set to filter the discovered
|
|
143
|
+
# `sig/` directories. Stale gems in the bundle install
|
|
144
|
+
# tree (out-of-band installs, version drift after a
|
|
145
|
+
# `bundle update`) are silently dropped so only gems the
|
|
146
|
+
# project actually declares contribute RBS.
|
|
147
|
+
locked = LockfileResolver.locked_gems(
|
|
148
|
+
lockfile_path: bundler_lockfile,
|
|
149
|
+
project_root: root,
|
|
150
|
+
auto_detect: bundler_auto_detect
|
|
151
|
+
)
|
|
152
|
+
gem_sig_paths = BundleSigDiscovery.discover(
|
|
153
|
+
bundle_path: bundler_bundle_path,
|
|
154
|
+
project_root: root,
|
|
155
|
+
auto_detect: bundler_auto_detect,
|
|
156
|
+
locked_gems: locked.empty? ? nil : locked
|
|
157
|
+
).map(&:to_s)
|
|
158
|
+
# O4 Layer 3 slice 2 — when `rbs collection install`
|
|
159
|
+
# has been run for the target project, parse the
|
|
160
|
+
# resulting `rbs_collection.lock.yaml` and feed each
|
|
161
|
+
# gem's `<collection_path>/<name>/<version>/` directory
|
|
162
|
+
# into `signature_paths:`. Stdlib-typed entries are
|
|
163
|
+
# skipped (already covered by `DEFAULT_LIBRARIES`).
|
|
164
|
+
collection_paths = RbsCollectionDiscovery.discover(
|
|
165
|
+
lockfile_path: rbs_collection_lockfile,
|
|
166
|
+
project_root: root,
|
|
167
|
+
auto_detect: rbs_collection_auto_detect
|
|
168
|
+
).map(&:to_s)
|
|
169
|
+
loader_signature_paths = resolved_paths + gem_sig_paths + collection_paths
|
|
111
170
|
merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
|
|
112
171
|
loader = RbsLoader.new(
|
|
113
172
|
libraries: merged_libraries,
|
|
114
|
-
signature_paths:
|
|
173
|
+
signature_paths: loader_signature_paths,
|
|
115
174
|
cache_store: cache_store
|
|
116
175
|
)
|
|
117
176
|
new(
|
|
@@ -119,9 +178,11 @@ module Rigor
|
|
|
119
178
|
plugin_registry: plugin_registry,
|
|
120
179
|
dependency_source_index: dependency_source_index,
|
|
121
180
|
rbs_extended_reporter: rbs_extended_reporter,
|
|
122
|
-
boundary_cross_reporter: boundary_cross_reporter
|
|
181
|
+
boundary_cross_reporter: boundary_cross_reporter,
|
|
182
|
+
synthetic_method_index: synthetic_method_index
|
|
123
183
|
)
|
|
124
184
|
end
|
|
185
|
+
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
|
|
125
186
|
|
|
126
187
|
private
|
|
127
188
|
|
|
@@ -185,6 +246,17 @@ module Rigor
|
|
|
185
246
|
class_known_in_rbs?(name)
|
|
186
247
|
end
|
|
187
248
|
|
|
249
|
+
# ADR-15 Phase 2b — returns the loader's read-only,
|
|
250
|
+
# `Ractor.shareable?` query surface as a frozen
|
|
251
|
+
# {Environment::Reflection}. Built lazily on first
|
|
252
|
+
# access; subsequent calls return the same instance.
|
|
253
|
+
# Returns `nil` when the environment carries no RBS
|
|
254
|
+
# loader (test-only `Environment.new` without
|
|
255
|
+
# `rbs_loader:`).
|
|
256
|
+
def reflection
|
|
257
|
+
@rbs_loader&.reflection
|
|
258
|
+
end
|
|
259
|
+
|
|
188
260
|
# Compares two class/module names using analyzer-owned class data.
|
|
189
261
|
# Returns `:equal`, `:subclass`, `:superclass`, `:disjoint`, or
|
|
190
262
|
# `:unknown`. The static registry handles built-ins cheaply; the RBS
|
|
@@ -327,10 +327,44 @@ module Rigor
|
|
|
327
327
|
)
|
|
328
328
|
return class_result if class_result.no?
|
|
329
329
|
|
|
330
|
-
|
|
330
|
+
# Parametrized-ancestor projection. When `actual <:= target`
|
|
331
|
+
# holds at the class level but the type-arg arities differ,
|
|
332
|
+
# the actual's parametrization has to be projected into the
|
|
333
|
+
# target's view before the element-wise covariance check.
|
|
334
|
+
# The canonical case is `Hash[K, V] <:= Enumerable[[K, V]]`:
|
|
335
|
+
# Hash carries two type_args, Enumerable carries one, and
|
|
336
|
+
# the inherited parametrization at the Enumerable boundary
|
|
337
|
+
# is `Tuple[K, V]`. RBS encodes this as
|
|
338
|
+
# `include Enumerable[[K, V]]` in `Hash`'s definition.
|
|
339
|
+
projected_other = project_to_target_arity(self_type, other_type) || other_type
|
|
340
|
+
args_result = accepts_nominal_args(self_type, projected_other, mode)
|
|
331
341
|
combine_results(class_result, args_result, mode)
|
|
332
342
|
end
|
|
333
343
|
|
|
344
|
+
# Returns `other_type` rewritten so its type_args have the
|
|
345
|
+
# same arity as `self_type.type_args`, or `nil` if no
|
|
346
|
+
# projection is known. Today only the Hash → Enumerable
|
|
347
|
+
# projection is hand-rolled; a general RBS-driven
|
|
348
|
+
# implementation that consults `definition.ancestors[i].args`
|
|
349
|
+
# for arbitrary subclass / module-include relations is the
|
|
350
|
+
# principled follow-up.
|
|
351
|
+
def project_to_target_arity(self_type, other_type)
|
|
352
|
+
return nil if self_type.type_args.size == other_type.type_args.size
|
|
353
|
+
return nil if self_type.type_args.empty? || other_type.type_args.empty?
|
|
354
|
+
|
|
355
|
+
if self_type.class_name == "Enumerable" &&
|
|
356
|
+
other_type.class_name == "Hash" &&
|
|
357
|
+
self_type.type_args.size == 1 &&
|
|
358
|
+
other_type.type_args.size == 2
|
|
359
|
+
return Type::Combinator.nominal_of(
|
|
360
|
+
"Hash",
|
|
361
|
+
type_args: [Type::Combinator.tuple_of(*other_type.type_args)]
|
|
362
|
+
)
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
nil
|
|
366
|
+
end
|
|
367
|
+
|
|
334
368
|
def project_tuple_to_nominal(tuple)
|
|
335
369
|
if tuple.elements.empty?
|
|
336
370
|
Type::Combinator.nominal_of(Array)
|
|
@@ -30,7 +30,16 @@ module Rigor
|
|
|
30
30
|
def initialize(path:, mutating_selectors: {})
|
|
31
31
|
@path = path
|
|
32
32
|
@mutating_selectors = mutating_selectors.transform_values(&:freeze).freeze
|
|
33
|
-
|
|
33
|
+
# ADR-15 Phase 4b.x — eager-load so the instance is
|
|
34
|
+
# safe to `Ractor.make_shareable`. Lazy init via
|
|
35
|
+
# `@catalog ||= load_catalog` would write to a
|
|
36
|
+
# potentially-frozen instance the first time a
|
|
37
|
+
# worker Ractor consults the catalog, raising
|
|
38
|
+
# `FrozenError`. The YAML parse is a once-per-process
|
|
39
|
+
# cost and the catalogs are constructed at module
|
|
40
|
+
# load time anyway, so eager init is free in
|
|
41
|
+
# practice.
|
|
42
|
+
@catalog = load_catalog
|
|
34
43
|
end
|
|
35
44
|
|
|
36
45
|
def safe_for_folding?(class_name, selector, kind: :instance)
|
|
@@ -52,7 +61,7 @@ module Rigor
|
|
|
52
61
|
end
|
|
53
62
|
|
|
54
63
|
def reset!
|
|
55
|
-
@catalog =
|
|
64
|
+
@catalog = load_catalog
|
|
56
65
|
end
|
|
57
66
|
|
|
58
67
|
private
|
|
@@ -72,9 +81,7 @@ module Rigor
|
|
|
72
81
|
per_class.include?(selector.to_sym) || per_class.include?(selector_str.to_sym)
|
|
73
82
|
end
|
|
74
83
|
|
|
75
|
-
|
|
76
|
-
@catalog ||= load_catalog
|
|
77
|
-
end
|
|
84
|
+
attr_reader :catalog
|
|
78
85
|
|
|
79
86
|
def load_catalog
|
|
80
87
|
return EMPTY_CATALOG unless File.exist?(@path)
|
|
@@ -68,15 +68,21 @@ module Rigor
|
|
|
68
68
|
# Used by tests to drop the cached catalog so a different
|
|
69
69
|
# path or content can be exercised. Production code MUST
|
|
70
70
|
# NOT call this during normal operation.
|
|
71
|
+
#
|
|
72
|
+
# ADR-15 Phase 4b.x — reset re-loads eagerly so the
|
|
73
|
+
# singleton-class `@catalog` ivar stays populated, and
|
|
74
|
+
# the loaded Hash is deep-shared via `Ractor.make_shareable`
|
|
75
|
+
# so a worker Ractor reading the ivar via `catalog.dig(...)`
|
|
76
|
+
# does not trip `Ractor::IsolationError`. Plain `.freeze`
|
|
77
|
+
# is insufficient: YAML parses to a nested Hash/Array/String
|
|
78
|
+
# graph and only the outer Hash would be frozen.
|
|
71
79
|
def reset!
|
|
72
|
-
@catalog =
|
|
80
|
+
@catalog = Ractor.make_shareable(load_catalog)
|
|
73
81
|
end
|
|
74
82
|
|
|
75
83
|
private
|
|
76
84
|
|
|
77
|
-
|
|
78
|
-
@catalog ||= load_catalog
|
|
79
|
-
end
|
|
85
|
+
attr_reader :catalog
|
|
80
86
|
|
|
81
87
|
def load_catalog
|
|
82
88
|
return EMPTY_CATALOG unless File.exist?(CATALOG_PATH)
|
|
@@ -87,6 +93,11 @@ module Rigor
|
|
|
87
93
|
EMPTY_CATALOG
|
|
88
94
|
end
|
|
89
95
|
end
|
|
96
|
+
|
|
97
|
+
# ADR-15 Phase 4b.x — eager-load on the main Ractor at
|
|
98
|
+
# module-load time so worker Ractors only READ the
|
|
99
|
+
# populated singleton-class `@catalog` ivar.
|
|
100
|
+
reset!
|
|
90
101
|
end
|
|
91
102
|
end
|
|
92
103
|
end
|
|
@@ -6,6 +6,7 @@ require_relative "../type"
|
|
|
6
6
|
require_relative "../ast"
|
|
7
7
|
require_relative "block_parameter_binder"
|
|
8
8
|
require_relative "fallback"
|
|
9
|
+
require_relative "macro_block_self_type"
|
|
9
10
|
require_relative "method_dispatcher"
|
|
10
11
|
|
|
11
12
|
module Rigor
|
|
@@ -1194,16 +1195,25 @@ module Rigor
|
|
|
1194
1195
|
arg_types: arg_types,
|
|
1195
1196
|
environment: scope.environment
|
|
1196
1197
|
)
|
|
1197
|
-
|
|
1198
|
+
# ADR-16 Tier A: when a registered plugin's `block_as_methods`
|
|
1199
|
+
# entry matches `(receiver_type, call_node.name)`, narrow the
|
|
1200
|
+
# block body's `self_type` to the receiver class's instance
|
|
1201
|
+
# type. The narrowing is `nil` for unmatched calls, leaving
|
|
1202
|
+
# the existing scope contract unchanged.
|
|
1203
|
+
narrowed_self = MacroBlockSelfType.narrow_self_type_for(
|
|
1204
|
+
scope: scope, call_node: call_node, receiver_type: receiver_type
|
|
1205
|
+
)
|
|
1206
|
+
block_return_for(block_arg, expected, narrowed_self_type: narrowed_self)
|
|
1198
1207
|
rescue StandardError
|
|
1199
1208
|
nil
|
|
1200
1209
|
end
|
|
1201
1210
|
|
|
1202
|
-
def block_return_for(block_arg, expected)
|
|
1211
|
+
def block_return_for(block_arg, expected, narrowed_self_type: nil)
|
|
1203
1212
|
case block_arg
|
|
1204
1213
|
when Prism::BlockNode
|
|
1205
1214
|
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_arg)
|
|
1206
1215
|
block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
1216
|
+
block_scope = block_scope.with_self_type(narrowed_self_type) if narrowed_self_type
|
|
1207
1217
|
type_block_body(block_arg, block_scope)
|
|
1208
1218
|
when Prism::BlockArgumentNode
|
|
1209
1219
|
symbol_block_return_type(block_arg, expected)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../type"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# ADR-16 Tier A — engine hook. Consults every registered
|
|
8
|
+
# plugin manifest's `block_as_methods` entries to decide
|
|
9
|
+
# whether a block call site qualifies for `Scope#self_type`
|
|
10
|
+
# narrowing.
|
|
11
|
+
#
|
|
12
|
+
# The match contract for a class-level DSL like Sinatra's
|
|
13
|
+
# `class MyApp < Sinatra::Base; get '/foo' do ... end; end`:
|
|
14
|
+
#
|
|
15
|
+
# - the call's lexical receiver type is `Singleton[X]`
|
|
16
|
+
# (the implicit-self in a class body, or an explicit
|
|
17
|
+
# `MyApp.get(...)` call);
|
|
18
|
+
# - the underlying class `X` equals or inherits from the
|
|
19
|
+
# entry's `receiver_constraint`;
|
|
20
|
+
# - the call's method name is in the entry's `verbs`.
|
|
21
|
+
#
|
|
22
|
+
# On a match the helper returns the **instance** type of
|
|
23
|
+
# the receiver class (`Nominal[X]`) — the narrowed
|
|
24
|
+
# `self_type` for the block body, matching Sinatra's
|
|
25
|
+
# runtime semantics where `Sinatra::Base#generate_method`
|
|
26
|
+
# turns the block into an instance method of the user's
|
|
27
|
+
# app class.
|
|
28
|
+
#
|
|
29
|
+
# Slice 1b ships the floor only (per ADR-16 § WD13):
|
|
30
|
+
# bare-identifier method lookups inside the block resolve
|
|
31
|
+
# through the inference engine's normal `self_type`-driven
|
|
32
|
+
# path, so methods declared on `Sinatra::Base` (RBS or
|
|
33
|
+
# otherwise) become visible. Precision additions —
|
|
34
|
+
# parameter-typed block params, declared per-verb argument
|
|
35
|
+
# contracts — are ceiling concerns for later slices.
|
|
36
|
+
module MacroBlockSelfType
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
# @param scope [Rigor::Scope]
|
|
40
|
+
# @param call_node [Prism::CallNode]
|
|
41
|
+
# @param receiver_type [Rigor::Type, nil]
|
|
42
|
+
# @return [Rigor::Type, nil] the narrowed self-type, or
|
|
43
|
+
# `nil` when no registered entry matches the call shape.
|
|
44
|
+
def narrow_self_type_for(scope:, call_node:, receiver_type:)
|
|
45
|
+
return nil if receiver_type.nil?
|
|
46
|
+
|
|
47
|
+
environment = scope&.environment
|
|
48
|
+
registry = environment&.plugin_registry
|
|
49
|
+
return nil if registry.nil? || registry.empty?
|
|
50
|
+
|
|
51
|
+
receiver_class_name = singleton_receiver_class_name(receiver_type)
|
|
52
|
+
return nil if receiver_class_name.nil?
|
|
53
|
+
|
|
54
|
+
verb = call_node.name
|
|
55
|
+
registry.plugins.each do |plugin|
|
|
56
|
+
plugin.manifest.block_as_methods.each do |entry| # rigor:disable undefined-method
|
|
57
|
+
return instance_type_for(receiver_class_name, environment) if matches?(entry, verb, receiver_class_name,
|
|
58
|
+
environment)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Tier A's match contract is intentionally narrow:
|
|
65
|
+
# class-level DSL calls (receiver is `Singleton[X]`) only.
|
|
66
|
+
# Instance-receiver calls and DSL forms whose block body
|
|
67
|
+
# binds a different `self` (Concern's `included do`,
|
|
68
|
+
# `instance_eval { ... }`) are handled by later slices
|
|
69
|
+
# (Concern walker, Tier D, etc.) — not Tier A.
|
|
70
|
+
def singleton_receiver_class_name(receiver_type)
|
|
71
|
+
return nil unless receiver_type.is_a?(Type::Singleton)
|
|
72
|
+
|
|
73
|
+
receiver_type.class_name
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def matches?(entry, verb, receiver_class_name, environment)
|
|
77
|
+
return false unless entry.verbs.include?(verb)
|
|
78
|
+
|
|
79
|
+
receiver_class_inherits_from?(receiver_class_name, entry.receiver_constraint, environment)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def receiver_class_inherits_from?(class_name, constraint, environment)
|
|
83
|
+
return true if class_name == constraint
|
|
84
|
+
|
|
85
|
+
ordering = environment.class_ordering(class_name, constraint)
|
|
86
|
+
%i[equal subclass].include?(ordering)
|
|
87
|
+
rescue StandardError
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def instance_type_for(class_name, environment)
|
|
92
|
+
environment.nominal_for_name(class_name) || Type::Nominal.new(class_name)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -1028,10 +1028,10 @@ module Rigor
|
|
|
1028
1028
|
# class's ancestor chain at lookup time; the catalog
|
|
1029
1029
|
# corresponds to the module-mode YAML at
|
|
1030
1030
|
# `data/builtins/ruby_core/<topic>.yml`.
|
|
1031
|
-
MODULE_CATALOGS = [
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1031
|
+
MODULE_CATALOGS = Ractor.make_shareable([
|
|
1032
|
+
[Comparable, Builtins::COMPARABLE_CATALOG, "Comparable"],
|
|
1033
|
+
[Enumerable, Builtins::ENUMERABLE_CATALOG, "Enumerable"]
|
|
1034
|
+
])
|
|
1035
1035
|
private_constant :MODULE_CATALOGS
|
|
1036
1036
|
|
|
1037
1037
|
# Returns the `(catalog, class_name)` pairs for every
|
|
@@ -1057,31 +1057,31 @@ module Rigor
|
|
|
1057
1057
|
# Otherwise a `DateTime` receiver would match the `Date`
|
|
1058
1058
|
# arm first and the catalog would consult the Date entry
|
|
1059
1059
|
# in `DATE_CATALOG` for the wrong class.
|
|
1060
|
-
CATALOG_BY_CLASS = [
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1060
|
+
CATALOG_BY_CLASS = Ractor.make_shareable([
|
|
1061
|
+
[Integer, [Builtins::NumericCatalog, "Integer"]],
|
|
1062
|
+
[Float, [Builtins::NumericCatalog, "Float"]],
|
|
1063
|
+
[String, [Builtins::STRING_CATALOG, "String"]],
|
|
1064
|
+
[Symbol, [Builtins::STRING_CATALOG, "Symbol"]],
|
|
1065
|
+
[Array, [Builtins::ARRAY_CATALOG, "Array"]],
|
|
1066
|
+
[Hash, [Builtins::HASH_CATALOG, "Hash"]],
|
|
1067
|
+
[Range, [Builtins::RANGE_CATALOG, "Range"]],
|
|
1068
|
+
[::Set, [Builtins::SET_CATALOG, "Set"]],
|
|
1069
|
+
[Time, [Builtins::TIME_CATALOG, "Time"]],
|
|
1070
|
+
[DateTime, [Builtins::DATE_CATALOG, "DateTime"]],
|
|
1071
|
+
[Date, [Builtins::DATE_CATALOG, "Date"]],
|
|
1072
|
+
[Rational, [Builtins::RATIONAL_CATALOG, "Rational"]],
|
|
1073
|
+
[Complex, [Builtins::COMPLEX_CATALOG, "Complex"]],
|
|
1074
|
+
[Pathname, [Builtins::PATHNAME_CATALOG, "Pathname"]],
|
|
1075
|
+
[Random, [Builtins::RANDOM_CATALOG, "Random"]],
|
|
1076
|
+
[Struct, [Builtins::STRUCT_CATALOG, "Struct"]],
|
|
1077
|
+
[Encoding, [Builtins::ENCODING_CATALOG, "Encoding"]],
|
|
1078
|
+
[Regexp, [Builtins::REGEXP_CATALOG, "Regexp"]],
|
|
1079
|
+
[MatchData, [Builtins::REGEXP_CATALOG, "MatchData"]],
|
|
1080
|
+
[Proc, [Builtins::PROC_CATALOG, "Proc"]],
|
|
1081
|
+
[Method, [Builtins::PROC_CATALOG, "Method"]],
|
|
1082
|
+
[UnboundMethod, [Builtins::PROC_CATALOG, "UnboundMethod"]],
|
|
1083
|
+
[Exception, [Builtins::EXCEPTION_CATALOG, "Exception"]]
|
|
1084
|
+
])
|
|
1085
1085
|
private_constant :CATALOG_BY_CLASS
|
|
1086
1086
|
|
|
1087
1087
|
# Returns `[catalog, class_name]` for receivers we have a
|
|
@@ -36,10 +36,10 @@ module Rigor
|
|
|
36
36
|
# the result into a `Constant<Rational>` / `Constant<Complex>`.
|
|
37
37
|
# The factory accepts the same shapes as Ruby:
|
|
38
38
|
# `Rational(a)`, `Rational(a, b)`, `Complex(a)`, `Complex(a, b)`.
|
|
39
|
-
NUMERIC_CONSTRUCTORS = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
NUMERIC_CONSTRUCTORS = Ractor.make_shareable({
|
|
40
|
+
Rational: Ractor.make_shareable(->(*args) { Rational(*args) }),
|
|
41
|
+
Complex: Ractor.make_shareable(->(*args) { Complex(*args) })
|
|
42
|
+
})
|
|
43
43
|
private_constant :NUMERIC_CONSTRUCTORS
|
|
44
44
|
|
|
45
45
|
# `Kernel#Integer(s)` predicate-aware refinement set
|
|
@@ -76,6 +76,21 @@ module Rigor
|
|
|
76
76
|
return nil unless receiver.is_a?(Type::BoundMethod)
|
|
77
77
|
return nil unless backward_method?(method_name)
|
|
78
78
|
|
|
79
|
+
# `Method#curry` is treated as identity on the carrier
|
|
80
|
+
# — `<bound>.curry` keeps the same
|
|
81
|
+
# `(receiver_type, method_name)` so a subsequent
|
|
82
|
+
# `<curried>.call` still routes through the recursive
|
|
83
|
+
# dispatch below. This is correct for the dominant
|
|
84
|
+
# no-arg form (`.curry.call`); partially-applied
|
|
85
|
+
# forms (`.curry(n).call(a)`) lose precision and fall
|
|
86
|
+
# through to RBS via the trailing
|
|
87
|
+
# `Type::Combinator.untyped`. A faithful
|
|
88
|
+
# `Type::CurriedBoundMethod(receiver_type,
|
|
89
|
+
# method_name, accumulated_args)` carrier is reserved
|
|
90
|
+
# for a future slice when concrete user demand
|
|
91
|
+
# surfaces.
|
|
92
|
+
return receiver if method_name == :curry
|
|
93
|
+
|
|
79
94
|
MethodDispatcher.dispatch(
|
|
80
95
|
receiver_type: receiver.receiver_type,
|
|
81
96
|
method_name: receiver.method_name,
|
|
@@ -92,7 +107,9 @@ module Rigor
|
|
|
92
107
|
# commonly used as a case-equality predicate, so we
|
|
93
108
|
# do NOT fold through it (the case/when narrowing path
|
|
94
109
|
# already special-cases `===` for branch typing).
|
|
95
|
-
|
|
110
|
+
# `Method#curry` rides through as identity (see the
|
|
111
|
+
# comment in `try_backward`).
|
|
112
|
+
BACKWARD_METHOD_NAMES = %i[call [] curry].freeze
|
|
96
113
|
private_constant :BACKWARD_METHOD_NAMES
|
|
97
114
|
|
|
98
115
|
def backward_method?(method_name)
|
|
@@ -26,7 +26,7 @@ module Rigor
|
|
|
26
26
|
# `Array#[](Range) -> Array[Elem]?` overload for a Range
|
|
27
27
|
# argument. (Surfaced during v0.1.1 self-analysis; see the
|
|
28
28
|
# "Interface-strictness on overload selection" item in
|
|
29
|
-
# `docs/
|
|
29
|
+
# `docs/ROADMAP.md`.)
|
|
30
30
|
# 3. **Pass 2 — gradual fall-back.** If no fully strict overload
|
|
31
31
|
# matches, accept the first arity-and-gradual-accept match
|
|
32
32
|
# (the v0.1.1 behaviour). Alias / Interface / Intersection
|
|
@@ -155,13 +155,13 @@ module Rigor
|
|
|
155
155
|
# tier ahead of RBS sees the more precise carrier so
|
|
156
156
|
# downstream narrowing (`if size > 0; …`) actually has a
|
|
157
157
|
# range to intersect with.
|
|
158
|
-
SIZE_RETURNING_NOMINALS = {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
158
|
+
SIZE_RETURNING_NOMINALS = Ractor.make_shareable({
|
|
159
|
+
"Array" => %i[size length count],
|
|
160
|
+
"String" => %i[length size bytesize],
|
|
161
|
+
"Hash" => %i[size length count],
|
|
162
|
+
"Set" => %i[size length count],
|
|
163
|
+
"Range" => %i[size length count]
|
|
164
|
+
})
|
|
165
165
|
private_constant :SIZE_RETURNING_NOMINALS
|
|
166
166
|
|
|
167
167
|
# When the difference removes the empty value of the
|
|
@@ -323,39 +323,45 @@ module Rigor
|
|
|
323
323
|
# `dispatch_nominal_size` so size-returning calls on
|
|
324
324
|
# a `Refined[String, *]` still tighten to
|
|
325
325
|
# `non_negative_int`.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
326
|
+
# ADR-15 Phase 4b.x — `Ractor.make_shareable` (not `.freeze`)
|
|
327
|
+
# because the keys are two-element Symbol arrays whose
|
|
328
|
+
# inner arrays are unfrozen under shallow `.freeze`.
|
|
329
|
+
# Surfaced on Discourse via `Ractor::IsolationError` when
|
|
330
|
+
# the dispatch loop's `REFINED_STRING_PROJECTIONS[[id, sym]]`
|
|
331
|
+
# lookup ran from a worker Ractor.
|
|
332
|
+
REFINED_STRING_PROJECTIONS = Ractor.make_shareable({
|
|
333
|
+
%i[lowercase downcase] => :refined_self,
|
|
334
|
+
%i[lowercase upcase] => :uppercase_string,
|
|
335
|
+
%i[uppercase upcase] => :refined_self,
|
|
336
|
+
%i[uppercase downcase] => :lowercase_string,
|
|
337
|
+
%i[numeric downcase] => :refined_self,
|
|
338
|
+
%i[numeric upcase] => :refined_self,
|
|
339
|
+
# Digit-only strings are case-invariant; the prefix
|
|
340
|
+
# letters in `0o…` / `0x…` are accepted by the
|
|
341
|
+
# predicate in either case so the predicate-subset
|
|
342
|
+
# is preserved across `#downcase` / `#upcase` even
|
|
343
|
+
# though the value-set element changes.
|
|
344
|
+
%i[decimal_int downcase] => :refined_self,
|
|
345
|
+
%i[decimal_int upcase] => :refined_self,
|
|
346
|
+
%i[octal_int downcase] => :refined_self,
|
|
347
|
+
%i[octal_int upcase] => :refined_self,
|
|
348
|
+
%i[hex_int downcase] => :refined_self,
|
|
349
|
+
%i[hex_int upcase] => :refined_self,
|
|
350
|
+
# v0.1.1 Track 1 slice 2 — `to_i` / `to_int` on a
|
|
351
|
+
# known digit-only string. `decimal-int-string`
|
|
352
|
+
# (`/\A\d+\z/`) and `numeric-string` (Rigor's
|
|
353
|
+
# numeric-string predicate, ASCII digits) are
|
|
354
|
+
# predicates over digit-only strings, so the parse
|
|
355
|
+
# is total over the carrier domain and the result
|
|
356
|
+
# is always `>= 0`. `non-negative-int` is the
|
|
357
|
+
# tightest carrier that captures both the lower
|
|
358
|
+
# bound and the integer-ness without inventing a
|
|
359
|
+
# narrower carrier.
|
|
360
|
+
%i[decimal_int to_i] => :non_negative_int,
|
|
361
|
+
%i[decimal_int to_int] => :non_negative_int,
|
|
362
|
+
%i[numeric to_i] => :non_negative_int,
|
|
363
|
+
%i[numeric to_int] => :non_negative_int
|
|
364
|
+
})
|
|
359
365
|
private_constant :REFINED_STRING_PROJECTIONS
|
|
360
366
|
|
|
361
367
|
def dispatch_refined(refined, method_name, args)
|