rigortype 0.1.5 → 0.1.7

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +76 -79
  3. data/lib/rigor/analysis/baseline.rb +347 -0
  4. data/lib/rigor/analysis/buffer_binding.rb +36 -0
  5. data/lib/rigor/analysis/check_rules.rb +68 -3
  6. data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
  7. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
  9. data/lib/rigor/analysis/project_scan.rb +39 -0
  10. data/lib/rigor/analysis/runner.rb +309 -22
  11. data/lib/rigor/analysis/worker_session.rb +14 -2
  12. data/lib/rigor/builtins/hkt_builtins.rb +342 -0
  13. data/lib/rigor/builtins/static_return_refinements.rb +142 -0
  14. data/lib/rigor/cache/store.rb +33 -3
  15. data/lib/rigor/cli/baseline_command.rb +377 -0
  16. data/lib/rigor/cli/lsp_command.rb +129 -0
  17. data/lib/rigor/cli/type_of_command.rb +44 -5
  18. data/lib/rigor/cli.rb +142 -13
  19. data/lib/rigor/configuration.rb +58 -2
  20. data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
  21. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  22. data/lib/rigor/environment/rbs_loader.rb +67 -2
  23. data/lib/rigor/environment/reporters.rb +40 -0
  24. data/lib/rigor/environment.rb +119 -9
  25. data/lib/rigor/flow_contribution/fact.rb +20 -10
  26. data/lib/rigor/inference/acceptance.rb +48 -3
  27. data/lib/rigor/inference/expression_typer.rb +64 -2
  28. data/lib/rigor/inference/hkt_body.rb +171 -0
  29. data/lib/rigor/inference/hkt_body_parser.rb +363 -0
  30. data/lib/rigor/inference/hkt_reducer.rb +256 -0
  31. data/lib/rigor/inference/hkt_registry.rb +223 -0
  32. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +125 -30
  33. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +32 -11
  34. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
  35. data/lib/rigor/inference/method_dispatcher.rb +174 -6
  36. data/lib/rigor/inference/narrowing.rb +103 -1
  37. data/lib/rigor/inference/project_patched_methods.rb +70 -0
  38. data/lib/rigor/inference/project_patched_scanner.rb +210 -0
  39. data/lib/rigor/inference/scope_indexer.rb +209 -19
  40. data/lib/rigor/inference/statement_evaluator.rb +172 -11
  41. data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
  42. data/lib/rigor/language_server/buffer_table.rb +63 -0
  43. data/lib/rigor/language_server/completion_provider.rb +438 -0
  44. data/lib/rigor/language_server/debouncer.rb +86 -0
  45. data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
  46. data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
  47. data/lib/rigor/language_server/folding_range_provider.rb +75 -0
  48. data/lib/rigor/language_server/hover_provider.rb +74 -0
  49. data/lib/rigor/language_server/hover_renderer.rb +312 -0
  50. data/lib/rigor/language_server/loop.rb +71 -0
  51. data/lib/rigor/language_server/project_context.rb +145 -0
  52. data/lib/rigor/language_server/selection_range_provider.rb +93 -0
  53. data/lib/rigor/language_server/server.rb +384 -0
  54. data/lib/rigor/language_server/signature_help_provider.rb +249 -0
  55. data/lib/rigor/language_server/synchronized_writer.rb +28 -0
  56. data/lib/rigor/language_server/uri.rb +40 -0
  57. data/lib/rigor/language_server.rb +29 -0
  58. data/lib/rigor/plugin/base.rb +63 -0
  59. data/lib/rigor/plugin/macro/heredoc_template.rb +127 -13
  60. data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
  61. data/lib/rigor/plugin/manifest.rb +54 -7
  62. data/lib/rigor/plugin/registry.rb +19 -0
  63. data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
  64. data/lib/rigor/rbs_extended.rb +82 -2
  65. data/lib/rigor/sig_gen/generator.rb +12 -3
  66. data/lib/rigor/type/app.rb +107 -0
  67. data/lib/rigor/type.rb +1 -0
  68. data/lib/rigor/version.rb +1 -1
  69. data/sig/rigor/environment.rbs +10 -4
  70. data/sig/rigor/inference.rbs +2 -0
  71. data/sig/rigor.rbs +4 -1
  72. metadata +56 -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.
