@sanbus/galley-core 0.0.1

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 (48) hide show
  1. package/README.md +15 -0
  2. package/build/builder.mjs +276 -0
  3. package/build/fixture.mjs +85 -0
  4. package/build/shim.mjs +216 -0
  5. package/dist/artifact.d.ts +38 -0
  6. package/dist/artifact.js +45 -0
  7. package/dist/artifact.js.map +1 -0
  8. package/dist/constants.d.ts +34 -0
  9. package/dist/constants.js +41 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/diagnostic.d.ts +34 -0
  12. package/dist/diagnostic.js +28 -0
  13. package/dist/diagnostic.js.map +1 -0
  14. package/dist/errors.d.ts +22 -0
  15. package/dist/errors.js +38 -0
  16. package/dist/errors.js.map +1 -0
  17. package/dist/index.d.ts +21 -0
  18. package/dist/index.js +16 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/names.d.ts +20 -0
  21. package/dist/names.js +29 -0
  22. package/dist/names.js.map +1 -0
  23. package/dist/node.d.ts +45 -0
  24. package/dist/node.js +137 -0
  25. package/dist/node.js.map +1 -0
  26. package/dist/port.d.ts +193 -0
  27. package/dist/port.js +14 -0
  28. package/dist/port.js.map +1 -0
  29. package/dist/procedures.d.ts +62 -0
  30. package/dist/procedures.js +154 -0
  31. package/dist/procedures.js.map +1 -0
  32. package/dist/session.d.ts +123 -0
  33. package/dist/session.js +599 -0
  34. package/dist/session.js.map +1 -0
  35. package/dist/text.d.ts +7 -0
  36. package/dist/text.js +16 -0
  37. package/dist/text.js.map +1 -0
  38. package/package.json +31 -0
  39. package/src/artifact.ts +62 -0
  40. package/src/constants.ts +47 -0
  41. package/src/diagnostic.ts +48 -0
  42. package/src/errors.ts +49 -0
  43. package/src/index.ts +53 -0
  44. package/src/node.ts +154 -0
  45. package/src/port.ts +213 -0
  46. package/src/procedures.ts +169 -0
  47. package/src/session.ts +747 -0
  48. package/src/text.ts +19 -0
