rigortype 0.1.4 → 0.1.6
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 +69 -56
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +11 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/fact_store.rb +15 -3
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +681 -19
- data/lib/rigor/analysis/worker_session.rb +339 -0
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/imported_refinements.rb +6 -2
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/builtins/static_return_refinements.rb +120 -0
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +72 -9
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +122 -10
- data/lib/rigor/configuration.rb +168 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- 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 +238 -7
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +179 -10
- data/lib/rigor/inference/acceptance.rb +83 -4
- 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 +59 -2
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- 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 +126 -31
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
- data/lib/rigor/inference/method_dispatcher.rb +282 -6
- data/lib/rigor/inference/method_parameter_binder.rb +21 -11
- data/lib/rigor/inference/narrowing.rb +127 -8
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +156 -12
- data/lib/rigor/inference/statement_evaluator.rb +106 -6
- 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 +599 -0
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -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 +315 -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 +127 -9
- data/lib/rigor/plugin/registry.rb +51 -2
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/app.rb +107 -0
- 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.rb +1 -0
- 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 +11 -4
- data/sig/rigor/inference.rbs +2 -0
- 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 +37 -2
- metadata +92 -1
data/lib/rigor/environment.rb
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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/reporters"
|
|
7
|
+
require_relative "environment/hkt_registry_holder"
|
|
8
|
+
require_relative "environment/bundle_sig_discovery"
|
|
9
|
+
require_relative "environment/lockfile_resolver"
|
|
10
|
+
require_relative "environment/rbs_collection_discovery"
|
|
11
|
+
require_relative "environment/rbs_coverage_report"
|
|
12
|
+
require_relative "inference/synthetic_method_index"
|
|
13
|
+
require_relative "inference/project_patched_methods"
|
|
14
|
+
require_relative "inference/hkt_registry"
|
|
15
|
+
require_relative "builtins/hkt_builtins"
|
|
5
16
|
require_relative "type_node/name_scope"
|
|
6
17
|
require_relative "type_node/resolver_chain"
|
|
7
18
|
|
|
@@ -40,11 +51,19 @@ module Rigor
|
|
|
40
51
|
pathname optparse json yaml fileutils tempfile tmpdir
|
|
41
52
|
stringio forwardable digest securerandom
|
|
42
53
|
uri logger date
|
|
54
|
+
pp delegate observable abbrev find tsort singleton
|
|
55
|
+
shellwords benchmark base64 did_you_mean
|
|
56
|
+
monitor mutex_m timeout
|
|
57
|
+
open3 erb etc ipaddr bigdecimal bigdecimal-math
|
|
58
|
+
prettyprint random-formatter time open-uri resolv
|
|
59
|
+
csv pstore objspace io-console cgi cgi-escape
|
|
60
|
+
strscan
|
|
43
61
|
prism rbs
|
|
44
62
|
].freeze
|
|
45
63
|
|
|
46
64
|
attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
|
|
47
|
-
:
|
|
65
|
+
:reporters, :name_scope,
|
|
66
|
+
:synthetic_method_index, :project_patched_methods
|
|
48
67
|
|
|
49
68
|
# @param class_registry [Rigor::Environment::ClassRegistry]
|
|
50
69
|
# @param rbs_loader [Rigor::Environment::RbsLoader, nil] when nil the
|
|
@@ -65,22 +84,105 @@ module Rigor
|
|
|
65
84
|
# sources the dispatcher consults BELOW RBS dispatch.
|
|
66
85
|
# When nil (the default), no dep-source contribution
|
|
67
86
|
# participates and the dispatcher tier is a no-op.
|
|
68
|
-
def initialize(class_registry: ClassRegistry.default, rbs_loader: nil,
|
|
87
|
+
def initialize(class_registry: ClassRegistry.default, rbs_loader: nil, # rubocop:disable Metrics/ParameterLists
|
|
69
88
|
plugin_registry: nil, dependency_source_index: nil,
|
|
70
|
-
rbs_extended_reporter: nil, boundary_cross_reporter: nil
|
|
89
|
+
rbs_extended_reporter: nil, boundary_cross_reporter: nil,
|
|
90
|
+
synthetic_method_index: nil, project_patched_methods: nil,
|
|
91
|
+
hkt_registry: nil)
|
|
71
92
|
@class_registry = class_registry
|
|
72
93
|
@rbs_loader = rbs_loader
|
|
73
94
|
@plugin_registry = plugin_registry
|
|
74
95
|
@dependency_source_index = dependency_source_index
|
|
75
|
-
|
|
76
|
-
|
|
96
|
+
# ADR-pending — reporters live in a mutable container so
|
|
97
|
+
# long-lived integrations (LSP `ProjectContext`) can swap
|
|
98
|
+
# them per `Runner.run` without rebuilding the env. The
|
|
99
|
+
# existing `#rbs_extended_reporter` / `#boundary_cross_reporter`
|
|
100
|
+
# accessors below preserve the public lookup shape.
|
|
101
|
+
@reporters = Reporters.new(
|
|
102
|
+
rbs_extended: rbs_extended_reporter,
|
|
103
|
+
boundary_cross: boundary_cross_reporter
|
|
104
|
+
)
|
|
105
|
+
@synthetic_method_index = synthetic_method_index || Inference::SyntheticMethodIndex::EMPTY
|
|
106
|
+
@project_patched_methods = project_patched_methods || Inference::ProjectPatchedMethods::EMPTY
|
|
107
|
+
# ADR-20 slice 2c + 2e — the per-env HKT registry
|
|
108
|
+
# consulted by the reducer when resolving `Type::App`
|
|
109
|
+
# carriers. Defaults to {Inference::HktRegistry::EMPTY};
|
|
110
|
+
# the {.default} / {.for_project} class methods seed it
|
|
111
|
+
# with the bundled builtins (`json::value`, …) plus any
|
|
112
|
+
# `%a{rigor:v1:hkt_register / hkt_define}` annotations
|
|
113
|
+
# the RBS loader exposes. The hkt_registry getter
|
|
114
|
+
# (defined below) MEMOIZES the result of merging the
|
|
115
|
+
# base with the RBS scan so the scan is paid at most
|
|
116
|
+
# once per Environment lifetime — and only when first
|
|
117
|
+
# consulted, leaving fast paths like `rigor check
|
|
118
|
+
# --cache-stats --no-stats` from doing the RBS env
|
|
119
|
+
# build at all.
|
|
120
|
+
@hkt_registry_base = hkt_registry || Inference::HktRegistry::EMPTY
|
|
121
|
+
@hkt_registry_holder = HktRegistryHolder.new
|
|
77
122
|
@name_scope = build_name_scope
|
|
78
123
|
freeze
|
|
79
124
|
end
|
|
80
125
|
|
|
126
|
+
# ADR-20 slices 2e + 6 — lazy HKT registry getter.
|
|
127
|
+
# Merge order on first call: builtins (base) ← plugin
|
|
128
|
+
# manifest aggregation ← RBS env scan. Last-write-wins on
|
|
129
|
+
# URI collisions so user-authored `.rbs` overlays beat
|
|
130
|
+
# plugin entries, which beat the bundled JSON_VALUE.
|
|
131
|
+
# Memoised; single-threaded use only (under the Ractor
|
|
132
|
+
# pool path each worker has its own Environment so
|
|
133
|
+
# cross-worker mutation is impossible; the LSP
|
|
134
|
+
# single-publish-at-a-time invariant serialises here).
|
|
135
|
+
def hkt_registry
|
|
136
|
+
@hkt_registry_holder.fetch do
|
|
137
|
+
with_plugin_overlay = if @plugin_registry.respond_to?(:hkt_overlay_registry)
|
|
138
|
+
@hkt_registry_base.merge(@plugin_registry.hkt_overlay_registry)
|
|
139
|
+
else
|
|
140
|
+
@hkt_registry_base
|
|
141
|
+
end
|
|
142
|
+
Inference::HktRegistry.scan_rbs_loader(
|
|
143
|
+
@rbs_loader,
|
|
144
|
+
base: with_plugin_overlay,
|
|
145
|
+
reporter: rbs_extended_reporter
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Backwards-compatible reporter accessors — every existing
|
|
151
|
+
# consumer (rbs_extended, method_dispatcher) calls these. The
|
|
152
|
+
# frozen `@reporters` container is mutable for slot reassignment
|
|
153
|
+
# via {#attach_reporters!} below.
|
|
154
|
+
def rbs_extended_reporter
|
|
155
|
+
@reporters.rbs_extended
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def boundary_cross_reporter
|
|
159
|
+
@reporters.boundary_cross
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Replaces the env's per-run reporter slots. Intended for
|
|
163
|
+
# long-lived integrations (LSP `ProjectContext`) that share one
|
|
164
|
+
# Environment instance across many `Runner.run` calls: each call
|
|
165
|
+
# attaches its own fresh reporter pair so per-call diagnostic
|
|
166
|
+
# events stay scoped to that call rather than accumulating
|
|
167
|
+
# across publishes.
|
|
168
|
+
#
|
|
169
|
+
# Single-threaded use only. Concurrent publishes against one
|
|
170
|
+
# Environment must serialise — the LSP `Server` debouncer +
|
|
171
|
+
# synchronized writer already enforces this for the editor
|
|
172
|
+
# path. The Ractor pool path builds a per-worker Environment
|
|
173
|
+
# and does not reach this surface.
|
|
174
|
+
def attach_reporters!(rbs_extended_reporter:, boundary_cross_reporter:)
|
|
175
|
+
@reporters.rbs_extended = rbs_extended_reporter
|
|
176
|
+
@reporters.boundary_cross = boundary_cross_reporter
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
81
180
|
class << self
|
|
82
181
|
def default
|
|
83
|
-
@default ||= new(
|
|
182
|
+
@default ||= new(
|
|
183
|
+
rbs_loader: RbsLoader.default,
|
|
184
|
+
hkt_registry: Builtins::HktBuiltins.registry
|
|
185
|
+
).freeze
|
|
84
186
|
end
|
|
85
187
|
|
|
86
188
|
# Builds an Environment that consults the project's local
|
|
@@ -104,24 +206,80 @@ module Rigor
|
|
|
104
206
|
# reflection artefacts) consult the cache. Pass `nil` (the
|
|
105
207
|
# default) to skip caching for this environment.
|
|
106
208
|
# @return [Rigor::Environment]
|
|
107
|
-
|
|
209
|
+
# rubocop:disable Metrics/MethodLength, Metrics/ParameterLists
|
|
210
|
+
def for_project(root: Dir.pwd, libraries: [], signature_paths: nil, cache_store: nil,
|
|
108
211
|
plugin_registry: nil, dependency_source_index: nil,
|
|
109
|
-
rbs_extended_reporter: nil, boundary_cross_reporter: nil
|
|
212
|
+
rbs_extended_reporter: nil, boundary_cross_reporter: nil,
|
|
213
|
+
bundler_bundle_path: nil, bundler_auto_detect: false,
|
|
214
|
+
bundler_lockfile: nil,
|
|
215
|
+
rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
|
|
216
|
+
synthetic_method_index: nil, project_patched_methods: nil)
|
|
110
217
|
resolved_paths = signature_paths || default_signature_paths(root)
|
|
218
|
+
# O4 MVP — append per-gem `sig/` directories discovered
|
|
219
|
+
# under the target project's bundler install root. Empty
|
|
220
|
+
# array when neither an explicit path nor auto-detection
|
|
221
|
+
# finds a bundle. Order: user `signature_paths:` win first
|
|
222
|
+
# (semantic precedence inside `RbsLoader.build_env_for`);
|
|
223
|
+
# gem-shipped sigs append last so user overrides stay
|
|
224
|
+
# authoritative.
|
|
225
|
+
#
|
|
226
|
+
# O4 Layer 3 — when a Gemfile.lock is available (explicit
|
|
227
|
+
# `bundler_lockfile:` or auto-detected next to the project
|
|
228
|
+
# root), use the locked gem set to filter the discovered
|
|
229
|
+
# `sig/` directories. Stale gems in the bundle install
|
|
230
|
+
# tree (out-of-band installs, version drift after a
|
|
231
|
+
# `bundle update`) are silently dropped so only gems the
|
|
232
|
+
# project actually declares contribute RBS.
|
|
233
|
+
locked = LockfileResolver.locked_gems(
|
|
234
|
+
lockfile_path: bundler_lockfile,
|
|
235
|
+
project_root: root,
|
|
236
|
+
auto_detect: bundler_auto_detect
|
|
237
|
+
)
|
|
238
|
+
gem_sig_paths = BundleSigDiscovery.discover(
|
|
239
|
+
bundle_path: bundler_bundle_path,
|
|
240
|
+
project_root: root,
|
|
241
|
+
auto_detect: bundler_auto_detect,
|
|
242
|
+
locked_gems: locked.empty? ? nil : locked
|
|
243
|
+
).map(&:to_s)
|
|
244
|
+
# O4 Layer 3 slice 2 — when `rbs collection install`
|
|
245
|
+
# has been run for the target project, parse the
|
|
246
|
+
# resulting `rbs_collection.lock.yaml` and feed each
|
|
247
|
+
# gem's `<collection_path>/<name>/<version>/` directory
|
|
248
|
+
# into `signature_paths:`. Stdlib-typed entries are
|
|
249
|
+
# skipped (already covered by `DEFAULT_LIBRARIES`).
|
|
250
|
+
collection_paths = RbsCollectionDiscovery.discover(
|
|
251
|
+
lockfile_path: rbs_collection_lockfile,
|
|
252
|
+
project_root: root,
|
|
253
|
+
auto_detect: rbs_collection_auto_detect
|
|
254
|
+
).map(&:to_s)
|
|
255
|
+
loader_signature_paths = resolved_paths + gem_sig_paths + collection_paths
|
|
111
256
|
merged_libraries = (DEFAULT_LIBRARIES + libraries.map(&:to_s)).uniq
|
|
112
257
|
loader = RbsLoader.new(
|
|
113
258
|
libraries: merged_libraries,
|
|
114
|
-
signature_paths:
|
|
259
|
+
signature_paths: loader_signature_paths,
|
|
115
260
|
cache_store: cache_store
|
|
116
261
|
)
|
|
262
|
+
# ADR-20 slice 2c + 2e — seed hkt_registry with the
|
|
263
|
+
# bundled builtins. The Environment's `#hkt_registry`
|
|
264
|
+
# getter then LAZILY merges in the RBS env scan on
|
|
265
|
+
# first call so fast paths that don't consult HKT
|
|
266
|
+
# (e.g. `rigor check --cache-stats --no-stats`) don't
|
|
267
|
+
# pay the eager env-build cost up front. URI
|
|
268
|
+
# collisions let the user-authored overlay win over
|
|
269
|
+
# the bundled builtin (last-write-wins per ADR-20
|
|
270
|
+
# OQ3 tentative).
|
|
117
271
|
new(
|
|
118
272
|
rbs_loader: loader,
|
|
119
273
|
plugin_registry: plugin_registry,
|
|
120
274
|
dependency_source_index: dependency_source_index,
|
|
121
275
|
rbs_extended_reporter: rbs_extended_reporter,
|
|
122
|
-
boundary_cross_reporter: boundary_cross_reporter
|
|
276
|
+
boundary_cross_reporter: boundary_cross_reporter,
|
|
277
|
+
synthetic_method_index: synthetic_method_index,
|
|
278
|
+
project_patched_methods: project_patched_methods,
|
|
279
|
+
hkt_registry: Builtins::HktBuiltins.registry
|
|
123
280
|
)
|
|
124
281
|
end
|
|
282
|
+
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
|
|
125
283
|
|
|
126
284
|
private
|
|
127
285
|
|
|
@@ -185,6 +343,17 @@ module Rigor
|
|
|
185
343
|
class_known_in_rbs?(name)
|
|
186
344
|
end
|
|
187
345
|
|
|
346
|
+
# ADR-15 Phase 2b — returns the loader's read-only,
|
|
347
|
+
# `Ractor.shareable?` query surface as a frozen
|
|
348
|
+
# {Environment::Reflection}. Built lazily on first
|
|
349
|
+
# access; subsequent calls return the same instance.
|
|
350
|
+
# Returns `nil` when the environment carries no RBS
|
|
351
|
+
# loader (test-only `Environment.new` without
|
|
352
|
+
# `rbs_loader:`).
|
|
353
|
+
def reflection
|
|
354
|
+
@rbs_loader&.reflection
|
|
355
|
+
end
|
|
356
|
+
|
|
188
357
|
# Compares two class/module names using analyzer-owned class data.
|
|
189
358
|
# Returns `:equal`, `:subclass`, `:superclass`, `:disjoint`, or
|
|
190
359
|
# `: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)
|
|
@@ -412,13 +446,45 @@ module Rigor
|
|
|
412
446
|
|
|
413
447
|
def accepts_nominal_from_constant(self_type, constant, mode)
|
|
414
448
|
ruby_class = resolve_class(self_type.class_name)
|
|
415
|
-
if ruby_class
|
|
416
|
-
|
|
449
|
+
return constant_is_a_result(ruby_class, constant, self_type, mode) if ruby_class
|
|
450
|
+
|
|
451
|
+
# The host process may not have required the constant's
|
|
452
|
+
# declared self_type (e.g. `BigDecimal` since Ruby 3.4
|
|
453
|
+
# is no longer a default gem). Fall back to inspecting
|
|
454
|
+
# the value's own class ancestor chain — always loadable
|
|
455
|
+
# because the value already exists. Required for
|
|
456
|
+
# OverloadSelector to reject `Integer#+(BigDecimal) ->
|
|
457
|
+
# BigDecimal` overloads contributed by `bigdecimal`'s
|
|
458
|
+
# RBS reopening when the actual arg is a Constant<Integer>.
|
|
459
|
+
ancestor_names = constant.value.class.ancestors.map(&:name)
|
|
460
|
+
if ancestor_names.include?(self_type.class_name)
|
|
461
|
+
Type::AcceptsResult.yes(
|
|
417
462
|
mode: mode,
|
|
418
|
-
reasons: "class #{self_type.class_name}
|
|
463
|
+
reasons: "Constant value class ancestors include #{self_type.class_name}"
|
|
464
|
+
)
|
|
465
|
+
else
|
|
466
|
+
Type::AcceptsResult.no(
|
|
467
|
+
mode: mode,
|
|
468
|
+
reasons: "Constant value class ancestors exclude #{self_type.class_name}"
|
|
469
|
+
)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def subtype_result_via_ancestors(actual_class, target_name, mode)
|
|
474
|
+
if actual_class.ancestors.map(&:name).include?(target_name)
|
|
475
|
+
Type::AcceptsResult.yes(
|
|
476
|
+
mode: mode,
|
|
477
|
+
reasons: "#{actual_class.name} ancestors include #{target_name}"
|
|
478
|
+
)
|
|
479
|
+
else
|
|
480
|
+
Type::AcceptsResult.no(
|
|
481
|
+
mode: mode,
|
|
482
|
+
reasons: "#{actual_class.name} ancestors exclude #{target_name} (target unloadable)"
|
|
419
483
|
)
|
|
420
484
|
end
|
|
485
|
+
end
|
|
421
486
|
|
|
487
|
+
def constant_is_a_result(ruby_class, constant, self_type, mode)
|
|
422
488
|
if constant.value.is_a?(ruby_class)
|
|
423
489
|
Type::AcceptsResult.yes(mode: mode, reasons: "Constant value is_a?(#{self_type.class_name})")
|
|
424
490
|
else
|
|
@@ -760,6 +826,19 @@ module Rigor
|
|
|
760
826
|
|
|
761
827
|
target_class = resolve_class(target_name)
|
|
762
828
|
actual_class = resolve_class(actual_name)
|
|
829
|
+
# When only `actual` resolves, we can still rule out
|
|
830
|
+
# `actual <:= target` by inspecting `actual`'s ancestor
|
|
831
|
+
# chain. The canonical case: `target=BigDecimal` is not
|
|
832
|
+
# loadable in the host process (no `require` in rigor's
|
|
833
|
+
# own runtime), but `actual=Integer` IS, and Integer's
|
|
834
|
+
# ancestors do not include `BigDecimal`, so the subtype
|
|
835
|
+
# relation MUST be `:no` rather than the conservative
|
|
836
|
+
# `:maybe`. The reverse asymmetry (target resolves,
|
|
837
|
+
# actual doesn't) does not let us conclude anything —
|
|
838
|
+
# the unloaded `actual` could be an unrelated class or
|
|
839
|
+
# a subclass of `target` we can't see, so we still
|
|
840
|
+
# answer `:maybe` there.
|
|
841
|
+
return subtype_result_via_ancestors(actual_class, target_name, mode) if target_class.nil? && actual_class
|
|
763
842
|
if target_class.nil? || actual_class.nil?
|
|
764
843
|
return Type::AcceptsResult.maybe(
|
|
765
844
|
mode: mode,
|
|
@@ -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
|
|
@@ -60,6 +61,10 @@ module Rigor
|
|
|
60
61
|
Prism::RationalNode => :type_of_literal_value,
|
|
61
62
|
Prism::SymbolNode => :symbol_type_for,
|
|
62
63
|
Prism::StringNode => :string_type_for,
|
|
64
|
+
Prism::XStringNode => :type_of_xstring,
|
|
65
|
+
Prism::InterpolatedXStringNode => :type_of_xstring,
|
|
66
|
+
Prism::SourceFileNode => :type_of_source_file,
|
|
67
|
+
Prism::SourceLineNode => :type_of_source_line,
|
|
63
68
|
Prism::TrueNode => :type_of_true,
|
|
64
69
|
Prism::FalseNode => :type_of_false,
|
|
65
70
|
Prism::NilNode => :type_of_nil,
|
|
@@ -143,6 +148,9 @@ module Rigor
|
|
|
143
148
|
Prism::AliasMethodNode => :type_of_nil_value,
|
|
144
149
|
Prism::AliasGlobalVariableNode => :type_of_nil_value,
|
|
145
150
|
Prism::UndefNode => :type_of_nil_value,
|
|
151
|
+
Prism::PostExecutionNode => :type_of_nil_value,
|
|
152
|
+
Prism::ShareableConstantNode => :type_of_shareable_constant,
|
|
153
|
+
Prism::ImplicitNode => :type_of_implicit,
|
|
146
154
|
Prism::ForwardingSuperNode => :type_of_dynamic_top,
|
|
147
155
|
Prism::BlockArgumentNode => :type_of_non_value,
|
|
148
156
|
# Parameters and blocks (non-value positions)
|
|
@@ -158,6 +166,7 @@ module Rigor
|
|
|
158
166
|
Prism::ForwardingParameterNode => :type_of_non_value,
|
|
159
167
|
Prism::NoKeywordsParameterNode => :type_of_non_value,
|
|
160
168
|
Prism::ImplicitRestNode => :type_of_non_value,
|
|
169
|
+
Prism::ItParametersNode => :type_of_non_value,
|
|
161
170
|
Prism::BlockNode => :type_of_dynamic_top,
|
|
162
171
|
Prism::SplatNode => :type_of_non_value,
|
|
163
172
|
# Control flow (Slice 3 phase 1): branch types are unioned, jumps
|
|
@@ -887,6 +896,45 @@ module Rigor
|
|
|
887
896
|
Type::Combinator.constant_of(unescaped)
|
|
888
897
|
end
|
|
889
898
|
|
|
899
|
+
# Backtick (`cmd`) and `%x{cmd}` invoke Kernel#` and always return a
|
|
900
|
+
# String. Even when the content is statically known, we widen to
|
|
901
|
+
# Nominal[String] because the runtime value depends on the
|
|
902
|
+
# subprocess output, not the source text.
|
|
903
|
+
def type_of_xstring(_node)
|
|
904
|
+
Type::Combinator.nominal_of(String)
|
|
905
|
+
end
|
|
906
|
+
|
|
907
|
+
# __FILE__ is the source file path. Always non-empty when
|
|
908
|
+
# parsing a real file (the path resolver gives the buffer
|
|
909
|
+
# name, which is at minimum `"(stdin)"` / `"-e"` / a real
|
|
910
|
+
# path — never the empty String). Widened to
|
|
911
|
+
# `non-empty-string` instead of `Nominal[String]` so
|
|
912
|
+
# downstream String-emptiness checks know the value cannot
|
|
913
|
+
# be `""`.
|
|
914
|
+
def type_of_source_file(_node)
|
|
915
|
+
Type::Combinator.non_empty_string
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
# __LINE__ is the line of the source literal. Ruby line
|
|
919
|
+
# numbers are 1-indexed, so `__LINE__` is always at least
|
|
920
|
+
# 1 — `positive-int` (Integer in `[1, +Inf)`) is the
|
|
921
|
+
# canonical refinement.
|
|
922
|
+
def type_of_source_line(_node)
|
|
923
|
+
Type::Combinator.positive_int
|
|
924
|
+
end
|
|
925
|
+
|
|
926
|
+
# `# shareable_constant_value:` magic comment wraps the next
|
|
927
|
+
# constant write. Type is the wrapped write's value.
|
|
928
|
+
def type_of_shareable_constant(node)
|
|
929
|
+
type_of(node.write)
|
|
930
|
+
end
|
|
931
|
+
|
|
932
|
+
# `{ x: }` shorthand hash. The implicit value is the call to
|
|
933
|
+
# `x` (or a local read of `x`). Delegate.
|
|
934
|
+
def type_of_implicit(node)
|
|
935
|
+
type_of(node.value)
|
|
936
|
+
end
|
|
937
|
+
|
|
890
938
|
def local_read(node)
|
|
891
939
|
scope.local(node.name) || dynamic_top
|
|
892
940
|
end
|
|
@@ -1194,16 +1242,25 @@ module Rigor
|
|
|
1194
1242
|
arg_types: arg_types,
|
|
1195
1243
|
environment: scope.environment
|
|
1196
1244
|
)
|
|
1197
|
-
|
|
1245
|
+
# ADR-16 Tier A: when a registered plugin's `block_as_methods`
|
|
1246
|
+
# entry matches `(receiver_type, call_node.name)`, narrow the
|
|
1247
|
+
# block body's `self_type` to the receiver class's instance
|
|
1248
|
+
# type. The narrowing is `nil` for unmatched calls, leaving
|
|
1249
|
+
# the existing scope contract unchanged.
|
|
1250
|
+
narrowed_self = MacroBlockSelfType.narrow_self_type_for(
|
|
1251
|
+
scope: scope, call_node: call_node, receiver_type: receiver_type
|
|
1252
|
+
)
|
|
1253
|
+
block_return_for(block_arg, expected, narrowed_self_type: narrowed_self)
|
|
1198
1254
|
rescue StandardError
|
|
1199
1255
|
nil
|
|
1200
1256
|
end
|
|
1201
1257
|
|
|
1202
|
-
def block_return_for(block_arg, expected)
|
|
1258
|
+
def block_return_for(block_arg, expected, narrowed_self_type: nil)
|
|
1203
1259
|
case block_arg
|
|
1204
1260
|
when Prism::BlockNode
|
|
1205
1261
|
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_arg)
|
|
1206
1262
|
block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
1263
|
+
block_scope = block_scope.with_self_type(narrowed_self_type) if narrowed_self_type
|
|
1207
1264
|
type_block_body(block_arg, block_scope)
|
|
1208
1265
|
when Prism::BlockArgumentNode
|
|
1209
1266
|
symbol_block_return_type(block_arg, expected)
|