@@ -39,13 +41,14 @@ module Rigor
39
41
 
40
42
  attr_reader :id, :version, :description, :protocols, :config_schema, :produces, :consumes,
41
43
  :owns_receivers, :type_node_resolvers, :block_as_methods, :heredoc_templates,
42
- :trait_registries, :external_files
44
+ :trait_registries, :external_files, :hkt_registrations, :hkt_definitions
43
45
 
44
46
  def initialize( # rubocop:disable Metrics/ParameterLists
45
47
  id:, version:,
46
48
  description: nil, protocols: [], config_schema: {},
47
49
  produces: [], consumes: [], owns_receivers: [], type_node_resolvers: [],
48
- block_as_methods: [], heredoc_templates: [], trait_registries: [], external_files: []
50
+ block_as_methods: [], heredoc_templates: [], trait_registries: [], external_files: [],
51
+ hkt_registrations: [], hkt_definitions: []
49
52
  )
50
53
  validate_id!(id)
51
54
  validate_version!(version)
@@ -58,9 +61,12 @@ module Rigor
58
61
  validate_heredoc_templates!(heredoc_templates)
59
62
  validate_trait_registries!(trait_registries)
60
63
  validate_external_files!(external_files)
64
+ validate_hkt_registrations!(hkt_registrations)
65
+ validate_hkt_definitions!(hkt_definitions)
61
66
 
62
67
  assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
63
- type_node_resolvers, block_as_methods, heredoc_templates, trait_registries, external_files)
68
+ type_node_resolvers, block_as_methods, heredoc_templates, trait_registries, external_files,
69
+ hkt_registrations, hkt_definitions)
64
70
  freeze
65
71
  end
66
72
 
@@ -68,7 +74,8 @@ module Rigor
68
74
 
69
75
  # rubocop:disable Metrics/ParameterLists, Metrics/AbcSize
70
76
  def assign_fields(id, version, description, protocols, config_schema, produces, consumes, owns_receivers,
71
- type_node_resolvers, block_as_methods, heredoc_templates, trait_registries, external_files)
77
+ type_node_resolvers, block_as_methods, heredoc_templates, trait_registries, external_files,
78
+ hkt_registrations, hkt_definitions)
72
79
  @id = id.dup.freeze
73
80
  @version = version.dup.freeze
74
81
  @description = description.nil? ? nil : description.to_s.dup.freeze
@@ -82,6 +89,8 @@ module Rigor
82
89
  @heredoc_templates = heredoc_templates.dup.freeze
83
90
  @trait_registries = trait_registries.dup.freeze
84
91
  @external_files = external_files.dup.freeze
92
+ @hkt_registrations = hkt_registrations.dup.freeze
93
+ @hkt_definitions = hkt_definitions.dup.freeze
85
94
  end
86
95
  # rubocop:enable Metrics/ParameterLists, Metrics/AbcSize
87
96
 
@@ -110,7 +119,7 @@ module Rigor
110
119
  errors
111
120
  end
112
121
 
113
- def to_h
122
+ def to_h # rubocop:disable Metrics/AbcSize
114
123
  {
115
124
  "id" => id,
116
125
  "version" => version,
@@ -124,7 +133,9 @@ module Rigor
124
133
  "block_as_methods" => block_as_methods.map(&:to_h),
125
134
  "heredoc_templates" => heredoc_templates.map(&:to_h),
126
135
  "trait_registries" => trait_registries.map(&:to_h),
127
- "external_files" => external_files.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 } }
128
139
  }
129
140
  end
130
141
 
@@ -279,6 +290,42 @@ module Rigor
279
290
  "Rigor::Plugin::Macro::ExternalFile instances, got #{entries.inspect}"
