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
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hkt_body"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# ADR-20 Slice 2a — reducer that walks a `Definition`'s
|
|
8
|
+
# `body_tree` against a concrete `Type::App` and returns a
|
|
9
|
+
# fully-typed `Rigor::Type`.
|
|
10
|
+
#
|
|
11
|
+
# Reduction is the operational interpretation of ADR-20
|
|
12
|
+
# § D4 ("Evaluation rules"):
|
|
13
|
+
#
|
|
14
|
+
# 1. **Resolve `F`.** Look up the registered body via
|
|
15
|
+
# `registry.definition(uri)`.
|
|
16
|
+
# 2. **Substitute arguments.** Walk the body tree, replacing
|
|
17
|
+
# `{HktBody::Param}` nodes with the matching positional
|
|
18
|
+
# arg from the application.
|
|
19
|
+
# 3. **Build types.** `{HktBody::TypeLeaf}` returns its
|
|
20
|
+
# wrapped type as-is; `{HktBody::Union}` and
|
|
21
|
+
# `{HktBody::NominalApp}` route their reduced children
|
|
22
|
+
# through `Type::Combinator.union` / `.nominal_of` so
|
|
23
|
+
# normalization applies.
|
|
24
|
+
# 4. **Recurse on `{HktBody::AppRef}` nodes.** Reduce the
|
|
25
|
+
# args first; if the resulting `(uri, args)` matches an
|
|
26
|
+
# App already on the current reduction stack, return the
|
|
27
|
+
# in-progress `Type::App` carrier as-is (lazy
|
|
28
|
+
# self-reference handling — the standard "tying the
|
|
29
|
+
# knot" trick for recursive type aliases like
|
|
30
|
+
# `json::value`). Otherwise build a fresh
|
|
31
|
+
# `Type::App` and recursively reduce it against the
|
|
32
|
+
# same registry, sharing the fuel budget.
|
|
33
|
+
# 5. **Fuel budget.** Each visited node consumes one unit.
|
|
34
|
+
# On exhaustion, reduction unwinds to `app.bound`.
|
|
35
|
+
#
|
|
36
|
+
# The reducer is **pure** with respect to its inputs (the
|
|
37
|
+
# registry + the App) but uses a per-call mutable state
|
|
38
|
+
# bag for fuel + cycle tracking. Concurrent reductions
|
|
39
|
+
# MUST allocate fresh reducers (or fresh `_reduce` calls)
|
|
40
|
+
# — the per-call state is not shared.
|
|
41
|
+
class HktReducer
|
|
42
|
+
DEFAULT_FUEL = 64
|
|
43
|
+
|
|
44
|
+
class FuelExhausted < StandardError; end
|
|
45
|
+
|
|
46
|
+
def initialize(registry)
|
|
47
|
+
raise ArgumentError, "registry must be an HktRegistry" unless registry.is_a?(HktRegistry)
|
|
48
|
+
|
|
49
|
+
@registry = registry
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Reduce `app` against the registry.
|
|
53
|
+
#
|
|
54
|
+
# @param app [Rigor::Type::App]
|
|
55
|
+
# @param fuel [Integer] reduction-step budget (default 64
|
|
56
|
+
# per ADR-20 WD3). Each visited body node costs one
|
|
57
|
+
# unit. On exhaustion the reduction returns `app.bound`.
|
|
58
|
+
# @return [Rigor::Type] the reduced type, or `app.bound`
|
|
59
|
+
# when reduction is impossible (URI not defined, arity
|
|
60
|
+
# mismatch, body_tree absent, fuel exhausted).
|
|
61
|
+
def reduce(app, fuel: DEFAULT_FUEL)
|
|
62
|
+
raise ArgumentError, "expected a Rigor::Type::App, got #{app.class}" unless app.is_a?(Type::App)
|
|
63
|
+
|
|
64
|
+
definition = @registry.definition(app.uri)
|
|
65
|
+
return app.bound if definition.nil? || definition.body_tree.nil?
|
|
66
|
+
return app.bound if definition.params.size != app.args.size
|
|
67
|
+
|
|
68
|
+
state = State.new(fuel: fuel)
|
|
69
|
+
begin
|
|
70
|
+
state.with_in_progress(app.uri, app.args, app) do
|
|
71
|
+
walk(definition.body_tree, bindings: bindings_for(definition, app.args), state: state) || app.bound
|
|
72
|
+
end
|
|
73
|
+
rescue FuelExhausted
|
|
74
|
+
app.bound
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def bindings_for(definition, args)
|
|
81
|
+
definition.params.zip(args).to_h
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def walk(node, bindings:, state:)
|
|
85
|
+
state.consume_fuel!
|
|
86
|
+
|
|
87
|
+
case node
|
|
88
|
+
when HktBody::TypeLeaf
|
|
89
|
+
node.type
|
|
90
|
+
when HktBody::Param
|
|
91
|
+
bindings.fetch(node.name) do
|
|
92
|
+
raise ArgumentError, "unknown param #{node.name.inspect}; declared: #{bindings.keys}"
|
|
93
|
+
end
|
|
94
|
+
when HktBody::Union
|
|
95
|
+
reduced = node.arms.map { |arm| walk(arm, bindings: bindings, state: state) }
|
|
96
|
+
Type::Combinator.union(*reduced)
|
|
97
|
+
when HktBody::NominalApp
|
|
98
|
+
reduced_args = node.args.map { |arg| walk(arg, bindings: bindings, state: state) }
|
|
99
|
+
Type::Combinator.nominal_of(node.class_name, type_args: reduced_args)
|
|
100
|
+
when HktBody::AppRef
|
|
101
|
+
reduced_args = node.args.map { |arg| walk(arg, bindings: bindings, state: state) }
|
|
102
|
+
reduce_app_ref(node.uri, reduced_args, state: state)
|
|
103
|
+
when HktBody::Conditional
|
|
104
|
+
walk_conditional(node, bindings: bindings, state: state)
|
|
105
|
+
else
|
|
106
|
+
raise ArgumentError, "unknown body node: #{node.class}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# ADR-20 § D3 conditional reduction. Resolves the test
|
|
111
|
+
# against the current bindings and picks a branch:
|
|
112
|
+
#
|
|
113
|
+
# - test = `yes` → return the reduced `then_branch`.
|
|
114
|
+
# - test = `no` → return the reduced `else_branch`.
|
|
115
|
+
# - test = `maybe` → widen to the union of both
|
|
116
|
+
# reduced branches (per ADR-20 WD7).
|
|
117
|
+
def walk_conditional(node, bindings:, state:)
|
|
118
|
+
verdict = evaluate_test(node.test, bindings: bindings, state: state)
|
|
119
|
+
case verdict
|
|
120
|
+
when :yes
|
|
121
|
+
walk(node.then_branch, bindings: bindings, state: state)
|
|
122
|
+
when :no
|
|
123
|
+
walk(node.else_branch, bindings: bindings, state: state)
|
|
124
|
+
else
|
|
125
|
+
# `:maybe` — widen to the union of both branches.
|
|
126
|
+
then_t = walk(node.then_branch, bindings: bindings, state: state)
|
|
127
|
+
else_t = walk(node.else_branch, bindings: bindings, state: state)
|
|
128
|
+
Type::Combinator.union(then_t, else_t)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def evaluate_test(test, bindings:, state:)
|
|
133
|
+
case test
|
|
134
|
+
when HktBody::TestSubtype
|
|
135
|
+
left = walk(test.left, bindings: bindings, state: state)
|
|
136
|
+
right = walk(test.right, bindings: bindings, state: state)
|
|
137
|
+
subtype_verdict(left, right)
|
|
138
|
+
when HktBody::TestEquality
|
|
139
|
+
left = walk(test.left, bindings: bindings, state: state)
|
|
140
|
+
right = walk(test.right, bindings: bindings, state: state)
|
|
141
|
+
equality_verdict(left, right)
|
|
142
|
+
when HktBody::TestMembership
|
|
143
|
+
left = walk(test.left, bindings: bindings, state: state)
|
|
144
|
+
options = test.options.map { |o| walk(o, bindings: bindings, state: state) }
|
|
145
|
+
membership_verdict(left, options)
|
|
146
|
+
else
|
|
147
|
+
raise ArgumentError, "unknown test node: #{test.class}"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# `left <: right`. Slice 7a verdict policy: structural
|
|
152
|
+
# equality is `:yes`; clearly-disjoint nominal /
|
|
153
|
+
# constant pairs are `:no`; everything else is `:maybe`
|
|
154
|
+
# (widens to the union per ADR-20 WD7 — robustness
|
|
155
|
+
# principle keeps us conservative on undecided tests).
|
|
156
|
+
def subtype_verdict(left, right)
|
|
157
|
+
return :yes if left == right
|
|
158
|
+
|
|
159
|
+
# Both Nominals with different class names AND
|
|
160
|
+
# neither carries Dynamic — `:no` (statically
|
|
161
|
+
# disjoint). For any other shape pair, `:maybe`.
|
|
162
|
+
return :no if disjoint_nominals?(left, right)
|
|
163
|
+
return :no if disjoint_constants?(left, right)
|
|
164
|
+
|
|
165
|
+
:maybe
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Structural equality verdict — same shape as
|
|
169
|
+
# subtype_verdict but symmetric.
|
|
170
|
+
def equality_verdict(left, right)
|
|
171
|
+
return :yes if left == right
|
|
172
|
+
return :no if disjoint_nominals?(left, right)
|
|
173
|
+
return :no if disjoint_constants?(left, right)
|
|
174
|
+
|
|
175
|
+
:maybe
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def membership_verdict(left, options)
|
|
179
|
+
verdicts = options.map { |o| equality_verdict(left, o) }
|
|
180
|
+
return :yes if verdicts.include?(:yes)
|
|
181
|
+
return :no if verdicts.all? { |v| v == :no }
|
|
182
|
+
|
|
183
|
+
:maybe
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def disjoint_nominals?(left, right)
|
|
187
|
+
return false unless left.is_a?(Type::Nominal) && right.is_a?(Type::Nominal)
|
|
188
|
+
|
|
189
|
+
left.class_name != right.class_name
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def disjoint_constants?(left, right)
|
|
193
|
+
return false unless left.is_a?(Type::Constant) && right.is_a?(Type::Constant)
|
|
194
|
+
|
|
195
|
+
left.value != right.value
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def reduce_app_ref(uri, reduced_args, state:)
|
|
199
|
+
# Cycle detection — when the same `(uri, args)` is
|
|
200
|
+
# already on the reduction stack, return the
|
|
201
|
+
# in-progress App carrier as-is so recursive type
|
|
202
|
+
# aliases (`Array[App[json::value, K]]` inside the
|
|
203
|
+
# `json::value` body) terminate.
|
|
204
|
+
existing = state.in_progress_for(uri, reduced_args)
|
|
205
|
+
return existing if existing
|
|
206
|
+
|
|
207
|
+
registration = @registry.registration(uri)
|
|
208
|
+
bound = registration&.bound || Type::Combinator.untyped
|
|
209
|
+
new_app = Type::App.new(uri, reduced_args, bound: bound)
|
|
210
|
+
|
|
211
|
+
definition = @registry.definition(uri)
|
|
212
|
+
return new_app if definition.nil? || definition.body_tree.nil?
|
|
213
|
+
return new_app if definition.params.size != reduced_args.size
|
|
214
|
+
|
|
215
|
+
state.with_in_progress(uri, reduced_args, new_app) do
|
|
216
|
+
walk(definition.body_tree, bindings: bindings_for(definition, reduced_args), state: state) || new_app
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Per-call mutable bag carrying the remaining fuel
|
|
221
|
+
# budget and the active reduction stack (for cycle
|
|
222
|
+
# detection). Not shared across `reduce` calls.
|
|
223
|
+
class State
|
|
224
|
+
def initialize(fuel:)
|
|
225
|
+
@fuel = fuel
|
|
226
|
+
@in_progress = {}
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def consume_fuel!
|
|
230
|
+
raise FuelExhausted if @fuel <= 0
|
|
231
|
+
|
|
232
|
+
@fuel -= 1
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def in_progress_for(uri, args)
|
|
236
|
+
@in_progress[[uri, args]]
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def with_in_progress(uri, args, app)
|
|
240
|
+
key = [uri, args]
|
|
241
|
+
previous = @in_progress[key]
|
|
242
|
+
@in_progress[key] = app
|
|
243
|
+
begin
|
|
244
|
+
yield
|
|
245
|
+
ensure
|
|
246
|
+
if previous.nil?
|
|
247
|
+
@in_progress.delete(key)
|
|
248
|
+
else
|
|
249
|
+
@in_progress[key] = previous
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "hkt_body"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Inference
|
|
7
|
+
# ADR-20 § "Decision D1 / D2" — registry of Lightweight HKT
|
|
8
|
+
# tag registrations + type-function bodies parsed off the
|
|
9
|
+
# `%a{rigor:v1:hkt_register: ...}` /
|
|
10
|
+
# `%a{rigor:v1:hkt_define: ...}` annotations in shipped
|
|
11
|
+
# `.rbs` files.
|
|
12
|
+
#
|
|
13
|
+
# Slice 1 keeps the registry **opaque**: it stores the
|
|
14
|
+
# registration metadata (arity, variance, bound) and the
|
|
15
|
+
# un-evaluated definition body (a raw String — Slice 2
|
|
16
|
+
# introduces the conditional / indexed-access evaluator that
|
|
17
|
+
# parses the body and reduces `Type::App` instances against
|
|
18
|
+
# it). The carrier never needs to read from the registry
|
|
19
|
+
# because Slice 1's `Type::App` carries its `bound` directly;
|
|
20
|
+
# the registry exists at this slice solely so the parser
|
|
21
|
+
# round-trip and downstream slices have a stable target API.
|
|
22
|
+
#
|
|
23
|
+
# The registry is immutable after construction. Callers that
|
|
24
|
+
# need to extend it (e.g. plugin registrations layered on top
|
|
25
|
+
# of stdlib registrations) MUST build a new registry via
|
|
26
|
+
# `merge` rather than mutating an existing one. This keeps the
|
|
27
|
+
# registry shareable across Ractor boundaries per ADR-15.
|
|
28
|
+
class HktRegistry
|
|
29
|
+
# Frozen value object recording one tag registration.
|
|
30
|
+
#
|
|
31
|
+
# - `uri`: namespaced Symbol per ADR-20 WD1 (must include
|
|
32
|
+
# `"::"`).
|
|
33
|
+
# - `arity`: positive Integer — the number of formal
|
|
34
|
+
# parameters the registered constructor takes.
|
|
35
|
+
# - `variance`: ordered Array of Symbols, one per
|
|
36
|
+
# parameter, each `:out` (covariant), `:in`
|
|
37
|
+
# (contravariant), or `:inv` (invariant; default).
|
|
38
|
+
# - `bound`: a `Rigor::Type` to erase to when an `App`
|
|
39
|
+
# referring to this URI cannot be reduced. Defaults to
|
|
40
|
+
# `Dynamic[Top]` (the parser fills in the default when
|
|
41
|
+
# the annotation omits `bound:`).
|
|
42
|
+
Registration = Data.define(:uri, :arity, :variance, :bound) do
|
|
43
|
+
def initialize(uri:, arity:, variance:, bound:)
|
|
44
|
+
raise ArgumentError, "uri must be a Symbol, got #{uri.class}" unless uri.is_a?(Symbol)
|
|
45
|
+
raise ArgumentError, "uri must be namespaced as `:a::b`, got #{uri.inspect}" unless uri.to_s.include?("::")
|
|
46
|
+
unless arity.is_a?(Integer) && arity.positive?
|
|
47
|
+
raise ArgumentError,
|
|
48
|
+
"arity must be a positive Integer, got #{arity.inspect}"
|
|
49
|
+
end
|
|
50
|
+
raise ArgumentError, "variance must be an Array, got #{variance.class}" unless variance.is_a?(Array)
|
|
51
|
+
raise ArgumentError, "variance must have #{arity} entries, got #{variance.size}" unless variance.size == arity
|
|
52
|
+
|
|
53
|
+
variance.each do |v|
|
|
54
|
+
unless %i[out in inv].include?(v)
|
|
55
|
+
raise ArgumentError, "variance entries must be :out, :in, or :inv, got #{v.inspect}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
raise ArgumentError, "bound must not be nil" if bound.nil?
|
|
59
|
+
|
|
60
|
+
super(uri: uri, arity: arity, variance: variance.dup.freeze, bound: bound)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Frozen value object recording one type-function
|
|
65
|
+
# definition.
|
|
66
|
+
#
|
|
67
|
+
# `body` is the raw String payload from the `%a{...}`
|
|
68
|
+
# annotation (Slice 1's parser populates it). It stays
|
|
69
|
+
# opaque until Slice 2b's body-string parser lands.
|
|
70
|
+
#
|
|
71
|
+
# `body_tree` is the optional evaluable form: a
|
|
72
|
+
# `Rigor::Inference::HktBody::*` node tree the Slice 2a
|
|
73
|
+
# reducer walks against the application's concrete
|
|
74
|
+
# arguments. Plugin and Rigor-bundled overlay authors
|
|
75
|
+
# construct it programmatically through
|
|
76
|
+
# {with_body_tree}; the Slice 2b string parser will set
|
|
77
|
+
# it from `body` once it ships. The reducer treats a
|
|
78
|
+
# `nil` `body_tree` as "definition not yet evaluable"
|
|
79
|
+
# and returns the registered bound.
|
|
80
|
+
Definition = Data.define(:uri, :params, :body, :body_tree, :source_path, :source_line) do
|
|
81
|
+
def initialize(uri:, params:, body:, body_tree: nil, source_path: nil, source_line: nil)
|
|
82
|
+
raise ArgumentError, "uri must be a Symbol, got #{uri.class}" unless uri.is_a?(Symbol)
|
|
83
|
+
raise ArgumentError, "params must be an Array, got #{params.class}" unless params.is_a?(Array)
|
|
84
|
+
|
|
85
|
+
params.each do |p|
|
|
86
|
+
raise ArgumentError, "params entries must be Symbols, got #{p.inspect}" unless p.is_a?(Symbol)
|
|
87
|
+
end
|
|
88
|
+
raise ArgumentError, "body must be a String, got #{body.class}" unless body.is_a?(String)
|
|
89
|
+
|
|
90
|
+
super(
|
|
91
|
+
uri: uri,
|
|
92
|
+
params: params.dup.freeze,
|
|
93
|
+
body: body,
|
|
94
|
+
body_tree: body_tree,
|
|
95
|
+
source_path: source_path,
|
|
96
|
+
source_line: source_line
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Convenience constructor for callers that have a body
|
|
102
|
+
# tree but no raw String — typically Rigor-bundled HKT
|
|
103
|
+
# overlays that build the body programmatically. The
|
|
104
|
+
# raw `body` slot is filled with an empty placeholder
|
|
105
|
+
# so existing consumers keep their type contract.
|
|
106
|
+
def self.definition_with_body_tree(uri:, params:, body_tree:, source_path: nil, source_line: nil)
|
|
107
|
+
Definition.new(
|
|
108
|
+
uri: uri,
|
|
109
|
+
params: params,
|
|
110
|
+
body: "",
|
|
111
|
+
body_tree: body_tree,
|
|
112
|
+
source_path: source_path,
|
|
113
|
+
source_line: source_line
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
attr_reader :registrations, :definitions
|
|
118
|
+
|
|
119
|
+
# @param registrations [Array<Registration>]
|
|
120
|
+
# @param definitions [Array<Definition>]
|
|
121
|
+
def initialize(registrations: [], definitions: [])
|
|
122
|
+
@registrations = registrations.to_h { |r| [r.uri, r] }.freeze
|
|
123
|
+
@definitions = definitions.to_h { |d| [d.uri, d] }.freeze
|
|
124
|
+
freeze
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def registered?(uri)
|
|
128
|
+
@registrations.key?(uri)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def defined?(uri)
|
|
132
|
+
@definitions.key?(uri)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def registration(uri)
|
|
136
|
+
@registrations[uri]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def definition(uri)
|
|
140
|
+
@definitions[uri]
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# @return [HktRegistry] a new registry whose entries are
|
|
144
|
+
# the union of this registry's and `other`'s. On URI
|
|
145
|
+
# collisions `other`'s entries win (last-write-wins; OQ3
|
|
146
|
+
# tentative).
|
|
147
|
+
def merge(other)
|
|
148
|
+
raise ArgumentError, "merge target must be an HktRegistry, got #{other.class}" unless other.is_a?(HktRegistry)
|
|
149
|
+
|
|
150
|
+
self.class.new(
|
|
151
|
+
registrations: @registrations.merge(other.registrations).values,
|
|
152
|
+
definitions: @definitions.merge(other.definitions).values
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def empty?
|
|
157
|
+
@registrations.empty? && @definitions.empty?
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# ADR-20 Slice 2a — reduce an `App` against this
|
|
161
|
+
# registry. Convenience wrapper around `HktReducer.new(self).reduce`.
|
|
162
|
+
# Each call allocates a fresh reducer; concurrent
|
|
163
|
+
# reductions are safe.
|
|
164
|
+
def reduce(app, fuel: HktReducer::DEFAULT_FUEL)
|
|
165
|
+
HktReducer.new(self).reduce(app, fuel: fuel)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# ADR-20 slice 2e — scan a Rigor RbsLoader for
|
|
169
|
+
# `rigor:v1:hkt_register` / `rigor:v1:hkt_define`
|
|
170
|
+
# annotations attached to class- or module-level
|
|
171
|
+
# declarations in the loaded RBS env, parse them via
|
|
172
|
+
# {Rigor::RbsExtended::HktDirectives}, and return a new
|
|
173
|
+
# registry that is the union of `base` and every parsed
|
|
174
|
+
# entry. Last-write-wins on URI collisions per
|
|
175
|
+
# {#merge}'s contract. Fail-soft on per-annotation parse
|
|
176
|
+
# errors (the reporter records an `:info` entry; the
|
|
177
|
+
# other annotations still apply).
|
|
178
|
+
#
|
|
179
|
+
# @param rbs_loader [Rigor::Environment::RbsLoader]
|
|
180
|
+
# @param base [HktRegistry] starting registry (typically
|
|
181
|
+
# the bundled `Rigor::Builtins::HktBuiltins.registry`).
|
|
182
|
+
# @param name_scope [Rigor::Environment::NameScope, nil]
|
|
183
|
+
# threaded through to the bound resolver for class-name
|
|
184
|
+
# lookups; safe to omit during scanning since hkt
|
|
185
|
+
# bounds are typically `untyped` or stdlib classes.
|
|
186
|
+
# @param reporter [#record, nil] same fail-soft reporter
|
|
187
|
+
# contract the other RBS-extended parsers use.
|
|
188
|
+
def self.scan_rbs_loader(rbs_loader, base: EMPTY, name_scope: nil, reporter: nil)
|
|
189
|
+
return base if rbs_loader.nil?
|
|
190
|
+
|
|
191
|
+
# Required lazily here to avoid a hard circular
|
|
192
|
+
# require between hkt_registry / hkt_directives;
|
|
193
|
+
# HktDirectives requires HktRegistry to construct its
|
|
194
|
+
# value objects.
|
|
195
|
+
require_relative "../rbs_extended/hkt_directives"
|
|
196
|
+
|
|
197
|
+
registrations = []
|
|
198
|
+
definitions = []
|
|
199
|
+
|
|
200
|
+
rbs_loader.each_class_decl_annotation do |annotation_string, source_location|
|
|
201
|
+
reg = Rigor::RbsExtended::HktDirectives.parse_register(
|
|
202
|
+
annotation_string, name_scope: name_scope, reporter: reporter, source_location: source_location
|
|
203
|
+
)
|
|
204
|
+
registrations << reg if reg
|
|
205
|
+
|
|
206
|
+
defn = Rigor::RbsExtended::HktDirectives.parse_define(
|
|
207
|
+
annotation_string, reporter: reporter, source_location: source_location
|
|
208
|
+
)
|
|
209
|
+
definitions << defn if defn
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
return base if registrations.empty? && definitions.empty?
|
|
213
|
+
|
|
214
|
+
overlay = new(registrations: registrations, definitions: definitions)
|
|
215
|
+
base.merge(overlay)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
EMPTY = new.freeze
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
require_relative "hkt_reducer"
|
|
@@ -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
|