@clear-capabilities/agentic-security-scanner 0.153.2 → 0.154.0

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 (134) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/bin/agentic-security.js +123 -19
  3. package/dist/1444.index.js +64 -19
  4. package/dist/1905.index.js +46 -3
  5. package/dist/2862.index.js +325 -0
  6. package/dist/3499.index.js +5 -1
  7. package/dist/3531.index.js +261 -0
  8. package/dist/3596.index.js +103 -0
  9. package/dist/4113.index.js +15 -3
  10. package/dist/4171.index.js +319 -0
  11. package/dist/5637.index.js +10 -4
  12. package/dist/6359.index.js +433 -0
  13. package/dist/7838.index.js +15 -3
  14. package/dist/8551.index.js +1164 -0
  15. package/dist/8587.index.js +410 -0
  16. package/dist/8714.index.js +98 -0
  17. package/dist/8798.index.js +218 -0
  18. package/dist/9220.index.js +35 -3
  19. package/dist/9560.index.js +26 -3
  20. package/dist/agentic-security.mjs +51 -51
  21. package/dist/agentic-security.mjs.sha256 +1 -1
  22. package/package.json +17 -2
  23. package/src/dataflow/CLAUDE.md +9 -0
  24. package/src/dataflow/catalog-haskell.js +37 -0
  25. package/src/dataflow/catalog.js +4 -0
  26. package/src/dataflow/engine.js +33 -3
  27. package/src/dataflow/privacy-catalog-haskell.js +50 -0
  28. package/src/dataflow/privacy-catalog.js +5 -1
  29. package/src/discovery/CLAUDE.md +6 -0
  30. package/src/discovery/index.js +12 -3
  31. package/src/discovery/lenses.js +4 -1
  32. package/src/discovery/partition.js +28 -0
  33. package/src/egress/redact.js +8 -0
  34. package/src/engine.js +125 -14
  35. package/src/ir/index.js +39 -2
  36. package/src/language/CLAUDE.md +74 -0
  37. package/src/language/accuracy.js +90 -0
  38. package/src/language/aibom.js +307 -0
  39. package/src/language/assurance.js +193 -0
  40. package/src/language/bom-validate.js +227 -0
  41. package/src/language/bom.js +236 -0
  42. package/src/language/bridges.js +392 -0
  43. package/src/language/compliance-map.js +61 -0
  44. package/src/language/context.js +199 -0
  45. package/src/language/contracts.js +339 -0
  46. package/src/language/discovery.js +359 -0
  47. package/src/language/engine-pass.js +120 -0
  48. package/src/language/fix-lifecycle.js +120 -0
  49. package/src/language/haskell-adapter.js +51 -0
  50. package/src/language/haskell-disclosure.js +170 -0
  51. package/src/language/haskell-findings.js +44 -0
  52. package/src/language/haskell-fix.js +409 -0
  53. package/src/language/haskell-grammar.js +78 -0
  54. package/src/language/haskell-guards.js +382 -0
  55. package/src/language/haskell-ir.js +1377 -0
  56. package/src/language/haskell-llm.js +128 -0
  57. package/src/language/haskell-manifests.js +1290 -0
  58. package/src/language/haskell-models.js +278 -0
  59. package/src/language/haskell-parser.js +1073 -0
  60. package/src/language/haskell-resolved-graph.js +414 -0
  61. package/src/language/haskell-sca.js +387 -0
  62. package/src/language/haskell-security-rules.js +332 -0
  63. package/src/language/haskell-supply.js +140 -0
  64. package/src/language/haskell-sweep.js +85 -0
  65. package/src/language/haskell-syntax.js +895 -0
  66. package/src/language/haskell-web.js +749 -0
  67. package/src/language/nix-adapter.js +42 -0
  68. package/src/language/nix-agents.js +155 -0
  69. package/src/language/nix-build-trust.js +790 -0
  70. package/src/language/nix-closure.js +331 -0
  71. package/src/language/nix-eval-isolation.js +274 -0
  72. package/src/language/nix-fix.js +268 -0
  73. package/src/language/nix-grammar.js +97 -0
  74. package/src/language/nix-inventory.js +379 -0
  75. package/src/language/nix-ir.js +642 -0
  76. package/src/language/nix-parser.js +908 -0
  77. package/src/language/nix-privacy.js +114 -0
  78. package/src/language/nix-sca.js +358 -0
  79. package/src/language/nix-script-taint.js +885 -0
  80. package/src/language/nix-secrets.js +396 -0
  81. package/src/language/nixos-hardening.js +564 -0
  82. package/src/language/nixos-module-resolver.js +595 -0
  83. package/src/language/nixos-option-catalog.js +96 -0
  84. package/src/language/pragma.js +150 -0
  85. package/src/language/resolved-pass.js +146 -0
  86. package/src/language/secrets.js +323 -0
  87. package/src/language/state-artifacts.js +78 -0
  88. package/src/language/support-registry.js +224 -0
  89. package/src/language/witness.js +185 -0
  90. package/src/lineage/CLAUDE.md +8 -0
  91. package/src/lineage/coverage.js +2 -0
  92. package/src/lineage/cross-repo-link.js +22 -0
  93. package/src/lineage/graph-builder.js +5 -1
  94. package/src/lineage/haskell-view.js +319 -0
  95. package/src/lineage/index.js +8 -1
  96. package/src/lineage/nix-view.js +198 -0
  97. package/src/lineage/sink-registry.js +10 -1
  98. package/src/lineage/source-registry.js +3 -0
  99. package/src/lineage/transform-catalog.js +12 -0
  100. package/src/llm-validator/index.js +16 -2
  101. package/src/lsp/server.js +69 -9
  102. package/src/mcp/CLAUDE.md +7 -0
  103. package/src/mcp/tools.js +26 -3
  104. package/src/pipeline/coverage-ledger.js +0 -0
  105. package/src/pipeline/scan-health.js +19 -1
  106. package/src/posture/accuracy-scorecard.js +51 -0
  107. package/src/posture/aibom.js +61 -3
  108. package/src/posture/artifact-registry.js +12 -0
  109. package/src/posture/auditor-walkthrough.js +41 -0
  110. package/src/posture/clustering.js +2 -2
  111. package/src/posture/evidence-bundle.js +7 -1
  112. package/src/posture/execution-proof.js +15 -0
  113. package/src/posture/falsification.js +7 -0
  114. package/src/posture/family-resolve.js +7 -0
  115. package/src/posture/fix-history.js +40 -0
  116. package/src/posture/fix-verify.js +15 -3
  117. package/src/posture/provenance/git-evidence.js +24 -0
  118. package/src/posture/provenance/origin-resolver.js +14 -2
  119. package/src/posture/provenance/predicate-replay.js +11 -2
  120. package/src/posture/provenance/sca-origin.js +20 -3
  121. package/src/posture/sbom-diff.js +7 -6
  122. package/src/posture/sbom.js +85 -32
  123. package/src/posture/sca-policy.js +10 -1
  124. package/src/posture/sca-verdict.js +8 -1
  125. package/src/posture/secret-history.js +62 -18
  126. package/src/posture/stable-id.js +21 -2
  127. package/src/posture/state-dir.js +21 -0
  128. package/src/posture/threat-model-auto.js +4 -4
  129. package/src/posture/triage.js +18 -1
  130. package/src/posture/watch-mode.js +34 -3
  131. package/src/pr-delta.js +9 -4
  132. package/src/report/index.js +35 -9
  133. package/src/runScan.js +56 -5
  134. package/src/sast/pipeline.js +34 -1