280
291
  end
281
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
+
282
329
  def coerce_consumes(consumes)
283
330
  unless consumes.is_a?(Array)
284
331
  raise ArgumentError, "plugin manifest consumes must be an Array, got #{consumes.inspect}"
@@ -85,6 +85,25 @@ module Rigor
85
85
  plugins.flat_map { |plugin| plugin.manifest.type_node_resolvers }
86
86
  end
87
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
+
88
107
  EMPTY = new.freeze
89
108
  end
90
109
  end
@@ -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
@@ -4,6 +4,7 @@ require_relative "type"
4
4
  require_relative "builtins/imported_refinements"
5
5
  require_relative "flow_contribution"
6
6
  require_relative "rbs_extended/reporter"
7
+ require_relative "rbs_extended/hkt_directives"
7
8
 
8
9
  module Rigor
9
10
  # Slice 7 phase 15 — first-preview reader for the
@@ -385,13 +386,15 @@ module Rigor
385
386
 
386
387
  name_scope = environment&.name_scope
387
388
  reporter = environment&.rbs_extended_reporter
389
+ hkt_registry = environment&.hkt_registry
388
390
 
389
391
  annotations.each do |annotation|
390
392
  type = parse_return_type_override(
391
393
  annotation.string,
392
394
  name_scope: name_scope,
393
395
  reporter: reporter,
394
- source_location: annotation.location
396
+ source_location: annotation.location,
397
+ hkt_registry: hkt_registry
395
398
  )
396
399
  return type if type
397
400
  end
@@ -417,10 +420,40 @@ module Rigor
417
420
  /x
418
421
  private_constant :RETURN_DIRECTIVE_PATTERN
419
422
 
420
- def parse_return_type_override(string, name_scope: nil, reporter: nil, source_location: nil)
423
+ # ADR-20 slice 2d recognises `App[<uri>, <ClassName>, ...]`
424
+ # syntax in a `rigor:v1:return:` payload before falling
425
+ # through to the refinement-name parser. The match captures
426
+ # the namespaced URI (`json::value`) plus a comma-separated
427
+ # list of bare class names (`String`, `Symbol`, `Integer`).
428
+ # Slice 2d keeps the arg vocabulary intentionally narrow;
429
+ # parameterised forms (`Array[T]`, `Hash[K, V]`), unions,
430
+ # and refinements inside `App[...]` wait for a follow-up
431
+ # slice's expression parser.
432
+ APP_PAYLOAD_PATTERN = /
433
+ \A
434
+ App\[
435
+ \s*
436
+ (?<uri>[a-z_][a-z0-9_]*(?:::[a-z_][a-z0-9_]*)+)
437
+ \s*,\s*
438
+ (?<args>[^\[\]]+?)
439
+ \s*\]
440
+ \z
441
+ /x
442
+ private_constant :APP_PAYLOAD_PATTERN
443
+
444
+ def parse_return_type_override(string, name_scope: nil, reporter: nil, source_location: nil, hkt_registry: nil)
421
445
  match = RETURN_DIRECTIVE_PATTERN.match(string)
422
446
  return nil if match.nil?
423
447
 
