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
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../inference/hkt_registry"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Plugin
|
|
5
7
|
# Value object describing one plugin's identity and metadata.
|
|
@@ -11,7 +13,7 @@ module Rigor
|
|
|
11
13
|
# The fields are pinned by ADR-2 § "Registration, Configuration,
|
|
12
14
|
# and Caching"; the v0.1.0 plugin contract surface treats this
|
|
13
15
|
# struct as the public manifest shape.
|
|
14
|
-
class Manifest
|
|
16
|
+
class Manifest # rubocop:disable Metrics/ClassLength
|
|
15
17
|
# Same regex {Rigor::Cache::Store::VALID_PRODUCER_ID} uses,
|
|
16
18
|
# so plugin ids round-trip through cache producer ids and
|
|
17
19
|
# `plugin.<id>.<rule>` diagnostic identifiers without escape.
|
|
@@ -38,12 +40,15 @@ module Rigor
|
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
attr_reader :id, :version, :description, :protocols, :config_schema, :produces, :consumes,
|
|
41
|
-
:owns_receivers, :type_node_resolvers
|
|
43
|
+
:owns_receivers, :type_node_resolvers, :block_as_methods, :heredoc_templates,
|
|
44
|
+
:trait_registries, :external_files, :hkt_registrations, :hkt_definitions
|
|
42
45
|
|
|
43
46
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
44
47
|
id:, version:,
|
|
45
48
|
description: nil, protocols: [], config_schema: {},
|
|
46
|
-
produces: [], consumes: [], owns_receivers: [], type_node_resolvers: []
|
|
49
|
+
produces: [], consumes: [], owns_receivers: [], type_node_resolvers: [],
|
|
50
|
+
block_as_methods: [], heredoc_templates: [], trait_registries: [], external_files: [],
|
|
51
|
+
hkt_registrations: [], hkt_definitions: []
|
|
47
52
|
)
|
|
48
53
|
validate_id!(id)
|
|
49
54
|
validate_version!(version)
|
|
@@ -52,17 +57,25 @@ module Rigor
|
|
|
52
57
|
validate_produces!(produces)
|
|
53
58
|
validate_owns_receivers!(owns_receivers)
|
|
54
59
|
validate_type_node_resolvers!(type_node_resolvers)
|
|
60
|
+
validate_block_as_methods!(block_as_methods)
|
|
61
|
+
validate_heredoc_templates!(heredoc_templates)
|
|
62
|
+
validate_trait_registries!(trait_registries)
|
|
63
|
+
validate_external_files!(external_files)
|
|
64
|
+
validate_hkt_registrations!(hkt_registrations)
|
|
65
|
+
validate_hkt_definitions!(hkt_definitions)
|
|
55
66
|
|
|
56
67
|
assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
|
|
57
|
-
type_node_resolvers
|
|
68
|
+
type_node_resolvers, block_as_methods, heredoc_templates, trait_registries, external_files,
|
|
69
|
+
hkt_registrations, hkt_definitions)
|
|
58
70
|
freeze
|
|
59
71
|
end
|
|
60
72
|
|
|
61
73
|
private
|
|
62
74
|
|
|
63
|
-
# rubocop:disable Metrics/ParameterLists
|
|
75
|
+
# rubocop:disable Metrics/ParameterLists, Metrics/AbcSize
|
|
64
76
|
def assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
|
|
65
|
-
type_node_resolvers
|
|
77
|
+
type_node_resolvers, block_as_methods, heredoc_templates, trait_registries, external_files,
|
|
78
|
+
hkt_registrations, hkt_definitions)
|
|
66
79
|
@id = id.dup.freeze
|
|
67
80
|
@version = version.dup.freeze
|
|
68
81
|
@description = description.nil? ? nil : description.to_s.dup.freeze
|
|
@@ -72,8 +85,14 @@ module Rigor
|
|
|
72
85
|
@consumes = coerce_consumes(consumes)
|
|
73
86
|
@owns_receivers = owns_receivers.map { |c| c.to_s.dup.freeze }.freeze
|
|
74
87
|
@type_node_resolvers = type_node_resolvers.dup.freeze
|
|
88
|
+
@block_as_methods = block_as_methods.dup.freeze
|
|
89
|
+
@heredoc_templates = heredoc_templates.dup.freeze
|
|
90
|
+
@trait_registries = trait_registries.dup.freeze
|
|
91
|
+
@external_files = external_files.dup.freeze
|
|
92
|
+
@hkt_registrations = hkt_registrations.dup.freeze
|
|
93
|
+
@hkt_definitions = hkt_definitions.dup.freeze
|
|
75
94
|
end
|
|
76
|
-
# rubocop:enable Metrics/ParameterLists
|
|
95
|
+
# rubocop:enable Metrics/ParameterLists, Metrics/AbcSize
|
|
77
96
|
|
|
78
97
|
public
|
|
79
98
|
|
|
@@ -100,7 +119,7 @@ module Rigor
|
|
|
100
119
|
errors
|
|
101
120
|
end
|
|
102
121
|
|
|
103
|
-
def to_h
|
|
122
|
+
def to_h # rubocop:disable Metrics/AbcSize
|
|
104
123
|
{
|
|
105
124
|
"id" => id,
|
|
106
125
|
"version" => version,
|
|
@@ -110,7 +129,13 @@ module Rigor
|
|
|
110
129
|
"produces" => produces.map(&:to_s),
|
|
111
130
|
"consumes" => consumes.map { |c| consumption_hash(c) },
|
|
112
131
|
"owns_receivers" => owns_receivers,
|
|
113
|
-
"type_node_resolvers" => type_node_resolvers.map { |r| r.class.name }
|
|
132
|
+
"type_node_resolvers" => type_node_resolvers.map { |r| r.class.name },
|
|
133
|
+
"block_as_methods" => block_as_methods.map(&:to_h),
|
|
134
|
+
"heredoc_templates" => heredoc_templates.map(&:to_h),
|
|
135
|
+
"trait_registries" => trait_registries.map(&:to_h),
|
|
136
|
+
"external_files" => external_files.map(&:to_h),
|
|
137
|
+
"hkt_registrations" => hkt_registrations.map(&:to_h),
|
|
138
|
+
"hkt_definitions" => hkt_definitions.map { |d| { "uri" => d.uri, "params" => d.params } }
|
|
114
139
|
}
|
|
115
140
|
end
|
|
116
141
|
|
|
@@ -208,6 +233,99 @@ module Rigor
|
|
|
208
233
|
"Rigor::Plugin::TypeNodeResolver instances, got #{resolvers.inspect}"
|
|
209
234
|
end
|
|
210
235
|
|
|
236
|
+
# ADR-16 slice 1a — `block_as_methods:` declares the Tier A
|
|
237
|
+
# substrate entries the plugin contributes. Slice 1a carries
|
|
238
|
+
# the declarations on the manifest; the engine hook that
|
|
239
|
+
# actually narrows `Scope#self_type` for matching blocks
|
|
240
|
+
# arrives in a subsequent slice.
|
|
241
|
+
def validate_block_as_methods!(entries)
|
|
242
|
+
return if entries.is_a?(Array) && entries.all?(Macro::BlockAsMethod)
|
|
243
|
+
|
|
244
|
+
raise ArgumentError,
|
|
245
|
+
"plugin manifest block_as_methods must be an Array of " \
|
|
246
|
+
"Rigor::Plugin::Macro::BlockAsMethod instances, got #{entries.inspect}"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# ADR-16 slice 2a — `heredoc_templates:` declares the Tier C
|
|
250
|
+
# substrate entries (heredoc-template synthesis on class-level
|
|
251
|
+
# DSL calls). Slice 2a carries the declarations on the
|
|
252
|
+
# manifest; the pre-pass + `SyntheticMethodIndex` that actually
|
|
253
|
+
# emit synthetic methods arrive in slice 2b.
|
|
254
|
+
def validate_heredoc_templates!(entries)
|
|
255
|
+
return if entries.is_a?(Array) && entries.all?(Macro::HeredocTemplate)
|
|
256
|
+
|
|
257
|
+
raise ArgumentError,
|
|
258
|
+
"plugin manifest heredoc_templates must be an Array of " \
|
|
259
|
+
"Rigor::Plugin::Macro::HeredocTemplate instances, got #{entries.inspect}"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# ADR-16 slice 3a — `trait_registries:` declares the Tier B
|
|
263
|
+
# substrate entries (trait-inlining via bundled module
|
|
264
|
+
# registry). Slice 3a carries the declarations on the
|
|
265
|
+
# manifest; the scanner + per-method explosion through
|
|
266
|
+
# `SyntheticMethodIndex` (slice 2b primitive) arrives in
|
|
267
|
+
# slice 3b.
|
|
268
|
+
def validate_trait_registries!(entries)
|
|
269
|
+
return if entries.is_a?(Array) && entries.all?(Macro::TraitRegistry)
|
|
270
|
+
|
|
271
|
+
raise ArgumentError,
|
|
272
|
+
"plugin manifest trait_registries must be an Array of " \
|
|
273
|
+
"Rigor::Plugin::Macro::TraitRegistry instances, got #{entries.inspect}"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# ADR-16 slice 5a — `external_files:` declares the Tier D
|
|
277
|
+
# substrate entries (external-Ruby-file inclusion under a
|
|
278
|
+
# declared `self`). Slice 5a carries the declarations on
|
|
279
|
+
# the manifest; the engine integration that walks the
|
|
280
|
+
# matched files + narrows their entry scope is **queued for
|
|
281
|
+
# slice 5b**, gated on demonstrated demand from concrete
|
|
282
|
+
# plugin targets (Redmine webhook payloads, tDiary plugin
|
|
283
|
+
# loader, etc.). Plugin authors MAY declare entries today;
|
|
284
|
+
# the substrate does not yet act on them.
|
|
285
|
+
def validate_external_files!(entries)
|
|
286
|
+
return if entries.is_a?(Array) && entries.all?(Macro::ExternalFile)
|
|
287
|
+
|
|
288
|
+
raise ArgumentError,
|
|
289
|
+
"plugin manifest external_files must be an Array of " \
|
|
290
|
+
"Rigor::Plugin::Macro::ExternalFile instances, got #{entries.inspect}"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# ADR-20 slice 6 — `hkt_registrations:` declares the
|
|
294
|
+
# Lightweight HKT URI registrations this plugin ships
|
|
295
|
+
# (analogous to `%a{rigor:v1:hkt_register: ...}` directives
|
|
296
|
+
# but published via the manifest contract instead of a
|
|
297
|
+
# shipped `.rbs` file). Each entry MUST be an
|
|
298
|
+
# `Rigor::Inference::HktRegistry::Registration`. The
|
|
299
|
+
# registry aggregator on `Plugin::Registry` flattens
|
|
300
|
+
# entries from every loaded plugin and merges them into
|
|
301
|
+
# `env.hkt_registry` on top of `Builtins::HktBuiltins.registry`;
|
|
302
|
+
# user `.rbs` overlays merge on top of plugin entries
|
|
303
|
+
# last-write-wins.
|
|
304
|
+
def validate_hkt_registrations!(entries)
|
|
305
|
+
return if entries.is_a?(Array) && entries.all?(Inference::HktRegistry::Registration)
|
|
306
|
+
|
|
307
|
+
raise ArgumentError,
|
|
308
|
+
"plugin manifest hkt_registrations must be an Array of " \
|
|
309
|
+
"Rigor::Inference::HktRegistry::Registration instances, got #{entries.inspect}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# ADR-20 slice 6 — `hkt_definitions:` declares the
|
|
313
|
+
# plugin's HKT type-function bodies (analogous to
|
|
314
|
+
# `%a{rigor:v1:hkt_define: ...}` directives). Each entry
|
|
315
|
+
# MUST be an `Rigor::Inference::HktRegistry::Definition`
|
|
316
|
+
# — typically built via
|
|
317
|
+
# `Rigor::Inference::HktRegistry.definition_with_body_tree(...)`
|
|
318
|
+
# so plugin authors can build the body programmatically
|
|
319
|
+
# via {Rigor::Inference::HktBody}'s node-constructor API
|
|
320
|
+
# without parsing a string.
|
|
321
|
+
def validate_hkt_definitions!(entries)
|
|
322
|
+
return if entries.is_a?(Array) && entries.all?(Inference::HktRegistry::Definition)
|
|
323
|
+
|
|
324
|
+
raise ArgumentError,
|
|
325
|
+
"plugin manifest hkt_definitions must be an Array of " \
|
|
326
|
+
"Rigor::Inference::HktRegistry::Definition instances, got #{entries.inspect}"
|
|
327
|
+
end
|
|
328
|
+
|
|
211
329
|
def coerce_consumes(consumes)
|
|
212
330
|
unless consumes.is_a?(Array)
|
|
213
331
|
raise ArgumentError, "plugin manifest consumes must be an Array, got #{consumes.inspect}"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "blueprint"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Plugin
|
|
5
7
|
# Read-side query API over the plugins loaded for a single
|
|
@@ -13,20 +15,48 @@ module Rigor
|
|
|
13
15
|
# the order in which {Rigor::Plugin::Loader} resolved
|
|
14
16
|
# configuration entries, which is project-config order with
|
|
15
17
|
# plugin-id alphabetical as the tie-breaker.
|
|
18
|
+
#
|
|
19
|
+
# ADR-15 Phase 3 — alongside the instantiated `plugins`, the
|
|
20
|
+
# registry carries `blueprints`: a frozen, Ractor-shareable
|
|
21
|
+
# `Array<Blueprint>` that records how to re-instantiate the
|
|
22
|
+
# same plugin set in a worker Ractor. The eventual Phase 4
|
|
23
|
+
# pool ships `blueprints` across the boundary and calls
|
|
24
|
+
# {.materialize} per-Ractor; the live `plugins` carriage on
|
|
25
|
+
# the coordinator registry stays unchanged.
|
|
16
26
|
class Registry
|
|
17
|
-
attr_reader :plugins, :load_errors
|
|
27
|
+
attr_reader :plugins, :load_errors, :blueprints
|
|
18
28
|
|
|
19
29
|
# @param plugins [Array<Rigor::Plugin::Base>] instantiated
|
|
20
30
|
# plugin instances in deterministic order.
|
|
21
31
|
# @param load_errors [Array<Rigor::Plugin::LoadError>] failures
|
|
22
32
|
# surfaced during loading. Each error is also turned into a
|
|
23
33
|
# diagnostic by the runner.
|
|
24
|
-
|
|
34
|
+
# @param blueprints [Array<Rigor::Plugin::Blueprint>] frozen,
|
|
35
|
+
# Ractor-shareable replay descriptors aligned 1:1 with
|
|
36
|
+
# `plugins`. The loader fills this in; callers that
|
|
37
|
+
# construct Registry manually MAY pass `[]` and accept
|
|
38
|
+
# that {.materialize} cannot replay the set.
|
|
39
|
+
def initialize(plugins: [], load_errors: [], blueprints: [])
|
|
25
40
|
@plugins = plugins.dup.freeze
|
|
26
41
|
@load_errors = load_errors.dup.freeze
|
|
42
|
+
@blueprints = blueprints.dup.freeze
|
|
27
43
|
freeze
|
|
28
44
|
end
|
|
29
45
|
|
|
46
|
+
# ADR-15 Phase 3 — build a fresh Registry from the supplied
|
|
47
|
+
# blueprint set by replaying {Blueprint#materialize} per
|
|
48
|
+
# entry against `services`. The returned registry carries
|
|
49
|
+
# NEW plugin instances (mutable per-Ractor accumulators
|
|
50
|
+
# included) and the same blueprint set, so a worker can
|
|
51
|
+
# hand the materialised registry to Environment without
|
|
52
|
+
# losing the replay handle. `load_errors` is intentionally
|
|
53
|
+
# empty: load-time failures already surfaced in the
|
|
54
|
+
# coordinator registry and don't repeat per worker.
|
|
55
|
+
def self.materialize(blueprints:, services:)
|
|
56
|
+
plugins = blueprints.map { |bp| bp.materialize(services: services) }
|
|
57
|
+
new(plugins: plugins, blueprints: blueprints, load_errors: [])
|
|
58
|
+
end
|
|
59
|
+
|
|
30
60
|
def find(id)
|
|
31
61
|
id_s = id.to_s
|
|
32
62
|
plugins.find { |plugin| plugin.manifest.id == id_s }
|
|
@@ -55,6 +85,25 @@ module Rigor
|
|
|
55
85
|
plugins.flat_map { |plugin| plugin.manifest.type_node_resolvers }
|
|
56
86
|
end
|
|
57
87
|
|
|
88
|
+
# ADR-20 slice 6 — aggregate every loaded plugin's
|
|
89
|
+
# manifest-declared HKT registrations + definitions
|
|
90
|
+
# into a single `Inference::HktRegistry` overlay that
|
|
91
|
+
# `Environment#hkt_registry` merges on top of the
|
|
92
|
+
# bundled `Builtins::HktBuiltins.registry`. Last
|
|
93
|
+
# plugin to register a URI wins (registration order
|
|
94
|
+
# determined by the user's `plugins:` list); user
|
|
95
|
+
# `.rbs` overlays merge on top of this overlay last.
|
|
96
|
+
# Returns `Inference::HktRegistry::EMPTY` when no
|
|
97
|
+
# plugin contributes HKT entries so callers can skip
|
|
98
|
+
# the merge.
|
|
99
|
+
def hkt_overlay_registry
|
|
100
|
+
registrations = plugins.flat_map { |plugin| plugin.manifest.hkt_registrations }
|
|
101
|
+
definitions = plugins.flat_map { |plugin| plugin.manifest.hkt_definitions }
|
|
102
|
+
return Inference::HktRegistry::EMPTY if registrations.empty? && definitions.empty?
|
|
103
|
+
|
|
104
|
+
Inference::HktRegistry.new(registrations: registrations, definitions: definitions)
|
|
105
|
+
end
|
|
106
|
+
|
|
58
107
|
EMPTY = new.freeze
|
|
59
108
|
end
|
|
60
109
|
end
|
data/lib/rigor/plugin.rb
CHANGED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../inference/hkt_registry"
|
|
4
|
+
require_relative "../inference/hkt_body_parser"
|
|
5
|
+
|
|
6
|
+
module Rigor
|
|
7
|
+
module RbsExtended
|
|
8
|
+
# ADR-20 § "Decision D6" parser for the two new HKT
|
|
9
|
+
# directives that live in `.rbs` files at module / class
|
|
10
|
+
# scope:
|
|
11
|
+
#
|
|
12
|
+
# - `%a{rigor:v1:hkt_register: uri=<uri> arity=<int>
|
|
13
|
+
# variance=<v1>,<v2>,... bound=<class_name_or_untyped>}` —
|
|
14
|
+
# registers a defunctionalised type-constructor URI
|
|
15
|
+
# together with its arity, per-position variance, and
|
|
16
|
+
# erasure bound.
|
|
17
|
+
# - `%a{rigor:v1:hkt_define: uri=<uri> params=<P1>,<P2>,...
|
|
18
|
+
# body=<body_text>}` — binds the URI to a type-function
|
|
19
|
+
# body that {HktBodyParser} parses into an
|
|
20
|
+
# {HktBody::Union} tree.
|
|
21
|
+
#
|
|
22
|
+
# ## Payload format
|
|
23
|
+
#
|
|
24
|
+
# **Space-separated `key=value` pairs.** The format is
|
|
25
|
+
# constrained by RBS's `%a{...}` annotation grammar, which
|
|
26
|
+
# does NOT accept arbitrary nested punctuation (a JSON
|
|
27
|
+
# payload with quotes / nested braces will fail RBS
|
|
28
|
+
# parsing). Each value is a bare token: no quoting, no
|
|
29
|
+
# escaping. Values that contain spaces or `=` signs MUST
|
|
30
|
+
# be encoded via the `body=` key, which is special-cased
|
|
31
|
+
# to gobble everything from `body=` to the end of the
|
|
32
|
+
# payload — see `parse_define`.
|
|
33
|
+
#
|
|
34
|
+
# Example annotations (write inside a class / module
|
|
35
|
+
# declaration so the annotation attaches to the decl
|
|
36
|
+
# RBS parses):
|
|
37
|
+
#
|
|
38
|
+
# %a{rigor:v1:hkt_register: uri=json::value arity=1
|
|
39
|
+
# variance=out bound=untyped}
|
|
40
|
+
# %a{rigor:v1:hkt_define: uri=json::value params=K
|
|
41
|
+
# body=nil | true | false | Integer | Float | String |
|
|
42
|
+
# Array[App[json::value, K]] |
|
|
43
|
+
# Hash[K, App[json::value, K]]}
|
|
44
|
+
# module JsonOverlay
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
# ## Bound vocabulary
|
|
48
|
+
#
|
|
49
|
+
# - `untyped` resolves to `Rigor::Type::Combinator.untyped`
|
|
50
|
+
# (i.e. `Dynamic[Top]`, the ADR-20 WD2 default).
|
|
51
|
+
# - A bare class name (`String`, `Integer`, …) resolves
|
|
52
|
+
# through `name_scope.nominal_for_name(...)` when
|
|
53
|
+
# supplied, falling back to a raw `Rigor::Type::Nominal`
|
|
54
|
+
# otherwise.
|
|
55
|
+
# - Anything else falls back to `untyped` and emits an
|
|
56
|
+
# `:info` diagnostic via the supplied reporter (fail-soft
|
|
57
|
+
# so an unrecognised bound never crashes the loader).
|
|
58
|
+
#
|
|
59
|
+
# Richer bound forms (parameterised generics, unions,
|
|
60
|
+
# refinements) wait for a follow-up slice's expression
|
|
61
|
+
# parser.
|
|
62
|
+
module HktDirectives
|
|
63
|
+
module_function
|
|
64
|
+
|
|
65
|
+
REGISTER_DIRECTIVE = "rigor:v1:hkt_register:"
|
|
66
|
+
DEFINE_DIRECTIVE = "rigor:v1:hkt_define:"
|
|
67
|
+
|
|
68
|
+
DEFAULT_VARIANCE = :inv
|
|
69
|
+
DEFAULT_BOUND_LITERAL = "untyped"
|
|
70
|
+
|
|
71
|
+
# Parses one `%a{rigor:v1:hkt_register: ...}` payload
|
|
72
|
+
# string and returns a `Registration`, or `nil` when the
|
|
73
|
+
# string is not an hkt_register directive (so callers can
|
|
74
|
+
# walk a list of annotations without each having to
|
|
75
|
+
# pre-filter).
|
|
76
|
+
def parse_register(string, name_scope: nil, reporter: nil, source_location: nil)
|
|
77
|
+
payload = extract_payload(string, REGISTER_DIRECTIVE)
|
|
78
|
+
return nil if payload.nil?
|
|
79
|
+
|
|
80
|
+
kvs = parse_kv_payload(payload, body_key: nil)
|
|
81
|
+
|
|
82
|
+
uri = symbolize_uri(kvs["uri"], reporter: reporter, source_location: source_location)
|
|
83
|
+
return nil if uri.nil?
|
|
84
|
+
|
|
85
|
+
arity = coerce_arity(kvs["arity"], reporter: reporter, source_location: source_location)
|
|
86
|
+
return nil if arity.nil?
|
|
87
|
+
|
|
88
|
+
variance = coerce_variance(kvs["variance"], arity, reporter: reporter, source_location: source_location)
|
|
89
|
+
return nil if variance.nil?
|
|
90
|
+
|
|
91
|
+
bound = resolve_bound(
|
|
92
|
+
kvs["bound"] || DEFAULT_BOUND_LITERAL,
|
|
93
|
+
name_scope: name_scope,
|
|
94
|
+
reporter: reporter,
|
|
95
|
+
source_location: source_location
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
Inference::HktRegistry::Registration.new(
|
|
99
|
+
uri: uri,
|
|
100
|
+
arity: arity,
|
|
101
|
+
variance: variance,
|
|
102
|
+
bound: bound
|
|
103
|
+
)
|
|
104
|
+
rescue ArgumentError => e
|
|
105
|
+
record_hkt_error(reporter, "hkt_register: #{e.message}", source_location)
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Parses one `%a{rigor:v1:hkt_define: ...}` payload
|
|
110
|
+
# string and returns a `Definition`, or `nil` when the
|
|
111
|
+
# string is not an hkt_define directive.
|
|
112
|
+
def parse_define(string, reporter: nil, source_location: nil)
|
|
113
|
+
payload = extract_payload(string, DEFINE_DIRECTIVE)
|
|
114
|
+
return nil if payload.nil?
|
|
115
|
+
|
|
116
|
+
kvs = parse_kv_payload(payload, body_key: "body")
|
|
117
|
+
|
|
118
|
+
uri = symbolize_uri(kvs["uri"], reporter: reporter, source_location: source_location)
|
|
119
|
+
return nil if uri.nil?
|
|
120
|
+
|
|
121
|
+
params = coerce_params(kvs["params"], reporter: reporter, source_location: source_location)
|
|
122
|
+
return nil if params.nil?
|
|
123
|
+
|
|
124
|
+
body = kvs["body"]
|
|
125
|
+
unless body.is_a?(String)
|
|
126
|
+
record_hkt_error(reporter, "hkt_define: missing body=", source_location)
|
|
127
|
+
return nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
body_tree = parse_body_tree(body, params, reporter: reporter, source_location: source_location)
|
|
131
|
+
|
|
132
|
+
Inference::HktRegistry::Definition.new(
|
|
133
|
+
uri: uri,
|
|
134
|
+
params: params,
|
|
135
|
+
body: body,
|
|
136
|
+
body_tree: body_tree,
|
|
137
|
+
source_path: source_path_of(source_location),
|
|
138
|
+
source_line: source_line_of(source_location)
|
|
139
|
+
)
|
|
140
|
+
rescue ArgumentError => e
|
|
141
|
+
record_hkt_error(reporter, "hkt_define: #{e.message}", source_location)
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def extract_payload(string, directive)
|
|
146
|
+
return nil if string.nil?
|
|
147
|
+
|
|
148
|
+
idx = string.index(directive)
|
|
149
|
+
return nil if idx.nil?
|
|
150
|
+
|
|
151
|
+
payload = string[(idx + directive.size)..].to_s.strip
|
|
152
|
+
# Strip trailing `}` of the wrapping `%a{...}` form if
|
|
153
|
+
# the caller passed the raw annotation string. The
|
|
154
|
+
# parser also accepts a pre-extracted payload.
|
|
155
|
+
payload = payload.sub(/\}\z/, "") if payload.end_with?("}") && !balanced_braces?(payload)
|
|
156
|
+
payload.empty? ? nil : payload
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def balanced_braces?(string)
|
|
160
|
+
depth = 0
|
|
161
|
+
string.each_char do |ch|
|
|
162
|
+
case ch
|
|
163
|
+
when "{" then depth += 1
|
|
164
|
+
when "}"
|
|
165
|
+
depth -= 1
|
|
166
|
+
return false if depth.negative?
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
depth.zero?
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Parses a space-separated `key=value [key=value ...]`
|
|
173
|
+
# payload into a Hash. When `body_key` is supplied AND
|
|
174
|
+
# that key appears, everything from `<body_key>=` to
|
|
175
|
+
# the end of the payload becomes the value (body
|
|
176
|
+
# contents typically include spaces, `|`, `[]` etc.
|
|
177
|
+
# that the simple tokenizer cannot otherwise carry).
|
|
178
|
+
KV_KEY_PATTERN = /(?<![\w.])([a-z_]\w*)=/
|
|
179
|
+
private_constant :KV_KEY_PATTERN
|
|
180
|
+
|
|
181
|
+
def parse_kv_payload(payload, body_key:)
|
|
182
|
+
result = {}
|
|
183
|
+
# Find every `<key>=` boundary; each value runs to
|
|
184
|
+
# the next boundary or end of string.
|
|
185
|
+
markers = []
|
|
186
|
+
payload.scan(KV_KEY_PATTERN) { markers << [::Regexp.last_match[1], ::Regexp.last_match.end(0)] }
|
|
187
|
+
markers.each_with_index do |(key, value_start), i|
|
|
188
|
+
if body_key && key == body_key
|
|
189
|
+
result[key] = payload[value_start..].to_s.strip
|
|
190
|
+
break
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
value_end = markers[i + 1] ? markers[i + 1][1] - markers[i + 1][0].size - 1 : payload.size
|
|
194
|
+
result[key] = payload[value_start...value_end].to_s.strip
|
|
195
|
+
end
|
|
196
|
+
result
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# ADR-20 slice 2b — parse the body String into an
|
|
200
|
+
# `HktBody::*` tree via {Inference::HktBodyParser.parse}.
|
|
201
|
+
# On parse failure: emit a fail-soft `:info` reporter
|
|
202
|
+
# entry and return `nil` so the resulting Definition
|
|
203
|
+
# keeps its `body` String slot but `body_tree` stays
|
|
204
|
+
# absent (the reducer falls back to `app.bound` at call
|
|
205
|
+
# time per ADR-20 D5).
|
|
206
|
+
def parse_body_tree(body, params, reporter:, source_location:)
|
|
207
|
+
return nil if body.nil? || body.empty?
|
|
208
|
+
|
|
209
|
+
Inference::HktBodyParser.parse(body, params: params)
|
|
210
|
+
rescue Inference::HktBodyParser::ParseError => e
|
|
211
|
+
record_hkt_error(reporter, "hkt_define body parse error: #{e.message}", source_location)
|
|
212
|
+
nil
|
|
213
|
+
rescue ArgumentError => e
|
|
214
|
+
record_hkt_error(reporter, "hkt_define body construction error: #{e.message}", source_location)
|
|
215
|
+
nil
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def symbolize_uri(raw, reporter:, source_location:)
|
|
219
|
+
if raw.nil? || raw.empty?
|
|
220
|
+
record_hkt_error(reporter, "uri= is required", source_location)
|
|
221
|
+
return nil
|
|
222
|
+
end
|
|
223
|
+
unless raw.include?(Type::App::URI_SEPARATOR)
|
|
224
|
+
record_hkt_error(
|
|
225
|
+
reporter,
|
|
226
|
+
"uri must be namespaced as `a::b` per ADR-20 WD1, got #{raw.inspect}",
|
|
227
|
+
source_location
|
|
228
|
+
)
|
|
229
|
+
return nil
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
raw.to_sym
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def coerce_arity(raw, reporter:, source_location:)
|
|
236
|
+
if raw && /\A\d+\z/.match?(raw) && raw.to_i.positive?
|
|
237
|
+
raw.to_i
|
|
238
|
+
else
|
|
239
|
+
record_hkt_error(reporter, "arity must be a positive Integer, got #{raw.inspect}", source_location)
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def coerce_variance(raw, arity, reporter:, source_location:)
|
|
245
|
+
# Omitted variance defaults to `[:inv] * arity` per ADR-20 WD4.
|
|
246
|
+
variance =
|
|
247
|
+
if raw.nil? || raw.empty?
|
|
248
|
+
Array.new(arity, DEFAULT_VARIANCE)
|
|
249
|
+
else
|
|
250
|
+
raw.split(",").map { |v| v.strip.to_sym }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
unless variance.size == arity
|
|
254
|
+
record_hkt_error(reporter, "variance length #{variance.size} does not match arity #{arity}", source_location)
|
|
255
|
+
return nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
unless variance.all? { |v| %i[out in inv].include?(v) }
|
|
259
|
+
record_hkt_error(
|
|
260
|
+
reporter,
|
|
261
|
+
"variance entries must be `out` / `in` / `inv`, got #{variance.inspect}",
|
|
262
|
+
source_location
|
|
263
|
+
)
|
|
264
|
+
return nil
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
variance
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def coerce_params(raw, reporter:, source_location:)
|
|
271
|
+
if raw.nil? || raw.empty?
|
|
272
|
+
record_hkt_error(reporter, "params= is required (comma-separated UCName list)", source_location)
|
|
273
|
+
return nil
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
raw.split(",").map { |p| p.strip.to_sym }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def resolve_bound(raw, name_scope:, reporter:, source_location:)
|
|
280
|
+
return Type::Combinator.untyped if raw.nil? || raw.strip.empty?
|
|
281
|
+
return Type::Combinator.untyped if raw.strip == DEFAULT_BOUND_LITERAL
|
|
282
|
+
|
|
283
|
+
class_name = raw.strip
|
|
284
|
+
if /\A(?:::)?(?:[A-Z]\w*)(?:::[A-Z]\w*)*\z/.match?(class_name)
|
|
285
|
+
normalized = class_name.sub(/\A::/, "")
|
|
286
|
+
return Type::Nominal.new(normalized) if name_scope.nil?
|
|
287
|
+
|
|
288
|
+
if name_scope.respond_to?(:nominal_for_name)
|
|
289
|
+
resolved = name_scope.nominal_for_name(normalized)
|
|
290
|
+
return resolved if resolved
|
|
291
|
+
end
|
|
292
|
+
return Type::Nominal.new(normalized)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
record_hkt_error(
|
|
296
|
+
reporter,
|
|
297
|
+
"bound `#{raw}` not recognised (accepts `untyped` or a bare class name); falling back to `untyped`",
|
|
298
|
+
source_location
|
|
299
|
+
)
|
|
300
|
+
Type::Combinator.untyped
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def source_path_of(source_location)
|
|
304
|
+
return nil if source_location.nil?
|
|
305
|
+
|
|
306
|
+
source_location.respond_to?(:name) ? source_location.name : nil
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def source_line_of(source_location)
|
|
310
|
+
return nil if source_location.nil?
|
|
311
|
+
|
|
312
|
+
source_location.respond_to?(:start_line) ? source_location.start_line : nil
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def record_hkt_error(reporter, message, source_location)
|
|
316
|
+
return if reporter.nil?
|
|
317
|
+
|
|
318
|
+
if reporter.respond_to?(:record)
|
|
319
|
+
reporter.record(directive: "hkt", message: message, source_location: source_location)
|
|
320
|
+
elsif reporter.respond_to?(:<<)
|
|
321
|
+
reporter << { directive: "hkt", message: message, source_location: source_location }
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|