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.
Files changed (107) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -56
  3. data/lib/rigor/analysis/buffer_binding.rb +36 -0
  4. data/lib/rigor/analysis/check_rules.rb +11 -1
  5. data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
  6. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
  8. data/lib/rigor/analysis/fact_store.rb +15 -3
  9. data/lib/rigor/analysis/project_scan.rb +39 -0
  10. data/lib/rigor/analysis/result.rb +11 -3
  11. data/lib/rigor/analysis/run_stats.rb +193 -0
  12. data/lib/rigor/analysis/runner.rb +681 -19
  13. data/lib/rigor/analysis/worker_session.rb +339 -0
  14. data/lib/rigor/builtins/hkt_builtins.rb +342 -0
  15. data/lib/rigor/builtins/imported_refinements.rb +6 -2
  16. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  17. data/lib/rigor/builtins/static_return_refinements.rb +120 -0
  18. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  19. data/lib/rigor/cache/store.rb +72 -9
  20. data/lib/rigor/cli/lsp_command.rb +129 -0
  21. data/lib/rigor/cli/type_of_command.rb +44 -5
  22. data/lib/rigor/cli.rb +122 -10
  23. data/lib/rigor/configuration.rb +168 -7
  24. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  25. data/lib/rigor/environment/class_registry.rb +12 -3
  26. data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
  27. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  28. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  29. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  30. data/lib/rigor/environment/rbs_loader.rb +238 -7
  31. data/lib/rigor/environment/reflection.rb +152 -0
  32. data/lib/rigor/environment/reporters.rb +40 -0
  33. data/lib/rigor/environment.rb +179 -10
  34. data/lib/rigor/inference/acceptance.rb +83 -4
  35. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  36. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  37. data/lib/rigor/inference/expression_typer.rb +59 -2
  38. data/lib/rigor/inference/hkt_body.rb +171 -0
  39. data/lib/rigor/inference/hkt_body_parser.rb +363 -0
  40. data/lib/rigor/inference/hkt_reducer.rb +256 -0
  41. data/lib/rigor/inference/hkt_registry.rb +223 -0
  42. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  43. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
  44. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  45. data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
  46. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +126 -31
  47. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
  48. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
  49. data/lib/rigor/inference/method_dispatcher.rb +282 -6
  50. data/lib/rigor/inference/method_parameter_binder.rb +21 -11
  51. data/lib/rigor/inference/narrowing.rb +127 -8
  52. data/lib/rigor/inference/project_patched_methods.rb +70 -0
  53. data/lib/rigor/inference/project_patched_scanner.rb +210 -0
  54. data/lib/rigor/inference/scope_indexer.rb +156 -12
  55. data/lib/rigor/inference/statement_evaluator.rb +106 -6
  56. data/lib/rigor/inference/synthetic_method.rb +86 -0
  57. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  58. data/lib/rigor/inference/synthetic_method_scanner.rb +599 -0
  59. data/lib/rigor/language_server/buffer_table.rb +63 -0
  60. data/lib/rigor/language_server/completion_provider.rb +438 -0
  61. data/lib/rigor/language_server/debouncer.rb +86 -0
  62. data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
  63. data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
  64. data/lib/rigor/language_server/folding_range_provider.rb +75 -0
  65. data/lib/rigor/language_server/hover_provider.rb +74 -0
  66. data/lib/rigor/language_server/hover_renderer.rb +312 -0
  67. data/lib/rigor/language_server/loop.rb +71 -0
  68. data/lib/rigor/language_server/project_context.rb +145 -0
  69. data/lib/rigor/language_server/selection_range_provider.rb +93 -0
  70. data/lib/rigor/language_server/server.rb +384 -0
  71. data/lib/rigor/language_server/signature_help_provider.rb +249 -0
  72. data/lib/rigor/language_server/synchronized_writer.rb +28 -0
  73. data/lib/rigor/language_server/uri.rb +40 -0
  74. data/lib/rigor/language_server.rb +29 -0
  75. data/lib/rigor/plugin/base.rb +63 -0
  76. data/lib/rigor/plugin/blueprint.rb +60 -0
  77. data/lib/rigor/plugin/loader.rb +3 -1
  78. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  79. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  80. data/lib/rigor/plugin/macro/heredoc_template.rb +315 -0
  81. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  82. data/lib/rigor/plugin/macro.rb +31 -0
  83. data/lib/rigor/plugin/manifest.rb +127 -9
  84. data/lib/rigor/plugin/registry.rb +51 -2
  85. data/lib/rigor/plugin.rb +1 -0
  86. data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
  87. data/lib/rigor/rbs_extended.rb +82 -2
  88. data/lib/rigor/sig_gen/generator.rb +12 -3
  89. data/lib/rigor/trinary.rb +15 -11
  90. data/lib/rigor/type/app.rb +107 -0
  91. data/lib/rigor/type/bot.rb +6 -3
  92. data/lib/rigor/type/combinator.rb +12 -1
  93. data/lib/rigor/type/integer_range.rb +7 -7
  94. data/lib/rigor/type/refined.rb +18 -12
  95. data/lib/rigor/type/top.rb +4 -3
  96. data/lib/rigor/type.rb +1 -0
  97. data/lib/rigor/type_node/generic.rb +7 -1
  98. data/lib/rigor/type_node/identifier.rb +9 -1
  99. data/lib/rigor/type_node/string_literal.rb +4 -1
  100. data/lib/rigor/version.rb +1 -1
  101. data/sig/rigor/environment.rbs +11 -4
  102. data/sig/rigor/inference.rbs +2 -0
  103. data/sig/rigor/plugin/blueprint.rbs +7 -0
  104. data/sig/rigor/plugin/manifest.rbs +1 -1
  105. data/sig/rigor/plugin/registry.rbs +14 -1
  106. data/sig/rigor.rbs +37 -2
  107. 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
- def initialize(plugins: [], load_errors: [])
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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "plugin/type_node_resolver"
4
+ require_relative "plugin/macro"
4
5
  require_relative "plugin/manifest"
5
6
  require_relative "plugin/access_denied_error"
6
7
  require_relative "plugin/trust_policy"
@@ -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