448
+ app_type = parse_app_payload(
449
+ match[:payload],
450
+ name_scope: name_scope,
451
+ reporter: reporter,
452
+ source_location: source_location,
453
+ hkt_registry: hkt_registry
454
+ )
455
+ return app_type if app_type
456
+
424
457
  type = Builtins::ImportedRefinements.parse(
425
458
  match[:payload],
426
459
  name_scope: name_scope,
@@ -431,6 +464,53 @@ module Rigor
431
464
  type
432
465
  end
433
466
 
467
+ # ADR-20 slice 2d. Parses `App[<uri>, <ClassName>, ...]`
468
+ # syntax into a `Rigor::Type::App`. When `hkt_registry` is
469
+ # supplied and the URI is registered with a body_tree, the
470
+ # `App` is reduced eagerly via {Inference::HktRegistry#reduce}
471
+ # so call sites observe the unfolded form (e.g.
472
+ # `Union[nil, true, false, ..., Array[App[json::value,
473
+ # String]], Hash[String, App[json::value, String]]]`)
474
+ # rather than the opaque carrier. When the registry is
475
+ # absent or the URI is unregistered, the carrier with its
476
+ # registry-supplied bound (or `untyped` as a last-resort
477
+ # fallback) is returned as-is.
478
+ def parse_app_payload(payload, name_scope: nil, reporter: nil, source_location: nil, hkt_registry: nil)
479
+ match = APP_PAYLOAD_PATTERN.match(payload)
480
+ return nil if match.nil?
481
+
482
+ uri = match[:uri].to_sym
483
+ arg_classes = match[:args].split(",").map(&:strip)
484
+ args = arg_classes.map { |name| resolve_app_arg(name, name_scope: name_scope) }
485
+
486
+ if args.any?(&:nil?)
487
+ record_unresolved(reporter, "App payload `#{payload}`: unresolved arg class name", source_location)
488
+ return nil
489
+ end
490
+
491
+ registration = hkt_registry&.registration(uri)
492
+ bound = registration&.bound || Type::Combinator.untyped
493
+ app = Type::App.new(uri, args, bound: bound)
494
+
495
+ return app if hkt_registry.nil? || !hkt_registry.defined?(uri)
496
+
497
+ reduced = hkt_registry.reduce(app)
498
+ reduced || app
499
+ end
500
+
501
+ def resolve_app_arg(class_name, name_scope: nil)
502
+ return nil unless /\A(?:::)?(?:[A-Z]\w*)(?:::[A-Z]\w*)*\z/.match?(class_name)
503
+
504
+ normalized = class_name.sub(/\A::/, "")
505
+ return Type::Nominal.new(normalized) if name_scope.nil?
506
+
507
+ if name_scope.respond_to?(:nominal_for_name)
508
+ resolved = name_scope.nominal_for_name(normalized)
509
+ return resolved if resolved
510
+ end
511
+ Type::Nominal.new(normalized)
512
+ end
513
+
434
514
  # Returned for `rigor:v1:param: <name> <refinement>`. The
435
515
  # parameter name is a Ruby identifier (Symbol); the type
436
516
  # is any `Rigor::Type` the refinement parser resolves
@@ -52,6 +52,15 @@ module Rigor
52
52
  @paths = paths
53
53
  @observations = normalize_observations(observations)
54
54
  @include_private = include_private
55
+ # Per-file scratch state. `analyse_file` resets each
56
+ # one to a fresh container for every file walked so
57
+ # candidates from one file don't leak into another;
58
+ # initialising empty here gives downstream consumers
59
+ # (`build_candidate`, `method_def_prefix`) a never-nil
60
+ # invariant without per-call-site defensive guards.
61
+ @namespace_kinds = {}
62
+ @module_function_methods = Set.new
63
+ @class_shells = Set.new
55
64
  end
56
65
 
57
66
  # Lifts legacy plain-`Array[Type]` observation entries
@@ -270,8 +279,8 @@ module Rigor
270
279
  # `Const = Data.define(...)` declarations.
271
280
  def build_candidate(**)
272
281
  MethodCandidate.new(
273
- namespace_kinds: @namespace_kinds || {},
274
- class_shells: (@class_shells || Set.new).to_a,
282
+ namespace_kinds: @namespace_kinds,
283
+ class_shells: @class_shells.to_a,
275
284
  **
276
285
  )
277
286
  end
@@ -282,7 +291,7 @@ module Rigor
282
291
  # dispatch at runtime), or "def " (plain instance).
283
292
  def method_def_prefix(class_name, method_name, kind)
284
293
  return "def self." if kind == :singleton
285
- return "def self?." if @module_function_methods&.include?([class_name, method_name])
294
+ return "def self?." if @module_function_methods.include?([class_name, method_name])
286
295
 
287
296
  "def "
288
297
  end