@sanbus/galley-node 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.
package/src/ffi.ts ADDED
@@ -0,0 +1,1498 @@
1
+ /**
2
+ * Node adapter for the Galley JavaScript bindings: low-level koffi bindings
3
+ * over `bindings/c/galley.h`, implementing the core `FfiPort`.
4
+ *
5
+ * This is the single FFI boundary for the Node runtime (mirroring
6
+ * `bindings/python/_galley.c` and `bindings/go/assets/wrapper.go.tmpl`).
7
+ * Library discovery, memory copying, and integer normalization live here;
8
+ * all session logic lives in `@sanbus/galley-core`. No caller touches koffi
9
+ * directly outside this module and `dispatch.ts`.
10
+ */
11
+
12
+ import { Buffer } from "node:buffer";
13
+ import * as fs from "node:fs";
14
+ import { createRequire } from "node:module";
15
+ import * as path from "node:path";
16
+ import process from "node:process";
17
+ import type {
18
+ FfiPort,
19
+ Handle,
20
+ SessionCOptions,
21
+ TreeSnapshot,
22
+ WalkedStep,
23
+ } from "@sanbus/galley-core";
24
+ import { GalleyError, resolveArtifact, artifactFileName } from "@sanbus/galley-core";
25
+ const require = createRequire(import.meta.url);
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
27
+ const koffi = require("koffi") as typeof import("koffi");
28
+
29
+ export type LibraryHandle = ReturnType<typeof koffi.load>;
30
+
31
+ // Cached library and path
32
+ let cached: GalleyFFI | null = null;
33
+ let cachedPath: string | null = null;
34
+
35
+ // Global struct definition — koffi keeps a process-wide type registry, so
36
+ // defining the same struct twice with the same name would throw
37
+ // "Duplicate type name". Define once for all loads.
38
+ const GalleyCOptionsType = koffi.struct("GalleyCOptions", {
39
+ max_errors: "int",
40
+ recovery_window: "int",
41
+ stack_overflow_recovery: "int",
42
+ syntax_error_stack_depth: "uint",
43
+ verbosity: "int",
44
+ ast_preallocation_ratio: "double",
45
+ ast_preallocation_cap: "uint64_t",
46
+ });
47
+
48
+ export interface GalleyFFI {
49
+ libPath: string;
50
+ lib: LibraryHandle;
51
+ // version / metadata
52
+ galley_version: () => string;
53
+ galley_parser_type: () => bigint | number;
54
+ galley_error_recovery_mode: () => bigint | number;
55
+ galley_has_ast: () => number;
56
+ galley_has_procedures: () => number;
57
+ galley_allows_no_ast_tree_procedures: () => number;
58
+ galley_source_retention_enabled: () => number;
59
+ galley_has_position_tracking: () => number;
60
+ galley_has_input_streaming: () => number;
61
+ galley_uses_verbatim: () => number;
62
+ galley_stack_overflow_recovery_available: () => number;
63
+ galley_symbol_count: () => bigint | number;
64
+ galley_variable_count: () => bigint | number;
65
+ galley_status_string: (status: bigint | number) => string | null;
66
+
67
+ // symbol table
68
+ galley_symbol_name: (
69
+ session: bigint,
70
+ index: bigint | number,
71
+ outData: unknown[],
72
+ outLen: unknown[],
73
+ ) => bigint | number;
74
+ galley_symbol_is_terminal: (session: bigint, index: bigint | number) => number;
75
+ galley_variable_name: (
76
+ session: bigint,
77
+ index: bigint | number,
78
+ outData: unknown[],
79
+ outLen: unknown[],
80
+ ) => bigint | number;
81
+
82
+ // session
83
+ galley_session_create: () => bigint;
84
+ galley_session_create_ex: (options: unknown) => bigint;
85
+ galley_session_destroy: (session: bigint) => void;
86
+ galley_session_set_message_override: (
87
+ session: bigint,
88
+ name: string,
89
+ nameLen: number | bigint,
90
+ message: string,
91
+ messageLen: number | bigint,
92
+ ) => bigint | number;
93
+
94
+ // parse
95
+ galley_parse_sentinel: (session: bigint, input: string) => bigint | number;
96
+ galley_parse: (session: bigint, data: unknown, len: number | bigint) => bigint | number;
97
+ galley_parse_file: (session: bigint, p: string) => bigint | number;
98
+ galley_last_position: (session: bigint, outLine: unknown[], outCol: unknown[]) => bigint | number;
99
+
100
+ // node / tree
101
+ galley_node_count: (session: bigint) => bigint | number;
102
+ galley_reserve_nodes: (session: bigint, cap: bigint | number) => bigint | number;
103
+ galley_node_capacity: (session: bigint) => bigint | number;
104
+ galley_root_node: (session: bigint) => bigint | number;
105
+ galley_node_is_valid: (session: bigint, node: bigint | number) => number;
106
+ galley_node_child_count: (session: bigint, node: bigint | number) => number;
107
+ galley_node_first_child: (session: bigint, node: bigint | number) => bigint | number;
108
+ galley_node_last_child: (session: bigint, node: bigint | number) => bigint | number;
109
+ galley_node_next_sibling: (session: bigint, node: bigint | number) => bigint | number;
110
+ galley_node_prior_sibling: (session: bigint, node: bigint | number) => bigint | number;
111
+ galley_node_parent: (session: bigint, node: bigint | number) => bigint | number;
112
+ galley_walker_create: (
113
+ session: bigint,
114
+ node: bigint | number,
115
+ skipSemanticErrors: number,
116
+ ) => bigint;
117
+ galley_walker_next: (
118
+ walker: bigint,
119
+ outNode: unknown[],
120
+ outDepth: unknown[],
121
+ outIsSemanticError: unknown[],
122
+ ) => number;
123
+ galley_walker_skip_children: (walker: bigint) => void;
124
+ galley_walker_destroy: (walker: bigint) => void;
125
+ galley_node_symbol_name: (
126
+ session: bigint,
127
+ node: bigint | number,
128
+ outData: unknown[],
129
+ outLen: unknown[],
130
+ ) => bigint | number;
131
+ galley_node_text: (
132
+ session: bigint,
133
+ node: bigint | number,
134
+ outData: unknown[],
135
+ outLen: unknown[],
136
+ ) => bigint | number;
137
+ galley_node_span: (
138
+ session: bigint,
139
+ node: bigint | number,
140
+ outStart: unknown[],
141
+ outLen: unknown[],
142
+ ) => bigint | number;
143
+ galley_node_line_column: (
144
+ session: bigint,
145
+ node: bigint | number,
146
+ outLine: unknown[],
147
+ outCol: unknown[],
148
+ ) => bigint | number;
149
+ galley_node_variable_index: (session: bigint, node: bigint | number) => bigint | number;
150
+ galley_tree_snapshot: (
151
+ session: bigint,
152
+ outParent: BigUint64Array,
153
+ outFirstChild: BigUint64Array,
154
+ outNext: BigUint64Array,
155
+ outChildCount: Uint32Array,
156
+ outVariable: BigInt64Array,
157
+ outSpanStart: BigUint64Array,
158
+ outSpanLen: BigUint64Array,
159
+ capacity: bigint | number,
160
+ ) => bigint | number;
161
+
162
+ // diagnostics (singular)
163
+ galley_has_diagnostic: (session: bigint) => number;
164
+ galley_diagnostic_kind: (session: bigint) => bigint | number;
165
+ galley_diagnostic_message: (session: bigint, out: unknown[]) => bigint | number;
166
+ galley_diagnostic_message_ansi: (session: bigint, out: unknown[]) => bigint | number;
167
+ galley_diagnostic_position: (session: bigint, outLine: unknown[], outCol: unknown[]) => bigint | number;
168
+ galley_diagnostic_unexpected_token: (
169
+ session: bigint,
170
+ outData: unknown[],
171
+ outLen: unknown[],
172
+ ) => bigint | number;
173
+ galley_diagnostic_expected_count: (session: bigint) => bigint | number;
174
+ galley_diagnostic_expected_at: (
175
+ session: bigint,
176
+ index: bigint | number,
177
+ outData: unknown[],
178
+ outLen: unknown[],
179
+ ) => bigint | number;
180
+ galley_diagnostic_context_count: (session: bigint) => bigint | number;
181
+ galley_diagnostic_context_at: (
182
+ session: bigint,
183
+ index: bigint | number,
184
+ outData: unknown[],
185
+ outLen: unknown[],
186
+ ) => bigint | number;
187
+ galley_diagnostic_indentation: (
188
+ session: bigint,
189
+ outSpaces: unknown[],
190
+ outWidth: unknown[],
191
+ ) => bigint | number;
192
+ galley_syntax_error_count: (session: bigint) => bigint | number;
193
+ galley_semantic_error_count: (session: bigint) => bigint | number;
194
+ galley_diagnostic_semantic: (
195
+ session: bigint,
196
+ outVariable: unknown[],
197
+ outVariableLen: unknown[],
198
+ outMessage: unknown[],
199
+ outMessageLen: unknown[],
200
+ ) => bigint | number;
201
+ galley_recorded_semantic: (
202
+ session: bigint,
203
+ diagIndex: bigint | number,
204
+ outVariable: unknown[],
205
+ outVariableLen: unknown[],
206
+ outMessage: unknown[],
207
+ outMessageLen: unknown[],
208
+ ) => bigint | number;
209
+
210
+ // recovery (singular)
211
+ galley_diagnostic_recovery_kind: (session: bigint) => bigint | number;
212
+ galley_diagnostic_recovery_terminal: (
213
+ session: bigint,
214
+ outData: unknown[],
215
+ outLen: unknown[],
216
+ ) => bigint | number;
217
+ galley_diagnostic_recovery_resume: (session: bigint, out: unknown[]) => bigint | number;
218
+ galley_diagnostic_recovery_lhs_variable: (
219
+ session: bigint,
220
+ outData: unknown[],
221
+ outLen: unknown[],
222
+ ) => bigint | number;
223
+ galley_diagnostic_recovery_production: (
224
+ session: bigint,
225
+ outVar: unknown[],
226
+ outLen: unknown[],
227
+ outIndex: unknown[],
228
+ ) => bigint | number;
229
+ galley_diagnostic_recovery_occurrence: (
230
+ session: bigint,
231
+ outParent: unknown[],
232
+ outParentLen: unknown[],
233
+ outRhs: unknown[],
234
+ outSym: unknown[],
235
+ outVar: unknown[],
236
+ outVarLen: unknown[],
237
+ ) => bigint | number;
238
+
239
+ // recorded
240
+ galley_recorded_diagnostic_count: (session: bigint) => bigint | number;
241
+ galley_recorded_diagnostic_kind: (session: bigint, idx: bigint | number) => bigint | number;
242
+ galley_recorded_diagnostic_position: (
243
+ session: bigint,
244
+ idx: bigint | number,
245
+ outLine: unknown[],
246
+ outCol: unknown[],
247
+ ) => bigint | number;
248
+ galley_recorded_unexpected_token: (
249
+ session: bigint,
250
+ idx: bigint | number,
251
+ outData: unknown[],
252
+ outLen: unknown[],
253
+ ) => bigint | number;
254
+ galley_recorded_diagnostic_message: (session: bigint, idx: bigint | number, out: unknown[]) => bigint | number;
255
+ galley_recorded_indentation: (
256
+ session: bigint,
257
+ idx: bigint | number,
258
+ outSpaces: unknown[],
259
+ outWidth: unknown[],
260
+ ) => bigint | number;
261
+ galley_recorded_expected_count: (session: bigint, idx: bigint | number) => bigint | number;
262
+ galley_recorded_expected_token: (
263
+ session: bigint,
264
+ idx: bigint | number,
265
+ tokenIdx: bigint | number,
266
+ outData: unknown[],
267
+ outLen: unknown[],
268
+ ) => bigint | number;
269
+ galley_recorded_context_count: (session: bigint, idx: bigint | number) => bigint | number;
270
+ galley_recorded_context_name: (
271
+ session: bigint,
272
+ idx: bigint | number,
273
+ ctxIdx: bigint | number,
274
+ outData: unknown[],
275
+ outLen: unknown[],
276
+ ) => bigint | number;
277
+ galley_recorded_recovery_kind: (session: bigint, idx: bigint | number) => bigint | number;
278
+ galley_recorded_recovery_terminal: (
279
+ session: bigint,
280
+ idx: bigint | number,
281
+ outData: unknown[],
282
+ outLen: unknown[],
283
+ ) => bigint | number;
284
+ galley_recorded_recovery_resume: (session: bigint, idx: bigint | number, out: unknown[]) => bigint | number;
285
+ galley_recorded_recovery_lhs_variable: (
286
+ session: bigint,
287
+ idx: bigint | number,
288
+ outData: unknown[],
289
+ outLen: unknown[],
290
+ ) => bigint | number;
291
+ galley_recorded_recovery_production: (
292
+ session: bigint,
293
+ idx: bigint | number,
294
+ outVar: unknown[],
295
+ outLen: unknown[],
296
+ outIdx: unknown[],
297
+ ) => bigint | number;
298
+ galley_recorded_recovery_occurrence: (
299
+ session: bigint,
300
+ idx: bigint | number,
301
+ outParent: unknown[],
302
+ outParentLen: unknown[],
303
+ outRhs: unknown[],
304
+ outSym: unknown[],
305
+ outVar: unknown[],
306
+ outVarLen: unknown[],
307
+ ) => bigint | number;
308
+
309
+ // tree editing
310
+ galley_tree_append_children: (session: bigint, parent: bigint | number, first: bigint | number) => bigint | number;
311
+ galley_tree_insert_before: (session: bigint, target: bigint | number, first: bigint | number) => bigint | number;
312
+ galley_tree_insert_after: (session: bigint, target: bigint | number, first: bigint | number) => bigint | number;
313
+ galley_tree_remove_siblings: (
314
+ session: bigint,
315
+ node: bigint | number,
316
+ count: number | bigint,
317
+ outHead: unknown[],
318
+ ) => bigint | number;
319
+ galley_tree_remove_self: (session: bigint, node: bigint | number, outHead: unknown[]) => bigint | number;
320
+ galley_tree_promote_children_over_wrapper: (session: bigint, wrapper: bigint | number, outHead: unknown[]) => bigint | number;
321
+ galley_tree_clean_children: (session: bigint, node: bigint | number, outHead: unknown[]) => bigint | number;
322
+ galley_tree_unlink_wrapper: (session: bigint, wrapper: bigint | number) => bigint | number;
323
+ galley_tree_insert_children_at: (
324
+ session: bigint,
325
+ parent: bigint | number,
326
+ index: number | bigint,
327
+ first: bigint | number,
328
+ ) => bigint | number;
329
+ galley_tree_remove_children_at: (
330
+ session: bigint,
331
+ parent: bigint | number,
332
+ index: number | bigint,
333
+ count: number | bigint,
334
+ outHead: unknown[],
335
+ ) => bigint | number;
336
+
337
+ // procedure dispatch (shared JS shim; see @sanbus/galley-core/build/shim.mjs)
338
+ // ID path (current builds); the name-carrying symbol is the fallback for
339
+ // libraries that predate integer hook IDs.
340
+ galley_install_js_dispatch_id: ((target: unknown) => void) | null;
341
+ galley_install_js_dispatch: ((target: unknown) => void) | null;
342
+ galley_js_procedure_count: (() => number | bigint) | null;
343
+ galley_js_procedure_name_ptr: ((index: number) => bigint) | null;
344
+ galley_js_procedure_name_len: ((index: number) => number | bigint) | null;
345
+ // selective dispatch gates (null on C-procedure or stale libraries)
346
+ galley_js_procedure_enable: ((name: string, nameLen: number | bigint) => number | bigint) | null;
347
+ galley_js_procedure_clear: (() => void) | null;
348
+
349
+ // procedure-hook state; tree queries use galley_node_* on the session
350
+ galley_procedure_session: (args: bigint) => bigint;
351
+ galley_procedure_current_node: (args: bigint) => bigint;
352
+ galley_procedure_set_current_node: (args: bigint, node: bigint | number) => void;
353
+ galley_procedure_drop_self: (args: bigint) => bigint | number;
354
+ galley_procedure_drop_children: (args: bigint) => bigint | number;
355
+ galley_procedure_drop_if_empty: (args: bigint) => bigint | number;
356
+ galley_procedure_replace_with_children: (args: bigint) => bigint | number;
357
+ galley_procedure_context_line: (args: bigint) => number;
358
+ galley_procedure_context_column: (args: bigint) => number;
359
+ galley_procedure_report_semantic_error: (
360
+ args: bigint,
361
+ message: string,
362
+ messageLen: number | bigint,
363
+ ) => bigint | number;
364
+
365
+ // struct for options
366
+ GalleyCOptions: ReturnType<typeof koffi.struct>;
367
+ }
368
+
369
+ // --- library discovery -------------------------------------------------
370
+ // One place, named up front: an explicit path or GALLEY_LIBRARY_PATH.
371
+ // Anything else is a loud error, never a search.
372
+
373
+ const BUILD_HINT =
374
+ `Build it first: npx galley-js-node <language-dir>\n` +
375
+ `or set GALLEY_LIBRARY_PATH=/path/to/${libFileName()}`;
376
+
377
+ export function libFileName(base = "galley-js-node"): string {
378
+ return artifactFileName(base, process.platform);
379
+ }
380
+
381
+ function exists(p: string): boolean {
382
+ try {
383
+ fs.accessSync(p);
384
+ return true;
385
+ } catch {
386
+ return false;
387
+ }
388
+ }
389
+
390
+ export function findLibrary(explicit?: string): string {
391
+ return resolveArtifact(explicit, {
392
+ getEnv: (name) => process.env[name],
393
+ resolvePath: (candidate) => path.resolve(candidate),
394
+ existsSync: exists,
395
+ buildHint: BUILD_HINT,
396
+ });
397
+ }
398
+
399
+ // --- loader ------------------------------------------------------------
400
+
401
+ export function loadLibrary(explicitPath?: string): GalleyFFI {
402
+ const normalizedExplicit = explicitPath ? path.resolve(explicitPath) : undefined;
403
+ if (cached && (!normalizedExplicit || cachedPath === normalizedExplicit)) return cached;
404
+
405
+ const libPath = findLibrary(normalizedExplicit);
406
+
407
+ const lib = koffi.load(libPath);
408
+
409
+ const ffi: GalleyFFI = {
410
+ libPath,
411
+ lib,
412
+ GalleyCOptions: GalleyCOptionsType,
413
+
414
+ galley_version: lib.func("str galley_version()"),
415
+ galley_parser_type: lib.func("int64_t galley_parser_type()"),
416
+ galley_error_recovery_mode: lib.func("int64_t galley_error_recovery_mode()"),
417
+ galley_has_ast: lib.func("int galley_has_ast()"),
418
+ galley_has_procedures: lib.func("int galley_has_procedures()"),
419
+ galley_allows_no_ast_tree_procedures: lib.func("int galley_allows_no_ast_tree_procedures()"),
420
+ galley_source_retention_enabled: lib.func("int galley_source_retention_enabled()"),
421
+ galley_has_position_tracking: lib.func("int galley_has_position_tracking()"),
422
+ galley_has_input_streaming: lib.func("int galley_has_input_streaming()"),
423
+ galley_uses_verbatim: lib.func("int galley_uses_verbatim()"),
424
+ galley_stack_overflow_recovery_available: lib.func(
425
+ "int galley_stack_overflow_recovery_available()",
426
+ ),
427
+ galley_symbol_count: lib.func("uint64_t galley_symbol_count()"),
428
+ galley_variable_count: lib.func("uint64_t galley_variable_count()"),
429
+ galley_status_string: lib.func("str galley_status_string(int64_t status)"),
430
+
431
+ galley_symbol_name: lib.func(
432
+ "int64_t galley_symbol_name(void *session, uint64_t index, _Out_ void **out_data, _Out_ size_t *out_len)",
433
+ ),
434
+ galley_symbol_is_terminal: lib.func("int galley_symbol_is_terminal(void *session, uint64_t index)"),
435
+ galley_variable_name: lib.func(
436
+ "int64_t galley_variable_name(void *session, uint64_t index, _Out_ void **out_data, _Out_ size_t *out_len)",
437
+ ),
438
+
439
+ galley_session_create: lib.func("void *galley_session_create()"),
440
+ galley_session_create_ex: lib.func("void *galley_session_create_ex(GalleyCOptions *options)"),
441
+ galley_session_destroy: lib.func("void galley_session_destroy(void *session)"),
442
+ galley_session_set_message_override: lib.func(
443
+ "int64_t galley_session_set_message_override(void *session, str name, size_t name_len, str message, size_t message_len)",
444
+ ),
445
+
446
+ galley_parse_sentinel: lib.func("int64_t galley_parse_sentinel(void *session, str input)"),
447
+ galley_parse: lib.func("int64_t galley_parse(void *session, const void *data, size_t len)"),
448
+ galley_parse_file: lib.func("int64_t galley_parse_file(void *session, str path)"),
449
+ galley_last_position: lib.func(
450
+ "int64_t galley_last_position(void *session, _Out_ uint32_t *out_line, _Out_ uint32_t *out_column)",
451
+ ),
452
+
453
+ galley_node_count: lib.func("uint64_t galley_node_count(void *session)"),
454
+ galley_reserve_nodes: lib.func("int64_t galley_reserve_nodes(void *session, uint64_t capacity)"),
455
+ galley_node_capacity: lib.func("uint64_t galley_node_capacity(void *session)"),
456
+ galley_root_node: lib.func("uint64_t galley_root_node(void *session)"),
457
+ galley_node_is_valid: lib.func("int galley_node_is_valid(void *session, uint64_t node)"),
458
+ galley_node_child_count: lib.func("uint32_t galley_node_child_count(void *session, uint64_t node)"),
459
+ galley_node_first_child: lib.func("uint64_t galley_node_first_child(void *session, uint64_t node)"),
460
+ galley_node_last_child: lib.func("uint64_t galley_node_last_child(void *session, uint64_t node)"),
461
+ galley_node_next_sibling: lib.func("uint64_t galley_node_next_sibling(void *session, uint64_t node)"),
462
+ galley_node_prior_sibling: lib.func("uint64_t galley_node_prior_sibling(void *session, uint64_t node)"),
463
+ galley_node_parent: lib.func("uint64_t galley_node_parent(void *session, uint64_t node)"),
464
+ galley_walker_create: lib.func("void *galley_walker_create(void *session, uint64_t node, int skip_semantic_errors)"),
465
+ galley_walker_next: lib.func(
466
+ "int galley_walker_next(void *walker, _Out_ uint64_t *out_node, _Out_ uint32_t *out_depth, _Out_ int *out_is_semantic_error)",
467
+ ),
468
+ galley_walker_skip_children: lib.func("void galley_walker_skip_children(void *walker)"),
469
+ galley_walker_destroy: lib.func("void galley_walker_destroy(void *walker)"),
470
+ galley_node_symbol_name: lib.func(
471
+ "int64_t galley_node_symbol_name(void *session, uint64_t node, _Out_ void **out_data, _Out_ size_t *out_len)",
472
+ ),
473
+ galley_node_text: lib.func(
474
+ "int64_t galley_node_text(void *session, uint64_t node, _Out_ void **out_data, _Out_ size_t *out_len)",
475
+ ),
476
+ galley_node_span: lib.func(
477
+ "int64_t galley_node_span(void *session, uint64_t node, _Out_ uint64_t *out_start, _Out_ uint64_t *out_len)",
478
+ ),
479
+ galley_node_line_column: lib.func(
480
+ "int64_t galley_node_line_column(void *session, uint64_t node, _Out_ uint32_t *out_line, _Out_ uint32_t *out_column)",
481
+ ),
482
+ galley_node_variable_index: lib.func("int64_t galley_node_variable_index(void *session, uint64_t node)"),
483
+ galley_tree_snapshot: lib.func(
484
+ "int64_t galley_tree_snapshot(void *session, uint64_t *out_parent, uint64_t *out_first_child, uint64_t *out_next, uint32_t *out_child_count, int64_t *out_variable, uint64_t *out_span_start, uint64_t *out_span_len, uint64_t capacity)",
485
+ ),
486
+
487
+ galley_has_diagnostic: lib.func("int galley_has_diagnostic(void *session)"),
488
+ galley_diagnostic_kind: lib.func("int64_t galley_diagnostic_kind(void *session)"),
489
+ galley_diagnostic_message: lib.func("int64_t galley_diagnostic_message(void *session, _Out_ str *out)"),
490
+ galley_diagnostic_message_ansi: lib.func(
491
+ "int64_t galley_diagnostic_message_ansi(void *session, _Out_ str *out)",
492
+ ),
493
+ galley_diagnostic_position: lib.func(
494
+ "int64_t galley_diagnostic_position(void *session, _Out_ uint32_t *out_line, _Out_ uint32_t *out_column)",
495
+ ),
496
+ galley_diagnostic_unexpected_token: lib.func(
497
+ "int64_t galley_diagnostic_unexpected_token(void *session, _Out_ void **out_data, _Out_ size_t *out_len)",
498
+ ),
499
+ galley_diagnostic_expected_count: lib.func("int64_t galley_diagnostic_expected_count(void *session)"),
500
+ galley_diagnostic_expected_at: lib.func(
501
+ "int64_t galley_diagnostic_expected_at(void *session, uint64_t index, _Out_ void **out_data, _Out_ size_t *out_len)",
502
+ ),
503
+ galley_diagnostic_context_count: lib.func("int64_t galley_diagnostic_context_count(void *session)"),
504
+ galley_diagnostic_context_at: lib.func(
505
+ "int64_t galley_diagnostic_context_at(void *session, uint64_t index, _Out_ void **out_data, _Out_ size_t *out_len)",
506
+ ),
507
+ galley_diagnostic_indentation: lib.func(
508
+ "int64_t galley_diagnostic_indentation(void *session, _Out_ uint32_t *out_spaces, _Out_ uint32_t *out_width)",
509
+ ),
510
+ galley_syntax_error_count: lib.func("int64_t galley_syntax_error_count(void *session)"),
511
+ galley_diagnostic_recovery_kind: lib.func("int64_t galley_diagnostic_recovery_kind(void *session)"),
512
+ galley_diagnostic_recovery_terminal: lib.func(
513
+ "int64_t galley_diagnostic_recovery_terminal(void *session, _Out_ void **out_data, _Out_ size_t *out_len)",
514
+ ),
515
+ galley_diagnostic_recovery_resume: lib.func(
516
+ "int64_t galley_diagnostic_recovery_resume(void *session, _Out_ int64_t *out)",
517
+ ),
518
+ galley_diagnostic_recovery_lhs_variable: lib.func(
519
+ "int64_t galley_diagnostic_recovery_lhs_variable(void *session, _Out_ void **out_data, _Out_ size_t *out_len)",
520
+ ),
521
+ galley_diagnostic_recovery_production: lib.func(
522
+ "int64_t galley_diagnostic_recovery_production(void *session, _Out_ void **out_var, _Out_ size_t *out_var_len, _Out_ uint32_t *out_rhs)",
523
+ ),
524
+ galley_diagnostic_recovery_occurrence: lib.func(
525
+ "int64_t galley_diagnostic_recovery_occurrence(void *session, _Out_ void **out_parent, _Out_ size_t *out_parent_len, _Out_ uint32_t *out_rhs, _Out_ uint32_t *out_sym, _Out_ void **out_var, _Out_ size_t *out_var_len)",
526
+ ),
527
+
528
+ galley_recorded_diagnostic_count: lib.func("int64_t galley_recorded_diagnostic_count(void *session)"),
529
+ galley_recorded_diagnostic_kind: lib.func(
530
+ "int64_t galley_recorded_diagnostic_kind(void *session, uint64_t diag_index)",
531
+ ),
532
+ galley_recorded_diagnostic_position: lib.func(
533
+ "int64_t galley_recorded_diagnostic_position(void *session, uint64_t diag_index, _Out_ uint32_t *out_line, _Out_ uint32_t *out_column)",
534
+ ),
535
+ galley_recorded_unexpected_token: lib.func(
536
+ "int64_t galley_recorded_unexpected_token(void *session, uint64_t diag_index, _Out_ void **out_data, _Out_ size_t *out_len)",
537
+ ),
538
+ galley_recorded_diagnostic_message: lib.func(
539
+ "int64_t galley_recorded_diagnostic_message(void *session, uint64_t diag_index, _Out_ str *out)",
540
+ ),
541
+ galley_recorded_indentation: lib.func(
542
+ "int64_t galley_recorded_indentation(void *session, uint64_t diag_index, _Out_ uint32_t *out_spaces, _Out_ uint32_t *out_width)",
543
+ ),
544
+ galley_recorded_expected_count: lib.func(
545
+ "int64_t galley_recorded_expected_count(void *session, uint64_t diag_index)",
546
+ ),
547
+ galley_recorded_expected_token: lib.func(
548
+ "int64_t galley_recorded_expected_token(void *session, uint64_t diag_index, uint64_t token_index, _Out_ void **out_data, _Out_ size_t *out_len)",
549
+ ),
550
+ galley_recorded_context_count: lib.func(
551
+ "int64_t galley_recorded_context_count(void *session, uint64_t diag_index)",
552
+ ),
553
+ galley_recorded_context_name: lib.func(
554
+ "int64_t galley_recorded_context_name(void *session, uint64_t diag_index, uint64_t ctx_index, _Out_ void **out_data, _Out_ size_t *out_len)",
555
+ ),
556
+ galley_recorded_recovery_kind: lib.func(
557
+ "int64_t galley_recorded_diagnostic_recovery_kind(void *session, uint64_t diag_index)",
558
+ ),
559
+ galley_recorded_recovery_terminal: lib.func(
560
+ "int64_t galley_recorded_recovery_terminal(void *session, uint64_t diag_index, _Out_ void **out_data, _Out_ size_t *out_len)",
561
+ ),
562
+ galley_recorded_recovery_resume: lib.func(
563
+ "int64_t galley_recorded_recovery_resume(void *session, uint64_t diag_index, _Out_ int64_t *out)",
564
+ ),
565
+ galley_recorded_recovery_lhs_variable: lib.func(
566
+ "int64_t galley_recorded_recovery_lhs_variable(void *session, uint64_t diag_index, _Out_ void **out_data, _Out_ size_t *out_len)",
567
+ ),
568
+ galley_recorded_recovery_production: lib.func(
569
+ "int64_t galley_recorded_recovery_production(void *session, uint64_t diag_index, _Out_ void **out_var, _Out_ size_t *out_var_len, _Out_ uint32_t *out_rhs)",
570
+ ),
571
+ galley_recorded_recovery_occurrence: lib.func(
572
+ "int64_t galley_recorded_recovery_occurrence(void *session, uint64_t diag_index, _Out_ void **out_parent, _Out_ size_t *out_parent_len, _Out_ uint32_t *out_rhs, _Out_ uint32_t *out_sym, _Out_ void **out_var, _Out_ size_t *out_var_len)",
573
+ ),
574
+
575
+ galley_tree_append_children: lib.func(
576
+ "int64_t galley_tree_append_children(void *session, uint64_t parent, uint64_t first)",
577
+ ),
578
+ galley_tree_insert_before: lib.func(
579
+ "int64_t galley_tree_insert_before(void *session, uint64_t target, uint64_t first)",
580
+ ),
581
+ galley_tree_insert_after: lib.func(
582
+ "int64_t galley_tree_insert_after(void *session, uint64_t target, uint64_t first)",
583
+ ),
584
+ galley_tree_remove_siblings: lib.func(
585
+ "int64_t galley_tree_remove_siblings(void *session, uint64_t node, size_t count, _Out_ uint64_t *out_head)",
586
+ ),
587
+ galley_tree_remove_self: lib.func(
588
+ "int64_t galley_tree_remove_self(void *session, uint64_t node, _Out_ uint64_t *out_head)",
589
+ ),
590
+ galley_tree_promote_children_over_wrapper: lib.func(
591
+ "int64_t galley_tree_promote_children_over_wrapper(void *session, uint64_t wrapper, _Out_ uint64_t *out_head)",
592
+ ),
593
+ galley_tree_clean_children: lib.func(
594
+ "int64_t galley_tree_clean_children(void *session, uint64_t node, _Out_ uint64_t *out_head)",
595
+ ),
596
+ galley_tree_unlink_wrapper: lib.func("int64_t galley_tree_unlink_wrapper(void *session, uint64_t wrapper)"),
597
+ galley_tree_insert_children_at: lib.func(
598
+ "int64_t galley_tree_insert_children_at(void *session, uint64_t parent, size_t index, uint64_t first)",
599
+ ),
600
+ galley_tree_remove_children_at: lib.func(
601
+ "int64_t galley_tree_remove_children_at(void *session, uint64_t parent, size_t index, size_t count, _Out_ uint64_t *out_head)",
602
+ ),
603
+
604
+ galley_install_js_dispatch_id: (() => {
605
+ try {
606
+ return lib.func("void galley_install_js_dispatch_id(void *target)") as unknown as (
607
+ target: unknown,
608
+ ) => void;
609
+ } catch {
610
+ return null;
611
+ }
612
+ })(),
613
+
614
+ galley_install_js_dispatch: (() => {
615
+ try {
616
+ return lib.func("void galley_install_js_dispatch(void *target)") as unknown as (
617
+ target: unknown,
618
+ ) => void;
619
+ } catch {
620
+ return null;
621
+ }
622
+ })(),
623
+
624
+ galley_js_procedure_count: (() => {
625
+ try {
626
+ return lib.func("uint32_t galley_js_procedure_count()") as unknown as () => number | bigint;
627
+ } catch {
628
+ return null;
629
+ }
630
+ })(),
631
+
632
+ galley_js_procedure_name_ptr: (() => {
633
+ try {
634
+ return lib.func("void *galley_js_procedure_name_ptr(uint32_t index)") as unknown as (
635
+ index: number,
636
+ ) => bigint;
637
+ } catch {
638
+ return null;
639
+ }
640
+ })(),
641
+
642
+ galley_js_procedure_name_len: (() => {
643
+ try {
644
+ return lib.func("size_t galley_js_procedure_name_len(uint32_t index)") as unknown as (
645
+ index: number,
646
+ ) => number | bigint;
647
+ } catch {
648
+ return null;
649
+ }
650
+ })(),
651
+
652
+ galley_js_procedure_enable: (() => {
653
+ try {
654
+ return lib.func("int galley_js_procedure_enable(str name, size_t name_len)") as unknown as (
655
+ name: string,
656
+ nameLen: number | bigint,
657
+ ) => number | bigint;
658
+ } catch {
659
+ return null;
660
+ }
661
+ })(),
662
+
663
+ galley_js_procedure_clear: (() => {
664
+ try {
665
+ return lib.func("void galley_js_procedure_clear()") as unknown as () => void;
666
+ } catch {
667
+ return null;
668
+ }
669
+ })(),
670
+
671
+ galley_procedure_session: lib.func("void *galley_procedure_session(void *args)"),
672
+ galley_procedure_current_node: lib.func("uint64_t galley_procedure_current_node(void *args)"),
673
+ galley_procedure_set_current_node: lib.func(
674
+ "void galley_procedure_set_current_node(void *args, uint64_t node)",
675
+ ),
676
+ galley_procedure_drop_self: lib.func("int64_t galley_procedure_drop_self(void *args)"),
677
+ galley_procedure_drop_children: lib.func("int64_t galley_procedure_drop_children(void *args)"),
678
+ galley_procedure_drop_if_empty: lib.func("int64_t galley_procedure_drop_if_empty(void *args)"),
679
+ galley_procedure_replace_with_children: lib.func(
680
+ "int64_t galley_procedure_replace_with_children(void *args)",
681
+ ),
682
+ galley_procedure_context_line: lib.func("uint32_t galley_procedure_context_line(void *args)"),
683
+ galley_procedure_context_column: lib.func(
684
+ "uint32_t galley_procedure_context_column(void *args)",
685
+ ),
686
+ galley_procedure_report_semantic_error: lib.func(
687
+ "int64_t galley_procedure_report_semantic_error(void *args, str message, size_t message_len)",
688
+ ),
689
+ galley_diagnostic_semantic: lib.func(
690
+ "int64_t galley_diagnostic_semantic(void *session, _Out_ void **out_variable, _Out_ size_t *out_variable_len, _Out_ void **out_message, _Out_ size_t *out_message_len)",
691
+ ),
692
+ galley_recorded_semantic: lib.func(
693
+ "int64_t galley_recorded_semantic(void *session, uint64_t diag_index, _Out_ void **out_variable, _Out_ size_t *out_variable_len, _Out_ void **out_message, _Out_ size_t *out_message_len)",
694
+ ),
695
+ galley_semantic_error_count: lib.func("int64_t galley_semantic_error_count(void *session)"),
696
+ };
697
+
698
+ cached = ffi;
699
+ cachedPath = libPath;
700
+ return ffi;
701
+ }
702
+
703
+ // Helpers -----------------------------------------------------------------
704
+
705
+ export function toBigInt(v: bigint | number): bigint {
706
+ return typeof v === "bigint" ? v : BigInt(v);
707
+ }
708
+
709
+ export function isOk(status: bigint | number): boolean {
710
+ const n = typeof status === "bigint" ? status : BigInt(status);
711
+ return n >= 0n;
712
+ }
713
+
714
+ export function toNumber(v: bigint | number): number {
715
+ return typeof v === "bigint" ? Number(v) : v;
716
+ }
717
+
718
+ /** Copy (ptr,len) into Uint8Array owning its bytes. */
719
+ export function copyBytes(ptr: bigint, len: bigint | number): Uint8Array {
720
+ const l = typeof len === "bigint" ? Number(len) : len;
721
+ if (ptr === 0n || l === 0) return new Uint8Array(0);
722
+ // koffi.decode with array copies
723
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
724
+ const arr = koffi.decode(ptr as any, koffi.array("uint8_t", l)) as Uint8Array;
725
+ // copy to detach from underlying memory that koffi may reuse
726
+ return new Uint8Array(arr);
727
+ }
728
+
729
+ export function copyStringBytes(ptr: bigint, len: bigint | number): string {
730
+ return Buffer.from(copyBytes(ptr, len)).toString("utf-8");
731
+ }
732
+
733
+ /** Decode core-side bytes for koffi `str` parameters (lossless UTF-8 round trip). */
734
+ function decodeParam(bytes: Uint8Array): string {
735
+ return Buffer.from(bytes).toString("utf-8");
736
+ }
737
+
738
+ // --- FfiPort implementation ----------------------------------------------
739
+
740
+ type StatusFn = (outData: unknown[], outLen: unknown[]) => bigint | number;
741
+
742
+ function isNegative(st: bigint | number): boolean {
743
+ return typeof st === "bigint" ? st < 0n : (st as number) < 0;
744
+ }
745
+
746
+ /**
747
+ * Node's {@link FfiPort}: normalizes koffi's `bigint | number` unions and
748
+ * `_Out_` arrays into the structured values the core expects. All memory
749
+ * copying happens here; the core only sees owned bytes.
750
+ */
751
+ export class NodePort implements FfiPort {
752
+ readonly ffi: GalleyFFI;
753
+ readonly libraryPath: string;
754
+
755
+ constructor(ffi: GalleyFFI) {
756
+ this.ffi = ffi;
757
+ this.libraryPath = ffi.libPath;
758
+ }
759
+
760
+ // -- module-level queries --------------------------------------------
761
+
762
+ version(): string {
763
+ return this.ffi.galley_version();
764
+ }
765
+
766
+ parserType(): number {
767
+ return toNumber(this.ffi.galley_parser_type());
768
+ }
769
+
770
+ errorRecoveryMode(): number {
771
+ return toNumber(this.ffi.galley_error_recovery_mode());
772
+ }
773
+
774
+ hasAst(): boolean {
775
+ return this.ffi.galley_has_ast() !== 0;
776
+ }
777
+
778
+ hasProcedures(): boolean {
779
+ return this.ffi.galley_has_procedures() !== 0;
780
+ }
781
+
782
+ allowsNoAstTreeProcedures(): boolean {
783
+ return this.ffi.galley_allows_no_ast_tree_procedures() !== 0;
784
+ }
785
+
786
+ sourceRetentionEnabled(): boolean {
787
+ return this.ffi.galley_source_retention_enabled() !== 0;
788
+ }
789
+
790
+ hasPositionTracking(): boolean {
791
+ return this.ffi.galley_has_position_tracking() !== 0;
792
+ }
793
+
794
+ hasInputStreaming(): boolean {
795
+ return this.ffi.galley_has_input_streaming() !== 0;
796
+ }
797
+
798
+ usesVerbatim(): boolean {
799
+ return this.ffi.galley_uses_verbatim() !== 0;
800
+ }
801
+
802
+ stackOverflowRecoveryAvailable(): boolean {
803
+ return this.ffi.galley_stack_overflow_recovery_available() !== 0;
804
+ }
805
+
806
+ symbolCount(): number {
807
+ return toNumber(this.ffi.galley_symbol_count());
808
+ }
809
+
810
+ variableCount(): number {
811
+ return toNumber(this.ffi.galley_variable_count());
812
+ }
813
+
814
+ statusString(status: number): string | null {
815
+ return this.ffi.galley_status_string(status);
816
+ }
817
+
818
+ // -- sessions ---------------------------------------------------------
819
+
820
+ createSession(options: SessionCOptions | null): Handle {
821
+ let handle: bigint;
822
+ if (options === null) {
823
+ handle = this.ffi.galley_session_create() as bigint;
824
+ } else {
825
+ const cOptions: Record<string, unknown> = {
826
+ max_errors: options.maxErrors,
827
+ recovery_window: options.recoveryWindow,
828
+ stack_overflow_recovery: options.stackOverflowRecovery,
829
+ syntax_error_stack_depth: options.syntaxErrorStackDepth,
830
+ verbosity: options.verbosity,
831
+ ast_preallocation_ratio: options.astPreallocationRatio,
832
+ ast_preallocation_cap: options.astPreallocationCap,
833
+ };
834
+ // @ts-ignore koffi expects object for struct pointer
835
+ handle = this.ffi.galley_session_create_ex(cOptions as never) as bigint;
836
+ }
837
+ if (handle === 0n || handle === null || handle === undefined) return null;
838
+ return handle;
839
+ }
840
+
841
+ destroySession(handle: Handle): void {
842
+ this.ffi.galley_session_destroy(handle as bigint);
843
+ }
844
+
845
+ setMessageOverride(handle: Handle, name: Uint8Array, message: Uint8Array): number {
846
+ return toNumber(
847
+ this.ffi.galley_session_set_message_override(
848
+ handle as bigint,
849
+ decodeParam(name),
850
+ name.length,
851
+ decodeParam(message),
852
+ message.length,
853
+ ),
854
+ );
855
+ }
856
+
857
+ // -- parsing ----------------------------------------------------------
858
+
859
+ parse(handle: Handle, data: Uint8Array): number {
860
+ return toNumber(this.ffi.galley_parse(handle as bigint, data, data.length));
861
+ }
862
+
863
+ parseFile(handle: Handle, filePath: string): number {
864
+ return toNumber(this.ffi.galley_parse_file(handle as bigint, filePath));
865
+ }
866
+
867
+ lastPosition(handle: Handle): [number, number] | null {
868
+ const outLine: unknown[] = [0];
869
+ const outCol: unknown[] = [0];
870
+ const st = this.ffi.galley_last_position(handle as bigint, outLine, outCol);
871
+ if (isNegative(st)) return null;
872
+ return [outLine[0] as number, outCol[0] as number];
873
+ }
874
+
875
+ // -- arena and navigation ----------------------------------------------
876
+
877
+ nodeCount(handle: Handle): number {
878
+ return toNumber(this.ffi.galley_node_count(handle as bigint));
879
+ }
880
+
881
+ reserveNodes(handle: Handle, capacity: bigint): number {
882
+ return toNumber(this.ffi.galley_reserve_nodes(handle as bigint, capacity));
883
+ }
884
+
885
+ nodeCapacity(handle: Handle): number {
886
+ return toNumber(this.ffi.galley_node_capacity(handle as bigint));
887
+ }
888
+
889
+ rootNode(handle: Handle): bigint {
890
+ return toBigInt(this.ffi.galley_root_node(handle as bigint));
891
+ }
892
+
893
+ nodeValid(handle: Handle, node: bigint): boolean {
894
+ return this.ffi.galley_node_is_valid(handle as bigint, node) !== 0;
895
+ }
896
+
897
+ childCount(handle: Handle, node: bigint): number {
898
+ return this.ffi.galley_node_child_count(handle as bigint, node);
899
+ }
900
+
901
+ firstChild(handle: Handle, node: bigint): bigint {
902
+ return toBigInt(this.ffi.galley_node_first_child(handle as bigint, node));
903
+ }
904
+
905
+ lastChild(handle: Handle, node: bigint): bigint {
906
+ return toBigInt(this.ffi.galley_node_last_child(handle as bigint, node));
907
+ }
908
+
909
+ nextSibling(handle: Handle, node: bigint): bigint {
910
+ return toBigInt(this.ffi.galley_node_next_sibling(handle as bigint, node));
911
+ }
912
+
913
+ priorSibling(handle: Handle, node: bigint): bigint {
914
+ return toBigInt(this.ffi.galley_node_prior_sibling(handle as bigint, node));
915
+ }
916
+
917
+ parent(handle: Handle, node: bigint): bigint {
918
+ return toBigInt(this.ffi.galley_node_parent(handle as bigint, node));
919
+ }
920
+
921
+ treeSnapshot(handle: Handle): TreeSnapshot {
922
+ // No await between sizing and filling, so the count cannot change.
923
+ // Retry once on mismatch for safety against future async hooks.
924
+ for (let attempt = 0; attempt < 2; attempt++) {
925
+ const count = this.nodeCount(handle);
926
+ const parent = new BigUint64Array(count);
927
+ const firstChild = new BigUint64Array(count);
928
+ const next = new BigUint64Array(count);
929
+ const childCount = new Uint32Array(count);
930
+ const variable = new BigInt64Array(count);
931
+ const spanStart = new BigUint64Array(count);
932
+ const spanLen = new BigUint64Array(count);
933
+ const total = toNumber(
934
+ this.ffi.galley_tree_snapshot(
935
+ handle as bigint, parent, firstChild, next, childCount,
936
+ variable, spanStart, spanLen, BigInt(count),
937
+ ),
938
+ );
939
+ if (isNegative(total)) throw new GalleyError("galley_tree_snapshot failed", total);
940
+ if (total === count) {
941
+ return { count, parent, firstChild, next, childCount, variable, spanStart, spanLen };
942
+ }
943
+ }
944
+ throw new GalleyError("node count changed during galley_tree_snapshot", -8);
945
+ }
946
+
947
+ // -- walker ------------------------------------------------------------
948
+
949
+ walkerCreate(handle: Handle, node: bigint, skipSemanticErrors: boolean): Handle | null {
950
+ const walker = this.ffi.galley_walker_create(handle as bigint, node, skipSemanticErrors ? 1 : 0);
951
+ if (walker === 0n || walker === null || walker === undefined) return null;
952
+ return walker as bigint;
953
+ }
954
+
955
+ walkerNext(walker: Handle): WalkedStep | null {
956
+ const outNode: unknown[] = [0n];
957
+ const outDepth: unknown[] = [0];
958
+ const outFlag: unknown[] = [0];
959
+ const yielded = this.ffi.galley_walker_next(walker as bigint, outNode, outDepth, outFlag);
960
+ if (yielded === 0) return null;
961
+ return {
962
+ node: toBigInt(outNode[0] as bigint),
963
+ depth: Number(outDepth[0]),
964
+ isSemanticError: (outFlag[0] as number) !== 0,
965
+ };
966
+ }
967
+
968
+ walkerSkipChildren(walker: Handle): void {
969
+ this.ffi.galley_walker_skip_children(walker as bigint);
970
+ }
971
+
972
+ walkerDestroy(walker: Handle): void {
973
+ this.ffi.galley_walker_destroy(walker as bigint);
974
+ }
975
+
976
+ // -- node accessors -----------------------------------------------------
977
+
978
+ nodeSymbolName(handle: Handle, node: bigint): Uint8Array | null {
979
+ const outData: unknown[] = [null];
980
+ const outLen: unknown[] = [0];
981
+ const st = this.ffi.galley_node_symbol_name(handle as bigint, node, outData, outLen);
982
+ if (isNegative(st)) return null;
983
+ return copyBytes(outData[0] as bigint, outLen[0] as bigint | number);
984
+ }
985
+
986
+ nodeText(handle: Handle, node: bigint): Uint8Array | null {
987
+ const outData: unknown[] = [null];
988
+ const outLen: unknown[] = [0];
989
+ const st = this.ffi.galley_node_text(handle as bigint, node, outData, outLen);
990
+ if (isNegative(st)) return null;
991
+ return copyBytes(outData[0] as bigint, outLen[0] as bigint | number);
992
+ }
993
+
994
+ nodeSpan(handle: Handle, node: bigint): [bigint, bigint] | null {
995
+ const outStart: unknown[] = [0n];
996
+ const outLen: unknown[] = [0n];
997
+ const st = this.ffi.galley_node_span(handle as bigint, node, outStart, outLen);
998
+ if (isNegative(st)) return null;
999
+ return [toBigInt(outStart[0] as bigint), toBigInt(outLen[0] as bigint)];
1000
+ }
1001
+
1002
+ nodeLineColumn(handle: Handle, node: bigint): [number, number] | null {
1003
+ const outLine: unknown[] = [0];
1004
+ const outCol: unknown[] = [0];
1005
+ const st = this.ffi.galley_node_line_column(handle as bigint, node, outLine, outCol);
1006
+ if (isNegative(st)) return null;
1007
+ return [outLine[0] as number, outCol[0] as number];
1008
+ }
1009
+
1010
+ nodeVariableIndex(handle: Handle, node: bigint): number {
1011
+ return toNumber(this.ffi.galley_node_variable_index(handle as bigint, node));
1012
+ }
1013
+
1014
+ symbolNameAt(handle: Handle, index: number): Uint8Array | null {
1015
+ const outData: unknown[] = [null];
1016
+ const outLen: unknown[] = [0];
1017
+ const st = this.ffi.galley_symbol_name(handle as bigint, BigInt(index), outData, outLen);
1018
+ if (isNegative(st)) return null;
1019
+ return copyBytes(outData[0] as bigint, outLen[0] as bigint);
1020
+ }
1021
+
1022
+ symbolIsTerminal(handle: Handle, index: number): boolean {
1023
+ return this.ffi.galley_symbol_is_terminal(handle as bigint, BigInt(index)) !== 0;
1024
+ }
1025
+
1026
+ variableNameAt(handle: Handle, index: number): Uint8Array | null {
1027
+ const outData: unknown[] = [null];
1028
+ const outLen: unknown[] = [0];
1029
+ const st = this.ffi.galley_variable_name(handle as bigint, BigInt(index), outData, outLen);
1030
+ if (isNegative(st)) return null;
1031
+ return copyBytes(outData[0] as bigint, outLen[0] as bigint);
1032
+ }
1033
+
1034
+ // -- diagnostics ---------------------------------------------------------
1035
+
1036
+ #tryCopyBytes(fn: StatusFn): Uint8Array | null {
1037
+ const outData: unknown[] = [null];
1038
+ const outLen: unknown[] = [0];
1039
+ const st = fn(outData, outLen);
1040
+ if (isNegative(st)) return null;
1041
+ const ptr = outData[0] as bigint | null;
1042
+ if (ptr === null || ptr === 0n) return null;
1043
+ return copyBytes(ptr as bigint, outLen[0] as bigint);
1044
+ }
1045
+
1046
+ #readSemanticPair(
1047
+ fn: (
1048
+ outVariable: unknown[],
1049
+ outVariableLen: unknown[],
1050
+ outMessage: unknown[],
1051
+ outMessageLen: unknown[],
1052
+ ) => bigint | number,
1053
+ ): [string, string] | null {
1054
+ const outVariable: unknown[] = [null];
1055
+ const outVariableLen: unknown[] = [0];
1056
+ const outMessage: unknown[] = [null];
1057
+ const outMessageLen: unknown[] = [0];
1058
+ const st = fn(outVariable, outVariableLen, outMessage, outMessageLen);
1059
+ if (isNegative(st)) return null;
1060
+ if (outVariable[0] === null || outMessage[0] === null) return null;
1061
+ return [
1062
+ copyStringBytes(outVariable[0] as bigint, outVariableLen[0] as bigint),
1063
+ copyStringBytes(outMessage[0] as bigint, outMessageLen[0] as bigint),
1064
+ ];
1065
+ }
1066
+
1067
+ hasDiagnostic(handle: Handle): boolean {
1068
+ return this.ffi.galley_has_diagnostic(handle as bigint) !== 0;
1069
+ }
1070
+
1071
+ diagnosticKind(handle: Handle): number {
1072
+ return toNumber(this.ffi.galley_diagnostic_kind(handle as bigint));
1073
+ }
1074
+
1075
+ diagnosticMessage(handle: Handle): string | null {
1076
+ const out: unknown[] = [null];
1077
+ if (toNumber(this.ffi.galley_diagnostic_message(handle as bigint, out)) !== 0) return null;
1078
+ return out[0] as string;
1079
+ }
1080
+
1081
+ diagnosticMessageAnsi(handle: Handle): string | null {
1082
+ const out: unknown[] = [null];
1083
+ if (toNumber(this.ffi.galley_diagnostic_message_ansi(handle as bigint, out)) !== 0) return null;
1084
+ return out[0] as string;
1085
+ }
1086
+
1087
+ diagnosticPosition(handle: Handle): [number, number] | null {
1088
+ const outLine: unknown[] = [0];
1089
+ const outCol: unknown[] = [0];
1090
+ if (isNegative(this.ffi.galley_diagnostic_position(handle as bigint, outLine, outCol)))
1091
+ return null;
1092
+ return [outLine[0] as number, outCol[0] as number];
1093
+ }
1094
+
1095
+ diagnosticUnexpectedToken(handle: Handle): Uint8Array | null {
1096
+ const h = handle as bigint;
1097
+ return this.#tryCopyBytes((od, ol) =>
1098
+ this.ffi.galley_diagnostic_unexpected_token(h, od, ol),
1099
+ );
1100
+ }
1101
+
1102
+ diagnosticExpectedCount(handle: Handle): number {
1103
+ return toNumber(this.ffi.galley_diagnostic_expected_count(handle as bigint));
1104
+ }
1105
+
1106
+ diagnosticExpectedAt(handle: Handle, index: number): Uint8Array | null {
1107
+ const h = handle as bigint;
1108
+ return this.#tryCopyBytes((od, ol) =>
1109
+ this.ffi.galley_diagnostic_expected_at(h, index, od, ol),
1110
+ );
1111
+ }
1112
+
1113
+ diagnosticContextCount(handle: Handle): number {
1114
+ return toNumber(this.ffi.galley_diagnostic_context_count(handle as bigint));
1115
+ }
1116
+
1117
+ diagnosticContextAt(handle: Handle, index: number): Uint8Array | null {
1118
+ const h = handle as bigint;
1119
+ return this.#tryCopyBytes((od, ol) =>
1120
+ this.ffi.galley_diagnostic_context_at(h, index, od, ol),
1121
+ );
1122
+ }
1123
+
1124
+ syntaxErrorCount(handle: Handle): number {
1125
+ return toNumber(this.ffi.galley_syntax_error_count(handle as bigint));
1126
+ }
1127
+
1128
+ semanticErrorCount(handle: Handle): number {
1129
+ return toNumber(this.ffi.galley_semantic_error_count(handle as bigint));
1130
+ }
1131
+
1132
+ diagnosticSemantic(handle: Handle): [string, string] | null {
1133
+ const h = handle as bigint;
1134
+ return this.#readSemanticPair((ov, ovl, om, oml) =>
1135
+ this.ffi.galley_diagnostic_semantic(h, ov, ovl, om, oml),
1136
+ );
1137
+ }
1138
+
1139
+ diagnosticIndentation(handle: Handle): [number, number] | null {
1140
+ const outSpaces: unknown[] = [0];
1141
+ const outWidth: unknown[] = [0];
1142
+ if (toNumber(this.ffi.galley_diagnostic_indentation(handle as bigint, outSpaces, outWidth)) !== 0)
1143
+ return null;
1144
+ return [outSpaces[0] as number, outWidth[0] as number];
1145
+ }
1146
+
1147
+ diagnosticRecoveryKind(handle: Handle): number {
1148
+ return toNumber(this.ffi.galley_diagnostic_recovery_kind(handle as bigint));
1149
+ }
1150
+
1151
+ diagnosticRecoveryTerminal(handle: Handle): Uint8Array | null {
1152
+ const h = handle as bigint;
1153
+ return this.#tryCopyBytes((od, ol) =>
1154
+ this.ffi.galley_diagnostic_recovery_terminal(h, od, ol),
1155
+ );
1156
+ }
1157
+
1158
+ diagnosticRecoveryResume(handle: Handle): number | null {
1159
+ const out: unknown[] = [0n];
1160
+ if (toNumber(this.ffi.galley_diagnostic_recovery_resume(handle as bigint, out)) !== 0)
1161
+ return null;
1162
+ return toNumber(out[0] as bigint);
1163
+ }
1164
+
1165
+ diagnosticRecoveryLhsVariable(handle: Handle): string | null {
1166
+ const h = handle as bigint;
1167
+ const outData: unknown[] = [null];
1168
+ const outLen: unknown[] = [0];
1169
+ const st = this.ffi.galley_diagnostic_recovery_lhs_variable(h, outData, outLen);
1170
+ if (isNegative(st) || outData[0] === null) return null;
1171
+ return copyStringBytes(outData[0] as bigint, outLen[0] as bigint);
1172
+ }
1173
+
1174
+ diagnosticRecoveryProduction(handle: Handle): [string, number] | null {
1175
+ const outVar: unknown[] = [null];
1176
+ const outLen: unknown[] = [0];
1177
+ const outIdx: unknown[] = [0];
1178
+ if (
1179
+ toNumber(this.ffi.galley_diagnostic_recovery_production(handle as bigint, outVar, outLen, outIdx)) !==
1180
+ 0
1181
+ )
1182
+ return null;
1183
+ return [copyStringBytes(outVar[0] as bigint, outLen[0] as bigint), outIdx[0] as number];
1184
+ }
1185
+
1186
+ diagnosticRecoveryOccurrence(handle: Handle): [string, number, number, string] | null {
1187
+ const outParent: unknown[] = [null];
1188
+ const outParentLen: unknown[] = [0];
1189
+ const outRhs: unknown[] = [0];
1190
+ const outSym: unknown[] = [0];
1191
+ const outVar: unknown[] = [null];
1192
+ const outVarLen: unknown[] = [0];
1193
+ if (
1194
+ toNumber(
1195
+ this.ffi.galley_diagnostic_recovery_occurrence(
1196
+ handle as bigint,
1197
+ outParent,
1198
+ outParentLen,
1199
+ outRhs,
1200
+ outSym,
1201
+ outVar,
1202
+ outVarLen,
1203
+ ),
1204
+ ) !== 0
1205
+ )
1206
+ return null;
1207
+ return [
1208
+ copyStringBytes(outParent[0] as bigint, outParentLen[0] as bigint),
1209
+ outRhs[0] as number,
1210
+ outSym[0] as number,
1211
+ copyStringBytes(outVar[0] as bigint, outVarLen[0] as bigint),
1212
+ ];
1213
+ }
1214
+
1215
+ recordedDiagnosticCount(handle: Handle): number {
1216
+ return toNumber(this.ffi.galley_recorded_diagnostic_count(handle as bigint));
1217
+ }
1218
+
1219
+ recordedDiagnosticKind(handle: Handle, diagIndex: number): number {
1220
+ return toNumber(this.ffi.galley_recorded_diagnostic_kind(handle as bigint, diagIndex));
1221
+ }
1222
+
1223
+ recordedDiagnosticPosition(handle: Handle, diagIndex: number): [number, number] | null {
1224
+ const outLine: unknown[] = [0];
1225
+ const outCol: unknown[] = [0];
1226
+ if (isNegative(this.ffi.galley_recorded_diagnostic_position(handle as bigint, diagIndex, outLine, outCol)))
1227
+ return null;
1228
+ return [outLine[0] as number, outCol[0] as number];
1229
+ }
1230
+
1231
+ recordedUnexpectedToken(handle: Handle, diagIndex: number): Uint8Array | null {
1232
+ const h = handle as bigint;
1233
+ return this.#tryCopyBytes((od, ol) =>
1234
+ this.ffi.galley_recorded_unexpected_token(h, diagIndex, od, ol),
1235
+ );
1236
+ }
1237
+
1238
+ recordedDiagnosticMessage(handle: Handle, diagIndex: number): string | null {
1239
+ const out: unknown[] = [null];
1240
+ if (
1241
+ toNumber(this.ffi.galley_recorded_diagnostic_message(handle as bigint, diagIndex, out)) !== 0
1242
+ )
1243
+ return null;
1244
+ return out[0] as string;
1245
+ }
1246
+
1247
+ recordedIndentation(handle: Handle, diagIndex: number): [number, number] | null {
1248
+ const outSpaces: unknown[] = [0];
1249
+ const outWidth: unknown[] = [0];
1250
+ if (
1251
+ toNumber(this.ffi.galley_recorded_indentation(handle as bigint, diagIndex, outSpaces, outWidth)) !==
1252
+ 0
1253
+ )
1254
+ return null;
1255
+ return [outSpaces[0] as number, outWidth[0] as number];
1256
+ }
1257
+
1258
+ recordedSemantic(handle: Handle, diagIndex: number): [string, string] | null {
1259
+ const h = handle as bigint;
1260
+ return this.#readSemanticPair((ov, ovl, om, oml) =>
1261
+ this.ffi.galley_recorded_semantic(h, diagIndex, ov, ovl, om, oml),
1262
+ );
1263
+ }
1264
+
1265
+ recordedExpectedCount(handle: Handle, diagIndex: number): number {
1266
+ return toNumber(this.ffi.galley_recorded_expected_count(handle as bigint, diagIndex));
1267
+ }
1268
+
1269
+ recordedExpectedToken(handle: Handle, diagIndex: number, tokenIndex: number): Uint8Array | null {
1270
+ const h = handle as bigint;
1271
+ return this.#tryCopyBytes((od, ol) =>
1272
+ this.ffi.galley_recorded_expected_token(h, diagIndex, tokenIndex, od, ol),
1273
+ );
1274
+ }
1275
+
1276
+ recordedContextCount(handle: Handle, diagIndex: number): number {
1277
+ return toNumber(this.ffi.galley_recorded_context_count(handle as bigint, diagIndex));
1278
+ }
1279
+
1280
+ recordedContextName(handle: Handle, diagIndex: number, contextIndex: number): Uint8Array | null {
1281
+ const h = handle as bigint;
1282
+ return this.#tryCopyBytes((od, ol) =>
1283
+ this.ffi.galley_recorded_context_name(h, diagIndex, contextIndex, od, ol),
1284
+ );
1285
+ }
1286
+
1287
+ recordedRecoveryKind(handle: Handle, diagIndex: number): number {
1288
+ return toNumber(this.ffi.galley_recorded_recovery_kind(handle as bigint, diagIndex));
1289
+ }
1290
+
1291
+ recordedRecoveryTerminal(handle: Handle, diagIndex: number): Uint8Array | null {
1292
+ const h = handle as bigint;
1293
+ return this.#tryCopyBytes((od, ol) =>
1294
+ this.ffi.galley_recorded_recovery_terminal(h, diagIndex, od, ol),
1295
+ );
1296
+ }
1297
+
1298
+ recordedRecoveryResume(handle: Handle, diagIndex: number): number | null {
1299
+ const out: unknown[] = [0n];
1300
+ if (toNumber(this.ffi.galley_recorded_recovery_resume(handle as bigint, diagIndex, out)) !== 0)
1301
+ return null;
1302
+ return toNumber(out[0] as bigint);
1303
+ }
1304
+
1305
+ recordedRecoveryLhsVariable(handle: Handle, diagIndex: number): string | null {
1306
+ const h = handle as bigint;
1307
+ const outData: unknown[] = [null];
1308
+ const outLen: unknown[] = [0];
1309
+ const st = this.ffi.galley_recorded_recovery_lhs_variable(h, diagIndex, outData, outLen);
1310
+ if (isNegative(st) || outData[0] === null) return null;
1311
+ return copyStringBytes(outData[0] as bigint, outLen[0] as bigint);
1312
+ }
1313
+
1314
+ recordedRecoveryProduction(handle: Handle, diagIndex: number): [string, number] | null {
1315
+ const outVar: unknown[] = [null];
1316
+ const outLen: unknown[] = [0];
1317
+ const outIdx: unknown[] = [0];
1318
+ if (
1319
+ toNumber(
1320
+ this.ffi.galley_recorded_recovery_production(handle as bigint, diagIndex, outVar, outLen, outIdx),
1321
+ ) !== 0
1322
+ )
1323
+ return null;
1324
+ return [copyStringBytes(outVar[0] as bigint, outLen[0] as bigint), outIdx[0] as number];
1325
+ }
1326
+
1327
+ recordedRecoveryOccurrence(
1328
+ handle: Handle,
1329
+ diagIndex: number,
1330
+ ): [string, number, number, string] | null {
1331
+ const outParent: unknown[] = [null];
1332
+ const outParentLen: unknown[] = [0];
1333
+ const outRhs: unknown[] = [0];
1334
+ const outSym: unknown[] = [0];
1335
+ const outVar: unknown[] = [null];
1336
+ const outVarLen: unknown[] = [0];
1337
+ if (
1338
+ toNumber(
1339
+ this.ffi.galley_recorded_recovery_occurrence(
1340
+ handle as bigint,
1341
+ diagIndex,
1342
+ outParent,
1343
+ outParentLen,
1344
+ outRhs,
1345
+ outSym,
1346
+ outVar,
1347
+ outVarLen,
1348
+ ),
1349
+ ) !== 0
1350
+ )
1351
+ return null;
1352
+ return [
1353
+ copyStringBytes(outParent[0] as bigint, outParentLen[0] as bigint),
1354
+ outRhs[0] as number,
1355
+ outSym[0] as number,
1356
+ copyStringBytes(outVar[0] as bigint, outVarLen[0] as bigint),
1357
+ ];
1358
+ }
1359
+
1360
+ // -- tree editing ----------------------------------------------------------
1361
+
1362
+ treeAppendChildren(handle: Handle, parent: bigint, first: bigint): number {
1363
+ return toNumber(this.ffi.galley_tree_append_children(handle as bigint, parent, first));
1364
+ }
1365
+
1366
+ treeInsertBefore(handle: Handle, target: bigint, first: bigint): number {
1367
+ return toNumber(this.ffi.galley_tree_insert_before(handle as bigint, target, first));
1368
+ }
1369
+
1370
+ treeInsertAfter(handle: Handle, target: bigint, first: bigint): number {
1371
+ return toNumber(this.ffi.galley_tree_insert_after(handle as bigint, target, first));
1372
+ }
1373
+
1374
+ treeRemoveSiblings(handle: Handle, node: bigint, count: number): { status: number; head: bigint } {
1375
+ const outHead: unknown[] = [0n];
1376
+ const st = this.ffi.galley_tree_remove_siblings(handle as bigint, node, count, outHead);
1377
+ return { status: toNumber(st), head: toBigInt(outHead[0] as bigint) };
1378
+ }
1379
+
1380
+ treeRemoveSelf(handle: Handle, node: bigint): { status: number; head: bigint } {
1381
+ const outHead: unknown[] = [0n];
1382
+ const st = this.ffi.galley_tree_remove_self(handle as bigint, node, outHead);
1383
+ return { status: toNumber(st), head: toBigInt(outHead[0] as bigint) };
1384
+ }
1385
+
1386
+ treePromoteChildrenOverWrapper(handle: Handle, wrapper: bigint): { status: number; head: bigint } {
1387
+ const outHead: unknown[] = [0n];
1388
+ const st = this.ffi.galley_tree_promote_children_over_wrapper(handle as bigint, wrapper, outHead);
1389
+ return { status: toNumber(st), head: toBigInt(outHead[0] as bigint) };
1390
+ }
1391
+
1392
+ treeCleanChildren(handle: Handle, node: bigint): { status: number; head: bigint } {
1393
+ const outHead: unknown[] = [0n];
1394
+ const st = this.ffi.galley_tree_clean_children(handle as bigint, node, outHead);
1395
+ return { status: toNumber(st), head: toBigInt(outHead[0] as bigint) };
1396
+ }
1397
+
1398
+ treeUnlinkWrapper(handle: Handle, wrapper: bigint): number {
1399
+ return toNumber(this.ffi.galley_tree_unlink_wrapper(handle as bigint, wrapper));
1400
+ }
1401
+
1402
+ treeInsertChildrenAt(handle: Handle, parent: bigint, index: number, first: bigint): number {
1403
+ return toNumber(this.ffi.galley_tree_insert_children_at(handle as bigint, parent, index, first));
1404
+ }
1405
+
1406
+ treeRemoveChildrenAt(
1407
+ handle: Handle,
1408
+ parent: bigint,
1409
+ index: number,
1410
+ count: number,
1411
+ ): { status: number; head: bigint } {
1412
+ const outHead: unknown[] = [0n];
1413
+ const st = this.ffi.galley_tree_remove_children_at(handle as bigint, parent, index, count, outHead);
1414
+ return { status: toNumber(st), head: toBigInt(outHead[0] as bigint) };
1415
+ }
1416
+
1417
+ // -- procedure hooks ----------------------------------------------------------
1418
+
1419
+ procCurrentNode(args: Handle): bigint {
1420
+ return toBigInt(this.ffi.galley_procedure_current_node(args as bigint));
1421
+ }
1422
+
1423
+ procSetCurrentNode(args: Handle, node: bigint): void {
1424
+ this.ffi.galley_procedure_set_current_node(args as bigint, node);
1425
+ }
1426
+
1427
+ procDropSelf(args: Handle): number {
1428
+ return toNumber(this.ffi.galley_procedure_drop_self(args as bigint));
1429
+ }
1430
+
1431
+ procDropChildren(args: Handle): number {
1432
+ return toNumber(this.ffi.galley_procedure_drop_children(args as bigint));
1433
+ }
1434
+
1435
+ procDropIfEmpty(args: Handle): number {
1436
+ return toNumber(this.ffi.galley_procedure_drop_if_empty(args as bigint));
1437
+ }
1438
+
1439
+ procReplaceWithChildren(args: Handle): number {
1440
+ return toNumber(this.ffi.galley_procedure_replace_with_children(args as bigint));
1441
+ }
1442
+
1443
+ procContextLine(args: Handle): number {
1444
+ return this.ffi.galley_procedure_context_line(args as bigint);
1445
+ }
1446
+
1447
+ procContextColumn(args: Handle): number {
1448
+ return this.ffi.galley_procedure_context_column(args as bigint);
1449
+ }
1450
+
1451
+ procReportSemanticError(args: Handle, message: Uint8Array): number {
1452
+ return toNumber(
1453
+ this.ffi.galley_procedure_report_semantic_error(args as bigint, decodeParam(message), message.length),
1454
+ );
1455
+ }
1456
+
1457
+ syncProcedures(names: string[]): void {
1458
+ if (this.ffi.galley_js_procedure_clear === null || this.ffi.galley_js_procedure_enable === null) return;
1459
+ this.ffi.galley_js_procedure_clear();
1460
+ for (const name of names) {
1461
+ this.ffi.galley_js_procedure_enable(name, name.length);
1462
+ }
1463
+ // Warm the ID table outside any parse so the hot path never queries.
1464
+ this.procedureNames();
1465
+ }
1466
+
1467
+ #procedureNameTable: string[] | null = null;
1468
+
1469
+ procedureNames(): string[] {
1470
+ if (this.#procedureNameTable !== null) return this.#procedureNameTable;
1471
+ const table: string[] = [];
1472
+ const count = this.ffi.galley_js_procedure_count;
1473
+ const namePtr = this.ffi.galley_js_procedure_name_ptr;
1474
+ const nameLen = this.ffi.galley_js_procedure_name_len;
1475
+ if (count !== null && namePtr !== null && nameLen !== null) {
1476
+ const n = toNumber(count());
1477
+ for (let i = 0; i < n; i++) {
1478
+ const ptr = namePtr(i);
1479
+ if (ptr === 0n || ptr === null) break;
1480
+ table.push(copyStringBytes(ptr as bigint, nameLen(i)));
1481
+ }
1482
+ }
1483
+ this.#procedureNameTable = table;
1484
+ return table;
1485
+ }
1486
+ }
1487
+
1488
+ const portCache = new Map<string, NodePort>();
1489
+
1490
+ /** Port for the library at `explicitPath` (or default discovery), cached per path. */
1491
+ export function getNodePort(explicitPath?: string): NodePort {
1492
+ const ffi = loadLibrary(explicitPath);
1493
+ const cached = portCache.get(ffi.libPath);
1494
+ if (cached) return cached;
1495
+ const port = new NodePort(ffi);
1496
+ portCache.set(ffi.libPath, port);
1497
+ return port;
1498
+ }