@clear-capabilities/agentic-security-scanner 0.153.1 → 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.
- package/CHANGELOG.md +67 -0
- package/bin/agentic-security.js +123 -19
- package/dist/1444.index.js +64 -19
- package/dist/1905.index.js +46 -3
- package/dist/2862.index.js +325 -0
- package/dist/3499.index.js +5 -1
- package/dist/3531.index.js +261 -0
- package/dist/3596.index.js +103 -0
- package/dist/4113.index.js +15 -3
- package/dist/4171.index.js +319 -0
- package/dist/5637.index.js +10 -4
- package/dist/6359.index.js +433 -0
- package/dist/7838.index.js +15 -3
- package/dist/8551.index.js +1164 -0
- package/dist/8587.index.js +410 -0
- package/dist/8714.index.js +98 -0
- package/dist/8798.index.js +218 -0
- package/dist/9220.index.js +35 -3
- package/dist/9560.index.js +26 -3
- package/dist/agentic-security.mjs +51 -51
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +18 -3
- package/src/dataflow/CLAUDE.md +9 -0
- package/src/dataflow/catalog-haskell.js +37 -0
- package/src/dataflow/catalog.js +4 -0
- package/src/dataflow/engine.js +33 -3
- package/src/dataflow/privacy-catalog-haskell.js +50 -0
- package/src/dataflow/privacy-catalog.js +5 -1
- package/src/discovery/CLAUDE.md +6 -0
- package/src/discovery/index.js +12 -3
- package/src/discovery/lenses.js +4 -1
- package/src/discovery/partition.js +28 -0
- package/src/egress/redact.js +8 -0
- package/src/engine.js +140 -19
- package/src/ir/index.js +39 -2
- package/src/language/CLAUDE.md +74 -0
- package/src/language/accuracy.js +90 -0
- package/src/language/aibom.js +307 -0
- package/src/language/assurance.js +193 -0
- package/src/language/bom-validate.js +227 -0
- package/src/language/bom.js +236 -0
- package/src/language/bridges.js +392 -0
- package/src/language/compliance-map.js +61 -0
- package/src/language/context.js +199 -0
- package/src/language/contracts.js +339 -0
- package/src/language/discovery.js +359 -0
- package/src/language/engine-pass.js +120 -0
- package/src/language/fix-lifecycle.js +120 -0
- package/src/language/haskell-adapter.js +51 -0
- package/src/language/haskell-disclosure.js +170 -0
- package/src/language/haskell-findings.js +44 -0
- package/src/language/haskell-fix.js +409 -0
- package/src/language/haskell-grammar.js +78 -0
- package/src/language/haskell-guards.js +382 -0
- package/src/language/haskell-ir.js +1377 -0
- package/src/language/haskell-llm.js +128 -0
- package/src/language/haskell-manifests.js +1290 -0
- package/src/language/haskell-models.js +278 -0
- package/src/language/haskell-parser.js +1073 -0
- package/src/language/haskell-resolved-graph.js +414 -0
- package/src/language/haskell-sca.js +387 -0
- package/src/language/haskell-security-rules.js +332 -0
- package/src/language/haskell-supply.js +140 -0
- package/src/language/haskell-sweep.js +85 -0
- package/src/language/haskell-syntax.js +895 -0
- package/src/language/haskell-web.js +749 -0
- package/src/language/nix-adapter.js +42 -0
- package/src/language/nix-agents.js +155 -0
- package/src/language/nix-build-trust.js +790 -0
- package/src/language/nix-closure.js +331 -0
- package/src/language/nix-eval-isolation.js +274 -0
- package/src/language/nix-fix.js +268 -0
- package/src/language/nix-grammar.js +97 -0
- package/src/language/nix-inventory.js +379 -0
- package/src/language/nix-ir.js +642 -0
- package/src/language/nix-parser.js +908 -0
- package/src/language/nix-privacy.js +114 -0
- package/src/language/nix-sca.js +358 -0
- package/src/language/nix-script-taint.js +885 -0
- package/src/language/nix-secrets.js +396 -0
- package/src/language/nixos-hardening.js +564 -0
- package/src/language/nixos-module-resolver.js +595 -0
- package/src/language/nixos-option-catalog.js +96 -0
- package/src/language/pragma.js +150 -0
- package/src/language/resolved-pass.js +146 -0
- package/src/language/secrets.js +323 -0
- package/src/language/state-artifacts.js +78 -0
- package/src/language/support-registry.js +224 -0
- package/src/language/witness.js +185 -0
- package/src/lineage/CLAUDE.md +8 -0
- package/src/lineage/coverage.js +2 -0
- package/src/lineage/cross-repo-link.js +22 -0
- package/src/lineage/graph-builder.js +5 -1
- package/src/lineage/haskell-view.js +319 -0
- package/src/lineage/index.js +8 -1
- package/src/lineage/nix-view.js +198 -0
- package/src/lineage/sink-registry.js +10 -1
- package/src/lineage/source-registry.js +3 -0
- package/src/lineage/transform-catalog.js +12 -0
- package/src/llm-validator/index.js +16 -2
- package/src/lsp/server.js +69 -9
- package/src/mcp/CLAUDE.md +7 -0
- package/src/mcp/tools.js +26 -3
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/accuracy-scorecard.js +51 -0
- package/src/posture/aibom.js +61 -3
- package/src/posture/artifact-registry.js +12 -0
- package/src/posture/auditor-walkthrough.js +41 -0
- package/src/posture/clustering.js +2 -2
- package/src/posture/evidence-bundle.js +7 -1
- package/src/posture/execution-proof.js +15 -0
- package/src/posture/falsification.js +7 -0
- package/src/posture/family-resolve.js +7 -0
- package/src/posture/fix-history.js +40 -0
- package/src/posture/fix-verify.js +15 -3
- package/src/posture/provenance/git-evidence.js +24 -0
- package/src/posture/provenance/origin-resolver.js +14 -2
- package/src/posture/provenance/predicate-replay.js +11 -2
- package/src/posture/provenance/sca-origin.js +20 -3
- package/src/posture/sbom-diff.js +7 -6
- package/src/posture/sbom.js +85 -32
- package/src/posture/sca-policy.js +10 -1
- package/src/posture/sca-verdict.js +8 -1
- package/src/posture/secret-history.js +62 -18
- package/src/posture/stable-id.js +21 -2
- package/src/posture/state-dir.js +21 -0
- package/src/posture/threat-model-auto.js +4 -4
- package/src/posture/triage.js +18 -1
- package/src/posture/watch-mode.js +34 -3
- package/src/pr-delta.js +9 -4
- package/src/report/index.js +35 -9
- package/src/runScan.js +56 -5
- package/src/sast/code-injection-multilang.js +15 -0
- package/src/sast/go-extended.js +11 -0
- package/src/sast/pipeline.js +34 -1
|
@@ -0,0 +1,1073 @@
|
|
|
1
|
+
// Haskell parser with original source locations (HS-001).
|
|
2
|
+
//
|
|
3
|
+
// Static, dependency-free and bounded. It lexes Haskell (nested comments,
|
|
4
|
+
// pragmas, strings with gaps, qualified names, literate Bird and LaTeX styles),
|
|
5
|
+
// resolves the layout rule, and extracts imports, declarations, calls,
|
|
6
|
+
// expressions, records and do blocks. It never executes anything: no GHC, no
|
|
7
|
+
// Template Haskell splice, no preprocessor.
|
|
8
|
+
//
|
|
9
|
+
// Locations. Preprocessing (literate unwrapping, CPP and hsc directive removal)
|
|
10
|
+
// is done by overwriting characters with spaces of the same UTF-16 length, never
|
|
11
|
+
// by deleting them, so every token offset is an offset into the ORIGINAL file.
|
|
12
|
+
// A span is {startLine, startColumn, endLine, endColumn, startByte, endByte,
|
|
13
|
+
// startOffset, endOffset}: lines are 1-based, columns are 0-based UTF-16 code
|
|
14
|
+
// units, bytes are UTF-8 offsets into the original, and end positions are
|
|
15
|
+
// exclusive.
|
|
16
|
+
//
|
|
17
|
+
// What is never invented. Comment text, string text, quasi-quote bodies and
|
|
18
|
+
// preprocessor lines are not tokens, so nothing inside them can become a call.
|
|
19
|
+
// A top-level declaration that contains a syntax error contributes no calls; the
|
|
20
|
+
// error is recorded with the count of calls it withheld. Both branches of a CPP
|
|
21
|
+
// conditional are parsed and their calls flagged `conditional`.
|
|
22
|
+
//
|
|
23
|
+
// Budgets. Bytes, tokens, nesting depth, comment nesting, steps and wall clock
|
|
24
|
+
// are bounded; a bound hit returns status `budget_exceeded` with the budget
|
|
25
|
+
// named and no partial analysis, and never throws into the caller.
|
|
26
|
+
|
|
27
|
+
import { loadHaskellGrammar } from './haskell-grammar.js';
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_PARSE_BUDGETS = Object.freeze({
|
|
30
|
+
maxBytes: 4 * 1024 * 1024,
|
|
31
|
+
maxTokens: 400_000,
|
|
32
|
+
maxDepth: 200,
|
|
33
|
+
maxCommentNesting: 200,
|
|
34
|
+
maxSteps: 8_000_000,
|
|
35
|
+
deadlineMs: 8000,
|
|
36
|
+
maxErrors: 200,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
class BudgetError extends Error {
|
|
40
|
+
constructor(budget, limit) {
|
|
41
|
+
super(`parser budget "${budget}" exceeded (limit ${limit})`);
|
|
42
|
+
this.budget = budget;
|
|
43
|
+
this.limit = limit;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── character classes ──────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
const SYM = new Set('!#$%&*+./<=>?@\\^|-~:'.split(''));
|
|
50
|
+
const ID_START_U = /[\p{L}_]/u;
|
|
51
|
+
const ID_CHAR_U = /[\p{L}\p{N}_']/u;
|
|
52
|
+
const UPPER_U = /\p{Lu}/u;
|
|
53
|
+
|
|
54
|
+
function isIdStart(c) {
|
|
55
|
+
if (c === undefined) return false;
|
|
56
|
+
const x = c.charCodeAt(0);
|
|
57
|
+
if (x < 128) return (x >= 65 && x <= 90) || (x >= 97 && x <= 122) || x === 95;
|
|
58
|
+
return ID_START_U.test(c);
|
|
59
|
+
}
|
|
60
|
+
function isIdChar(c) {
|
|
61
|
+
if (c === undefined) return false;
|
|
62
|
+
const x = c.charCodeAt(0);
|
|
63
|
+
if (x < 128) return (x >= 65 && x <= 90) || (x >= 97 && x <= 122) || x === 95 || (x >= 48 && x <= 57) || x === 39;
|
|
64
|
+
return ID_CHAR_U.test(c);
|
|
65
|
+
}
|
|
66
|
+
function isUpper(c) {
|
|
67
|
+
const x = c.charCodeAt(0);
|
|
68
|
+
return x < 128 ? x >= 65 && x <= 90 : UPPER_U.test(c);
|
|
69
|
+
}
|
|
70
|
+
const isSym = (c) => c !== undefined && SYM.has(c);
|
|
71
|
+
const isSpace = (c) => c === ' ' || c === '\t' || c === '\n' || c === '\r' || c === '\f' || c === '\v' || c === '\u00a0' || c === '\ufeff';
|
|
72
|
+
|
|
73
|
+
const OPENERS = { '(': ')', '[': ']', '{': '}' };
|
|
74
|
+
const CLOSERS = new Set([')', ']', '}']);
|
|
75
|
+
const ATOM_END = new Set(['varid', 'qvarid', 'conid', 'qconid', 'int', 'float', 'char', 'string', 'wild', 'quasiquote', 'thname', ')', ']', '}']);
|
|
76
|
+
const ATOM_START = new Set(['varid', 'qvarid', 'conid', 'qconid', 'int', 'float', 'char', 'string', 'wild', 'quasiquote', 'thname']);
|
|
77
|
+
|
|
78
|
+
// ── locations ──────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
function makeLocator(source) {
|
|
81
|
+
const n = source.length;
|
|
82
|
+
const starts = [0];
|
|
83
|
+
for (let i = source.indexOf('\n'); i !== -1; i = source.indexOf('\n', i + 1)) starts.push(i + 1);
|
|
84
|
+
let bytes = null;
|
|
85
|
+
// eslint-disable-next-line no-control-regex
|
|
86
|
+
if (/[^\x00-\x7f]/.test(source)) {
|
|
87
|
+
bytes = new Uint32Array(n + 1);
|
|
88
|
+
let acc = 0;
|
|
89
|
+
for (let i = 0; i < n; i++) {
|
|
90
|
+
bytes[i] = acc;
|
|
91
|
+
const x = source.charCodeAt(i);
|
|
92
|
+
if (x < 0x80) acc += 1;
|
|
93
|
+
else if (x < 0x800) acc += 2;
|
|
94
|
+
else if (x >= 0xd800 && x <= 0xdbff) acc += 4;
|
|
95
|
+
else if (x >= 0xdc00 && x <= 0xdfff) acc += 0;
|
|
96
|
+
else acc += 3;
|
|
97
|
+
}
|
|
98
|
+
bytes[n] = acc;
|
|
99
|
+
}
|
|
100
|
+
const lineOf = (off) => {
|
|
101
|
+
let lo = 0;
|
|
102
|
+
let hi = starts.length - 1;
|
|
103
|
+
while (lo < hi) {
|
|
104
|
+
const mid = (lo + hi + 1) >> 1;
|
|
105
|
+
if (starts[mid] <= off) lo = mid; else hi = mid - 1;
|
|
106
|
+
}
|
|
107
|
+
return lo + 1;
|
|
108
|
+
};
|
|
109
|
+
const byte = (off) => (bytes ? bytes[Math.min(off, n)] : off);
|
|
110
|
+
const span = (s, e) => {
|
|
111
|
+
const sl = lineOf(s);
|
|
112
|
+
const el = lineOf(Math.max(s, e));
|
|
113
|
+
return {
|
|
114
|
+
startLine: sl, startColumn: s - starts[sl - 1],
|
|
115
|
+
endLine: el, endColumn: Math.max(s, e) - starts[el - 1],
|
|
116
|
+
startByte: byte(s), endByte: byte(Math.max(s, e)),
|
|
117
|
+
startOffset: s, endOffset: Math.max(s, e),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
return { starts, lineOf, byte, span, lineCount: starts.length };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Text of a span, read back from the ORIGINAL source by byte range (test and tooling helper). */
|
|
124
|
+
export function spanText(source, span) {
|
|
125
|
+
return Buffer.from(source, 'utf8').subarray(span.startByte, span.endByte).toString('utf8');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── preprocessing (length-preserving) ──────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
const blank = (s) => s.replace(/[^\r\n]/g, ' ');
|
|
131
|
+
|
|
132
|
+
function dialectOf(file) {
|
|
133
|
+
const f = String(file || '').toLowerCase();
|
|
134
|
+
if (/\.lhs(-boot)?$/.test(f)) return 'literate';
|
|
135
|
+
if (/\.hsc$/.test(f)) return 'hsc';
|
|
136
|
+
return 'haskell';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function unwrapLiterate(source) {
|
|
140
|
+
const latex = /^[ \t]*\\begin\{code\}/m.test(source);
|
|
141
|
+
let out = '';
|
|
142
|
+
let inCode = false;
|
|
143
|
+
let pos = 0;
|
|
144
|
+
while (pos <= source.length) {
|
|
145
|
+
let nl = source.indexOf('\n', pos);
|
|
146
|
+
if (nl === -1) nl = source.length;
|
|
147
|
+
const line = source.slice(pos, nl);
|
|
148
|
+
if (latex) {
|
|
149
|
+
if (/^[ \t]*\\begin\{code\}/.test(line)) { inCode = true; out += blank(line); }
|
|
150
|
+
else if (/^[ \t]*\\end\{code\}/.test(line)) { inCode = false; out += blank(line); }
|
|
151
|
+
else out += inCode ? line : blank(line);
|
|
152
|
+
} else if (line.startsWith('>')) out += ' ' + line.slice(1);
|
|
153
|
+
else out += blank(line);
|
|
154
|
+
if (nl < source.length) out += '\n';
|
|
155
|
+
pos = nl + 1;
|
|
156
|
+
}
|
|
157
|
+
return { code: out, style: latex ? 'latex' : 'bird' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function stripDirectives(code, dialect, grammar, loc) {
|
|
161
|
+
const cppNames = new Set(grammar.cppDirectives);
|
|
162
|
+
const lines = code.split('\n');
|
|
163
|
+
const directives = [];
|
|
164
|
+
const depthAtLine = new Int32Array(lines.length + 2);
|
|
165
|
+
let depth = 0;
|
|
166
|
+
let off = 0;
|
|
167
|
+
for (let i = 0; i < lines.length; i++) {
|
|
168
|
+
let line = lines[i];
|
|
169
|
+
const m = /^#\s*([A-Za-z]+)/.exec(line);
|
|
170
|
+
const shebang = i === 0 && line.startsWith('#!');
|
|
171
|
+
const hscLine = dialect === 'hsc' && /^#/.test(line);
|
|
172
|
+
if (shebang || hscLine || (m && cppNames.has(m[1]))) {
|
|
173
|
+
const name = shebang ? 'shebang' : (m ? m[1] : 'hsc');
|
|
174
|
+
directives.push({ name, line: i + 1, span: loc.span(off, off + line.length) });
|
|
175
|
+
if (name === 'if' || name === 'ifdef' || name === 'ifndef') depth++;
|
|
176
|
+
else if (name === 'endif' && depth > 0) depth--;
|
|
177
|
+
depthAtLine[i + 1] = depth;
|
|
178
|
+
let cur = blank(line);
|
|
179
|
+
// A directive continued with a trailing backslash swallows the next line too.
|
|
180
|
+
let k = i;
|
|
181
|
+
while (/\\\r?$/.test(lines[k]) && k + 1 < lines.length) {
|
|
182
|
+
k++;
|
|
183
|
+
cur += '\n' + blank(lines[k]);
|
|
184
|
+
depthAtLine[k + 1] = depth;
|
|
185
|
+
}
|
|
186
|
+
if (k > i) {
|
|
187
|
+
off += lines[i].length + 1;
|
|
188
|
+
for (let q = i + 1; q <= k; q++) off += lines[q].length + 1;
|
|
189
|
+
lines.splice(i, k - i + 1, ...cur.split('\n'));
|
|
190
|
+
i = k;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
lines[i] = cur;
|
|
194
|
+
line = cur;
|
|
195
|
+
} else {
|
|
196
|
+
depthAtLine[i + 1] = depth;
|
|
197
|
+
if (dialect === 'hsc') lines[i] = line.replace(/#\{[^}\n]*\}/g, (s) => ' '.repeat(s.length));
|
|
198
|
+
}
|
|
199
|
+
off += line.length + 1;
|
|
200
|
+
}
|
|
201
|
+
return { code: lines.join('\n'), directives, depthAtLine };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── lexer ──────────────────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
function lexAll(code, loc, ctx, flags) {
|
|
207
|
+
const g = ctx.grammar;
|
|
208
|
+
const keywords = new Set(g.keywords);
|
|
209
|
+
const reserved = new Set(g.reservedOps);
|
|
210
|
+
const n = code.length;
|
|
211
|
+
const tokens = [];
|
|
212
|
+
const comments = [];
|
|
213
|
+
const pragmas = [];
|
|
214
|
+
const errors = [];
|
|
215
|
+
const boundaries = [];
|
|
216
|
+
const hasTabs = code.includes('\t');
|
|
217
|
+
let i = 0;
|
|
218
|
+
let prevEndLine = 0;
|
|
219
|
+
let sawCode = false;
|
|
220
|
+
const NUM = /0[xX][0-9a-fA-F_]+|0[oO][0-7_]+|0[bB][01_]+|\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d[\d_]*)?/y;
|
|
221
|
+
const QQ = /\[([A-Za-z_][\w'.]*)?(\|\|?)/y;
|
|
222
|
+
const CHAR_ESC = /'\\.[^'\n]{0,8}'/y;
|
|
223
|
+
|
|
224
|
+
const push = (k, v, s, e, extra) => {
|
|
225
|
+
if (tokens.length >= ctx.budgets.maxTokens) throw new BudgetError('maxTokens', ctx.budgets.maxTokens);
|
|
226
|
+
const line = loc.lineOf(s);
|
|
227
|
+
const el = e > s ? loc.lineOf(e - 1) : line;
|
|
228
|
+
let col = s - loc.starts[line - 1];
|
|
229
|
+
let lcol = col;
|
|
230
|
+
if (hasTabs) {
|
|
231
|
+
lcol = 0;
|
|
232
|
+
for (let q = loc.starts[line - 1]; q < s; q++) lcol = code[q] === '\t' ? (Math.floor(lcol / 8) + 1) * 8 : lcol + 1;
|
|
233
|
+
}
|
|
234
|
+
const t = { k, v, s, e, line, el, col, lcol, fol: prevEndLine < line };
|
|
235
|
+
if (extra) Object.assign(t, extra);
|
|
236
|
+
prevEndLine = el;
|
|
237
|
+
sawCode = true;
|
|
238
|
+
tokens.push(t);
|
|
239
|
+
return t;
|
|
240
|
+
};
|
|
241
|
+
const err = (kind, s, e, detail) => {
|
|
242
|
+
if (errors.length < ctx.budgets.maxErrors) errors.push({ kind, offset: s, endOffset: e, detail });
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
while (i < n) {
|
|
246
|
+
ctx.tick();
|
|
247
|
+
const c = code[i];
|
|
248
|
+
if (isSpace(c)) { i++; continue; }
|
|
249
|
+
|
|
250
|
+
if (c === '{' && code[i + 1] === '-') {
|
|
251
|
+
const isPragma = code[i + 2] === '#';
|
|
252
|
+
let depth = 1;
|
|
253
|
+
let j = i + 2;
|
|
254
|
+
while (j < n) {
|
|
255
|
+
if (code[j] === '{' && code[j + 1] === '-') {
|
|
256
|
+
depth++;
|
|
257
|
+
if (depth > ctx.budgets.maxCommentNesting) throw new BudgetError('maxCommentNesting', ctx.budgets.maxCommentNesting);
|
|
258
|
+
j += 2;
|
|
259
|
+
} else if (code[j] === '-' && code[j + 1] === '}') {
|
|
260
|
+
depth--;
|
|
261
|
+
j += 2;
|
|
262
|
+
if (depth === 0) break;
|
|
263
|
+
} else j++;
|
|
264
|
+
}
|
|
265
|
+
const closed = depth === 0;
|
|
266
|
+
if (!closed) {
|
|
267
|
+
err('unterminated-comment', i, i + 2, 'block comment is never closed; the rest of the file is comment text');
|
|
268
|
+
j = n;
|
|
269
|
+
}
|
|
270
|
+
const sp = loc.span(i, j);
|
|
271
|
+
if (isPragma && closed) {
|
|
272
|
+
const text = code.slice(i, j);
|
|
273
|
+
const m = /^\{-#\s*([A-Za-z_]+)/.exec(text);
|
|
274
|
+
pragmas.push({ name: m ? m[1].toUpperCase() : '', span: sp, headerPosition: !sawCode, text: text.slice(3, -3).trim() });
|
|
275
|
+
} else comments.push({ kind: isPragma ? 'pragma' : 'block', span: sp, terminated: closed, headerPosition: !sawCode });
|
|
276
|
+
i = j;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (c === '-' && code[i + 1] === '-') {
|
|
280
|
+
let j = i;
|
|
281
|
+
while (code[j] === '-') j++;
|
|
282
|
+
if (j >= n || !isSym(code[j])) {
|
|
283
|
+
let e = code.indexOf('\n', j);
|
|
284
|
+
if (e === -1) e = n;
|
|
285
|
+
comments.push({ kind: 'line', span: loc.span(i, e), terminated: true, headerPosition: !sawCode });
|
|
286
|
+
i = e;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (c === '"') {
|
|
292
|
+
let j = i + 1;
|
|
293
|
+
let ok = false;
|
|
294
|
+
while (true) {
|
|
295
|
+
const ch = code[j];
|
|
296
|
+
if (j >= n || ch === '\n') break;
|
|
297
|
+
if (ch === '\\') {
|
|
298
|
+
const nx = code[j + 1];
|
|
299
|
+
if (nx !== undefined && isSpace(nx)) {
|
|
300
|
+
j += 2;
|
|
301
|
+
while (j < n && isSpace(code[j])) j++;
|
|
302
|
+
if (code[j] === '\\') j++; else break;
|
|
303
|
+
} else j += 2;
|
|
304
|
+
} else if (ch === '"') { j++; ok = true; break; } else j++;
|
|
305
|
+
}
|
|
306
|
+
if (j > n) j = n;
|
|
307
|
+
if (!ok) err('unterminated-string', i, Math.max(i + 1, j), 'string literal is not closed on its line');
|
|
308
|
+
push('string', code.slice(i, j), i, j, ok ? null : { bad: true });
|
|
309
|
+
i = j;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (c === "'") {
|
|
314
|
+
CHAR_ESC.lastIndex = i;
|
|
315
|
+
let m = code[i + 1] === '\\' ? CHAR_ESC.exec(code) : null;
|
|
316
|
+
if (m) { push('char', m[0], i, i + m[0].length); i += m[0].length; continue; }
|
|
317
|
+
const cp = code.codePointAt(i + 1);
|
|
318
|
+
const w = cp !== undefined && cp > 0xffff ? 2 : 1;
|
|
319
|
+
if (cp !== undefined && code[i + 1] !== "'" && code[i + 1] !== '\n' && code[i + 1 + w] === "'") {
|
|
320
|
+
push('char', code.slice(i, i + w + 2), i, i + w + 2);
|
|
321
|
+
i += w + 2;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (flags.th) {
|
|
325
|
+
let j = i + 1;
|
|
326
|
+
if (code[j] === "'") j++;
|
|
327
|
+
if (isIdStart(code[j]) || code[j] === '(' || code[j] === '[') {
|
|
328
|
+
if (isIdStart(code[j])) { while (isIdChar(code[j]) || (code[j] === '.' && isIdStart(code[j + 1]))) j++; }
|
|
329
|
+
else j += 1;
|
|
330
|
+
push('thname', code.slice(i, j), i, j);
|
|
331
|
+
boundaries.push({ kind: 'th-name-quote', span: loc.span(i, j), detail: 'Template Haskell name quote; resolved at compile time' });
|
|
332
|
+
i = j;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
push('tick', "'", i, i + 1);
|
|
337
|
+
i++;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (c === '[' && (flags.th || flags.qq)) {
|
|
342
|
+
QQ.lastIndex = i;
|
|
343
|
+
const m = QQ.exec(code);
|
|
344
|
+
if (m && (m[1] ? true : flags.th)) {
|
|
345
|
+
const close = m[2] === '||' ? '||]' : '|]';
|
|
346
|
+
let e = code.indexOf(close, i + m[0].length);
|
|
347
|
+
let ok = true;
|
|
348
|
+
if (e === -1) { ok = false; e = n; err('unterminated-quasiquote', i, i + m[0].length, 'quasi-quotation is never closed'); } else e += close.length;
|
|
349
|
+
push('quasiquote', null, i, e, { quoter: m[1] || (m[2] === '||' ? 'typed-quote' : 'quote') });
|
|
350
|
+
boundaries.push({
|
|
351
|
+
kind: m[1] && !['e', 'd', 't', 'p'].includes(m[1]) ? 'quasiquote' : 'th-quote',
|
|
352
|
+
span: loc.span(i, e),
|
|
353
|
+
detail: `${m[1] || 'quote'} body is opaque to the parser${ok ? '' : ' (unterminated)'}`,
|
|
354
|
+
quoter: m[1] || null,
|
|
355
|
+
});
|
|
356
|
+
i = e;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (isIdStart(c)) {
|
|
362
|
+
const readWord = (p) => { let q = p; while (q < n && isIdChar(code[q])) q++; while (code[q] === '#') q++; return q; };
|
|
363
|
+
let end = readWord(i);
|
|
364
|
+
if (!isUpper(c)) {
|
|
365
|
+
const word = code.slice(i, end);
|
|
366
|
+
if (keywords.has(word)) push('kw', word, i, end);
|
|
367
|
+
else if (word === '_') push('wild', word, i, end);
|
|
368
|
+
else push('varid', word, i, end, { name: word });
|
|
369
|
+
i = end;
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
let lastSeg = i;
|
|
373
|
+
let segs = 0;
|
|
374
|
+
let cur = end;
|
|
375
|
+
let done = null;
|
|
376
|
+
while (code[cur] === '.') {
|
|
377
|
+
const nx = code[cur + 1];
|
|
378
|
+
if (isIdStart(nx)) {
|
|
379
|
+
const e2 = readWord(cur + 1);
|
|
380
|
+
if (isUpper(nx)) { lastSeg = cur + 1; segs++; cur = e2; continue; }
|
|
381
|
+
done = { k: 'qvarid', qual: code.slice(i, cur), name: code.slice(cur + 1, e2), end: e2 };
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
if (isSym(nx)) {
|
|
385
|
+
let e2 = cur + 1;
|
|
386
|
+
while (isSym(code[e2])) e2++;
|
|
387
|
+
const op = code.slice(cur + 1, e2);
|
|
388
|
+
done = { k: op[0] === ':' ? 'qconsym' : 'qop', qual: code.slice(i, cur), name: op, end: e2 };
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
if (done) { push(done.k, code.slice(i, done.end), i, done.end, { qual: done.qual, name: done.name }); i = done.end; continue; }
|
|
393
|
+
if (segs > 0) push('qconid', code.slice(i, cur), i, cur, { qual: code.slice(i, lastSeg - 1), name: code.slice(lastSeg, cur) });
|
|
394
|
+
else push('conid', code.slice(i, cur), i, cur, { name: code.slice(i, cur) });
|
|
395
|
+
i = cur;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (c >= '0' && c <= '9') {
|
|
400
|
+
NUM.lastIndex = i;
|
|
401
|
+
const m = NUM.exec(code);
|
|
402
|
+
const txt = m ? m[0] : c;
|
|
403
|
+
const isF = /^[0-9]/.test(txt) && !/^0[xXoObB]/.test(txt) && /[.eE]/.test(txt);
|
|
404
|
+
push(isF ? 'float' : 'int', txt, i, i + txt.length);
|
|
405
|
+
i += txt.length;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (c === '(' || c === ')' || c === '[' || c === ']' || c === ',' || c === ';' || c === '`' || c === '{' || c === '}') {
|
|
410
|
+
push(c, c, i, i + 1);
|
|
411
|
+
i++;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (isSym(c)) {
|
|
416
|
+
let j = i;
|
|
417
|
+
while (isSym(code[j])) j++;
|
|
418
|
+
const op = code.slice(i, j);
|
|
419
|
+
if (flags.th && (op === '$' || op === '$$') && (code[j] === '(' || isIdStart(code[j])) && (i === 0 || !isIdChar(code[i - 1]))) {
|
|
420
|
+
boundaries.push({ kind: 'th-splice', span: loc.span(i, j), detail: 'Template Haskell splice; its expansion is generated at compile time and is not evaluated' });
|
|
421
|
+
}
|
|
422
|
+
push(reserved.has(op) ? 'rop' : (op[0] === ':' ? 'consym' : 'op'), op, i, j);
|
|
423
|
+
i = j;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
err('invalid-character', i, i + 1, `unexpected character U+${c.codePointAt(0).toString(16).padStart(4, '0')}`);
|
|
428
|
+
i++;
|
|
429
|
+
}
|
|
430
|
+
return { tokens, comments, pragmas, errors, boundaries };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ── driver ─────────────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
function emptyResult(file, dialect) {
|
|
436
|
+
return {
|
|
437
|
+
language: 'haskell', file, dialect, ok: false, status: 'failed', complete: false,
|
|
438
|
+
module: null, imports: [], functions: [], signatures: [], declarations: [], calls: [], expressions: [],
|
|
439
|
+
records: [], doBlocks: [], comments: [], pragmas: [], errors: [], boundaries: [], uncertainty: [], gaps: [],
|
|
440
|
+
stats: { bytes: 0, tokens: 0, steps: 0 },
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function parseHaskell(source, opts = {}) {
|
|
445
|
+
const file = opts.file || '';
|
|
446
|
+
const dialect = dialectOf(file);
|
|
447
|
+
const res = emptyResult(file, dialect);
|
|
448
|
+
const budgets = { ...DEFAULT_PARSE_BUDGETS, ...(opts.budgets || {}) };
|
|
449
|
+
if (typeof source !== 'string') {
|
|
450
|
+
res.errors.push({ kind: 'parser-exception', detail: 'source must be a string' });
|
|
451
|
+
return res;
|
|
452
|
+
}
|
|
453
|
+
res.stats.bytes = Buffer.byteLength(source, 'utf8');
|
|
454
|
+
const g = loadHaskellGrammar({ grammarSource: opts.grammarSource });
|
|
455
|
+
if (!g.available) {
|
|
456
|
+
res.status = 'missing_grammar';
|
|
457
|
+
res.gaps.push({ ...g.gap, capability: 'parse' });
|
|
458
|
+
return res;
|
|
459
|
+
}
|
|
460
|
+
if (opts.mode === 'ghc') {
|
|
461
|
+
res.gaps.push({ kind: 'optional-mode-unavailable', capability: 'parse', mode: 'ghc', detail: 'GHC-assisted parsing is not available; the shipped grammar was used and GHC was not run' });
|
|
462
|
+
}
|
|
463
|
+
const started = Date.now();
|
|
464
|
+
const ctx = {
|
|
465
|
+
grammar: g.grammar, budgets, steps: 0,
|
|
466
|
+
tick() {
|
|
467
|
+
this.steps++;
|
|
468
|
+
if (this.steps > budgets.maxSteps) throw new BudgetError('maxSteps', budgets.maxSteps);
|
|
469
|
+
if ((this.steps & 2047) === 0 && Date.now() - started > budgets.deadlineMs) throw new BudgetError('deadlineMs', budgets.deadlineMs);
|
|
470
|
+
},
|
|
471
|
+
};
|
|
472
|
+
try {
|
|
473
|
+
if (res.stats.bytes > budgets.maxBytes) throw new BudgetError('maxBytes', budgets.maxBytes);
|
|
474
|
+
analyze(source, file, dialect, ctx, res);
|
|
475
|
+
res.stats.steps = ctx.steps;
|
|
476
|
+
res.status = res.errors.length ? 'parsed_with_errors' : 'parsed';
|
|
477
|
+
res.ok = true;
|
|
478
|
+
res.complete = res.errors.length === 0 && !res.boundaries.some((b) => b.kind !== 'cpp' && b.kind !== 'ffi');
|
|
479
|
+
} catch (e) {
|
|
480
|
+
const fresh = emptyResult(file, dialect);
|
|
481
|
+
fresh.stats = { ...res.stats, steps: ctx.steps };
|
|
482
|
+
fresh.gaps = res.gaps;
|
|
483
|
+
if (e instanceof BudgetError) {
|
|
484
|
+
fresh.status = 'budget_exceeded';
|
|
485
|
+
fresh.budget = { name: e.budget, limit: e.limit };
|
|
486
|
+
fresh.errors.push({ kind: 'budget-exceeded', budget: e.budget, limit: e.limit, detail: e.message });
|
|
487
|
+
fresh.uncertainty.push({ kind: 'partial-parse', detail: `parser budget ${e.budget} exceeded; the file was not analyzed` });
|
|
488
|
+
} else {
|
|
489
|
+
fresh.status = 'failed';
|
|
490
|
+
fresh.errors.push({ kind: 'parser-exception', detail: String((e && e.message) || e) });
|
|
491
|
+
}
|
|
492
|
+
return fresh;
|
|
493
|
+
}
|
|
494
|
+
return res;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function analyze(source, file, dialect, ctx, res) {
|
|
498
|
+
const g = ctx.grammar;
|
|
499
|
+
const loc = makeLocator(source);
|
|
500
|
+
let code = source;
|
|
501
|
+
if (dialect === 'literate') {
|
|
502
|
+
const lit = unwrapLiterate(code);
|
|
503
|
+
code = lit.code;
|
|
504
|
+
res.dialect = lit.style === 'latex' ? 'literate-latex' : 'literate-bird';
|
|
505
|
+
}
|
|
506
|
+
const pre = stripDirectives(code, dialect, g, loc);
|
|
507
|
+
code = pre.code;
|
|
508
|
+
const cppLines = pre.directives.filter((d) => d.name !== 'shebang');
|
|
509
|
+
if (cppLines.length && dialect !== 'hsc') {
|
|
510
|
+
res.boundaries.push({ kind: 'cpp', span: cppLines[0].span, detail: `${cppLines.length} preprocessor line(s) removed in place; every conditional branch was parsed`, count: cppLines.length });
|
|
511
|
+
}
|
|
512
|
+
if (dialect === 'hsc') {
|
|
513
|
+
res.boundaries.push({ kind: 'hsc', span: loc.span(0, Math.min(source.length, 1)), detail: 'hsc2hs source: # directives and #{...} blanked in place, never run through hsc2hs' });
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let lexed = lexAll(code, loc, ctx, { th: false, qq: false });
|
|
517
|
+
const exts = new Set();
|
|
518
|
+
for (const p of lexed.pragmas) {
|
|
519
|
+
if (p.name === 'LANGUAGE' && p.headerPosition) for (const e of p.text.replace(/^LANGUAGE/i, '').split(',')) exts.add(e.trim());
|
|
520
|
+
}
|
|
521
|
+
const th = exts.has('TemplateHaskell') || exts.has('TemplateHaskellQuotes');
|
|
522
|
+
const qq = exts.has('QuasiQuotes');
|
|
523
|
+
if (th || qq) lexed = lexAll(code, loc, ctx, { th, qq });
|
|
524
|
+
const T = lexed.tokens;
|
|
525
|
+
res.stats.tokens = T.length;
|
|
526
|
+
res.comments = lexed.comments;
|
|
527
|
+
res.pragmas = lexed.pragmas.map((p) => ({ name: p.name, text: p.text, span: p.span }));
|
|
528
|
+
res.boundaries.push(...lexed.boundaries);
|
|
529
|
+
const errors = [...lexed.errors];
|
|
530
|
+
|
|
531
|
+
// Generated-source disclosure from the header comments only.
|
|
532
|
+
const base = String(file).split(/[\\/]/).pop() || '';
|
|
533
|
+
const headerText = lexed.comments.filter((c) => c.headerPosition).slice(0, 5).map((c) => source.slice(c.span.startOffset, c.span.endOffset)).join('\n');
|
|
534
|
+
if (/^Paths_[\w']+\.hs$/.test(base) || /@generated|\bgenerated by\b|auto-?generated|DO NOT EDIT/i.test(headerText)) {
|
|
535
|
+
res.boundaries.push({ kind: 'generated', span: loc.span(0, 0), detail: 'file declares itself generated; treat its contents as generated source' });
|
|
536
|
+
}
|
|
537
|
+
for (const d of pre.directives) if (d.name === 'shebang') res.comments.push({ kind: 'shebang', span: d.span, terminated: true });
|
|
538
|
+
|
|
539
|
+
const N = T.length;
|
|
540
|
+
// ── module header ──
|
|
541
|
+
let bodyStart = 0;
|
|
542
|
+
if (N && T[0].k === 'kw' && T[0].v === 'module') {
|
|
543
|
+
let depth = 0;
|
|
544
|
+
let j = 1;
|
|
545
|
+
for (; j < N; j++) {
|
|
546
|
+
ctx.tick();
|
|
547
|
+
const k = T[j].k;
|
|
548
|
+
if (k === '(' || k === '[' || k === '{') depth++;
|
|
549
|
+
else if (k === ')' || k === ']' || k === '}') depth--;
|
|
550
|
+
else if (k === 'kw' && T[j].v === 'where' && depth <= 0) break;
|
|
551
|
+
}
|
|
552
|
+
const nameTok = T[1] && (T[1].k === 'conid' || T[1].k === 'qconid') ? T[1] : null;
|
|
553
|
+
if (j >= N) {
|
|
554
|
+
errors.push({ kind: 'module-header', offset: T[0].s, endOffset: T[0].e, detail: 'module header has no `where`' });
|
|
555
|
+
bodyStart = Math.min(N, 2);
|
|
556
|
+
} else bodyStart = j + 1;
|
|
557
|
+
const exportsOpen = T[2] && T[2].k === '(' ? 2 : -1;
|
|
558
|
+
const exportNames = [];
|
|
559
|
+
if (exportsOpen > 0) for (let q = exportsOpen + 1; q < Math.min(j, N); q++) if (['varid', 'conid', 'qvarid', 'qconid'].includes(T[q].k)) exportNames.push(T[q].v);
|
|
560
|
+
res.module = { name: nameTok ? nameTok.v : null, exports: exportsOpen > 0 ? exportNames : null, span: loc.span(T[0].s, T[Math.min(j, N - 1)].e) };
|
|
561
|
+
}
|
|
562
|
+
let topIndent = 0;
|
|
563
|
+
let explicitTop = false;
|
|
564
|
+
if (bodyStart < N) {
|
|
565
|
+
if (T[bodyStart].k === '{') explicitTop = true;
|
|
566
|
+
else topIndent = T[bodyStart].lcol;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// ── bracket matching with layout resynchronisation ──
|
|
570
|
+
const match = new Int32Array(N).fill(-1);
|
|
571
|
+
const stack = [];
|
|
572
|
+
const unclosed = (idx) => errors.push({ kind: 'unclosed-bracket', offset: T[idx].s, endOffset: T[idx].e, detail: `\`${T[idx].v}\` is never closed` });
|
|
573
|
+
for (let i = 0; i < N; i++) {
|
|
574
|
+
ctx.tick();
|
|
575
|
+
const t = T[i];
|
|
576
|
+
if (!explicitTop && i >= bodyStart && stack.length && t.fol && t.lcol <= topIndent && !CLOSERS.has(t.k)) {
|
|
577
|
+
for (const idx of stack) unclosed(idx);
|
|
578
|
+
stack.length = 0;
|
|
579
|
+
}
|
|
580
|
+
if (OPENERS[t.k]) {
|
|
581
|
+
stack.push(i);
|
|
582
|
+
if (stack.length > ctx.budgets.maxDepth) throw new BudgetError('maxDepth', ctx.budgets.maxDepth);
|
|
583
|
+
} else if (CLOSERS.has(t.k)) {
|
|
584
|
+
const top = stack.length ? stack[stack.length - 1] : -1;
|
|
585
|
+
if (top >= 0 && OPENERS[T[top].k] === t.k) { match[top] = i; match[i] = top; stack.pop(); }
|
|
586
|
+
else errors.push({ kind: 'unmatched-bracket', offset: t.s, endOffset: t.e, detail: `\`${t.v}\` has no matching opener` });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
for (const idx of stack) unclosed(idx);
|
|
590
|
+
|
|
591
|
+
// ── layout blocks and items ──
|
|
592
|
+
const cut = (idx, end) => (idx >= end ? end : idx);
|
|
593
|
+
const splitItems = (a, b, indent) => {
|
|
594
|
+
const items = [];
|
|
595
|
+
let start = a;
|
|
596
|
+
let i = a;
|
|
597
|
+
while (i < b) {
|
|
598
|
+
ctx.tick();
|
|
599
|
+
const t = T[i];
|
|
600
|
+
if (OPENERS[t.k] && match[i] > i) { i = match[i] + 1; continue; }
|
|
601
|
+
if (t.k === ';') {
|
|
602
|
+
if (i > start) items.push({ a: start, b: i });
|
|
603
|
+
start = i + 1;
|
|
604
|
+
} else if (indent !== null && i > start && t.fol && t.lcol === indent) {
|
|
605
|
+
items.push({ a: start, b: i });
|
|
606
|
+
start = i;
|
|
607
|
+
}
|
|
608
|
+
i++;
|
|
609
|
+
}
|
|
610
|
+
if (start < b) items.push({ a: start, b });
|
|
611
|
+
return items;
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const openBlock = (k, rangeEnd, parentIndent, termComma) => {
|
|
615
|
+
const kw = T[k].v;
|
|
616
|
+
const a = k + 1;
|
|
617
|
+
if (a >= rangeEnd) return { a, b: a, items: [], indent: -1 };
|
|
618
|
+
if (T[a].k === '{' && match[a] > a) {
|
|
619
|
+
const b = match[a];
|
|
620
|
+
return { a: a + 1, b: b + 1, items: splitItems(a + 1, b, null), indent: -1, explicit: true };
|
|
621
|
+
}
|
|
622
|
+
const indent = T[a].lcol;
|
|
623
|
+
if (indent <= parentIndent) return { a, b: a, items: [], indent };
|
|
624
|
+
let ifOpen = 0; let thenOpen = 0; let caseOpen = 0; let letOpen = 0;
|
|
625
|
+
let j = a;
|
|
626
|
+
for (; j < rangeEnd; j++) {
|
|
627
|
+
ctx.tick();
|
|
628
|
+
const t = T[j];
|
|
629
|
+
if (j > a && t.fol && t.lcol < indent) break;
|
|
630
|
+
if (OPENERS[t.k] && match[j] > j) { j = match[j]; continue; }
|
|
631
|
+
if (CLOSERS.has(t.k)) break;
|
|
632
|
+
if (t.k === ',' && termComma) break;
|
|
633
|
+
if (t.k === 'kw') {
|
|
634
|
+
const v = t.v;
|
|
635
|
+
if (v === 'in') { if (letOpen > 0) letOpen--; else break; }
|
|
636
|
+
else if (v === 'let') letOpen++;
|
|
637
|
+
else if (v === 'case') caseOpen++;
|
|
638
|
+
else if (v === 'of') { if (caseOpen > 0) caseOpen--; else break; }
|
|
639
|
+
else if (v === 'if') ifOpen++;
|
|
640
|
+
else if (v === 'then') { if (ifOpen > 0) { ifOpen--; thenOpen++; } else break; }
|
|
641
|
+
else if (v === 'else') { if (thenOpen > 0) thenOpen--; else break; }
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
void kw;
|
|
645
|
+
return { a, b: j, items: splitItems(a, j, indent), indent };
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
// ── extraction ──
|
|
649
|
+
const work = [];
|
|
650
|
+
const fnIndex = new Map();
|
|
651
|
+
const callsOut = [];
|
|
652
|
+
const spanOf = (a, b) => loc.span(T[a].s, T[Math.max(a, b - 1)].e);
|
|
653
|
+
const condAt = (t) => pre.depthAtLine[t.line] > 0;
|
|
654
|
+
const topRanges = [];
|
|
655
|
+
|
|
656
|
+
const registerFunction = (key, rec) => {
|
|
657
|
+
let f = fnIndex.get(key);
|
|
658
|
+
if (!f) {
|
|
659
|
+
f = { name: rec.name, kind: rec.kind, scope: rec.scope, parent: rec.parent, arity: rec.arity, guardCount: 0, clauses: [], span: null, signature: null, _s: rec.s, _e: rec.e };
|
|
660
|
+
fnIndex.set(key, f);
|
|
661
|
+
res.functions.push(f);
|
|
662
|
+
}
|
|
663
|
+
f._e = Math.max(f._e, rec.e);
|
|
664
|
+
f.guardCount += rec.guards;
|
|
665
|
+
f.clauses.push({ span: loc.span(rec.s, rec.e), arity: rec.arity, guardCount: rec.guards, bodySpan: rec.body });
|
|
666
|
+
f.span = loc.span(f._s_ ?? f._s, f._e);
|
|
667
|
+
return f;
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
const argsOf = (head, b) => {
|
|
671
|
+
let j = head + 1;
|
|
672
|
+
let count = 0;
|
|
673
|
+
let lastEnd = T[head].e;
|
|
674
|
+
while (j < b) {
|
|
675
|
+
ctx.tick();
|
|
676
|
+
const t = T[j];
|
|
677
|
+
if (ATOM_START.has(t.k)) {
|
|
678
|
+
count++; lastEnd = t.e; j++;
|
|
679
|
+
if (T[j] && T[j].k === '{' && T[j].s === t.e && match[j] > j && (t.k === 'conid' || t.k === 'qconid' || t.k === 'varid')) { lastEnd = T[match[j]].e; j = match[j] + 1; }
|
|
680
|
+
} else if ((t.k === '(' || t.k === '[') && match[j] > j) { count++; lastEnd = T[match[j]].e; j = match[j] + 1; }
|
|
681
|
+
else if (t.k === 'rop' && t.v === '@' && j + 1 < b && t.e === T[j + 1].s && (j === 0 || T[j - 1].e < t.s)) {
|
|
682
|
+
const nx = T[j + 1];
|
|
683
|
+
if ((nx.k === '(' || nx.k === '[') && match[j + 1] > j + 1) j = match[j + 1] + 1; else j += 2;
|
|
684
|
+
} else break;
|
|
685
|
+
}
|
|
686
|
+
return { count, lastEnd };
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// Scan one item. role: decl | alt | stmt. Returns nothing; pushes extracted facts.
|
|
690
|
+
const scanItem = (role, a, b, c) => {
|
|
691
|
+
// Locate the end of the pattern/LHS region.
|
|
692
|
+
let i = a;
|
|
693
|
+
let state = 'expr';
|
|
694
|
+
let eqop = '=';
|
|
695
|
+
let guardCount = 0;
|
|
696
|
+
let bodyStart = -1;
|
|
697
|
+
let lastBody = -1;
|
|
698
|
+
const closeBody = (endIdx) => { if (bodyStart >= 0 && endIdx > bodyStart) { res.expressions.push({ kind: 'clause-body', span: spanOf(bodyStart, endIdx), function: c.fn }); lastBody = endIdx; } bodyStart = -1; };
|
|
699
|
+
|
|
700
|
+
if (role === 'decl') { i = c.rhs; eqop = '='; }
|
|
701
|
+
else if (role === 'alt') {
|
|
702
|
+
eqop = '->';
|
|
703
|
+
let p = a;
|
|
704
|
+
while (p < b) {
|
|
705
|
+
ctx.tick();
|
|
706
|
+
const t = T[p];
|
|
707
|
+
if (OPENERS[t.k] && match[p] > p) { p = match[p] + 1; continue; }
|
|
708
|
+
if ((t.k === 'rop' && (t.v === '->' || t.v === '|'))) break;
|
|
709
|
+
p++;
|
|
710
|
+
}
|
|
711
|
+
if (p >= b) { errors.push({ kind: 'expected-arrow', offset: T[a].s, endOffset: T[b - 1].e, detail: 'case alternative has no `->`' }); return; }
|
|
712
|
+
i = p;
|
|
713
|
+
} else {
|
|
714
|
+
let p = a;
|
|
715
|
+
let found = -1;
|
|
716
|
+
while (p < b) {
|
|
717
|
+
ctx.tick();
|
|
718
|
+
const t = T[p];
|
|
719
|
+
if (OPENERS[t.k] && match[p] > p) { p = match[p] + 1; continue; }
|
|
720
|
+
if (t.k === 'kw' && (t.v === 'let' || t.v === 'do' || t.v === 'of' || t.v === 'where' || t.v === 'if' || t.v === 'case')) break;
|
|
721
|
+
if (t.k === 'rop' && t.v === '<-') { found = p; break; }
|
|
722
|
+
p++;
|
|
723
|
+
}
|
|
724
|
+
if (found >= 0) {
|
|
725
|
+
res.expressions.push({ kind: 'bind', span: spanOf(a, b), function: c.fn });
|
|
726
|
+
i = found + 1;
|
|
727
|
+
} else i = a;
|
|
728
|
+
eqop = null;
|
|
729
|
+
}
|
|
730
|
+
if (role !== 'stmt' && T[i] && T[i].k === 'rop' && T[i].v === '|') state = 'guard';
|
|
731
|
+
else if (role !== 'stmt' && T[i] && T[i].k === 'rop' && T[i].v === eqop) { i++; state = 'expr'; }
|
|
732
|
+
if (state === 'guard') { guardCount++; i++; }
|
|
733
|
+
if (state === 'expr') bodyStart = i;
|
|
734
|
+
|
|
735
|
+
const caseStack = [];
|
|
736
|
+
let multiIf = false;
|
|
737
|
+
let skipType = -1; // bracket depth marker not needed; ends at closer/comma
|
|
738
|
+
let prev = null;
|
|
739
|
+
let depth = 0;
|
|
740
|
+
let qualStart = state === 'guard' ? i : -1;
|
|
741
|
+
let typeUntilDepth = null;
|
|
742
|
+
|
|
743
|
+
while (i < b) {
|
|
744
|
+
ctx.tick();
|
|
745
|
+
const t = T[i];
|
|
746
|
+
// Guard qualifiers: `pat <- e` skips the pattern.
|
|
747
|
+
if (state === 'guard' && qualStart === i) {
|
|
748
|
+
let p = i;
|
|
749
|
+
while (p < b) {
|
|
750
|
+
const q = T[p];
|
|
751
|
+
if (OPENERS[q.k] && match[p] > p) { p = match[p] + 1; continue; }
|
|
752
|
+
if (q.k === ',' || (q.k === 'rop' && (q.v === eqop || q.v === '<-'))) break;
|
|
753
|
+
if (q.k === 'kw' && q.v === 'let') break;
|
|
754
|
+
p++;
|
|
755
|
+
}
|
|
756
|
+
if (p < b && T[p].k === 'rop' && T[p].v === '<-') { i = p + 1; qualStart = -1; prev = T[p]; continue; }
|
|
757
|
+
qualStart = -1;
|
|
758
|
+
}
|
|
759
|
+
if (typeUntilDepth !== null) {
|
|
760
|
+
// inside a type annotation: skip to its end
|
|
761
|
+
if (OPENERS[t.k] && match[i] > i) { i = match[i] + 1; continue; }
|
|
762
|
+
if (CLOSERS.has(t.k) || t.k === ',' || (t.k === 'rop' && (t.v === '=' || t.v === '|' || t.v === '<-')) || (t.k === 'kw' && ['then', 'else', 'of', 'in', 'where'].includes(t.v))) typeUntilDepth = null;
|
|
763
|
+
else { prev = t; i++; continue; }
|
|
764
|
+
}
|
|
765
|
+
if (t.k === 'rop' && t.v === '|' && !multiIf && role !== 'stmt' && depth === 0) {
|
|
766
|
+
closeBody(i);
|
|
767
|
+
state = 'guard'; guardCount++; qualStart = i + 1; i++; prev = null; continue;
|
|
768
|
+
}
|
|
769
|
+
if (state === 'guard' && t.k === ',') { qualStart = i + 1; i++; prev = null; continue; }
|
|
770
|
+
if (state === 'guard' && t.k === 'rop' && t.v === eqop) { state = 'expr'; bodyStart = i + 1; i++; prev = null; continue; }
|
|
771
|
+
if (t.k === 'kw' && c.layout.has(t.v)) {
|
|
772
|
+
const termComma = state === 'guard';
|
|
773
|
+
const blk = openBlock(i, b, c.indent, termComma);
|
|
774
|
+
const role2 = t.v === 'do' || t.v === 'mdo' ? 'stmt' : (t.v === 'of' ? 'alt' : 'decl');
|
|
775
|
+
const scope2 = t.v === 'where' ? 'where' : (t.v === 'let' ? 'let' : c.scope);
|
|
776
|
+
for (const it of blk.items) work.push({ role: role2, a: it.a, b: it.b, indent: blk.indent, scope: scope2, parent: c.fn, depth: c.depth + 1, layout: c.layout, fn: c.fn });
|
|
777
|
+
const endTok = Math.max(i, blk.b - 1);
|
|
778
|
+
if (t.v === 'do' || t.v === 'mdo') {
|
|
779
|
+
res.doBlocks.push({ span: loc.span(t.s, T[endTok].e), statements: blk.items.length, binds: blk.items.filter((it) => { for (let p = it.a; p < it.b; p++) { if (OPENERS[T[p].k] && match[p] > p) { p = match[p]; continue; } if (T[p].k === 'rop' && T[p].v === '<-') return true; } return false; }).length, function: c.fn });
|
|
780
|
+
res.expressions.push({ kind: 'do', span: loc.span(t.s, T[endTok].e), function: c.fn });
|
|
781
|
+
} else if (t.v === 'of') {
|
|
782
|
+
const cs = caseStack.length ? caseStack.pop() : null;
|
|
783
|
+
res.expressions.push({ kind: 'case', span: loc.span(cs !== null ? T[cs].s : t.s, T[endTok].e), function: c.fn });
|
|
784
|
+
} else if (t.v === 'let') res.expressions.push({ kind: 'let', span: loc.span(t.s, T[endTok].e), function: c.fn });
|
|
785
|
+
i = Math.max(i + 1, blk.b);
|
|
786
|
+
prev = T[i - 1];
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (t.k === 'rop' && t.v === '\\') {
|
|
790
|
+
if (T[i + 1] && T[i + 1].k === 'kw' && T[i + 1].v === 'case') {
|
|
791
|
+
const blk = openBlock(i + 1, b, c.indent, state === 'guard');
|
|
792
|
+
for (const it of blk.items) work.push({ role: 'alt', a: it.a, b: it.b, indent: blk.indent, scope: c.scope, parent: c.fn, depth: c.depth + 1, layout: c.layout, fn: c.fn });
|
|
793
|
+
const endTok = Math.max(i + 1, blk.b - 1);
|
|
794
|
+
res.expressions.push({ kind: 'lambda-case', span: loc.span(t.s, T[endTok].e), function: c.fn });
|
|
795
|
+
i = Math.max(i + 2, blk.b);
|
|
796
|
+
prev = T[i - 1];
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
let p = i + 1;
|
|
800
|
+
while (p < b) {
|
|
801
|
+
const q = T[p];
|
|
802
|
+
if (OPENERS[q.k] && match[p] > p) { p = match[p] + 1; continue; }
|
|
803
|
+
if (q.k === 'rop' && q.v === '->') break;
|
|
804
|
+
p++;
|
|
805
|
+
}
|
|
806
|
+
if (p >= b) { errors.push({ kind: 'malformed-lambda', offset: t.s, endOffset: t.e, detail: 'lambda has no `->`' }); i = b; continue; }
|
|
807
|
+
i = p + 1;
|
|
808
|
+
prev = T[p];
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
if (t.k === 'kw') {
|
|
812
|
+
if (t.v === 'case') caseStack.push(i);
|
|
813
|
+
else if (t.v === 'if' && T[i + 1] && T[i + 1].k === 'rop' && T[i + 1].v === '|') multiIf = true;
|
|
814
|
+
prev = t; i++; continue;
|
|
815
|
+
}
|
|
816
|
+
if (t.k === 'rop' && t.v === '::') { typeUntilDepth = 0; prev = t; i++; continue; }
|
|
817
|
+
if (t.k === '{' && match[i] > i && prev && (prev.k === 'conid' || prev.k === 'qconid' || prev.k === ')' || (prev.k === 'varid' && prev.e === t.s)) ) {
|
|
818
|
+
const close = match[i];
|
|
819
|
+
const fields = [];
|
|
820
|
+
let d = 0;
|
|
821
|
+
for (let p = i + 1; p < close; p++) {
|
|
822
|
+
const q = T[p];
|
|
823
|
+
if (OPENERS[q.k] && match[p] > p) { p = match[p]; continue; }
|
|
824
|
+
void d;
|
|
825
|
+
if ((q.k === 'varid' || q.k === 'qvarid') && T[p + 1] && (T[p + 1].k === 'rop' && T[p + 1].v === '=') && (p === i + 1 || T[p - 1].k === ',')) fields.push({ name: q.v, span: loc.span(q.s, q.e) });
|
|
826
|
+
else if ((q.k === 'varid') && (p === i + 1 || T[p - 1].k === ',') && (T[p + 1] ? (T[p + 1].k === ',' || p + 1 === close) : true)) fields.push({ name: q.v, span: loc.span(q.s, q.e), pun: true });
|
|
827
|
+
}
|
|
828
|
+
res.records.push({ kind: prev.k === 'conid' || prev.k === 'qconid' ? 'construction' : 'update', name: prev.k === 'conid' || prev.k === 'qconid' ? prev.v : null, fields, span: loc.span(prev.s, T[close].e), function: c.fn });
|
|
829
|
+
prev = T[i]; depth = 0; i++; continue;
|
|
830
|
+
}
|
|
831
|
+
if (t.k === '`' && T[i + 1] && (T[i + 1].k === 'varid' || T[i + 1].k === 'qvarid') && T[i + 2] && T[i + 2].k === '`') {
|
|
832
|
+
const h = T[i + 1];
|
|
833
|
+
callsOut.push({ kind: 'call', callee: h.v, name: h.name || h.v, qualifier: h.qual || null, infix: true, argCount: 2, span: loc.span(T[i].s, T[i + 2].e), calleeSpan: loc.span(h.s, h.e), function: c.fn, scope: c.scope, conditional: condAt(h) });
|
|
834
|
+
prev = T[i + 2]; i += 3; continue;
|
|
835
|
+
}
|
|
836
|
+
if ((t.k === 'varid' || t.k === 'qvarid') && t.line >= 1) {
|
|
837
|
+
const atomPrev = prev && ATOM_END.has(prev.k);
|
|
838
|
+
const fwdPrev = prev && prev.k === 'op' && ctx.grammar.forwardingOps.includes(prev.v);
|
|
839
|
+
const typeApp = prev && prev.k === 'rop' && prev.v === '@' && prev.e === t.s;
|
|
840
|
+
if (!typeApp && (!atomPrev || fwdPrev) && !(prev && prev.k === 'tick')) {
|
|
841
|
+
const { count, lastEnd } = argsOf(i, b);
|
|
842
|
+
if (count > 0 || fwdPrev) {
|
|
843
|
+
callsOut.push({ kind: 'call', callee: t.v, name: t.name || t.v, qualifier: t.qual || null, infix: false, argCount: count, span: loc.span(t.s, lastEnd), calleeSpan: loc.span(t.s, t.e), function: c.fn, scope: c.scope, conditional: condAt(t) });
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
prev = t;
|
|
848
|
+
i++;
|
|
849
|
+
}
|
|
850
|
+
closeBody(b);
|
|
851
|
+
return { guardCount, lastBody };
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
const handleDecl = (it, c) => {
|
|
855
|
+
const { a, b } = it;
|
|
856
|
+
let p = a;
|
|
857
|
+
let mark = -1;
|
|
858
|
+
while (p < b) {
|
|
859
|
+
ctx.tick();
|
|
860
|
+
const t = T[p];
|
|
861
|
+
if (OPENERS[t.k] && match[p] > p) { p = match[p] + 1; continue; }
|
|
862
|
+
if (t.k === 'rop' && (t.v === '=' || t.v === '|' || t.v === '::')) { mark = p; break; }
|
|
863
|
+
if (t.k === 'kw' && (t.v === 'where' || t.v === 'of')) break;
|
|
864
|
+
p++;
|
|
865
|
+
}
|
|
866
|
+
if (mark < 0) {
|
|
867
|
+
if (c.scope === 'top' && ctx.th) {
|
|
868
|
+
res.boundaries.push({ kind: 'th-top-level-splice', span: spanOf(a, b), detail: 'top-level Template Haskell splice; declarations it generates are not visible' });
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
errors.push({ kind: 'expected-declaration', offset: T[a].s, endOffset: T[b - 1].e, detail: 'item is neither a binding nor a signature' });
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (T[mark].v === '::') {
|
|
875
|
+
const names = [];
|
|
876
|
+
for (let q = a; q < mark; q++) {
|
|
877
|
+
if (T[q].k === 'varid') names.push(T[q].v);
|
|
878
|
+
else if (T[q].k === '(' && match[q] === q + 2 && /op|consym/.test(T[q + 1].k)) { names.push(T[q + 1].v); q += 2; }
|
|
879
|
+
}
|
|
880
|
+
res.signatures.push({ names, span: spanOf(a, b), scope: c.scope });
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
// clause
|
|
884
|
+
let name = null;
|
|
885
|
+
let kind = 'function';
|
|
886
|
+
let arity = 0;
|
|
887
|
+
const isPrefixMark = (q) => T[q].k === 'op' && q + 1 < mark && T[q].e === T[q + 1].s && (q === a || T[q - 1].e < T[q].s);
|
|
888
|
+
let infixAt = -1;
|
|
889
|
+
for (let q = a; q < mark; q++) {
|
|
890
|
+
if (OPENERS[T[q].k] && match[q] > q) { q = match[q]; continue; }
|
|
891
|
+
if (T[q].k === 'consym') { infixAt = -2; break; }
|
|
892
|
+
if (q > a && T[q].k === 'op' && !isPrefixMark(q)) { infixAt = q; break; }
|
|
893
|
+
if (T[q].k === '`' && T[q + 1] && T[q + 1].k === 'varid' && T[q + 2] && T[q + 2].k === '`') { infixAt = q + 1; break; }
|
|
894
|
+
}
|
|
895
|
+
if (infixAt === -2) kind = 'pattern-binding';
|
|
896
|
+
else if (infixAt >= 0) { name = T[infixAt].v; arity = 2; }
|
|
897
|
+
else if (T[a].k === 'varid') {
|
|
898
|
+
name = T[a].v;
|
|
899
|
+
for (let q = a + 1; q < mark; q++) {
|
|
900
|
+
const t = T[q];
|
|
901
|
+
if (t.k === 'op' && isPrefixMark(q)) continue;
|
|
902
|
+
if (t.k === 'rop' && t.v === '@' ) { q++; if (OPENERS[T[q] && T[q].k] && match[q] > q) q = match[q]; continue; }
|
|
903
|
+
if (OPENERS[t.k] && match[q] > q) q = match[q];
|
|
904
|
+
arity++;
|
|
905
|
+
}
|
|
906
|
+
} else if (T[a].k === '(' && match[a] === a + 2 && T[a + 1] && (T[a + 1].k === 'op' || T[a + 1].k === 'consym')) {
|
|
907
|
+
name = T[a + 1].v;
|
|
908
|
+
for (let q = a + 3; q < mark; q++) { if (OPENERS[T[q].k] && match[q] > q) q = match[q]; arity++; }
|
|
909
|
+
} else kind = 'pattern-binding';
|
|
910
|
+
const rec = scanItem('decl', a, b, { ...c, rhs: mark, fn: name || c.fn });
|
|
911
|
+
const body = rec && rec.lastBody > 0 ? spanOf(Math.min(mark + 1, b - 1), rec.lastBody) : null;
|
|
912
|
+
const endTok = b - 1;
|
|
913
|
+
const bodyFirst = Math.min(mark + 1, b - 1);
|
|
914
|
+
const f = registerFunction(`${c.blockId}:${name === null ? `#${a}` : name}`, {
|
|
915
|
+
name, kind, scope: c.scope, parent: c.parent || null, arity, guards: rec ? rec.guardCount : 0,
|
|
916
|
+
s: T[a].s, e: T[endTok].e, body: b > mark + 1 ? loc.span(T[bodyFirst].s, T[endTok].e) : null,
|
|
917
|
+
});
|
|
918
|
+
void body;
|
|
919
|
+
f.span = loc.span(f._s, f._e);
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const parseImport = (a, b) => {
|
|
923
|
+
let j = a + 1;
|
|
924
|
+
const rec = { module: null, qualified: false, as: null, hiding: false, safe: false, package: null, items: null, span: spanOf(a, b) };
|
|
925
|
+
while (j < b) {
|
|
926
|
+
const t = T[j];
|
|
927
|
+
if (t.k === 'varid' && (t.v === 'qualified' || t.v === 'safe')) { if (t.v === 'qualified') rec.qualified = true; else rec.safe = true; j++; }
|
|
928
|
+
else if (t.k === 'string' && !rec.module) { rec.package = t.v; j++; }
|
|
929
|
+
else break;
|
|
930
|
+
}
|
|
931
|
+
if (j < b && (T[j].k === 'conid' || T[j].k === 'qconid')) { rec.module = T[j].v; rec.moduleSpan = loc.span(T[j].s, T[j].e); j++; }
|
|
932
|
+
else errors.push({ kind: 'malformed-import', offset: T[a].s, endOffset: T[b - 1].e, detail: 'import has no module name' });
|
|
933
|
+
while (j < b) {
|
|
934
|
+
const t = T[j];
|
|
935
|
+
if (t.k === 'varid' && t.v === 'qualified') { rec.qualified = true; j++; }
|
|
936
|
+
else if (t.k === 'varid' && t.v === 'as' && T[j + 1] && (T[j + 1].k === 'conid' || T[j + 1].k === 'qconid')) { rec.as = T[j + 1].v; j += 2; }
|
|
937
|
+
else if (t.k === 'varid' && t.v === 'hiding') { rec.hiding = true; j++; }
|
|
938
|
+
else if (t.k === '(' && match[j] > j) {
|
|
939
|
+
rec.items = [];
|
|
940
|
+
for (let q = j + 1; q < match[j]; q++) if (['varid', 'conid'].includes(T[q].k)) rec.items.push(T[q].v);
|
|
941
|
+
j = match[j] + 1;
|
|
942
|
+
} else j++;
|
|
943
|
+
}
|
|
944
|
+
res.imports.push(rec);
|
|
945
|
+
};
|
|
946
|
+
|
|
947
|
+
const topItems = explicitTop && bodyStart < N && match[bodyStart] > bodyStart
|
|
948
|
+
? splitItems(bodyStart + 1, match[bodyStart], null)
|
|
949
|
+
: splitItems(bodyStart, N, topIndent);
|
|
950
|
+
ctx.th = th;
|
|
951
|
+
const layout = new Set(g.layoutKeywords);
|
|
952
|
+
let blockSeq = 0;
|
|
953
|
+
for (const it of topItems) {
|
|
954
|
+
topRanges.push({ s: T[it.a].s, e: T[it.b - 1].e });
|
|
955
|
+
const t0 = T[it.a];
|
|
956
|
+
const c0 = { scope: 'top', indent: topIndent, depth: 0, layout, blockId: 'top', fn: null, parent: null };
|
|
957
|
+
if (t0.k === 'kw' && g.declarationKeywords.includes(t0.v)) {
|
|
958
|
+
if (t0.v === 'import') parseImport(it.a, it.b);
|
|
959
|
+
else if (t0.v === 'foreign') {
|
|
960
|
+
const dir = T[it.a + 1] && T[it.a + 1].v;
|
|
961
|
+
const nameTok = T.slice(it.a, it.b).find((x, qi, arr) => x.k === 'varid' && arr[qi + 1] && arr[qi + 1].v === '::');
|
|
962
|
+
res.declarations.push({ kind: 'foreign', direction: dir, name: nameTok ? nameTok.v : null, span: spanOf(it.a, it.b) });
|
|
963
|
+
res.boundaries.push({ kind: 'ffi', span: spanOf(it.a, it.b), detail: `foreign ${dir || ''} declaration; the foreign code is not analyzed` });
|
|
964
|
+
} else if (t0.v === 'data' || t0.v === 'newtype') {
|
|
965
|
+
const decl = { kind: t0.v, name: null, constructors: [], fields: [], span: spanOf(it.a, it.b) };
|
|
966
|
+
let q = it.a + 1;
|
|
967
|
+
for (; q < it.b; q++) {
|
|
968
|
+
if (T[q].k === 'conid') { decl.name = T[q].v; break; }
|
|
969
|
+
if (T[q].k === 'rop' && T[q].v === '=') break;
|
|
970
|
+
}
|
|
971
|
+
for (let p = it.a; p < it.b; p++) {
|
|
972
|
+
const tk = T[p];
|
|
973
|
+
if (tk.k === 'rop' && (tk.v === '=' || tk.v === '|') && T[p + 1] && T[p + 1].k === 'conid') decl.constructors.push(T[p + 1].v);
|
|
974
|
+
if (tk.k === '{' && match[p] > p) {
|
|
975
|
+
for (let r = p + 1; r < match[p]; r++) {
|
|
976
|
+
if (OPENERS[T[r].k] && match[r] > r) { r = match[r]; continue; }
|
|
977
|
+
if (T[r].k === 'varid' && (r === p + 1 || T[r - 1].k === ',')) {
|
|
978
|
+
let m2 = r;
|
|
979
|
+
const names = [];
|
|
980
|
+
while (T[m2] && T[m2].k === 'varid') { names.push(T[m2]); if (T[m2 + 1] && T[m2 + 1].k === ',') m2 += 2; else { m2++; break; } }
|
|
981
|
+
if (T[m2] && T[m2].k === 'rop' && T[m2].v === '::') for (const nt of names) decl.fields.push({ name: nt.v, span: loc.span(nt.s, nt.e) });
|
|
982
|
+
r = m2 - 1;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
res.records.push({ kind: 'declaration', name: decl.name, fields: decl.fields.slice(), span: loc.span(T[p].s, T[match[p]].e), function: null });
|
|
986
|
+
p = match[p];
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
res.declarations.push(decl);
|
|
990
|
+
} else if (t0.v === 'class' || t0.v === 'instance') {
|
|
991
|
+
let w = -1;
|
|
992
|
+
for (let q = it.a + 1; q < it.b; q++) {
|
|
993
|
+
if (OPENERS[T[q].k] && match[q] > q) { q = match[q]; continue; }
|
|
994
|
+
if (T[q].k === 'kw' && T[q].v === 'where') { w = q; break; }
|
|
995
|
+
}
|
|
996
|
+
const nameTok = T.slice(it.a + 1, w < 0 ? it.b : w).find((x) => x.k === 'conid' || x.k === 'qconid');
|
|
997
|
+
res.declarations.push({ kind: t0.v, name: nameTok ? nameTok.v : null, span: spanOf(it.a, it.b) });
|
|
998
|
+
if (w >= 0) {
|
|
999
|
+
const blk = openBlock(w, it.b, topIndent, false);
|
|
1000
|
+
const bid = `${t0.v}${blockSeq++}`;
|
|
1001
|
+
for (const sub of blk.items) work.push({ role: 'decl', a: sub.a, b: sub.b, indent: blk.indent, scope: t0.v, parent: nameTok ? nameTok.v : null, depth: 1, layout, fn: null, blockId: bid });
|
|
1002
|
+
}
|
|
1003
|
+
} else res.declarations.push({ kind: t0.v, name: null, span: spanOf(it.a, it.b) });
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
handleDecl(it, c0);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// Worklist for nested blocks, iterative and depth-bounded.
|
|
1010
|
+
let wi = 0;
|
|
1011
|
+
const blockIds = new Map();
|
|
1012
|
+
while (wi < work.length) {
|
|
1013
|
+
ctx.tick();
|
|
1014
|
+
const w = work[wi++];
|
|
1015
|
+
if (w.depth > ctx.budgets.maxDepth) throw new BudgetError('maxDepth', ctx.budgets.maxDepth);
|
|
1016
|
+
const key = `${w.role}:${w.indent}:${w.parent}:${w.scope}`;
|
|
1017
|
+
if (!w.blockId) {
|
|
1018
|
+
if (!blockIds.has(key + ':' + T[w.a].line)) blockIds.set(key + ':' + T[w.a].line, `b${blockIds.size}`);
|
|
1019
|
+
w.blockId = blockIds.get(key + ':' + T[w.a].line);
|
|
1020
|
+
}
|
|
1021
|
+
if (w.role === 'decl') handleDecl({ a: w.a, b: w.b }, w);
|
|
1022
|
+
else scanItem(w.role, w.a, w.b, w);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// ── errors → withheld calls ──
|
|
1026
|
+
const bad = [];
|
|
1027
|
+
for (const e of errors) {
|
|
1028
|
+
const r = topRanges.find((x) => e.offset >= x.s && e.offset <= x.e) || null;
|
|
1029
|
+
if (r) bad.push(r);
|
|
1030
|
+
}
|
|
1031
|
+
bad.sort((x, y) => x.s - y.s);
|
|
1032
|
+
const merged = [];
|
|
1033
|
+
for (const r of bad) {
|
|
1034
|
+
const last = merged[merged.length - 1];
|
|
1035
|
+
if (last && r.s <= last.e) last.e = Math.max(last.e, r.e); else merged.push({ ...r });
|
|
1036
|
+
}
|
|
1037
|
+
const inBad = (off) => {
|
|
1038
|
+
let lo = 0; let hi = merged.length - 1;
|
|
1039
|
+
while (lo <= hi) {
|
|
1040
|
+
const mid = (lo + hi) >> 1;
|
|
1041
|
+
if (off < merged[mid].s) hi = mid - 1; else if (off > merged[mid].e) lo = mid + 1; else return mid;
|
|
1042
|
+
}
|
|
1043
|
+
return -1;
|
|
1044
|
+
};
|
|
1045
|
+
const withheld = new Map();
|
|
1046
|
+
for (const call of callsOut) {
|
|
1047
|
+
const idx = merged.length ? inBad(call.span.startOffset) : -1;
|
|
1048
|
+
if (idx >= 0) withheld.set(idx, (withheld.get(idx) || 0) + 1); else res.calls.push(call);
|
|
1049
|
+
}
|
|
1050
|
+
for (const f of res.functions) {
|
|
1051
|
+
if (merged.length && inBad(f._s) >= 0) f.partial = true;
|
|
1052
|
+
delete f._s; delete f._e; delete f._s_;
|
|
1053
|
+
}
|
|
1054
|
+
res.expressions = res.expressions.filter((x) => merged.length === 0 || inBad(x.span.startOffset) < 0);
|
|
1055
|
+
res.doBlocks = res.doBlocks.filter((x) => merged.length === 0 || inBad(x.span.startOffset) < 0);
|
|
1056
|
+
res.records = res.records.filter((x) => merged.length === 0 || inBad(x.span.startOffset) < 0);
|
|
1057
|
+
|
|
1058
|
+
const total = errors.length;
|
|
1059
|
+
const outErrors = errors.slice(0, ctx.budgets.maxErrors);
|
|
1060
|
+
for (const e of outErrors) {
|
|
1061
|
+
const idx = merged.length ? inBad(e.offset) : -1;
|
|
1062
|
+
res.errors.push({ kind: e.kind, detail: e.detail, span: loc.span(e.offset, e.endOffset), withheldCalls: idx >= 0 ? (withheld.get(idx) || 0) : 0 });
|
|
1063
|
+
}
|
|
1064
|
+
if (total > outErrors.length) res.errors.push({ kind: 'errors-truncated', detail: `${total - outErrors.length} further syntax error(s) not listed` });
|
|
1065
|
+
|
|
1066
|
+
// ── uncertainty ──
|
|
1067
|
+
if (res.boundaries.some((x) => x.kind === 'cpp' || x.kind === 'hsc')) res.uncertainty.push({ kind: 'preprocessed' });
|
|
1068
|
+
if (res.boundaries.some((x) => x.kind === 'generated' || x.kind.startsWith('th-') || x.kind === 'quasiquote')) res.uncertainty.push({ kind: 'generated-source' });
|
|
1069
|
+
if (res.errors.length) res.uncertainty.push({ kind: 'partial-parse' });
|
|
1070
|
+
res.functions.sort((x, y) => x.span.startOffset - y.span.startOffset);
|
|
1071
|
+
res.calls.sort((x, y) => x.span.startOffset - y.span.startOffset);
|
|
1072
|
+
void cut;
|
|
1073
|
+
}
|