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
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Inference
5
+ # ADR-20 Slice 2a — node types for the parsed body of a
6
+ # type-function `Definition`. Each node represents one
7
+ # piece of a Rigor-side type expression that the reducer
8
+ # ({HktReducer}) walks against a concrete argument list.
9
+ #
10
+ # Slice 2a ships a programmatic constructor surface only:
11
+ # plugin and Rigor-bundled overlay authors build a body
12
+ # tree by hand using these node types. The string-grammar
13
+ # parser that reads `Definition#body` (the raw String slot
14
+ # already populated by Slice 1's `HktDirectives.parse_define`)
15
+ # into a tree is Slice 2b's deliverable; until it ships, the
16
+ # `body` String stays opaque and `body_tree` is the
17
+ # evaluable form.
18
+ #
19
+ # The five node types cover the JSON.parse and dry-monads
20
+ # use cases ADR-20 § Implementation slicing names as
21
+ # near-term adopters:
22
+ #
23
+ # - {TypeLeaf} — wraps a fully-built `Rigor::Type`
24
+ # (use for atoms like `nil`, `Constant<true>`,
25
+ # `Nominal[Integer]`).
26
+ # - {Param} — reference to a formal parameter
27
+ # declared in the enclosing `Definition#params` list
28
+ # (e.g. `K` in `json::value[K]`). The reducer
29
+ # substitutes from the application's `args`.
30
+ # - {AppRef} — abstract HKT application; the reducer
31
+ # resolves it via the registry, or returns the `App`
32
+ # carrier as-is when the reference is self-recursive
33
+ # (lazy "tying-the-knot" handling that lets recursive
34
+ # sums like `json::value` reduce without infinite
35
+ # expansion).
36
+ # - {Union} — N-ary union of arms.
37
+ # - {NominalApp} — parameterised nominal class
38
+ # (`Array[X]`, `Hash[K, V]`) whose type args are
39
+ # themselves body nodes.
40
+ #
41
+ # Every node is a frozen `Data.define` value; structural
42
+ # equality is by-field.
43
+ module HktBody
44
+ # Wraps a pre-built `Rigor::Type` value. Use for atoms
45
+ # that need no substitution (e.g. `Nominal[Integer]`,
46
+ # `Constant<nil>`).
47
+ TypeLeaf = Data.define(:type) do
48
+ def initialize(type:)
49
+ raise ArgumentError, "type must not be nil" if type.nil?
50
+
51
+ super
52
+ end
53
+ end
54
+
55
+ # Reference to a formal parameter the enclosing
56
+ # `Definition#params` declared. The reducer substitutes
57
+ # this node with the matching positional arg from the
58
+ # `App` being reduced; an unknown name raises during
59
+ # reduction (the parser, when it ships, MUST reject
60
+ # unknown names earlier).
61
+ Param = Data.define(:name) do
62
+ def initialize(name:)
63
+ raise ArgumentError, "name must be a Symbol, got #{name.class}" unless name.is_a?(Symbol)
64
+
65
+ super
66
+ end
67
+ end
68
+
69
+ # Abstract HKT application — the reducer's primary
70
+ # recursion point. `uri` is a namespaced Symbol
71
+ # matching some `Registration` in the registry; `args`
72
+ # is an Array of body nodes (each gets substituted /
73
+ # resolved before being used).
74
+ AppRef = Data.define(:uri, :args) do
75
+ def initialize(uri:, args:)
76
+ raise ArgumentError, "uri must be a Symbol, got #{uri.class}" unless uri.is_a?(Symbol)
77
+ raise ArgumentError, "uri must be namespaced as `:a::b`, got #{uri.inspect}" unless uri.to_s.include?("::")
78
+ raise ArgumentError, "args must be an Array, got #{args.class}" unless args.is_a?(Array)
79
+ raise ArgumentError, "args must be non-empty" if args.empty?
80
+
81
+ super(uri: uri, args: args.dup.freeze)
82
+ end
83
+ end
84
+
85
+ # N-ary union. The reducer builds the result through
86
+ # `Type::Combinator.union(*reduced_arms)` so
87
+ # normalization (flattening, dedup, Bot drop) applies.
88
+ Union = Data.define(:arms) do
89
+ def initialize(arms:)
90
+ raise ArgumentError, "arms must be an Array, got #{arms.class}" unless arms.is_a?(Array)
91
+ raise ArgumentError, "arms must be non-empty" if arms.empty?
92
+
93
+ super(arms: arms.dup.freeze)
94
+ end
95
+ end
96
+
97
+ # Parameterised nominal class. `class_name` is the
98
+ # Ruby class name (`"Array"`, `"Hash"`); `args` is an
99
+ # Array of body nodes for the type arguments. The
100
+ # reducer builds the result through
101
+ # `Type::Combinator.nominal_of(class_name, type_args:
102
+ # reduced_args)`.
103
+ NominalApp = Data.define(:class_name, :args) do
104
+ def initialize(class_name:, args:)
105
+ unless class_name.is_a?(String) && !class_name.empty?
106
+ raise ArgumentError, "class_name must be a non-empty String, got #{class_name.inspect}"
107
+ end
108
+ raise ArgumentError, "args must be an Array, got #{args.class}" unless args.is_a?(Array)
109
+ raise ArgumentError, "args must be non-empty (use TypeLeaf with Nominal for raw class refs)" if args.empty?
110
+
111
+ super(class_name: class_name, args: args.dup.freeze)
112
+ end
113
+ end
114
+
115
+ # ADR-20 § D3 — conditional type form. `test` is a
116
+ # {TestSubtype} / {TestEquality} / {TestMembership}
117
+ # value object the reducer evaluates against the
118
+ # current bindings; `then_branch` / `else_branch` are
119
+ # body nodes. The reducer's trinary handling:
120
+ #
121
+ # - test = `yes` → return the reduced `then_branch`.
122
+ # - test = `no` → return the reduced `else_branch`.
123
+ # - test = `maybe` → widen to the union of both
124
+ # reduced branches (per ADR-20 WD7 / robustness
125
+ # principle).
126
+ Conditional = Data.define(:test, :then_branch, :else_branch) do
127
+ def initialize(test:, then_branch:, else_branch:)
128
+ raise ArgumentError, "test must not be nil" if test.nil?
129
+ raise ArgumentError, "then_branch must not be nil" if then_branch.nil?
130
+ raise ArgumentError, "else_branch must not be nil" if else_branch.nil?
131
+
132
+ super
133
+ end
134
+ end
135
+
136
+ # `left <: right` — subtype check. `left` is typically
137
+ # a {Param} reference; `right` is any body expression.
138
+ TestSubtype = Data.define(:left, :right) do
139
+ def initialize(left:, right:)
140
+ raise ArgumentError, "left/right must not be nil" if left.nil? || right.nil?
141
+
142
+ super
143
+ end
144
+ end
145
+
146
+ # `left == right` — structural equality. Useful for
147
+ # discriminating against literal constants
148
+ # (`E == :symbol`).
149
+ TestEquality = Data.define(:left, :right) do
150
+ def initialize(left:, right:)
151
+ raise ArgumentError, "left/right must not be nil" if left.nil? || right.nil?
152
+
153
+ super
154
+ end
155
+ end
156
+
157
+ # `left in [opt1, opt2, ...]` — set membership. Each
158
+ # `option` is a body node; the test passes iff `left`
159
+ # is structurally equal to any of the options.
160
+ TestMembership = Data.define(:left, :options) do
161
+ def initialize(left:, options:)
162
+ raise ArgumentError, "left must not be nil" if left.nil?
163
+ raise ArgumentError, "options must be an Array, got #{options.class}" unless options.is_a?(Array)
164
+ raise ArgumentError, "options must be non-empty" if options.empty?
165
+
166
+ super(left: left, options: options.dup.freeze)
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,363 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "hkt_body"
4
+ require_relative "../type"
5
+
6
+ module Rigor
7
+ module Inference
8
+ # ADR-20 slice 2b — parses the body of an
9
+ # `HktRegistry::Definition` (a `String`, as populated by
10
+ # Slice 1's `HktDirectives.parse_define` from
11
+ # `%a{rigor:v1:hkt_define}` payloads) into the `HktBody`
12
+ # node tree the Slice 2a reducer evaluates against.
13
+ #
14
+ # The minimum-viable grammar covered here is the
15
+ # union-of-atoms-and-parameterised-forms subset of ADR-20
16
+ # § D3 — sufficient for `JSON.parse`'s `json::value`
17
+ # recursive sum and for any other recursive-data-shape
18
+ # signatures (Lisp value trees, dry-types refinements
19
+ # without conditionals). The conditional / indexed-access
20
+ # forms (`E <: T ? A : B`, `E in [k1, k2]`) drafted in D3
21
+ # remain a follow-up slice — bodies that contain `?`
22
+ # raise `ParseError` and the calling directive parser
23
+ # drops the body_tree (the body String remains stored and
24
+ # the reducer falls back to `app.bound`).
25
+ #
26
+ # ## Grammar (slice 2b)
27
+ #
28
+ # body := union
29
+ # union := type_expr ("|" type_expr)*
30
+ # type_expr := atom | nominal_app | app_ref | param
31
+ # atom := "nil" | "true" | "false" | "bool" | "untyped"
32
+ # param := UCNAME (when UCNAME ∈ params)
33
+ # nominal_app := class_name ("[" type_expr ("," type_expr)* "]")?
34
+ # class_name := "::"? UCNAME ("::" UCNAME)*
35
+ # app_ref := "App" "[" uri "," type_expr ("," type_expr)* "]"
36
+ # uri := IDENT ("::" IDENT)+
37
+ # UCNAME := /[A-Z]\w*/
38
+ # IDENT := /[a-z_]\w*/
39
+ #
40
+ # ## Disambiguation
41
+ #
42
+ # The same syntactic UCNAME spells both a parameter
43
+ # reference (`K` when `params = [:K]`) and a nominal class
44
+ # name (`Integer`). The parser resolves by checking the
45
+ # `params` set passed to {.parse}; an unknown UCNAME is
46
+ # treated as a nominal class name. `App` is reserved at
47
+ # the head position of an `App[...]` form; using `App` as
48
+ # a class name is therefore not supported.
49
+ #
50
+ # Atoms are kept verbatim as `HktBody::TypeLeaf` nodes
51
+ # wrapping the appropriate `Rigor::Type::*` carrier:
52
+ # `nil` / `true` / `false` produce `Constant` carriers;
53
+ # `bool` produces the `Constant<true> | Constant<false>`
54
+ # union; `untyped` produces `Combinator.untyped`
55
+ # (i.e. `Dynamic[Top]`). Nominal class names produce raw
56
+ # `Type::Nominal` carriers (no `name_scope` resolution at
57
+ # this slice — the reducer trusts the name verbatim).
58
+ module HktBodyParser
59
+ class ParseError < StandardError; end
60
+
61
+ module_function
62
+
63
+ def parse(string, params:)
64
+ raise ArgumentError, "string must be a String, got #{string.class}" unless string.is_a?(String)
65
+ raise ArgumentError, "params must be an Array, got #{params.class}" unless params.is_a?(Array)
66
+
67
+ params_set = params.to_h { |p| [p.to_sym, true] }
68
+ tokens = Tokenizer.new(string).tokenize
69
+ parser = Parser.new(tokens, params_set)
70
+ result = parser.parse_union
71
+ parser.expect_eof!
72
+ result
73
+ end
74
+
75
+ Token = Data.define(:kind, :value, :pos)
76
+
77
+ class Tokenizer
78
+ SCANNER_REGEX = /
79
+ \G
80
+ (?:
81
+ (?<ws>\s+)
82
+ | (?<lb>\[)
83
+ | (?<rb>\])
84
+ | (?<lparen>\()
85
+ | (?<rparen>\))
86
+ | (?<comma>,)
87
+ | (?<pipe>\|)
88
+ | (?<sub><:)
89
+ | (?<eq>==)
90
+ | (?<sep>::)
91
+ | (?<colon>:(?!:))
92
+ | (?<question>\?)
93
+ | (?<ident>[a-z_][a-zA-Z0-9_]*)
94
+ | (?<ucname>[A-Z][a-zA-Z0-9_]*)
95
+ )
96
+ /x
97
+
98
+ def initialize(string)
99
+ @string = string
100
+ end
101
+
102
+ TOKEN_KINDS = SCANNER_REGEX.named_captures.keys.freeze
103
+ private_constant :TOKEN_KINDS
104
+
105
+ def tokenize
106
+ tokens = []
107
+ pos = 0
108
+ while pos < @string.size
109
+ match = SCANNER_REGEX.match(@string, pos)
110
+ raise ParseError, "unexpected character at position #{pos}: #{@string[pos].inspect}" if match.nil?
111
+
112
+ kind = TOKEN_KINDS.find { |k| match[k] }
113
+ raise ParseError, "internal tokenizer error at position #{pos}" if kind.nil?
114
+
115
+ value = match[kind.to_sym]
116
+ raise ParseError, "internal tokenizer error: no match for #{kind}" if value.nil?
117
+
118
+ pos += value.size
119
+ next if kind == "ws"
120
+
121
+ tokens << Token.new(kind: kind.to_sym, value: value, pos: pos - value.size)
122
+ end
123
+ tokens
124
+ end
125
+ end
126
+
127
+ class Parser
128
+ def initialize(tokens, params_set)
129
+ @tokens = tokens
130
+ @pos = 0
131
+ @params_set = params_set
132
+ end
133
+
134
+ def parse_union
135
+ arms = [parse_type_expr]
136
+ while peek_kind == :pipe
137
+ consume
138
+ arms << parse_type_expr
139
+ end
140
+ return arms.first if arms.size == 1
141
+
142
+ HktBody::Union.new(arms: arms)
143
+ end
144
+
145
+ def parse_type_expr
146
+ tok = peek
147
+ raise ParseError, "unexpected end of input; expected type expression" if tok.nil?
148
+
149
+ case tok.kind
150
+ when :lparen then parse_conditional
151
+ when :ident then parse_lowercase_atom
152
+ when :ucname then parse_ucname_form
153
+ when :sep then parse_classname_with_leading_sep
154
+ else
155
+ raise ParseError, "unexpected token #{tok.kind} (#{tok.value.inspect}) at position #{tok.pos}"
156
+ end
157
+ end
158
+
159
+ # ADR-20 § D3 conditional parser. Grammar:
160
+ #
161
+ # conditional := "(" test "?" union ":" union ")"
162
+ # test := type_expr ("<:" | "==") type_expr
163
+ #
164
+ # Parens delimit a conditional unambiguously — bare
165
+ # `(type_expr)` grouping is not supported at this slice
166
+ # (no use case yet). Branches can be unions; test sides
167
+ # are single arms (wrap in `App[my_union, ...]` if you
168
+ # need a union there). `in [opt1, opt2]` membership
169
+ # tests are programmatically supported via
170
+ # `HktBody::TestMembership` but the parser does not yet
171
+ # recognise the `in` keyword (no concrete demand yet).
172
+ def parse_conditional
173
+ expect!(:lparen)
174
+ test = parse_test
175
+ expect!(:question)
176
+ then_branch = parse_union
177
+ expect!(:colon)
178
+ else_branch = parse_union
179
+ expect!(:rparen)
180
+ HktBody::Conditional.new(test: test, then_branch: then_branch, else_branch: else_branch)
181
+ end
182
+
183
+ def parse_test
184
+ left = parse_type_expr
185
+ op = peek
186
+ case op&.kind
187
+ when :sub
188
+ consume
189
+ HktBody::TestSubtype.new(left: left, right: parse_type_expr)
190
+ when :eq
191
+ consume
192
+ HktBody::TestEquality.new(left: left, right: parse_type_expr)
193
+ when :ident
194
+ parse_in_membership(left, op_token: op)
195
+ else
196
+ actual = op.nil? ? "end of input" : "#{op.kind} (#{op.value.inspect})"
197
+ raise ParseError, "expected `<:`, `==`, or `in` in conditional test, got #{actual}"
198
+ end
199
+ end
200
+
201
+ # `left in [opt1, opt2, ...]` membership test.
202
+ # Distinguished from a lowercase atom by the
203
+ # subsequent `[` — the only place an identifier
204
+ # `in` is permitted at this position is membership
205
+ # syntax.
206
+ def parse_in_membership(left, op_token:)
207
+ unless op_token.value == "in"
208
+ raise ParseError,
209
+ "expected `<:`, `==`, or `in` in conditional test, got ident (#{op_token.value.inspect})"
210
+ end
211
+
212
+ consume # in
213
+ expect!(:lb)
214
+ options = [parse_type_expr]
215
+ while peek_kind == :comma
216
+ consume
217
+ options << parse_type_expr
218
+ end
219
+ expect!(:rb)
220
+ HktBody::TestMembership.new(left: left, options: options)
221
+ end
222
+
223
+ def parse_lowercase_atom
224
+ tok = consume
225
+ type = case tok.value
226
+ when "nil" then Type::Constant.new(nil)
227
+ when "true" then Type::Constant.new(true)
228
+ when "false" then Type::Constant.new(false)
229
+ when "bool" then Type::Combinator.union(Type::Constant.new(true), Type::Constant.new(false))
230
+ when "untyped" then Type::Combinator.untyped
231
+ else raise ParseError, "unknown atom #{tok.value.inspect} at position #{tok.pos}"
232
+ end
233
+ HktBody::TypeLeaf.new(type: type)
234
+ end
235
+
236
+ def parse_ucname_form
237
+ tok = peek
238
+ return parse_app_ref if tok.value == "App"
239
+
240
+ if @params_set.key?(tok.value.to_sym) && !class_continuation?
241
+ consume
242
+ return HktBody::Param.new(name: tok.value.to_sym)
243
+ end
244
+
245
+ parse_nominal_or_param_with_args
246
+ end
247
+
248
+ # Returns true when the current UCName is followed by
249
+ # `::` (qualified class name continuation) or `[`
250
+ # (parameterised application). In either case the
251
+ # token is a nominal, not a param ref — Slice 2b's
252
+ # `Param` nodes are always single bare identifiers.
253
+ def class_continuation?
254
+ next_tok = @tokens[@pos + 1]
255
+ next_tok && %i[sep lb].include?(next_tok.kind)
256
+ end
257
+
258
+ def parse_nominal_or_param_with_args
259
+ class_name = parse_class_name
260
+ if peek_kind == :lb
261
+ consume
262
+ args = parse_arg_list
263
+ expect!(:rb)
264
+ HktBody::NominalApp.new(class_name: class_name, args: args)
265
+ else
266
+ HktBody::TypeLeaf.new(type: Type::Nominal.new(class_name))
267
+ end
268
+ end
269
+
270
+ def parse_classname_with_leading_sep
271
+ # The leading "::" form (`::Foo::Bar`). Consume the
272
+ # separator so the rest threads through parse_class_name.
273
+ consume
274
+ tok = peek
275
+ raise ParseError, "expected class name after `::`" if tok.nil? || tok.kind != :ucname
276
+
277
+ parse_nominal_or_param_with_args
278
+ end
279
+
280
+ def parse_class_name
281
+ parts = [expect!(:ucname).value]
282
+ while peek_kind == :sep && @tokens[@pos + 1]&.kind == :ucname
283
+ consume # ::
284
+ parts << expect!(:ucname).value
285
+ end
286
+ parts.join("::")
287
+ end
288
+
289
+ def parse_app_ref
290
+ tok = consume
291
+ raise ParseError, "expected `App[...]`, got #{tok.value.inspect}" unless tok.value == "App"
292
+
293
+ expect!(:lb)
294
+ uri = parse_uri
295
+ expect!(:comma)
296
+ args = parse_arg_list
297
+ expect!(:rb)
298
+ HktBody::AppRef.new(uri: uri, args: args)
299
+ end
300
+
301
+ def parse_uri
302
+ parts = [expect!(:ident).value]
303
+ while peek_kind == :sep
304
+ consume
305
+ parts << expect!(:ident).value
306
+ end
307
+ raise ParseError, "uri must be namespaced (`a::b`), got #{parts.first.inspect}" if parts.size < 2
308
+
309
+ parts.join("::").to_sym
310
+ end
311
+
312
+ # Arg list for `Foo[A, B, C]` and `App[uri, A, B]`
313
+ # forms. Each arg is parsed as a union so per-arg
314
+ # `A | B` forms work (`Array[K | nil]`); the COMMA
315
+ # at the top level still separates args, so
316
+ # `Hash[K, V]` reads as two args (each a single-arm
317
+ # union that collapses to the arm) rather than one
318
+ # union of two.
319
+ def parse_arg_list
320
+ args = [parse_union]
321
+ while peek_kind == :comma
322
+ consume
323
+ args << parse_union
324
+ end
325
+ args
326
+ end
327
+
328
+ def expect_eof!
329
+ return if @pos >= @tokens.size
330
+
331
+ tok = @tokens[@pos]
332
+ raise ParseError, "expected end of input, got #{tok.kind} (#{tok.value.inspect}) at position #{tok.pos}"
333
+ end
334
+
335
+ private
336
+
337
+ def peek
338
+ @tokens[@pos]
339
+ end
340
+
341
+ def peek_kind
342
+ @tokens[@pos]&.kind
343
+ end
344
+
345
+ def consume
346
+ tok = @tokens[@pos]
347
+ @pos += 1
348
+ tok
349
+ end
350
+
351
+ def expect!(kind)
352
+ tok = @tokens[@pos]
353
+ if tok.nil? || tok.kind != kind
354
+ actual = tok.nil? ? "end of input" : "#{tok.kind} (#{tok.value.inspect})"
355
+ raise ParseError, "expected #{kind}, got #{actual}"
356
+ end
357
+ @pos += 1
358
+ tok
359
+ end
360
+ end
361
+ end
362
+ end
363
+ end