@@ -0,0 +1,895 @@
1
+ // Haskell expression/declaration syntax for the semantic IR (HS-002).
2
+ //
3
+ // Static and bounded. It lexes the ORIGINAL source (comments, pragmas and
4
+ // strings never become tokens), resolves the layout rule from token columns,
5
+ // and builds a small AST: declarations, clauses with guards and where blocks,
6
+ // patterns, and expressions with real operator precedence (`$`, `.`, `>>=`,
7
+ // `<$>`, `<*>`, backtick operators, sections). It never executes anything.
8
+ //
9
+ // What it does not do: type checking, typeclass resolution, fixity declarations
10
+ // (a fixed table is used), Template Haskell, quasi-quotes, CPP. A declaration
11
+ // it cannot parse becomes an `errors` entry and contributes no AST, so the
12
+ // caller can mark the module partial instead of inventing structure.
13
+
14
+ export class SyntaxBudgetError extends Error {}
15
+
16
+ const KW = new Set(['case', 'class', 'data', 'default', 'deriving', 'do', 'else', 'foreign', 'if', 'import', 'in', 'infix', 'infixl', 'infixr', 'instance', 'let', 'module', 'newtype', 'of', 'then', 'type', 'where']);
17
+ const RESERVED_OPS = new Set(['=', '|', '->', '<-', '::', '=>', '\\', '@', '~', '..']);
18
+ const SYM = new Set('!#$%&*+./<=>?@\\^|-~:'.split(''));
19
+ const OPEN = { '(': ')', '[': ']', '{': '}' };
20
+ const isSym = (c) => c !== undefined && SYM.has(c);
21
+ const isIdStart = (c) => c !== undefined && /[\p{L}_]/u.test(c);
22
+ const isIdChar = (c) => c !== undefined && /[\p{L}\p{N}_']/u.test(c);
23
+ const isUpper = (c) => c !== undefined && /\p{Lu}/u.test(c);
24
+ const NUM = /0[xX][0-9a-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?/y;
25
+
26
+ /**
27
+ * Token: {t, v, q?, line, col, first, off, end, cons?}
28
+ * t is var|con|op|rop|kw|int|float|str|chr|sp. `first` is true for the first
29
+ * token on its line; `col` is the 0-based column with tabs at 8.
30
+ */
31
+ export function lexHs(src) {
32
+ const T = [];
33
+ const n = src.length;
34
+ let i = 0;
35
+ let line = 1;
36
+ let col = 0;
37
+ let lineHasTok = false;
38
+ const adv = (k) => {
39
+ for (let j = 0; j < k && i < n; j++) {
40
+ const c = src[i++];
41
+ if (c === '\n') { line++; col = 0; lineHasTok = false; }
42
+ else if (c === '\t') col = (Math.floor(col / 8) + 1) * 8;
43
+ else col++;
44
+ }
45
+ };
46
+ const push = (t, v, extra, len) => {
47
+ T.push({ t, v, line, col, first: !lineHasTok, off: i, end: i + len, ...extra });
48
+ lineHasTok = true;
49
+ adv(len);
50
+ };
51
+ while (i < n) {
52
+ const c = src[i];
53
+ if (c === ' ' || c === '\t' || c === '\r' || c === '\n' || c === '\f') { adv(1); continue; }
54
+ if (c === '{' && src[i + 1] === '-') {
55
+ let depth = 0;
56
+ let j = i;
57
+ while (j < n) {
58
+ if (src[j] === '{' && src[j + 1] === '-') { depth++; j += 2; }
59
+ else if (src[j] === '-' && src[j + 1] === '}') { depth--; j += 2; if (depth === 0) break; }
60
+ else j++;
61
+ }
62
+ adv(j - i);
63
+ continue;
64
+ }
65
+ if (c === '-' && src[i + 1] === '-') {
66
+ let j = i;
67
+ while (src[j] === '-') j++;
68
+ if (!isSym(src[j])) {
69
+ while (j < n && src[j] !== '\n') j++;
70
+ adv(j - i);
71
+ continue;
72
+ }
73
+ }
74
+ if (c === '"') {
75
+ let j = i + 1;
76
+ while (j < n && src[j] !== '"') j += src[j] === '\\' ? 2 : 1;
77
+ j = Math.min(n, j + 1);
78
+ push('str', src.slice(i + 1, Math.max(i + 1, j - 1)), {}, j - i);
79
+ continue;
80
+ }
81
+ if (c === "'") {
82
+ if (src[i + 1] === '\\') {
83
+ let j = i + 2;
84
+ while (j < n && j < i + 12 && src[j] !== "'") j++;
85
+ if (src[j] === "'") { push('chr', src.slice(i + 1, j), {}, j + 1 - i); continue; }
86
+ } else if (src[i + 2] === "'" && src[i + 1] !== undefined) {
87
+ push('chr', src[i + 1], {}, 3);
88
+ continue;
89
+ }
90
+ adv(1);
91
+ continue;
92
+ }
93
+ if (c >= '0' && c <= '9') {
94
+ NUM.lastIndex = i;
95
+ const m = NUM.exec(src);
96
+ const txt = m ? m[0] : c;
97
+ push(/^[0-9]/.test(txt) && !/^0[xXoObB]/.test(txt) && /[.eE]/.test(txt) ? 'float' : 'int', txt, {}, txt.length);
98
+ continue;
99
+ }
100
+ if (isIdStart(c)) {
101
+ if (isUpper(c)) {
102
+ let j = i;
103
+ const segs = [];
104
+ let done = false;
105
+ for (;;) {
106
+ let k = j;
107
+ while (isIdChar(src[k])) k++;
108
+ segs.push(src.slice(j, k));
109
+ j = k;
110
+ if (src[j] === '.' && isIdStart(src[j + 1])) {
111
+ if (isUpper(src[j + 1])) { j++; continue; }
112
+ let m = j + 1;
113
+ while (isIdChar(src[m])) m++;
114
+ const name = src.slice(j + 1, m);
115
+ if (KW.has(name)) break;
116
+ push('var', name, { q: segs.join('.') }, m - i);
117
+ done = true;
118
+ break;
119
+ }
120
+ if (src[j] === '.' && isSym(src[j + 1])) {
121
+ let m = j + 1;
122
+ while (isSym(src[m])) m++;
123
+ const op = src.slice(j + 1, m);
124
+ push('op', op, { q: segs.join('.'), cons: op[0] === ':' }, m - i);
125
+ done = true;
126
+ break;
127
+ }
128
+ break;
129
+ }
130
+ if (!done) push('con', segs[segs.length - 1], segs.length > 1 ? { q: segs.slice(0, -1).join('.') } : {}, j - i);
131
+ continue;
132
+ }
133
+ let k = i;
134
+ while (isIdChar(src[k])) k++;
135
+ const v = src.slice(i, k);
136
+ push(KW.has(v) ? 'kw' : 'var', v, {}, k - i);
137
+ continue;
138
+ }
139
+ if (c === '(' || c === ')' || c === '[' || c === ']' || c === ',' || c === ';' || c === '`' || c === '{' || c === '}') {
140
+ push('sp', c, {}, 1);
141
+ continue;
142
+ }
143
+ if (isSym(c)) {
144
+ let j = i;
145
+ while (isSym(src[j])) j++;
146
+ const v = src.slice(i, j);
147
+ if (RESERVED_OPS.has(v)) push('rop', v, {}, j - i);
148
+ else push('op', v, { cons: v[0] === ':' }, j - i);
149
+ continue;
150
+ }
151
+ adv(1);
152
+ }
153
+ return T;
154
+ }
155
+
156
+ export function matchBrackets(T) {
157
+ const mate = new Int32Array(T.length).fill(-1);
158
+ const st = [];
159
+ for (let i = 0; i < T.length; i++) {
160
+ const t = T[i];
161
+ if (t.t !== 'sp') continue;
162
+ if (OPEN[t.v]) st.push(i);
163
+ else if (t.v === ')' || t.v === ']' || t.v === '}') {
164
+ const top = st.length ? st[st.length - 1] : -1;
165
+ if (top >= 0 && OPEN[T[top].v] === t.v) { mate[top] = i; mate[i] = top; st.pop(); }
166
+ }
167
+ }
168
+ return mate;
169
+ }
170
+
171
+ const FIX = {
172
+ '$': [0, 'r'], '$!': [0, 'r'], '>>=': [1, 'l'], '>>': [1, 'l'], '=<<': [1, 'r'], '>=>': [1, 'r'], '<=<': [1, 'r'],
173
+ '&': [1, 'l'], '<&>': [1, 'l'], '||': [2, 'r'], '&&': [3, 'r'], '<|>': [3, 'l'],
174
+ '==': [4, 'l'], '/=': [4, 'l'], '<': [4, 'l'], '<=': [4, 'l'], '>': [4, 'l'], '>=': [4, 'l'],
175
+ '<$>': [4, 'l'], '<$': [4, 'l'], '$>': [4, 'l'], '<*>': [4, 'l'], '<*': [4, 'l'], '*>': [4, 'l'], '<$!>': [4, 'l'],
176
+ '++': [5, 'r'], ':': [5, 'r'], '<>': [6, 'r'], '+': [6, 'l'], '-': [6, 'l'], '*': [7, 'l'], '/': [7, 'l'],
177
+ '^': [8, 'r'], '.': [9, 'r'], '!!': [9, 'l'], '!': [9, 'l'],
178
+ };
179
+ const BT_FIX = { elem: [4, 'l'], notElem: [4, 'l'], div: [7, 'l'], mod: [7, 'l'], quot: [7, 'l'], rem: [7, 'l'] };
180
+
181
+ /**
182
+ * Parse a Haskell source string.
183
+ * @returns {{tokens:object[], module:object|null, imports:object[], decls:object[], sigs:object[], data:object[], classes:object[], instances:object[], errors:object[], boundaries:object[]}}
184
+ */
185
+ export function parseSyntax(src, opts = {}) {
186
+ const T = lexHs(src);
187
+ const mate = matchBrackets(T);
188
+ const N = T.length;
189
+ const errors = [];
190
+ const maxSteps = opts.maxSteps || 3_000_000;
191
+ const maxDepth = opts.maxDepth || 160;
192
+ let steps = 0;
193
+ let depth = 0;
194
+ const tick = () => { if (++steps > maxSteps) throw new SyntaxBudgetError('maxSteps'); };
195
+ const err = (kind, tok, detail) => errors.push({ kind, line: tok ? tok.line : 1, detail });
196
+
197
+ let pos = 0;
198
+ let lim = 0;
199
+ let layoutIndent = -1;
200
+
201
+ const peek = () => (pos < lim ? T[pos] : null);
202
+ const isSp = (t, v) => t && t.t === 'sp' && t.v === v;
203
+ const isRop = (t, v) => t && t.t === 'rop' && t.v === v;
204
+ const isKw = (t, v) => t && t.t === 'kw' && t.v === v;
205
+ const groupEnd = (i) => (T[i] && T[i].t === 'sp' && OPEN[T[i].v] && mate[i] > i ? mate[i] + 1 : i + 1);
206
+
207
+ function sub(a, b, fn) {
208
+ const sp = pos; const sl = lim;
209
+ pos = a; lim = b;
210
+ let r;
211
+ try {
212
+ r = fn();
213
+ if (pos < lim) err('unparsed-tail', T[pos], 'tokens left after expression');
214
+ } finally { pos = sp; lim = sl; }
215
+ return r;
216
+ }
217
+ function splitTop(a, b, pred) {
218
+ const parts = [];
219
+ let s = a;
220
+ let i = a;
221
+ while (i < b) {
222
+ tick();
223
+ if (pred(T[i])) { parts.push([s, i]); s = i + 1; i++; continue; }
224
+ i = Math.min(b, groupEnd(i));
225
+ }
226
+ parts.push([s, b]);
227
+ return parts;
228
+ }
229
+ function findTop(a, b, pred) {
230
+ let i = a;
231
+ while (i < b) {
232
+ tick();
233
+ if (pred(T[i])) return i;
234
+ i = Math.min(b, groupEnd(i));
235
+ }
236
+ return -1;
237
+ }
238
+ const comma = (t) => isSp(t, ',');
239
+
240
+ // ── layout ──────────────────────────────────────────────────────────────
241
+ function splitItems(a, b, indent) {
242
+ const items = [];
243
+ let start = a;
244
+ let i = a;
245
+ while (i < b) {
246
+ tick();
247
+ const t = T[i];
248
+ // A new line at the block's indent starts a new item EVEN when that line opens a bracket
249
+ // (`("GET", ["a"]) -> ...`): the boundary test must run before the bracket is skipped.
250
+ if (indent !== null && i > start && t.first && t.col === indent && !isSp(t, ';')) {
251
+ items.push([start, i]);
252
+ start = i;
253
+ }
254
+ if (t.t === 'sp' && OPEN[t.v] && mate[i] > i) { i = mate[i] + 1; continue; }
255
+ if (isSp(t, ';')) {
256
+ if (i > start) items.push([start, i]);
257
+ start = i + 1;
258
+ }
259
+ i++;
260
+ }
261
+ if (start < b) items.push([start, b]);
262
+ return items;
263
+ }
264
+ function blockAfter(k, rangeEnd, parentIndent) {
265
+ const a = k + 1;
266
+ if (a >= rangeEnd) return { a, b: a, items: [], indent: -1 };
267
+ if (isSp(T[a], '{') && mate[a] > a) {
268
+ const b = mate[a];
269
+ return { a: a + 1, b: b + 1, items: splitItems(a + 1, b, null), indent: -1 };
270
+ }
271
+ const indent = T[a].col;
272
+ if (indent <= parentIndent) return { a, b: a, items: [], indent };
273
+ let ifOpen = 0; let thenOpen = 0; let caseOpen = 0; let letOpen = 0;
274
+ let j = a;
275
+ for (; j < rangeEnd; j++) {
276
+ tick();
277
+ const t = T[j];
278
+ if (j > a && t.first && t.col < indent) break;
279
+ // Haskell's parse-error(t) rule: a `where` that starts a line at the block's own
280
+ // indent cannot begin an item, so it closes the implicit block (do/of/let/where).
281
+ if (j > a && t.first && t.col === indent && t.t === 'kw' && t.v === 'where') break;
282
+ if (t.t === 'sp' && OPEN[t.v] && mate[j] > j) { j = mate[j]; continue; }
283
+ if (t.t === 'sp' && (t.v === ')' || t.v === ']' || t.v === '}')) break;
284
+ if (t.t === 'kw') {
285
+ const v = t.v;
286
+ if (v === 'in') { if (letOpen > 0) letOpen--; else break; }
287
+ else if (v === 'let') letOpen++;
288
+ else if (v === 'case') caseOpen++;
289
+ else if (v === 'of') { if (caseOpen > 0) caseOpen--; else break; }
290
+ else if (v === 'if') ifOpen++;
291
+ else if (v === 'then') { if (ifOpen > 0) { ifOpen--; thenOpen++; } else break; }
292
+ else if (v === 'else') { if (thenOpen > 0) thenOpen--; else break; }
293
+ }
294
+ }
295
+ return { a, b: j, items: splitItems(a, j, indent), indent };
296
+ }
297
+ function withIndent(ind, fn) {
298
+ const s = layoutIndent;
299
+ layoutIndent = ind;
300
+ try { return fn(); } finally { layoutIndent = s; }
301
+ }
302
+
303
+ // ── patterns ────────────────────────────────────────────────────────────
304
+ function apatAt(i, b) {
305
+ tick();
306
+ const t = T[i];
307
+ if (!t || i >= b) return null;
308
+ if (t.t === 'op' && t.v === '!' ) return apatAt(i + 1, b);
309
+ if (t.t === 'rop' && t.v === '~') return apatAt(i + 1, b);
310
+ if (t.t === 'var' && !t.q) {
311
+ if (isRop(T[i + 1], '@') && T[i + 1].off === t.end) {
312
+ const r = apatAt(i + 2, b);
313
+ if (r) return [{ t: 'pas', name: t.v, p: r[0] }, r[1]];
314
+ }
315
+ return [t.v === '_' ? { t: 'pwild' } : { t: 'pvar', name: t.v }, i + 1];
316
+ }
317
+ if (t.t === 'con') {
318
+ if (isSp(T[i + 1], '{') && mate[i + 1] > i + 1 && T[i + 1].off === t.end) {
319
+ const close = mate[i + 1];
320
+ const fields = [];
321
+ for (const [s, e] of splitTop(i + 2, close, comma)) {
322
+ if (s >= e) continue;
323
+ if (isRop(T[s], '..')) { fields.push({ name: '..', wildcard: true }); continue; }
324
+ const f = T[s];
325
+ if (f.t !== 'var') continue;
326
+ if (e - s >= 3 && isRop(T[s + 1], '=')) fields.push({ name: f.v, pat: patRange(s + 2, e) });
327
+ else fields.push({ name: f.v, pat: { t: 'pvar', name: f.v }, pun: true });
328
+ }
329
+ return [{ t: 'pcon', con: t.v, qual: t.q || null, args: [], fields }, close + 1];
330
+ }
331
+ return [{ t: 'pcon', con: t.v, qual: t.q || null, args: [], fields: [] }, i + 1];
332
+ }
333
+ if (t.t === 'int' || t.t === 'float' || t.t === 'str' || t.t === 'chr') return [{ t: 'plit', v: t.v }, i + 1];
334
+ if (isSp(t, '(') && mate[i] > i) {
335
+ const close = mate[i];
336
+ const parts = splitTop(i + 1, close, comma);
337
+ if (i + 1 === close) return [{ t: 'pcon', con: '()', args: [], fields: [] }, close + 1];
338
+ if (parts.length > 1) return [{ t: 'ptuple', items: parts.map(([s, e]) => patRange(s, e)) }, close + 1];
339
+ let [s, e] = parts[0];
340
+ const arrow = findTop(s, e, (x) => isRop(x, '->'));
341
+ if (arrow >= 0) s = arrow + 1;
342
+ return [patRange(s, e), close + 1];
343
+ }
344
+ if (isSp(t, '[') && mate[i] > i) {
345
+ const close = mate[i];
346
+ const items = i + 1 === close ? [] : splitTop(i + 1, close, comma).map(([s, e]) => patRange(s, e));
347
+ return [{ t: 'plist', items }, close + 1];
348
+ }
349
+ return null;
350
+ }
351
+ function patRange(a, b) {
352
+ const k = findTop(a, b, (x) => x.t === 'op' && x.cons);
353
+ if (k > a) return { t: 'pcon', con: T[k].v, qual: T[k].q || null, args: [patRange(a, k), patRange(k + 1, b)], fields: [] };
354
+ const r = apatAt(a, b);
355
+ if (!r) return { t: 'pwild', unknown: true };
356
+ const p = r[0];
357
+ let j = r[1];
358
+ if (p.t === 'pcon' && !p.args.length && !p.fields.length) {
359
+ while (j < b) {
360
+ const q = apatAt(j, b);
361
+ if (!q) break;
362
+ p.args.push(q[0]);
363
+ j = q[1];
364
+ }
365
+ }
366
+ return p;
367
+ }
368
+ function patVars(p, out = []) {
369
+ if (!p) return out;
370
+ if (p.t === 'pvar') out.push(p.name);
371
+ else if (p.t === 'pas') { out.push(p.name); patVars(p.p, out); }
372
+ else if (p.t === 'pcon') { for (const a of p.args) patVars(a, out); for (const f of p.fields) if (f.pat) patVars(f.pat, out); }
373
+ else if (p.t === 'ptuple' || p.t === 'plist') for (const a of p.items) patVars(a, out);
374
+ return out;
375
+ }
376
+
377
+ // ── expressions ─────────────────────────────────────────────────────────
378
+ function peekOp() {
379
+ const t = peek();
380
+ if (!t) return null;
381
+ if (t.t === 'op') return { name: t.v, qual: t.q || null, end: pos + 1, line: t.line };
382
+ if (isSp(t, '`') && T[pos + 1] && (T[pos + 1].t === 'var' || T[pos + 1].t === 'con') && isSp(T[pos + 2], '`') && pos + 2 < lim) {
383
+ const x = T[pos + 1];
384
+ return { name: x.v, qual: x.q || null, end: pos + 3, line: t.line, backtick: true };
385
+ }
386
+ return null;
387
+ }
388
+ function fixityOf(op) {
389
+ if (op.backtick) return BT_FIX[op.name] || [9, 'l'];
390
+ return FIX[op.name] || (op.name[0] === ':' ? [5, 'r'] : [9, 'l']);
391
+ }
392
+ function skipType() {
393
+ pos++;
394
+ while (pos < lim) {
395
+ const t = T[pos];
396
+ if (isSp(t, ',') || isSp(t, ';') || (t.t === 'kw' && ['then', 'else', 'of', 'in', 'where'].includes(t.v)) || (t.t === 'rop' && (t.v === '|' || t.v === '=' || t.v === '<-'))) break;
397
+ pos = Math.min(lim, groupEnd(pos));
398
+ }
399
+ }
400
+ function expr0() {
401
+ if (++depth > maxDepth) { depth--; throw new SyntaxBudgetError('maxDepth'); }
402
+ try {
403
+ let e = expr(0);
404
+ while (isRop(peek(), '::')) skipType();
405
+ return e;
406
+ } finally { depth--; }
407
+ }
408
+ function expr(minPrec) {
409
+ tick();
410
+ let left = operand();
411
+ for (;;) {
412
+ const op = peekOp();
413
+ if (!op) break;
414
+ const [prec, assoc] = fixityOf(op);
415
+ if (prec < minPrec) break;
416
+ pos = op.end;
417
+ const right = expr(assoc === 'r' ? prec : prec + 1);
418
+ left = { t: 'op', op: op.name, qual: op.qual, l: left, r: right, line: op.line };
419
+ }
420
+ return left;
421
+ }
422
+ function operand() {
423
+ const t = peek();
424
+ if (t && t.t === 'op' && t.v === '-') {
425
+ pos++;
426
+ return { t: 'neg', e: expr(7), line: t.line };
427
+ }
428
+ return exp10();
429
+ }
430
+ function exp10() {
431
+ const t = peek();
432
+ if (!t) { errors.push({ kind: 'expected-expression', line: 0, detail: 'empty expression' }); return { t: 'unknown' }; }
433
+ if (isRop(t, '\\')) {
434
+ pos++;
435
+ if (isKw(peek(), 'case')) {
436
+ const blk = blockAfter(pos, lim, layoutIndent);
437
+ pos = blk.b;
438
+ return { t: 'lamcase', alts: withIndent(blk.indent, () => blk.items.map(([a, b]) => parseAlt(a, b))), line: t.line };
439
+ }
440
+ const params = [];
441
+ while (pos < lim && !isRop(T[pos], '->')) {
442
+ const r = apatAt(pos, lim);
443
+ if (!r) { err('malformed-lambda', T[pos], 'bad lambda pattern'); pos = lim; return { t: 'unknown' }; }
444
+ params.push(r[0]);
445
+ pos = r[1];
446
+ }
447
+ pos++;
448
+ return { t: 'lam', params, body: expr0(), line: t.line };
449
+ }
450
+ if (t.t === 'kw') {
451
+ if (t.v === 'let') {
452
+ const blk = blockAfter(pos, lim, layoutIndent);
453
+ const decls = withIndent(blk.indent, () => parseDeclItems(blk.items));
454
+ pos = blk.b;
455
+ if (isKw(peek(), 'in')) { pos++; return { t: 'let', decls, body: expr0(), line: t.line }; }
456
+ err('let-without-in', t, 'let expression has no `in`');
457
+ return { t: 'unknown' };
458
+ }
459
+ if (t.v === 'if') {
460
+ pos++;
461
+ if (isRop(peek(), '|')) {
462
+ const guards = [];
463
+ while (isRop(peek(), '|')) {
464
+ pos++;
465
+ const arrow = findTop(pos, lim, (x) => isRop(x, '->'));
466
+ if (arrow < 0) break;
467
+ const quals = splitTop(pos, arrow, comma).map(([s, e]) => parseQual(s, e));
468
+ pos = arrow + 1;
469
+ guards.push({ quals, body: expr(0) });
470
+ }
471
+ return { t: 'multiif', guards, line: t.line };
472
+ }
473
+ const c = expr0();
474
+ if (isSp(peek(), ';')) pos++;
475
+ if (!isKw(peek(), 'then')) { err('if-without-then', t, 'if has no then'); return { t: 'unknown' }; }
476
+ pos++;
477
+ const a = expr0();
478
+ if (isSp(peek(), ';')) pos++;
479
+ if (!isKw(peek(), 'else')) { err('if-without-else', t, 'if has no else'); return { t: 'if', c, a, b: { t: 'unknown' }, line: t.line }; }
480
+ pos++;
481
+ return { t: 'if', c, a, b: expr0(), line: t.line };
482
+ }
483
+ if (t.v === 'case') {
484
+ pos++;
485
+ const scrut = expr0();
486
+ if (!isKw(peek(), 'of')) { err('case-without-of', t, 'case has no of'); return { t: 'unknown' }; }
487
+ const blk = blockAfter(pos, lim, layoutIndent);
488
+ pos = blk.b;
489
+ return { t: 'case', scrut, alts: withIndent(blk.indent, () => blk.items.map(([a, b]) => parseAlt(a, b))), line: t.line };
490
+ }
491
+ if (t.v === 'do') {
492
+ const blk = blockAfter(pos, lim, layoutIndent);
493
+ pos = blk.b;
494
+ return { t: 'do', stmts: withIndent(blk.indent, () => blk.items.map(([a, b]) => parseStmt(a, b)).filter(Boolean)), line: t.line, endLine: blk.b > 0 && T[blk.b - 1] ? T[blk.b - 1].line : t.line };
495
+ }
496
+ return { t: 'unknown', line: t.line };
497
+ }
498
+ return fexp();
499
+ }
500
+ function startsAexp() {
501
+ const t = peek();
502
+ if (!t) return false;
503
+ if (t.t === 'var' || t.t === 'con' || t.t === 'int' || t.t === 'float' || t.t === 'str' || t.t === 'chr') return true;
504
+ return isSp(t, '(') || isSp(t, '[');
505
+ }
506
+ function fexp() {
507
+ const first = peek();
508
+ const f = aexp();
509
+ const args = [];
510
+ for (;;) {
511
+ if (isRop(peek(), '@') && T[pos + 1] && peek().end === T[pos + 1].off) { pos += 2; continue; }
512
+ if (!startsAexp()) break;
513
+ args.push(aexp());
514
+ }
515
+ // BlockArguments: a trailing `do` / lambda / case is a final argument.
516
+ const t = peek();
517
+ if (t && ((t.t === 'kw' && (t.v === 'do' || t.v === 'case')) || isRop(t, '\\')) && (args.length || f.t === 'var')) args.push(exp10());
518
+ return args.length ? { t: 'app', f, args, line: first ? first.line : 0 } : f;
519
+ }
520
+ function aexp() {
521
+ const t = peek();
522
+ if (!t) return { t: 'unknown' };
523
+ let e;
524
+ if (t.t === 'var') { pos++; e = { t: 'var', name: t.v, qual: t.q || null, line: t.line }; }
525
+ else if (t.t === 'con') { pos++; e = { t: 'con', name: t.v, qual: t.q || null, line: t.line }; }
526
+ else if (t.t === 'int' || t.t === 'float' || t.t === 'str' || t.t === 'chr') { pos++; e = { t: 'lit', kind: t.t, v: t.v, line: t.line }; }
527
+ else if (isSp(t, '(') && mate[pos] > pos && mate[pos] < lim) { const close = mate[pos]; e = parenExpr(pos, close); pos = close + 1; }
528
+ else if (isSp(t, '[') && mate[pos] > pos && mate[pos] < lim) { const close = mate[pos]; e = listExpr(pos, close); pos = close + 1; }
529
+ else { err('unexpected-token', t, `unexpected token ${t.v}`); pos++; return { t: 'unknown', line: t.line }; }
530
+ while (isSp(peek(), '{') && mate[pos] > pos && mate[pos] < lim && (e.t === 'con' || e.t === 'var' || e.t === 'rec' || e.t === 'paren')) {
531
+ const close = mate[pos];
532
+ const fields = [];
533
+ for (const [s, en] of splitTop(pos + 1, close, comma)) {
534
+ if (s >= en) continue;
535
+ if (isRop(T[s], '..')) { fields.push({ name: '..', wildcard: true }); continue; }
536
+ const f = T[s];
537
+ if (f.t !== 'var') continue;
538
+ if (en - s >= 3 && isRop(T[s + 1], '=')) fields.push({ name: f.v, e: sub(s + 2, en, expr0) });
539
+ else fields.push({ name: f.v, e: { t: 'var', name: f.v, qual: null, line: f.line }, pun: true });
540
+ }
541
+ e = e.t === 'con' ? { t: 'rec', con: e, fields, line: t.line } : { t: 'recupd', base: e, fields, line: t.line };
542
+ pos = close + 1;
543
+ }
544
+ return e;
545
+ }
546
+ const isOpTok = (x) => x && x.t === 'op';
547
+ function parenExpr(open, close) {
548
+ const a = open + 1;
549
+ const line = T[open].line;
550
+ if (a === close) return { t: 'con', name: '()', qual: null, line };
551
+ const parts = splitTop(a, close, comma);
552
+ if (parts.length > 1) {
553
+ if (parts.every(([s, e]) => s >= e)) return { t: 'con', name: `(${','.repeat(parts.length - 1)})`, qual: null, line };
554
+ return { t: 'tuple', items: parts.map(([s, e]) => (s >= e ? { t: 'unknown' } : sub(s, e, expr0))), line };
555
+ }
556
+ const [s, e] = parts[0];
557
+ const first = T[s];
558
+ if (isOpTok(first) && e - s === 1) return { t: 'var', name: first.v, qual: first.q || null, op: true, line };
559
+ if (isOpTok(first) && first.v !== '-') return { t: 'rsec', op: first.v, qual: first.q || null, e: sub(s + 1, e, expr0), line };
560
+ if (isSp(first, '`') && T[s + 2] && isSp(T[s + 2], '`') && e - s > 3) return { t: 'rsec', op: T[s + 1].v, qual: T[s + 1].q || null, e: sub(s + 3, e, expr0), line, backtick: true };
561
+ const last = T[e - 1];
562
+ if (isOpTok(last) && e - s > 1) return { t: 'lsec', op: last.v, qual: last.q || null, e: sub(s, e - 1, expr0), line };
563
+ if (isSp(last, '`') && e - s > 3 && isSp(T[e - 3], '`')) return { t: 'lsec', op: T[e - 2].v, qual: T[e - 2].q || null, e: sub(s, e - 3, expr0), line, backtick: true };
564
+ return { t: 'paren', e: sub(s, e, expr0), line };
565
+ }
566
+ function listExpr(open, close) {
567
+ const a = open + 1;
568
+ const line = T[open].line;
569
+ if (a === close) return { t: 'list', items: [], line };
570
+ const bar = findTop(a, close, (x) => isRop(x, '|'));
571
+ if (bar > a) {
572
+ const head = sub(a, bar, expr0);
573
+ const quals = splitTop(bar + 1, close, comma).map(([s, e]) => parseQual(s, e));
574
+ return { t: 'listcomp', head, quals, line };
575
+ }
576
+ if (findTop(a, close, (x) => isRop(x, '..')) >= 0) {
577
+ const items = [];
578
+ for (const [s, e] of splitTop(a, close, (x) => isRop(x, '..') || isSp(x, ','))) if (s < e) items.push(sub(s, e, expr0));
579
+ return { t: 'range', items, line };
580
+ }
581
+ return { t: 'list', items: splitTop(a, close, comma).map(([s, e]) => (s >= e ? { t: 'unknown' } : sub(s, e, expr0))), line };
582
+ }
583
+
584
+ // ── guards, alternatives, statements, declarations ──────────────────────
585
+ function parseQual(a, b) {
586
+ if (isKw(T[a], 'let')) {
587
+ const blk = blockAfter(a, b, layoutIndent);
588
+ return { t: 'qlet', decls: withIndent(blk.indent, () => parseDeclItems(blk.items)) };
589
+ }
590
+ const k = findTop(a, b, (x) => isRop(x, '<-'));
591
+ if (k > a) return { t: 'qbind', pat: patRange(a, k), e: sub(k + 1, b, expr0) };
592
+ return { t: 'qbool', e: sub(a, b, expr0) };
593
+ }
594
+ // Parses the right-hand side starting at the `=`/`->`/`|` token index `from`.
595
+ function parseRhs(from, b, eq) {
596
+ return sub(from, b, () => {
597
+ let guards = null;
598
+ let body = null;
599
+ if (isRop(peek(), '|')) {
600
+ guards = [];
601
+ while (isRop(peek(), '|')) {
602
+ pos++;
603
+ const k = findTop(pos, lim, (x) => isRop(x, eq));
604
+ if (k < 0) { err('guard-without-body', T[pos - 1], 'guard has no body'); pos = lim; break; }
605
+ const quals = splitTop(pos, k, comma).map(([s, e]) => parseQual(s, e));
606
+ pos = k + 1;
607
+ guards.push({ quals, body: expr0() });
608
+ }
609
+ } else if (isRop(peek(), eq)) { pos++; body = expr0(); }
610
+ let where = [];
611
+ if (isKw(peek(), 'where')) {
612
+ const blk = blockAfter(pos, lim, layoutIndent);
613
+ where = withIndent(blk.indent, () => parseDeclItems(blk.items));
614
+ pos = blk.b;
615
+ }
616
+ return { guards, body, where };
617
+ });
618
+ }
619
+ function parseAlt(a, b) {
620
+ const k = findTop(a, b, (x) => isRop(x, '->') || isRop(x, '|'));
621
+ if (k < 0) { err('expected-arrow', T[a], 'case alternative has no `->`'); return { pat: { t: 'pwild' }, rhs: { guards: null, body: { t: 'unknown' }, where: [] }, line: T[a].line, bad: true }; }
622
+ return { pat: patRange(a, k), rhs: parseRhs(k, b, '->'), line: T[a].line, a, b };
623
+ }
624
+ function parseStmt(a, b) {
625
+ if (a >= b) return null;
626
+ if (isKw(T[a], 'let')) {
627
+ const blk = blockAfter(a, b, layoutIndent);
628
+ if (blk.b < b && isKw(T[blk.b], 'in')) return { t: 'sexpr', e: sub(a, b, expr0), line: T[a].line };
629
+ return { t: 'slet', decls: withIndent(blk.indent, () => parseDeclItems(blk.items)), line: T[a].line };
630
+ }
631
+ let k = -1;
632
+ for (let p = a; p < b;) {
633
+ tick();
634
+ const t = T[p];
635
+ if (t.t === 'kw' && ['let', 'do', 'of', 'where', 'if', 'case'].includes(t.v)) break;
636
+ if (isRop(t, '<-')) { k = p; break; }
637
+ if (isRop(t, '\\') || isRop(t, '->')) break;
638
+ p = groupEnd(p);
639
+ }
640
+ if (k > a) return { t: 'sbind', pat: patRange(a, k), e: sub(k + 1, b, expr0), line: T[a].line };
641
+ return { t: 'sexpr', e: sub(a, b, expr0), line: T[a].line };
642
+ }
643
+ function parseDeclItems(items) {
644
+ const out = [];
645
+ for (const [a, b] of items) {
646
+ try {
647
+ const d = parseDecl(a, b);
648
+ if (d) out.push(d);
649
+ } catch (e) {
650
+ if (e instanceof SyntaxBudgetError) throw e;
651
+ err('parser-exception', T[a], String(e && e.message));
652
+ }
653
+ }
654
+ return out;
655
+ }
656
+ function parseDecl(a, b) {
657
+ const mark = findTop(a, b, (x) => x.t === 'rop' && (x.v === '=' || x.v === '|' || x.v === '::'));
658
+ if (mark < 0) { err('expected-declaration', T[a], 'item is neither a binding nor a signature'); return null; }
659
+ if (T[mark].v === '::') {
660
+ const names = [];
661
+ for (let q = a; q < mark; q++) {
662
+ if (T[q].t === 'var') names.push(T[q].v);
663
+ else if (isSp(T[q], '(') && T[q + 1] && T[q + 1].t === 'op') names.push(T[q + 1].v);
664
+ }
665
+ return { t: 'sig', names, ta: mark + 1, tb: b, line: T[a].line };
666
+ }
667
+ let name = null;
668
+ let pats = [];
669
+ let patbind = null;
670
+ const prefixMark = (q) => T[q].t === 'op' && T[q].v === '!' && T[q + 1] && T[q].end === T[q + 1].off && (q === a || T[q - 1].end < T[q].off);
671
+ let infixAt = -1;
672
+ let infixEnd = -1;
673
+ for (let q = a; q < mark; q = groupEnd(q)) {
674
+ if (q > a && T[q].t === 'op' && T[q].cons) { infixAt = -2; break; }
675
+ if (q > a && T[q].t === 'op' && !prefixMark(q)) { infixAt = q; infixEnd = q + 1; break; }
676
+ if (isSp(T[q], '`') && T[q + 1] && T[q + 1].t === 'var' && isSp(T[q + 2], '`')) { infixAt = q + 1; infixEnd = q + 3; break; }
677
+ }
678
+ if (infixAt === -2) patbind = patRange(a, mark);
679
+ else if (infixAt >= 0) {
680
+ name = T[infixAt].v;
681
+ pats = [patRange(a, infixAt === infixEnd - 1 ? infixAt : infixAt), patRange(infixEnd, mark)];
682
+ if (isSp(T[infixAt - 1], '`')) pats[0] = patRange(a, infixAt - 1);
683
+ } else if (T[a].t === 'var' && !T[a].q) {
684
+ name = T[a].v;
685
+ for (let q = a + 1; q < mark;) {
686
+ const r = apatAt(q, mark);
687
+ if (!r) { err('bad-clause-pattern', T[q], 'unparseable argument pattern'); break; }
688
+ pats.push(r[0]);
689
+ q = r[1];
690
+ }
691
+ } else if (isSp(T[a], '(') && mate[a] === a + 2 && T[a + 1] && T[a + 1].t === 'op') {
692
+ name = T[a + 1].v;
693
+ for (let q = a + 3; q < mark;) {
694
+ const r = apatAt(q, mark);
695
+ if (!r) break;
696
+ pats.push(r[0]);
697
+ q = r[1];
698
+ }
699
+ } else patbind = patRange(a, mark);
700
+ const rhs = parseRhs(mark, b, '=');
701
+ const clause = { pats, rhs, line: T[a].line, endLine: T[b - 1].line, ta: a, tb: b };
702
+ return patbind ? { t: 'patbind', pat: patbind, rhs, line: T[a].line, ta: a, tb: b } : { t: 'clause', name, clause, line: T[a].line };
703
+ }
704
+ function parseData(a, b) {
705
+ const isNewtype = T[a].v === 'newtype';
706
+ let eq = findTop(a, b, (x) => isRop(x, '='));
707
+ let nameTok = null;
708
+ for (let q = a + 1; q < (eq < 0 ? b : eq); q++) if (T[q].t === 'con') { nameTok = T[q]; break; }
709
+ const decl = { t: 'data', newtype: isNewtype, name: nameTok ? nameTok.v : null, constructors: [], line: T[a].line };
710
+ if (eq < 0) return decl;
711
+ let end = findTop(eq + 1, b, (x) => isKw(x, 'deriving'));
712
+ if (end < 0) end = b;
713
+ for (const [s0, e] of splitTop(eq + 1, end, (x) => isRop(x, '|'))) {
714
+ let s = s0;
715
+ const ctx = findTop(s, e, (x) => isRop(x, '=>'));
716
+ if (ctx >= 0) s = ctx + 1;
717
+ while (s < e && (T[s].t === 'op' || (T[s].t === 'var' && T[s].v === 'forall'))) s++;
718
+ if (s >= e) continue;
719
+ let k = s;
720
+ let conTok = null;
721
+ if (T[k].t === 'con') conTok = T[k];
722
+ else if (isSp(T[k], '(') && T[k + 1] && T[k + 1].t === 'op') { conTok = T[k + 1]; k += 2; }
723
+ else {
724
+ const infix = findTop(s, e, (x) => x.t === 'op' && x.cons);
725
+ if (infix > s) { decl.constructors.push({ name: T[infix].v, fields: null, arity: 2 }); }
726
+ continue;
727
+ }
728
+ let fields = null;
729
+ let arity = 0;
730
+ if (isSp(T[k + 1], '{') && mate[k + 1] > k + 1) {
731
+ fields = [];
732
+ const close = mate[k + 1];
733
+ let i = k + 2;
734
+ while (i < close) {
735
+ const names = [];
736
+ while (i < close && T[i].t === 'var') { names.push(T[i].v); i++; if (isSp(T[i], ',')) i++; else break; }
737
+ if (isRop(T[i], '::')) {
738
+ i++;
739
+ while (i < close && !isSp(T[i], ',')) i = Math.min(close, groupEnd(i));
740
+ i++;
741
+ } else break;
742
+ for (const nme of names) fields.push(nme);
743
+ }
744
+ arity = fields.length;
745
+ } else {
746
+ for (let j = k + 1; j < e;) {
747
+ if (T[j].t === 'op' && T[j].v === '!') { j++; continue; }
748
+ if (T[j].t === 'var' || T[j].t === 'con' || isSp(T[j], '(') || isSp(T[j], '[')) { arity++; j = Math.min(e, groupEnd(j)); continue; }
749
+ j++;
750
+ }
751
+ }
752
+ decl.constructors.push({ name: conTok.v, fields, arity });
753
+ }
754
+ return decl;
755
+ }
756
+ function parseImport(a, b) {
757
+ let j = a + 1;
758
+ const rec = { module: null, qualified: false, as: null, hiding: false, items: null, typeAll: [], line: T[a].line };
759
+ while (j < b) {
760
+ const t = T[j];
761
+ if (t.t === 'var' && (t.v === 'qualified' || t.v === 'safe')) { if (t.v === 'qualified') rec.qualified = true; j++; }
762
+ else if (t.t === 'str' && !rec.module) j++;
763
+ else break;
764
+ }
765
+ if (j < b && T[j].t === 'con') { rec.module = (T[j].q ? `${T[j].q}.` : '') + T[j].v; j++; }
766
+ else { err('malformed-import', T[a], 'import has no module name'); return null; }
767
+ while (j < b) {
768
+ const t = T[j];
769
+ if (t.t === 'var' && t.v === 'qualified') { rec.qualified = true; j++; }
770
+ else if (t.t === 'var' && t.v === 'as' && T[j + 1] && T[j + 1].t === 'con') { rec.as = (T[j + 1].q ? `${T[j + 1].q}.` : '') + T[j + 1].v; j += 2; }
771
+ else if (t.t === 'var' && t.v === 'hiding') { rec.hiding = true; j++; }
772
+ else if (isSp(t, '(') && mate[j] > j) {
773
+ rec.items = [];
774
+ rec.typeAll = []; // types/classes imported with (..): they bring their fields and methods
775
+ for (let q = j + 1; q < mate[j]; q++) if (T[q].t === 'var' || T[q].t === 'con' || T[q].t === 'op') rec.items.push(T[q].v);
776
+ for (const [s0, e0] of splitTop(j + 1, mate[j], comma)) {
777
+ if (e0 - s0 >= 2 && T[s0].t === 'con' && isSp(T[s0 + 1], '(') && mate[s0 + 1] > s0 + 1 && T[s0 + 2] && isRop(T[s0 + 2], '..')) rec.typeAll.push(T[s0].v);
778
+ }
779
+ j = mate[j] + 1;
780
+ } else j++;
781
+ }
782
+ return rec;
783
+ }
784
+ function parseExports(a, close) {
785
+ const out = [];
786
+ for (const [s, e] of splitTop(a, close, comma)) {
787
+ if (s >= e) continue;
788
+ const t = T[s];
789
+ if (isKw(t, 'module') && T[s + 1] && T[s + 1].t === 'con') out.push({ kind: 'module', name: (T[s + 1].q ? `${T[s + 1].q}.` : '') + T[s + 1].v });
790
+ else if (t.t === 'var') out.push({ kind: 'var', name: t.v, qual: t.q || null });
791
+ else if (t.t === 'con') out.push({ kind: 'type', name: t.v, all: e - s > 1 });
792
+ else if (isSp(t, '(') && T[s + 1] && T[s + 1].t === 'op') out.push({ kind: 'var', name: T[s + 1].v, qual: null });
793
+ }
794
+ return out;
795
+ }
796
+
797
+ // ── module ──────────────────────────────────────────────────────────────
798
+ const res = { tokens: T, module: null, imports: [], decls: [], sigs: [], data: [], classes: [], instances: [], typeDecls: [], errors, boundaries: [] };
799
+ let bodyStart = 0;
800
+ if (N && isKw(T[0], 'module')) {
801
+ let j = 1;
802
+ for (; j < N;) {
803
+ if (isKw(T[j], 'where')) break;
804
+ j = groupEnd(j);
805
+ }
806
+ const nameTok = T[1] && T[1].t === 'con' ? T[1] : null;
807
+ let exportsList = null;
808
+ if (isSp(T[2], '(') && mate[2] > 2) exportsList = parseExports(3, mate[2]);
809
+ res.module = { name: nameTok ? (nameTok.q ? `${nameTok.q}.` : '') + nameTok.v : null, exports: exportsList, line: 1 };
810
+ bodyStart = j < N ? j + 1 : N;
811
+ }
812
+ const topExplicit = bodyStart < N && isSp(T[bodyStart], '{') && mate[bodyStart] > bodyStart;
813
+ const topIndent = bodyStart < N ? T[bodyStart].col : 0;
814
+ const topItems = topExplicit ? splitItems(bodyStart + 1, mate[bodyStart], null) : (bodyStart < N ? splitItems(bodyStart, N, topIndent) : []);
815
+ lim = N;
816
+ layoutIndent = topExplicit ? -1 : topIndent;
817
+ const topDecls = [];
818
+ for (const [a, b] of topItems) {
819
+ try {
820
+ const t0 = T[a];
821
+ if (t0.t === 'kw') {
822
+ if (t0.v === 'import') { const r = parseImport(a, b); if (r) res.imports.push(r); continue; }
823
+ if (t0.v === 'data' || t0.v === 'newtype') { res.data.push(parseData(a, b)); continue; }
824
+ if (t0.v === 'type') {
825
+ // `type API = ...` is not executable, but Servant describes a whole web API in one: keep its token range.
826
+ if (T[a + 1] && T[a + 1].t === 'con') res.typeDecls.push({ name: T[a + 1].v, line: t0.line, a, b });
827
+ continue;
828
+ }
829
+ if (t0.v === 'infix' || t0.v === 'infixl' || t0.v === 'infixr' || t0.v === 'default' || t0.v === 'deriving') continue;
830
+ if (t0.v === 'foreign') {
831
+ // `foreign import ccall unsafe "string.h strlen" c_strlen :: ...` names the Haskell-side function.
832
+ let fname = null;
833
+ for (let q = a; q < b; q++) if (isRop(T[q], '::')) { if (q > a && T[q - 1].t === 'var') fname = T[q - 1].v; break; }
834
+ res.boundaries.push({ kind: 'ffi', line: t0.line, detail: 'foreign declaration; the foreign code is not analyzed', ...(fname ? { name: fname } : {}) });
835
+ continue;
836
+ }
837
+ if (t0.v === 'class' || t0.v === 'instance') {
838
+ const w = findTop(a, b, (x) => isKw(x, 'where'));
839
+ const headEnd = w < 0 ? b : w;
840
+ const ctx = findTop(a, headEnd, (x) => isRop(x, '=>'));
841
+ let nameTok = null;
842
+ for (let q = (ctx >= 0 ? ctx + 1 : a + 1); q < headEnd; q++) if (T[q].t === 'con') { nameTok = T[q]; break; }
843
+ let decls = [];
844
+ if (w >= 0) {
845
+ const blk = blockAfter(w, b, topIndent);
846
+ decls = withIndent(blk.indent, () => parseDeclItems(blk.items));
847
+ }
848
+ const entry = { name: nameTok ? nameTok.v : null, qual: nameTok && nameTok.q ? nameTok.q : null, decls, line: t0.line, headText: T.slice(a + 1, headEnd).map((x) => x.v).join(' ') };
849
+ if (t0.v === 'class') res.classes.push(entry); else res.instances.push(entry);
850
+ continue;
851
+ }
852
+ err('unsupported-declaration', t0, `unsupported declaration keyword ${t0.v}`);
853
+ continue;
854
+ }
855
+ const d = parseDecl(a, b);
856
+ if (d) topDecls.push(d);
857
+ } catch (e) {
858
+ if (e instanceof SyntaxBudgetError) throw e;
859
+ err('parser-exception', T[a], String(e && e.message));
860
+ }
861
+ }
862
+ res.decls = topDecls;
863
+ return res;
864
+ }
865
+
866
+ /** Group parsed clause declarations by name, preserving order. */
867
+ export function groupDecls(decls) {
868
+ const funs = [];
869
+ const byName = new Map();
870
+ const patbinds = [];
871
+ const sigs = new Map();
872
+ for (const d of decls) {
873
+ if (d.t === 'sig') { for (const n of d.names) sigs.set(n, d); }
874
+ else if (d.t === 'patbind') patbinds.push(d);
875
+ else if (d.t === 'clause') {
876
+ let f = byName.get(d.name);
877
+ if (!f) { f = { name: d.name, clauses: [], line: d.line }; byName.set(d.name, f); funs.push(f); }
878
+ f.clauses.push(d.clause);
879
+ }
880
+ }
881
+ return { funs, patbinds, sigs };
882
+ }
883
+
884
+ export function patternVars(p) {
885
+ const out = [];
886
+ const walk = (x) => {
887
+ if (!x) return;
888
+ if (x.t === 'pvar') out.push(x.name);
889
+ else if (x.t === 'pas') { out.push(x.name); walk(x.p); }
890
+ else if (x.t === 'pcon') { for (const a of x.args) walk(a); for (const f of x.fields) if (f.pat) walk(f.pat); }
891
+ else if (x.t === 'ptuple' || x.t === 'plist') for (const a of x.items) walk(a);
892
+ };
893
+ walk(p);
894
+ return out;
895
+ }