package/src/session.ts ADDED
@@ -0,0 +1,747 @@
1
+ /**
2
+ * Parsing session bound to this library's parser.
3
+ * Mirrors Python/Rust/Go sessions over `bindings/c/galley.h`.
4
+ *
5
+ * Runtime-neutral: all native calls go through the injected {@link FfiPort}.
6
+ * Each adapter (`bindings/js/node`, `../bun`, `../deno`) subclasses `Session`
7
+ * to supply its port from `SessionOptions.libraryPath`.
8
+ */
9
+
10
+ import { INVALID_NODE } from "./constants.ts";
11
+ import type { Diagnostic } from "./diagnostic.ts";
12
+ import { GalleyError } from "./errors.ts";
13
+ import type { FfiPort, Handle, SessionCOptions, TreeSnapshot } from "./port.ts";
14
+ import { decodeUtf8, encodeUtf8 } from "./text.ts";
15
+ import { Node } from "./node.ts";
16
+ import { listProcedures, setParsingSession } from "./procedures.ts";
17
+
18
+ export interface SessionOptions {
19
+ maxErrors?: number; // default 10
20
+ recoveryWindow?: number; // default 500
21
+ stackOverflowRecovery?: boolean; // default false
22
+ syntaxErrorStackDepth?: number; // default 0
23
+ verbosity?: number; // default 0
24
+ astPreallocationRatio?: number; // default -1.0 (selects library default)
25
+ astPreallocationCap?: number | bigint; // default 0
26
+ messageOverrides?: Record<string, string>; // name -> message
27
+ libraryPath?: string; // override shared-library location (consumed by the adapter)
28
+ }
29
+
30
+ function defaultOptions(): Required<Omit<SessionOptions, "messageOverrides" | "libraryPath">> & {
31
+ messageOverrides: Record<string, string>;
32
+ } {
33
+ return {
34
+ maxErrors: 10,
35
+ recoveryWindow: 500,
36
+ stackOverflowRecovery: false,
37
+ syntaxErrorStackDepth: 0,
38
+ verbosity: 0,
39
+ astPreallocationRatio: -1.0,
40
+ astPreallocationCap: 0,
41
+ messageOverrides: {},
42
+ };
43
+ }
44
+
45
+ function toNodeAddress(node: Node | bigint | number): bigint {
46
+ if (typeof node === "bigint") return node;
47
+ if (typeof node === "number") return BigInt(node);
48
+ return (node as Node).address;
49
+ }
50
+
51
+ function isInvalid(addr: bigint): boolean {
52
+ return addr === INVALID_NODE;
53
+ }
54
+
55
+ function optNode(session: Session, addr: bigint): Node | null {
56
+ if (isInvalid(addr)) return null;
57
+ return new Node(session, addr);
58
+ }
59
+
60
+ export class Session {
61
+ #handle: Handle | null = null;
62
+ #port: FfiPort;
63
+ #closed = false;
64
+
65
+ constructor(port: FfiPort, options: SessionOptions = {}) {
66
+ const merged = { ...defaultOptions(), ...options };
67
+ const overrides = options.messageOverrides ?? merged.messageOverrides;
68
+ this.#port = port;
69
+
70
+ const hasNonDefault =
71
+ options.maxErrors !== undefined ||
72
+ options.recoveryWindow !== undefined ||
73
+ options.stackOverflowRecovery !== undefined ||
74
+ options.syntaxErrorStackDepth !== undefined ||
75
+ options.verbosity !== undefined ||
76
+ options.astPreallocationRatio !== undefined ||
77
+ options.astPreallocationCap !== undefined;
78
+
79
+ let cOptions: SessionCOptions | null = null;
80
+ if (hasNonDefault) {
81
+ cOptions = {
82
+ maxErrors: merged.maxErrors,
83
+ recoveryWindow: merged.recoveryWindow,
84
+ stackOverflowRecovery: merged.stackOverflowRecovery ? 1 : 0,
85
+ syntaxErrorStackDepth: merged.syntaxErrorStackDepth,
86
+ verbosity: merged.verbosity,
87
+ astPreallocationRatio: merged.astPreallocationRatio,
88
+ astPreallocationCap:
89
+ typeof merged.astPreallocationCap === "bigint"
90
+ ? merged.astPreallocationCap
91
+ : BigInt(merged.astPreallocationCap),
92
+ };
93
+ }
94
+
95
+ const handle = this.#port.createSession(cOptions);
96
+ if (handle === null || handle === undefined) {
97
+ throw new GalleyError("out of memory", -7, null);
98
+ }
99
+ this.#handle = handle;
100
+
101
+ for (const [name, message] of Object.entries(overrides)) {
102
+ const st = this.#port.setMessageOverride(this.#handle, encodeUtf8(name), encodeUtf8(message));
103
+ if (st < 0) {
104
+ const err = this.#errorFromStatus(st, "failed to register message override");
105
+ this.close();
106
+ throw err;
107
+ }
108
+ }
109
+ }
110
+
111
+ get isClosed(): boolean {
112
+ return this.#closed || this.#handle === null;
113
+ }
114
+
115
+ #requireHandle(): Handle {
116
+ if (this.#closed || this.#handle === null) throw new Error("session is closed");
117
+ return this.#handle;
118
+ }
119
+
120
+ #statusMessage(status: number): string {
121
+ const s = this.#port.statusString(status);
122
+ return s ?? "unknown galley error";
123
+ }
124
+
125
+ #errorFromStatus(status: number, fallback?: string): GalleyError {
126
+ let diag: Diagnostic | null = null;
127
+ try {
128
+ if (this.#handle !== null && this.#port.hasDiagnostic(this.#handle)) {
129
+ diag = this.#buildDiagnosticSingular();
130
+ }
131
+ } catch {
132
+ // ignore
133
+ }
134
+ const message = fallback ?? diag?.message ?? this.#statusMessage(status);
135
+ return new GalleyError(message, status, diag);
136
+ }
137
+
138
+ #checkStatus(status: number, fallback?: string): void {
139
+ if (status < 0) throw this.#errorFromStatus(status, fallback);
140
+ }
141
+
142
+ // -- parser metadata (mirror galley.h; bound to this session's artifact) --
143
+
144
+ version(): string {
145
+ return this.#port.version();
146
+ }
147
+
148
+ parserType(): number {
149
+ return this.#port.parserType();
150
+ }
151
+
152
+ errorRecoveryMode(): number {
153
+ return this.#port.errorRecoveryMode();
154
+ }
155
+
156
+ hasAst(): boolean {
157
+ return this.#port.hasAst();
158
+ }
159
+
160
+ hasProcedures(): boolean {
161
+ return this.#port.hasProcedures();
162
+ }
163
+
164
+ allowsNoAstTreeProcedures(): boolean {
165
+ return this.#port.allowsNoAstTreeProcedures();
166
+ }
167
+
168
+ sourceRetentionEnabled(): boolean {
169
+ return this.#port.sourceRetentionEnabled();
170
+ }
171
+
172
+ hasPositionTracking(): boolean {
173
+ return this.#port.hasPositionTracking();
174
+ }
175
+
176
+ hasInputStreaming(): boolean {
177
+ return this.#port.hasInputStreaming();
178
+ }
179
+
180
+ usesVerbatim(): boolean {
181
+ return this.#port.usesVerbatim();
182
+ }
183
+
184
+ stackOverflowRecoveryAvailable(): boolean {
185
+ return this.#port.stackOverflowRecoveryAvailable();
186
+ }
187
+
188
+ symbolCount(): number {
189
+ return this.#port.symbolCount();
190
+ }
191
+
192
+ variableCount(): number {
193
+ return this.#port.variableCount();
194
+ }
195
+
196
+ statusString(status: number): string | null {
197
+ return this.#port.statusString(status);
198
+ }
199
+
200
+ // -- lifecycle -------------------------------------------------------
201
+
202
+ close(): void {
203
+ if (this.#handle !== null) {
204
+ this.#port.destroySession(this.#handle);
205
+ this.#handle = null;
206
+ }
207
+ this.#closed = true;
208
+ }
209
+
210
+ /** For `using session = new Session()` (Explicit Resource Management). */
211
+ [Symbol.dispose](): void {
212
+ this.close();
213
+ }
214
+
215
+ // -- parsing ---------------------------------------------------------
216
+
217
+ parse(input: string | Uint8Array): number {
218
+ const handle = this.#requireHandle();
219
+ const buf = typeof input === "string" ? encodeUtf8(input) : input;
220
+ this.#port.syncProcedures(listProcedures());
221
+ const previous = setParsingSession(this);
222
+ let status: number;
223
+ try {
224
+ status = this.#port.parse(handle, buf);
225
+ } finally {
226
+ setParsingSession(previous);
227
+ }
228
+ if (status < 0) {
229
+ throw this.#errorFromStatus(status);
230
+ }
231
+ return status;
232
+ }
233
+
234
+ parseSentinel(input: string | Uint8Array): number {
235
+ return this.parse(input);
236
+ }
237
+
238
+ parseFile(filePath: string): number {
239
+ const handle = this.#requireHandle();
240
+ this.#port.syncProcedures(listProcedures());
241
+ const previous = setParsingSession(this);
242
+ let status: number;
243
+ try {
244
+ status = this.#port.parseFile(handle, filePath);
245
+ } finally {
246
+ setParsingSession(previous);
247
+ }
248
+ if (status < 0) {
249
+ throw this.#errorFromStatus(status);
250
+ }
251
+ return status;
252
+ }
253
+
254
+ // -- arena -----------------------------------------------------------
255
+
256
+ nodeCount(): number {
257
+ return this.#port.nodeCount(this.#requireHandle());
258
+ }
259
+
260
+ reserveNodes(capacity: number | bigint): void {
261
+ const h = this.#requireHandle();
262
+ const st = this.#port.reserveNodes(h, typeof capacity === "bigint" ? capacity : BigInt(capacity));
263
+ this.#checkStatus(st);
264
+ }
265
+
266
+ nodeCapacity(): number {
267
+ return this.#port.nodeCapacity(this.#requireHandle());
268
+ }
269
+
270
+ // -- navigation ------------------------------------------------------
271
+
272
+ rootNode(): Node | null {
273
+ const h = this.#requireHandle();
274
+ return optNode(this, this.#port.rootNode(h));
275
+ }
276
+
277
+ nodeValid(node: Node | bigint | number): boolean {
278
+ const h = this.#requireHandle();
279
+ return this.#port.nodeValid(h, toNodeAddress(node));
280
+ }
281
+
282
+ childCount(node: Node | bigint | number): number {
283
+ const h = this.#requireHandle();
284
+ return this.#port.childCount(h, toNodeAddress(node));
285
+ }
286
+
287
+ children(node: Node | bigint | number): Node[] {
288
+ const h = this.#requireHandle();
289
+ const addr = toNodeAddress(node);
290
+ const count = this.childCount(addr);
291
+ const out: Node[] = [];
292
+ let child = this.#port.firstChild(h, addr);
293
+ for (let i = 0; i < count; i++) {
294
+ if (isInvalid(child)) throw new Error("child count changed during iteration");
295
+ out.push(new Node(this, child));
296
+ child = this.#port.nextSibling(h, child);
297
+ }
298
+ return out;
299
+ }
300
+
301
+ firstChild(node: Node | bigint | number): Node | null {
302
+ const h = this.#requireHandle();
303
+ return optNode(this, this.#port.firstChild(h, toNodeAddress(node)));
304
+ }
305
+
306
+ lastChild(node: Node | bigint | number): Node | null {
307
+ const h = this.#requireHandle();
308
+ return optNode(this, this.#port.lastChild(h, toNodeAddress(node)));
309
+ }
310
+
311
+ nextSibling(node: Node | bigint | number): Node | null {
312
+ const h = this.#requireHandle();
313
+ return optNode(this, this.#port.nextSibling(h, toNodeAddress(node)));
314
+ }
315
+
316
+ priorSibling(node: Node | bigint | number): Node | null {
317
+ const h = this.#requireHandle();
318
+ return optNode(this, this.#port.priorSibling(h, toNodeAddress(node)));
319
+ }
320
+
321
+ parent(node: Node | bigint | number): Node | null {
322
+ const h = this.#requireHandle();
323
+ return optNode(this, this.#port.parent(h, toNodeAddress(node)));
324
+ }
325
+
326
+ /**
327
+ * Flat bulk read of the most recent successful parse in a single FFI
328
+ * crossing: one array slot per node address. Walk `parent`/`firstChild`/
329
+ * `next` directly instead of one call per node.
330
+ */
331
+ snapshot(): TreeSnapshot {
332
+ return this.#port.treeSnapshot(this.#requireHandle());
333
+ }
334
+
335
+ /**
336
+ * Pre-order walker over the subtree rooted at `root`, with the root at
337
+ * depth 0. Pass true to prune subtrees rooted at semantic-error nodes.
338
+ * Returns null for invalid roots and builds without AST construction.
339
+ * Close the walker (or use `using`) before closing the session or
340
+ * parsing again.
341
+ */
342
+ walk(root: Node | bigint | number, skipSemanticErrors = false): Walker | null {
343
+ const h = this.#requireHandle();
344
+ const handle = this.#port.walkerCreate(h, toNodeAddress(root), skipSemanticErrors);
345
+ if (handle === null || handle === undefined) return null;
346
+ return new Walker(this, this.#port, handle);
347
+ }
348
+
349
+ symbolNameBytes(node: Node | bigint | number): Uint8Array | null {
350
+ const h = this.#requireHandle();
351
+ return this.#port.nodeSymbolName(h, toNodeAddress(node));
352
+ }
353
+
354
+ symbolName(node: Node | bigint | number): string | null {
355
+ const bytes = this.symbolNameBytes(node);
356
+ if (bytes === null) return null;
357
+ return decodeUtf8(bytes);
358
+ }
359
+
360
+ text(node: Node | bigint | number): Uint8Array | null {
361
+ const h = this.#requireHandle();
362
+ return this.#port.nodeText(h, toNodeAddress(node));
363
+ }
364
+
365
+ span(node: Node | bigint | number): [bigint, bigint] | null {
366
+ const h = this.#requireHandle();
367
+ return this.#port.nodeSpan(h, toNodeAddress(node));
368
+ }
369
+
370
+ lineColumn(node: Node | bigint | number): [number, number] | null {
371
+ const h = this.#requireHandle();
372
+ return this.#port.nodeLineColumn(h, toNodeAddress(node));
373
+ }
374
+
375
+ variableIndex(node: Node | bigint | number): number | null {
376
+ const h = this.#requireHandle();
377
+ const idx = this.#port.nodeVariableIndex(h, toNodeAddress(node));
378
+ if (idx === -1) return null;
379
+ if (idx < 0) throw this.#errorFromStatus(idx);
380
+ return idx;
381
+ }
382
+
383
+ lastPosition(): [number, number] | null {
384
+ const h = this.#requireHandle();
385
+ return this.#port.lastPosition(h);
386
+ }
387
+
388
+ hasDiagnostic(): boolean {
389
+ const h = this.#requireHandle();
390
+ return this.#port.hasDiagnostic(h);
391
+ }
392
+
393
+ setMessageOverride(name: string, message: string): void {
394
+ const h = this.#requireHandle();
395
+ const st = this.#port.setMessageOverride(h, encodeUtf8(name), encodeUtf8(message));
396
+ this.#checkStatus(st);
397
+ }
398
+
399
+ // -- diagnostics -----------------------------------------------------
400
+
401
+ #buildDiagnosticSingular(): Diagnostic {
402
+ const h = this.#requireHandle();
403
+ const kind = this.#port.diagnosticKind(h);
404
+ const pos = this.#port.diagnosticPosition(h);
405
+ const line = pos ? pos[0] : 0;
406
+ const col = pos ? pos[1] : 0;
407
+
408
+ const message = this.#port.diagnosticMessage(h) ?? "";
409
+ const messageAnsi = this.#port.diagnosticMessageAnsi(h) ?? "";
410
+
411
+ const unexpected = this.#port.diagnosticUnexpectedToken(h);
412
+
413
+ const expectedTokens: Uint8Array[] = [];
414
+ const expCount = this.#port.diagnosticExpectedCount(h);
415
+ if (expCount > 0) {
416
+ for (let i = 0; i < expCount; i++) {
417
+ const b = this.#port.diagnosticExpectedAt(h, i);
418
+ if (b) expectedTokens.push(b);
419
+ }
420
+ }
421
+
422
+ const context: string[] = [];
423
+ const ctxCount = this.#port.diagnosticContextCount(h);
424
+ if (ctxCount > 0) {
425
+ for (let i = 0; i < ctxCount; i++) {
426
+ const b = this.#port.diagnosticContextAt(h, i);
427
+ if (b) context.push(decodeUtf8(b));
428
+ }
429
+ }
430
+
431
+ const syntaxErrorCount = this.#port.syntaxErrorCount(h);
432
+ const semanticErrorCount = this.#port.semanticErrorCount(h);
433
+ const semantic = this.#port.diagnosticSemantic(h);
434
+ const indentation = this.#port.diagnosticIndentation(h);
435
+ const recovery = this.#readRecoverySingular(h);
436
+
437
+ return {
438
+ kind,
439
+ line,
440
+ column: col,
441
+ message,
442
+ messageAnsi,
443
+ unexpectedToken: unexpected,
444
+ expectedTokens,
445
+ context,
446
+ syntaxErrorCount: syntaxErrorCount < 0 ? 0 : syntaxErrorCount,
447
+ semanticErrorCount: semanticErrorCount < 0 ? 0 : semanticErrorCount,
448
+ semantic,
449
+ indentation,
450
+ ...recovery,
451
+ };
452
+ }
453
+
454
+ diagnostic(): Diagnostic | null {
455
+ const h = this.#requireHandle();
456
+ if (!this.#port.hasDiagnostic(h)) return null;
457
+ return this.#buildDiagnosticSingular();
458
+ }
459
+
460
+ diagnostics(): Diagnostic[] {
461
+ const h = this.#requireHandle();
462
+ const count = this.#port.recordedDiagnosticCount(h);
463
+ if (count <= 0) return [];
464
+ const out: Diagnostic[] = [];
465
+ for (let i = 0; i < count; i++) {
466
+ const d = this.#buildRecordedDiagnostic(i);
467
+ if (d) out.push(d);
468
+ }
469
+ return out;
470
+ }
471
+
472
+ #buildRecordedDiagnostic(index: number): Diagnostic | null {
473
+ const h = this.#requireHandle();
474
+ const pos = this.#port.recordedDiagnosticPosition(h, index);
475
+ if (pos === null) return null;
476
+
477
+ const kind = this.#port.recordedDiagnosticKind(h, index);
478
+ const [line, col] = pos;
479
+
480
+ const message = this.#port.recordedDiagnosticMessage(h, index) ?? "";
481
+
482
+ const unexpected = this.#port.recordedUnexpectedToken(h, index);
483
+
484
+ const expectedTokens: Uint8Array[] = [];
485
+ const expCount = this.#port.recordedExpectedCount(h, index);
486
+ if (expCount > 0) {
487
+ for (let j = 0; j < expCount; j++) {
488
+ const b = this.#port.recordedExpectedToken(h, index, j);
489
+ if (b) expectedTokens.push(b);
490
+ }
491
+ }
492
+
493
+ const context: string[] = [];
494
+ const ctxCount = this.#port.recordedContextCount(h, index);
495
+ if (ctxCount > 0) {
496
+ for (let j = 0; j < ctxCount; j++) {
497
+ const b = this.#port.recordedContextName(h, index, j);
498
+ if (b) context.push(decodeUtf8(b));
499
+ }
500
+ }
501
+
502
+ const indentation = this.#port.recordedIndentation(h, index);
503
+ const semantic = this.#port.recordedSemantic(h, index);
504
+ const recovery = this.#readRecoveryRecorded(h, index);
505
+
506
+ return {
507
+ kind,
508
+ line,
509
+ column: col,
510
+ message,
511
+ messageAnsi: message,
512
+ unexpectedToken: unexpected,
513
+ expectedTokens,
514
+ context,
515
+ syntaxErrorCount: 0,
516
+ semanticErrorCount: 0,
517
+ semantic,
518
+ indentation,
519
+ ...recovery,
520
+ };
521
+ }
522
+
523
+ // -- recovery ---------------------------------------------------------
524
+
525
+ #readRecoverySingular(handle: Handle): Omit<
526
+ Diagnostic,
527
+ | "kind"
528
+ | "line"
529
+ | "column"
530
+ | "message"
531
+ | "messageAnsi"
532
+ | "unexpectedToken"
533
+ | "expectedTokens"
534
+ | "context"
535
+ | "syntaxErrorCount"
536
+ | "semanticErrorCount"
537
+ | "semantic"
538
+ | "indentation"
539
+ > {
540
+ const h = handle;
541
+ const kindVal = this.#port.diagnosticRecoveryKind(h);
542
+ const recoveryKind = kindVal === 0 ? null : kindVal;
543
+
544
+ const terminal = this.#port.diagnosticRecoveryTerminal(h);
545
+ const resume = this.#port.diagnosticRecoveryResume(h);
546
+ const lhs = this.#port.diagnosticRecoveryLhsVariable(h);
547
+ const production = this.#port.diagnosticRecoveryProduction(h);
548
+ const occurrence = this.#port.diagnosticRecoveryOccurrence(h);
549
+
550
+ return {
551
+ recoveryKind,
552
+ recoveryTerminal: terminal,
553
+ recoveryResume: resume,
554
+ recoveryLhsVariable: lhs,
555
+ recoveryProduction: production,
556
+ recoveryOccurrence: occurrence,
557
+ };
558
+ }
559
+
560
+ #readRecoveryRecorded(
561
+ handle: Handle,
562
+ idx: number,
563
+ ): Omit<
564
+ Diagnostic,
565
+ | "kind"
566
+ | "line"
567
+ | "column"
568
+ | "message"
569
+ | "messageAnsi"
570
+ | "unexpectedToken"
571
+ | "expectedTokens"
572
+ | "context"
573
+ | "syntaxErrorCount"
574
+ | "semanticErrorCount"
575
+ | "semantic"
576
+ | "indentation"
577
+ > {
578
+ const h = handle;
579
+ const kindVal = this.#port.recordedRecoveryKind(h, idx);
580
+ const recoveryKind = kindVal === 0 ? null : kindVal;
581
+
582
+ const terminal = this.#port.recordedRecoveryTerminal(h, idx);
583
+ const resume = this.#port.recordedRecoveryResume(h, idx);
584
+ const lhs = this.#port.recordedRecoveryLhsVariable(h, idx);
585
+ const production = this.#port.recordedRecoveryProduction(h, idx);
586
+ const occurrence = this.#port.recordedRecoveryOccurrence(h, idx);
587
+
588
+ return {
589
+ recoveryKind,
590
+ recoveryTerminal: terminal,
591
+ recoveryResume: resume,
592
+ recoveryLhsVariable: lhs,
593
+ recoveryProduction: production,
594
+ recoveryOccurrence: occurrence,
595
+ };
596
+ }
597
+
598
+ // -- tree editing ----------------------------------------------------
599
+
600
+ appendChildren(parent: Node | bigint, chain: Node | bigint): void {
601
+ const h = this.#requireHandle();
602
+ this.#checkStatus(this.#port.treeAppendChildren(h, toNodeAddress(parent), toNodeAddress(chain)));
603
+ }
604
+
605
+ insertBefore(target: Node | bigint, chain: Node | bigint): void {
606
+ const h = this.#requireHandle();
607
+ this.#checkStatus(this.#port.treeInsertBefore(h, toNodeAddress(target), toNodeAddress(chain)));
608
+ }
609
+
610
+ insertAfter(target: Node | bigint, chain: Node | bigint): void {
611
+ const h = this.#requireHandle();
612
+ this.#checkStatus(this.#port.treeInsertAfter(h, toNodeAddress(target), toNodeAddress(chain)));
613
+ }
614
+
615
+ removeSiblings(node: Node | bigint, count: number): Node | null {
616
+ const h = this.#requireHandle();
617
+ const { status, head } = this.#port.treeRemoveSiblings(h, toNodeAddress(node), count);
618
+ this.#checkStatus(status);
619
+ return optNode(this, head);
620
+ }
621
+
622
+ removeSelf(node: Node | bigint): Node | null {
623
+ const h = this.#requireHandle();
624
+ const { status, head } = this.#port.treeRemoveSelf(h, toNodeAddress(node));
625
+ this.#checkStatus(status);
626
+ return optNode(this, head);
627
+ }
628
+
629
+ promoteChildrenOverWrapper(wrapper: Node | bigint): Node | null {
630
+ const h = this.#requireHandle();
631
+ const { status, head } = this.#port.treePromoteChildrenOverWrapper(h, toNodeAddress(wrapper));
632
+ this.#checkStatus(status);
633
+ return optNode(this, head);
634
+ }
635
+
636
+ cleanChildren(node: Node | bigint): Node | null {
637
+ const h = this.#requireHandle();
638
+ const { status, head } = this.#port.treeCleanChildren(h, toNodeAddress(node));
639
+ this.#checkStatus(status);
640
+ return optNode(this, head);
641
+ }
642
+
643
+ unlinkWrapper(wrapper: Node | bigint): void {
644
+ const h = this.#requireHandle();
645
+ this.#checkStatus(this.#port.treeUnlinkWrapper(h, toNodeAddress(wrapper)));
646
+ }
647
+
648
+ insertChildrenAt(parent: Node | bigint, index: number, chain: Node | bigint): void {
649
+ const h = this.#requireHandle();
650
+ this.#checkStatus(
651
+ this.#port.treeInsertChildrenAt(h, toNodeAddress(parent), index, toNodeAddress(chain)),
652
+ );
653
+ }
654
+
655
+ removeChildrenAt(parent: Node | bigint, index: number, count: number): Node | null {
656
+ const h = this.#requireHandle();
657
+ const { status, head } = this.#port.treeRemoveChildrenAt(
658
+ h,
659
+ toNodeAddress(parent),
660
+ index,
661
+ count,
662
+ );
663
+ this.#checkStatus(status);
664
+ return optNode(this, head);
665
+ }
666
+
667
+ // -- symbol table ----------------------------------------------------
668
+
669
+ symbolNameAt(index: number): Uint8Array | null {
670
+ const h = this.#requireHandle();
671
+ return this.#port.symbolNameAt(h, index);
672
+ }
673
+
674
+ symbolIsTerminal(index: number): boolean {
675
+ const h = this.#requireHandle();
676
+ return this.#port.symbolIsTerminal(h, index);
677
+ }
678
+
679
+ variableNameAt(index: number): Uint8Array | null {
680
+ const h = this.#requireHandle();
681
+ return this.#port.variableNameAt(h, index);
682
+ }
683
+ }
684
+
685
+ /** One pre-order step of a {@link Walker}. */
686
+ export interface WalkStep {
687
+ node: Node;
688
+ depth: number;
689
+ isSemanticError: boolean;
690
+ }
691
+
692
+ /**
693
+ * Pre-order tree walker over the last successful parse, yielding one
694
+ * {@link WalkStep} per node. Shares the session's node storage: close the
695
+ * walker (or use `using`) before closing the session or parsing again.
696
+ * Created by {@link Session.walk}.
697
+ */
698
+ export class Walker implements IterableIterator<WalkStep> {
699
+ #session: Session;
700
+ #port: FfiPort;
701
+ #handle: Handle | null;
702
+
703
+ constructor(session: Session, port: FfiPort, handle: Handle) {
704
+ this.#session = session;
705
+ this.#port = port;
706
+ this.#handle = handle;
707
+ }
708
+
709
+ next(): IteratorResult<WalkStep> {
710
+ if (this.#handle === null) return { done: true, value: undefined };
711
+ const step = this.#port.walkerNext(this.#handle);
712
+ if (step === null) return { done: true, value: undefined };
713
+ return {
714
+ done: false,
715
+ value: {
716
+ node: new Node(this.#session, step.node),
717
+ depth: step.depth,
718
+ isSemanticError: step.isSemanticError,
719
+ },
720
+ };
721
+ }
722
+
723
+ [Symbol.iterator](): IterableIterator<WalkStep> {
724
+ return this;
725
+ }
726
+
727
+ /**
728
+ * Prunes the children of the last yielded step; iteration continues with
729
+ * its next sibling. No effect without a last step.
730
+ */
731
+ skipChildren(): void {
732
+ if (this.#handle === null) return;
733
+ this.#port.walkerSkipChildren(this.#handle);
734
+ }
735
+
736
+ close(): void {
737
+ if (this.#handle !== null) {
738
+ this.#port.walkerDestroy(this.#handle);
739
+ this.#handle = null;
740
+ }
741
+ }
742
+
743
+ /** For `using walker = session.walk(...)`. */
744
+ [Symbol.dispose](): void {
745
+ this.close();
746
+ }
747
+ }