@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/README.md ADDED
@@ -0,0 +1,15 @@
1
+ # @sanbus/galley-core
2
+
3
+ Runtime-neutral core for the Galley JavaScript bindings. Pure TypeScript:
4
+ no `node:`, `bun:`, or `Deno` imports (enforced by `"types": []` in
5
+ `tsconfig.json` — only `TextEncoder`/`TextDecoder`/`console` from standard
6
+ lib). Each runtime ships a thin adapter package implementing `FfiPort`
7
+ (`src/port.ts`) over the same `bindings/c/galley.h` shared library:
8
+
9
+ - `bindings/js/node` — Node via koffi
10
+ - `bindings/js/bun` — Bun via `bun:ffi`
11
+ - `bindings/js/deno` — Deno via `Deno.dlopen`
12
+
13
+ `Session`, `Node`, `Walker`, diagnostics, and the procedure registry live
14
+ here exactly once. Adapters own library discovery, memory copying, native
15
+ callback installation, and the public package surface.
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared parser-artifact builder for the Galley JavaScript bindings.
4
+ *
5
+ * Single gate behind every JavaScript build entry: `galley build` in the
6
+ * universal package, plus `@sanbus/galley-node`, `@sanbus/galley-bun`,
7
+ * `@sanbus/galley-wasm`, and the Deno `build.ts`. Callers pass targeting
8
+ * information (library name, wasm or native, platform, install layout);
9
+ * the gate owns everything else — checkout resolution, CLI bootstrap,
10
+ * parser generation, procedure-shim selection, and the consumer `zig
11
+ * build` invocation. No caller builds consumer arguments or picks shim
12
+ * emitters directly.
13
+ *
14
+ * The language directory must contain `ll.grm` and may contain
15
+ * `config.zig`, procedures, and `ll_error_messages.zig`, mirroring the
16
+ * other bindings:
17
+ *
18
+ * * `procedures.ts` / `procedures.js` — JavaScript hooks
19
+ * (`export function reduction_<Variable>(args)` /
20
+ * `export function hook_<name>(args)`), dispatched through a generated
21
+ * shim shared by the Node, Bun, and Deno adapters (native emitter) or
22
+ * through the wasm import (wasm emitter). This is the native-language
23
+ * path mirroring Rust's `procedures.rs`.
24
+ * * `procedures.c` / `procedures.cpp` — legacy C/C++ hooks compiled into
25
+ * the artifact, exactly like the C/C++ consumers.
26
+ * * `ll_error_messages.zig` / `lr_error_messages.zig` — custom syntax-error
27
+ * message hooks.
28
+ *
29
+ * JavaScript procedures take precedence over C procedures. When neither
30
+ * exists, the gate still generates the shim as a no-op fallback so the
31
+ * artifact links (hooks stay no-ops until JavaScript registers them),
32
+ * mirroring the always-shim model of the other bindings.
33
+ *
34
+ * The tool generates the parser (`--emit-metadata`) and builds the artifact
35
+ * through the generic consumer build directly next to the grammar, so the
36
+ * adapters can name it through an explicit path or `GALLEY_LIBRARY_PATH`.
37
+ *
38
+ * Environment: `ZIG_EXECUTABLE` (default `zig`) and `GALLEY_CHECKOUT`
39
+ * (required): an existing Galley working tree holding `build.zig`. To
40
+ * fetch a checkout for convenience, use
41
+ * `examples/scripts/fetch-galley.sh` — that cache is an examples-only
42
+ * convenience, not part of the bindings.
43
+ */
44
+
45
+ import { spawnSync } from "node:child_process";
46
+ import * as fs from "node:fs";
47
+ import { createRequire } from "node:module";
48
+ import * as path from "node:path";
49
+ import { fileURLToPath } from "node:url";
50
+ import { emitJsProcedureShim, emitJsProcedureShimWasm } from "./shim.mjs";
51
+
52
+ /**
53
+ * Canonical shared native build: one library serves the Node, Bun, and
54
+ * Deno adapters (the dispatch symbols are identical across them).
55
+ */
56
+ export const NATIVE_LIBRARY_BASE = "@sanbus/galley-node";
57
+ /** Canonical wasm build for the wasm adapter and the universal fallback leg. */
58
+ export const WASM_LIBRARY_BASE = "@sanbus/galley-wasm";
59
+
60
+ const WASM_TARGET = "wasm32-wasi";
61
+ const NATIVE_SHIM_FILE = "procedures_js.zig";
62
+ const WASM_SHIM_FILE = "procedures_wasm.zig";
63
+ const CORE_DIRECTORY = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
64
+
65
+ function fatal(message) {
66
+ console.error(`galley-bindings: ${message}`);
67
+ process.exit(1);
68
+ }
69
+
70
+ function run(command, argumentList, options = {}) {
71
+ console.log(`+ ${command} ${argumentList.map((argument) => JSON.stringify(argument)).join(" ")}`);
72
+ const result = spawnSync(command, argumentList, { stdio: "inherit", ...options });
73
+ if (result.error) fatal(`executable not found: ${command} (${result.error.message})`);
74
+ if (result.status !== 0) fatal(`command failed: ${command} ${argumentList.join(" ")} (exit ${result.status})`);
75
+ }
76
+
77
+ function capture(command, argumentList) {
78
+ const result = spawnSync(command, argumentList, { encoding: "utf-8" });
79
+ if (result.error) fatal(`failed to probe ${command}: ${result.error.message}`);
80
+ if (result.status !== 0) fatal(`command failed: ${command} ${argumentList.join(" ")}`);
81
+ return result.stdout ?? "";
82
+ }
83
+
84
+ function zigExecutable() {
85
+ return process.env.ZIG_EXECUTABLE ?? "zig";
86
+ }
87
+
88
+ /** The checkout `GALLEY_CHECKOUT` names, or a loud error. No guessing. */
89
+ export function resolveGalleyCheckout() {
90
+ const checkout = process.env.GALLEY_CHECKOUT;
91
+ if (!checkout) {
92
+ fatal("GALLEY_CHECKOUT is not set; point it at a Galley checkout (examples/scripts/fetch-galley.sh can fetch one)");
93
+ }
94
+ if (!fs.existsSync(path.join(checkout, "build.zig"))) {
95
+ fatal(`GALLEY_CHECKOUT=${checkout} is not a Galley repository checkout (no build.zig)`);
96
+ }
97
+ return path.resolve(checkout);
98
+ }
99
+
100
+ /**
101
+ * Confirm the owning adapter is installed and built. Whatever the install
102
+ * layout (symlinked `file:` package or `--install-links` copy), the
103
+ * runtime dependency must resolve from the owning directory the same way
104
+ * the built output loads it. Say so loudly instead of running a package
105
+ * manager behind your back. Pass `bindingsDirectory: null` to skip (the
106
+ * Deno wrapper runs from source with no install step).
107
+ */
108
+ export function ensureBindingsInstalled({ bindingsDirectory, dependencyName = null, installCommand = "npm install" }) {
109
+ const builtIndex = path.join(bindingsDirectory, "dist", "index.js");
110
+ if (!fs.existsSync(builtIndex)) {
111
+ fatal(`bindings not built: run npm run build in ${bindingsDirectory} first`);
112
+ }
113
+ if (dependencyName !== null) {
114
+ try {
115
+ createRequire(path.join(bindingsDirectory, "package.json")).resolve(dependencyName);
116
+ } catch {
117
+ fatal(`bindings not installed: run ${installCommand} in ${bindingsDirectory} first`);
118
+ }
119
+ }
120
+ }
121
+
122
+ let cachedArtifactNames = null;
123
+ async function loadArtifactNames() {
124
+ if (cachedArtifactNames) return cachedArtifactNames;
125
+ try {
126
+ cachedArtifactNames = await import("../dist/artifact.js");
127
+ return cachedArtifactNames;
128
+ } catch (error) {
129
+ fatal(`cannot load @sanbus/galley-core dist (${error.message}); run npm run build in ${CORE_DIRECTORY} first`);
130
+ }
131
+ }
132
+
133
+ function findJsProceduresFile(languageDirectory) {
134
+ const candidates = [
135
+ path.join(languageDirectory, "procedures.ts"),
136
+ path.join(languageDirectory, "procedures.js"),
137
+ ];
138
+ for (const candidate of candidates) {
139
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
140
+ }
141
+ return null;
142
+ }
143
+
144
+ /**
145
+ * Build one parser artifact next to the grammar. `wasm` selects the WASI
146
+ * reactor module (wasm shim, `-Dwasm` target); otherwise a native shared
147
+ * library (native shim). `platform` names the host for the native filename
148
+ * (`process.platform` under Node/Bun, `Deno.build.os` under Deno — both
149
+ * spellings map through the shared mapping). Returns the built path.
150
+ *
151
+ * `artifactFileName` / `wasmArtifactFileName` are the shared mapping from
152
+ * `@sanbus/galley-core`. Node-family wrappers omit them and the gate loads the
153
+ * built dist (their preflight guarantees it exists); the Deno wrapper
154
+ * passes them from core sources, which Deno consumes directly without
155
+ * ever building dist.
156
+ *
157
+ * @param {object} options
158
+ * @param {string} options.languageDirectory
159
+ * @param {string} options.libraryName
160
+ * @param {boolean} [options.wasm]
161
+ * @param {string} [options.platform]
162
+ * @param {string|null} [options.bindingsDirectory]
163
+ * @param {string|null} [options.dependencyName]
164
+ * @param {string} [options.installCommand]
165
+ * @param {boolean} [options.posixOnly]
166
+ * @param {Function|null} [options.artifactFileName]
167
+ * @param {Function|null} [options.wasmArtifactFileName]
168
+ * @returns {Promise<string>}
169
+ */
170
+ export async function buildParserArtifact({
171
+ languageDirectory,
172
+ libraryName,
173
+ wasm = false,
174
+ platform = process.platform,
175
+ bindingsDirectory = null,
176
+ dependencyName = null,
177
+ installCommand = "npm install",
178
+ posixOnly = true,
179
+ artifactFileName = null,
180
+ wasmArtifactFileName = null,
181
+ }) {
182
+ if (!languageDirectory) fatal("no language directory given");
183
+ if (!libraryName) fatal("no library name given");
184
+ if (posixOnly && (platform === "win32" || platform === "windows")) {
185
+ fatal("the JavaScript bindings target POSIX platforms");
186
+ }
187
+ if (bindingsDirectory !== null) {
188
+ ensureBindingsInstalled({ bindingsDirectory, dependencyName, installCommand });
189
+ }
190
+
191
+ const names =
192
+ artifactFileName && wasmArtifactFileName
193
+ ? { artifactFileName, wasmArtifactFileName }
194
+ : await loadArtifactNames();
195
+ const outputFileName = wasm ? names.wasmArtifactFileName(libraryName) : names.artifactFileName(libraryName, platform);
196
+
197
+ const languageDir = path.resolve(languageDirectory);
198
+ if (!fs.existsSync(path.join(languageDir, "ll.grm"))) fatal(`${languageDir} does not contain ll.grm`);
199
+
200
+ const galleySource = resolveGalleyCheckout();
201
+ const cli = path.join(galleySource, "zig-out", "bin", "galley");
202
+ if (!fs.existsSync(cli)) {
203
+ run(zigExecutable(), ["build", "-Doptimize=ReleaseFast", "install"], { cwd: galleySource });
204
+ }
205
+
206
+ const help = capture(cli, ["--help"]);
207
+ if (!help.includes("--emit-metadata")) {
208
+ fatal(
209
+ `the Galley at ${galleySource} is too old for the bindings workflow (no --emit-metadata support); update the checkout`,
210
+ );
211
+ }
212
+
213
+ run(cli, ["--emit-metadata", languageDir]);
214
+
215
+ // One library embeds one parser; the consumer build locates the file
216
+ // generation produced from `-Dlanguage-dir` and infers the family from
217
+ // the filename.
218
+ const jsProceduresFile = findJsProceduresFile(languageDir);
219
+ let proceduresZigSource = null;
220
+ let proceduresCSource = null;
221
+ const hasCProcedures =
222
+ fs.existsSync(path.join(languageDir, "procedures.c")) ||
223
+ fs.existsSync(path.join(languageDir, "procedures.cpp"));
224
+ // The wasm shim lives next to the native one under its own name so both
225
+ // builds can share one language directory.
226
+ const shimPath = path.join(languageDir, wasm ? WASM_SHIM_FILE : NATIVE_SHIM_FILE);
227
+ const emitShim = wasm ? emitJsProcedureShimWasm : emitJsProcedureShim;
228
+ if (jsProceduresFile !== null) {
229
+ if (hasCProcedures) {
230
+ console.error(`galley-bindings: both JS (${jsProceduresFile}) and C procedures found — using JS`);
231
+ }
232
+ console.error(`galley-bindings: using JS procedures from ${jsProceduresFile}`);
233
+ emitShim(path.join(languageDir, "metadata.json"), shimPath);
234
+ proceduresZigSource = shimPath;
235
+ } else if (hasCProcedures) {
236
+ if (fs.existsSync(path.join(languageDir, "procedures.zig"))) {
237
+ proceduresZigSource = path.join(languageDir, "procedures.zig");
238
+ }
239
+ if (fs.existsSync(path.join(languageDir, "procedures.c"))) {
240
+ proceduresCSource = path.join(languageDir, "procedures.c");
241
+ } else if (fs.existsSync(path.join(languageDir, "procedures.cpp"))) {
242
+ proceduresCSource = path.join(languageDir, "procedures.cpp");
243
+ }
244
+ } else if (fs.existsSync(path.join(languageDir, "procedures.zig"))) {
245
+ emitShim(path.join(languageDir, "metadata.json"), shimPath);
246
+ proceduresZigSource = shimPath;
247
+ }
248
+
249
+ const consumerArguments = [
250
+ "build",
251
+ "--build-file",
252
+ path.join(galleySource, "bindings/c/consumer/build.zig"),
253
+ `-Dlanguage-dir=${languageDir}`,
254
+ `-Dlib-name=${libraryName}`,
255
+ ...(wasm ? [`-Dtarget=${WASM_TARGET}`, "-Dwasm"] : []),
256
+ `-Doutput=${outputFileName}`,
257
+ "-Doptimize=ReleaseFast",
258
+ "--prefix",
259
+ languageDir,
260
+ "install",
261
+ ];
262
+ if (proceduresZigSource !== null) {
263
+ consumerArguments.splice(consumerArguments.length - 1, 0, `-Dprocedures-zig-source=${proceduresZigSource}`);
264
+ }
265
+ if (proceduresCSource !== null) {
266
+ consumerArguments.splice(consumerArguments.length - 1, 0, `-Dprocedures-c-source=${proceduresCSource}`);
267
+ }
268
+ // `config.zig` and `{ll,lr}_error_messages.zig` are inferred by the
269
+ // consumer build from the parser location.
270
+ run(zigExecutable(), consumerArguments, { cwd: galleySource });
271
+
272
+ const destination = path.join(languageDir, outputFileName);
273
+ if (!fs.existsSync(destination)) fatal(`expected library not found at ${destination}`);
274
+ console.log(`galley-bindings: built ${destination}; import from ${languageDir} (or set GALLEY_LIBRARY_PATH)`);
275
+ return destination;
276
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Shared test-fixture builder for the Galley JavaScript bindings.
3
+ *
4
+ * One implementation behind every JS binding suite (node, bun, deno, wasm,
5
+ * universal): each test resolves its parser artifact through
6
+ * `ensureTestLibrary` instead of borrowing a user-facing example build.
7
+ * The fixture grammar (`bindings/js/test-fixture`, verbatim keyvalue
8
+ * sources) is copied to a stable per-scope workdir under the system temp
9
+ * directory and built there with the adapter's own builder, so test runs
10
+ * never read or write `examples/`.
11
+ *
12
+ * - `GALLEY_LIBRARY_PATH`, when set, wins outright (explicit user artifact,
13
+ * no build). Otherwise the fixture is built and its path returned.
14
+ * - `GALLEY_CHECKOUT` must name the Galley checkout to build against
15
+ * (CI sets it). Unset is a loud error, never a guess.
16
+ * - The workdir path is stable per `scope`, so the builders' content-hash
17
+ * caches and zig's incremental cache stay warm across runs.
18
+ * - Builder output is captured and shown only on failure.
19
+ */
20
+
21
+ import { spawnSync } from "node:child_process";
22
+ import * as fs from "node:fs";
23
+ import * as os from "node:os";
24
+ import * as path from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+
27
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
28
+ const FIXTURE_DIR = path.resolve(HERE, "..", "..", "test-fixture");
29
+ const FIXTURE_FILES = ["ll.grm", "config.zig", "procedures.zig", "procedures.ts"];
30
+
31
+ /** The checkout GALLEY_CHECKOUT names, or a loud error. No guessing. */
32
+ function requireGalleyCheckout() {
33
+ const checkout = process.env.GALLEY_CHECKOUT;
34
+ if (!checkout) {
35
+ throw new Error(
36
+ "galley test fixture: set GALLEY_CHECKOUT to a Galley checkout (must contain build.zig)",
37
+ );
38
+ }
39
+ return checkout;
40
+ }
41
+
42
+ /**
43
+ * Return the parser artifact at `GALLEY_LIBRARY_PATH`, or build the shared
44
+ * fixture with `buildCommand` (argv prefix, workdir appended) and return
45
+ * `<workdir>/<libFileName>`. Throws loudly when the build fails.
46
+ */
47
+ export function ensureTestLibrary({ buildCommand, libFileName, scope }) {
48
+ if (process.env.GALLEY_LIBRARY_PATH) return process.env.GALLEY_LIBRARY_PATH;
49
+ if (!Array.isArray(buildCommand) || buildCommand.length === 0) {
50
+ throw new Error("galley test fixture: buildCommand must be a non-empty argv array");
51
+ }
52
+ if (!libFileName || !scope) {
53
+ throw new Error("galley test fixture: libFileName and scope are required");
54
+ }
55
+ const workDir = path.join(os.tmpdir(), "galley-js-test", scope);
56
+ fs.mkdirSync(workDir, { recursive: true });
57
+ for (const file of FIXTURE_FILES) {
58
+ fs.copyFileSync(path.join(FIXTURE_DIR, file), path.join(workDir, file));
59
+ }
60
+ requireGalleyCheckout();
61
+ const [command, ...prefix] = buildCommand;
62
+ const built = spawnSync(command, [...prefix, workDir], { encoding: "utf-8" });
63
+ if (built.error) {
64
+ throw new Error(`galley test fixture: cannot run ${command}: ${built.error.message}`);
65
+ }
66
+ // Deno's node:child_process may return a degenerate result instead of
67
+ // throwing (e.g. spawning without --allow-run): fail loudly here rather
68
+ // than printing "exit undefined" below.
69
+ if (typeof built.status !== "number") {
70
+ throw new Error(
71
+ `galley test fixture: no exit status from ${command}; ` +
72
+ "under Deno the test command needs --allow-run",
73
+ );
74
+ }
75
+ if (built.status !== 0) {
76
+ throw new Error(
77
+ `galley test fixture: build failed for ${workDir} (exit ${built.status})\n${built.stdout ?? ""}${built.stderr ?? ""}`,
78
+ );
79
+ }
80
+ const libPath = path.join(workDir, libFileName);
81
+ if (!fs.existsSync(libPath)) {
82
+ throw new Error(`galley test fixture: expected library not found at ${libPath}`);
83
+ }
84
+ return libPath;
85
+ }
package/build/shim.mjs ADDED
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shared procedure-shim generator for the Galley JavaScript bindings.
4
+ *
5
+ * Each adapter build script (`bindings/js/<runtime>/build.mjs`) imports
6
+ * `emitJsProcedureShim` to turn the generator's `procedures` hook list
7
+ * (metadata.json) into a dispatch shim: every hook forwards its integer ID
8
+ * (index into the hook list) through one JS callback installed via
9
+ * `galley_install_js_dispatch_id`. Only hooks the host enabled via
10
+ * `galley_js_procedure_enable` cross into the host; unenabled slots return
11
+ * after one boolean check (selective dispatch). The host resolves IDs to
12
+ * names once via `galley_js_procedure_count` /
13
+ * `galley_js_procedure_name_ptr` / `galley_js_procedure_name_len`, so the
14
+ * hot path copies and decodes no strings. The symbols are shared across
15
+ * the Node, Bun, and Deno adapters — one library build serves any of them.
16
+ * The generator owns the hook list; this module renders it.
17
+ */
18
+
19
+ import * as fs from "node:fs";
20
+
21
+ export const INSTALL_SYMBOL = "galley_install_js_dispatch_id";
22
+
23
+ const PREAMBLE = [
24
+ "const std = @import(\"std\");",
25
+ 'const root = @import("galley");',
26
+ "pub const Payload = struct {};",
27
+ "",
28
+ ];
29
+
30
+ export function readProcedureHooks(metadataPath) {
31
+ let metadata;
32
+ try {
33
+ metadata = fs.readFileSync(metadataPath, "utf-8");
34
+ } catch (e) {
35
+ throw new Error(`failed to read ${metadataPath}: ${e.message}`);
36
+ }
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(metadata);
40
+ } catch (e) {
41
+ throw new Error(`failed to parse ${metadataPath}: ${e.message}`);
42
+ }
43
+ if (!parsed || !Array.isArray(parsed.procedures) || parsed.procedures.length === 0) {
44
+ throw new Error(`${metadataPath} has no procedure hook list; update the Galley checkout`);
45
+ }
46
+ return parsed.procedures;
47
+ }
48
+
49
+ export function emitJsProcedureShim(metadataPath, outputPath) {
50
+ const hooks = readProcedureHooks(metadataPath);
51
+
52
+ const builder = [];
53
+ builder.push("// Generated by galley-js bindings; DO NOT EDIT.");
54
+ builder.push("// Procedure hooks dispatch through a JS callback registered");
55
+ builder.push("// by the host runtime (Node, Bun, or Deno); only enabled hooks cross,");
56
+ builder.push("// each carrying its integer hook ID (no strings on the hot path).");
57
+ for (const line of PREAMBLE) builder.push(line);
58
+ builder.push(
59
+ "var js_dispatch_target: ?*const fn (u32, ?*anyopaque) callconv(.c) void = null;",
60
+ );
61
+ for (const name of hooks) {
62
+ builder.push(`var js_enabled_${name}: bool = false;`);
63
+ }
64
+ builder.push("");
65
+ builder.push("const procedure_names = [_][]const u8{");
66
+ for (const name of hooks) {
67
+ builder.push(` "${name}",`);
68
+ }
69
+ builder.push("};");
70
+ builder.push("");
71
+ builder.push("fn dispatch(comptime id: u32, args: *root.data_structures.ProcedureArguments) void {");
72
+ builder.push(" if (js_dispatch_target) |target| {");
73
+ builder.push(" target(id, @ptrCast(args));");
74
+ builder.push(" }");
75
+ builder.push("}");
76
+ builder.push("");
77
+ hooks.forEach((name, id) => {
78
+ builder.push(`pub fn ${name}(args: *root.data_structures.ProcedureArguments) void {`);
79
+ builder.push(` if (!js_enabled_${name}) return;`);
80
+ builder.push(` dispatch(${id}, args);`);
81
+ builder.push("}");
82
+ builder.push("");
83
+ });
84
+ builder.push("const procedure_slots = [_]struct { name: []const u8, enabled: *bool }{");
85
+ for (const name of hooks) {
86
+ builder.push(` .{ .name = "${name}", .enabled = &js_enabled_${name} },`);
87
+ }
88
+ builder.push("};");
89
+ builder.push("");
90
+ builder.push(
91
+ `export fn ${INSTALL_SYMBOL}(target: *const fn (u32, ?*anyopaque) callconv(.c) void) void {`,
92
+ );
93
+ builder.push(" js_dispatch_target = target;");
94
+ builder.push("}");
95
+ builder.push("");
96
+ builder.push("export fn galley_js_procedure_count() u32 {");
97
+ builder.push(" return procedure_names.len;");
98
+ builder.push("}");
99
+ builder.push("");
100
+ builder.push("export fn galley_js_procedure_name_ptr(index: u32) ?[*]const u8 {");
101
+ builder.push(" if (index >= procedure_names.len) return null;");
102
+ builder.push(" return procedure_names[index].ptr;");
103
+ builder.push("}");
104
+ builder.push("");
105
+ builder.push("export fn galley_js_procedure_name_len(index: u32) usize {");
106
+ builder.push(" if (index >= procedure_names.len) return 0;");
107
+ builder.push(" return procedure_names[index].len;");
108
+ builder.push("}");
109
+ builder.push("");
110
+ builder.push("export fn galley_js_procedure_enable(name_ptr: [*]const u8, name_len: usize) c_int {");
111
+ builder.push(" const name = name_ptr[0..name_len];");
112
+ builder.push(" inline for (&procedure_slots) |*slot| {");
113
+ builder.push(" if (std.mem.eql(u8, slot.name, name)) {");
114
+ builder.push(" slot.enabled.* = true;");
115
+ builder.push(" return 1;");
116
+ builder.push(" }");
117
+ builder.push(" }");
118
+ builder.push(" return 0;");
119
+ builder.push("}");
120
+ builder.push("");
121
+ builder.push("export fn galley_js_procedure_clear() void {");
122
+ builder.push(" inline for (&procedure_slots) |*slot| {");
123
+ builder.push(" slot.enabled.* = false;");
124
+ builder.push(" }");
125
+ builder.push("}");
126
+ builder.push("");
127
+ fs.writeFileSync(outputPath, builder.join("\n"), "utf-8");
128
+ }
129
+
130
+ /**
131
+ * WebAssembly procedure-shim variant for `bindings/js/wasm`.
132
+ *
133
+ * Same hook set as {@link emitJsProcedureShim} (the generator's
134
+ * `procedures` list), but each hook forwards its integer ID through an
135
+ * imported host function instead of an installed function pointer: wasm
136
+ * modules cannot receive raw host addresses, so the adapter provides
137
+ * `galley_js_dispatch_id` in the `env` import object at instantiation. Only
138
+ * enabled hooks call the import (see `galley_js_procedure_enable`); the
139
+ * core registry stays the no-op backstop, matching native semantics.
140
+ */
141
+ export const DISPATCH_IMPORT_MODULE = "env";
142
+ export const DISPATCH_IMPORT_NAME = "galley_js_dispatch_id";
143
+
144
+ export function emitJsProcedureShimWasm(metadataPath, outputPath) {
145
+ const hooks = readProcedureHooks(metadataPath);
146
+ const builder = [];
147
+ builder.push("// Generated by galley-js bindings; DO NOT EDIT.");
148
+ builder.push("// Procedure hooks dispatch through the host-provided");
149
+ builder.push("// galley_js_dispatch_id import (see bindings/js/wasm); only enabled hooks call it,");
150
+ builder.push("// each carrying its integer hook ID (no strings on the hot path).");
151
+ builder.push('const std = @import("std");');
152
+ for (const line of PREAMBLE.slice(1)) builder.push(line);
153
+ for (const name of hooks) {
154
+ builder.push(`var js_enabled_${name}: bool = false;`);
155
+ }
156
+ builder.push("");
157
+ builder.push("const procedure_names = [_][]const u8{");
158
+ for (const name of hooks) {
159
+ builder.push(` "${name}",`);
160
+ }
161
+ builder.push("};");
162
+ builder.push("");
163
+ builder.push(
164
+ "extern fn galley_js_dispatch_id(hook_id: u32, args: ?*anyopaque) void;",
165
+ );
166
+ builder.push("");
167
+ builder.push("fn dispatch(comptime id: u32, args: *root.data_structures.ProcedureArguments) void {");
168
+ builder.push(" galley_js_dispatch_id(id, @ptrCast(args));");
169
+ builder.push("}");
170
+ builder.push("");
171
+ hooks.forEach((name, id) => {
172
+ builder.push(`pub fn ${name}(args: *root.data_structures.ProcedureArguments) void {`);
173
+ builder.push(` if (!js_enabled_${name}) return;`);
174
+ builder.push(` dispatch(${id}, args);`);
175
+ builder.push("}");
176
+ builder.push("");
177
+ });
178
+ builder.push("export fn galley_js_procedure_count() u32 {");
179
+ builder.push(" return procedure_names.len;");
180
+ builder.push("}");
181
+ builder.push("");
182
+ builder.push("export fn galley_js_procedure_name_ptr(index: u32) ?[*]const u8 {");
183
+ builder.push(" if (index >= procedure_names.len) return null;");
184
+ builder.push(" return procedure_names[index].ptr;");
185
+ builder.push("}");
186
+ builder.push("");
187
+ builder.push("export fn galley_js_procedure_name_len(index: u32) usize {");
188
+ builder.push(" if (index >= procedure_names.len) return 0;");
189
+ builder.push(" return procedure_names[index].len;");
190
+ builder.push("}");
191
+ builder.push("");
192
+ builder.push("const procedure_slots = [_]struct { name: []const u8, enabled: *bool }{");
193
+ for (const name of hooks) {
194
+ builder.push(` .{ .name = "${name}", .enabled = &js_enabled_${name} },`);
195
+ }
196
+ builder.push("};");
197
+ builder.push("");
198
+ builder.push("export fn galley_js_procedure_enable(name_ptr: [*]const u8, name_len: usize) c_int {");
199
+ builder.push(" const name = name_ptr[0..name_len];");
200
+ builder.push(" inline for (&procedure_slots) |*slot| {");
201
+ builder.push(" if (std.mem.eql(u8, slot.name, name)) {");
202
+ builder.push(" slot.enabled.* = true;");
203
+ builder.push(" return 1;");
204
+ builder.push(" }");
205
+ builder.push(" }");
206
+ builder.push(" return 0;");
207
+ builder.push("}");
208
+ builder.push("");
209
+ builder.push("export fn galley_js_procedure_clear() void {");
210
+ builder.push(" inline for (&procedure_slots) |*slot| {");
211
+ builder.push(" slot.enabled.* = false;");
212
+ builder.push(" }");
213
+ builder.push("}");
214
+ builder.push("");
215
+ fs.writeFileSync(outputPath, builder.join("\n"), "utf-8");
216
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Host capabilities artifact resolution needs from each adapter.
3
+ * Node, Bun, and wasm pass `process.env`, `path.resolve`, and an
4
+ * `fs.accessSync` probe; Deno passes `Deno.env.get`, the identity
5
+ * resolver (Deno reports the path it was given), and a `Deno.statSync`
6
+ * probe. Core stays runtime-neutral: no `node:`, `bun:`, or Deno imports.
7
+ */
8
+ export interface ArtifactHost {
9
+ getEnv(name: string): string | undefined;
10
+ resolvePath(candidate: string): string;
11
+ existsSync(candidate: string): boolean;
12
+ buildHint: string;
13
+ }
14
+ /**
15
+ * Shared library filename mapping for every JavaScript adapter.
16
+ * One library name per platform: `lib<base>.dylib` on macOS, `<base>.dll`
17
+ * on Windows (no `lib` prefix), `lib<base>.so` elsewhere. The platform
18
+ * string comes from the host (`process.platform` under Node/Bun, where
19
+ * Windows reports `win32`; `Deno.build.os`, where it reports `windows`);
20
+ * both spellings map to the Windows name here so the adapters cannot
21
+ * diverge. Each adapter keeps a thin `libFileName` wrapper passing its
22
+ * own base name; the mapping lives here.
23
+ */
24
+ export declare function artifactFileName(base: string, platform: string): string;
25
+ /**
26
+ * Shared wasm artifact filename. WebAssembly modules are platform-neutral:
27
+ * always `lib<base>.wasm`. The wasm adapter keeps a thin `wasmFileName`
28
+ * wrapper passing its base name.
29
+ */
30
+ export declare function wasmArtifactFileName(base: string): string;
31
+ /**
32
+ * Shared parser-artifact resolution for every JavaScript adapter.
33
+ * One place is named up front — an explicit path or GALLEY_LIBRARY_PATH.
34
+ * Anything else is a loud error, never a search. Each adapter keeps a
35
+ * thin `findLibrary` wrapper passing its host capabilities and its own
36
+ * build hint; the decision lives here.
37
+ */
38
+ export declare function resolveArtifact(explicit: string | undefined, host: ArtifactHost): string;
@@ -0,0 +1,45 @@
1
+ import { MissingArtifactError } from "./errors.js";
2
+ /**
3
+ * Shared library filename mapping for every JavaScript adapter.
4
+ * One library name per platform: `lib<base>.dylib` on macOS, `<base>.dll`
5
+ * on Windows (no `lib` prefix), `lib<base>.so` elsewhere. The platform
6
+ * string comes from the host (`process.platform` under Node/Bun, where
7
+ * Windows reports `win32`; `Deno.build.os`, where it reports `windows`);
8
+ * both spellings map to the Windows name here so the adapters cannot
9
+ * diverge. Each adapter keeps a thin `libFileName` wrapper passing its
10
+ * own base name; the mapping lives here.
11
+ */
12
+ export function artifactFileName(base, platform) {
13
+ if (platform === "darwin")
14
+ return `lib${base}.dylib`;
15
+ if (platform === "win32" || platform === "windows")
16
+ return `${base}.dll`;
17
+ return `lib${base}.so`;
18
+ }
19
+ /**
20
+ * Shared wasm artifact filename. WebAssembly modules are platform-neutral:
21
+ * always `lib<base>.wasm`. The wasm adapter keeps a thin `wasmFileName`
22
+ * wrapper passing its base name.
23
+ */
24
+ export function wasmArtifactFileName(base) {
25
+ return `lib${base}.wasm`;
26
+ }
27
+ /**
28
+ * Shared parser-artifact resolution for every JavaScript adapter.
29
+ * One place is named up front — an explicit path or GALLEY_LIBRARY_PATH.
30
+ * Anything else is a loud error, never a search. Each adapter keeps a
31
+ * thin `findLibrary` wrapper passing its host capabilities and its own
32
+ * build hint; the decision lives here.
33
+ */
34
+ export function resolveArtifact(explicit, host) {
35
+ const chosen = explicit || host.getEnv("GALLEY_LIBRARY_PATH");
36
+ if (!chosen) {
37
+ throw new MissingArtifactError("no parser artifact given; pass libraryPath or set GALLEY_LIBRARY_PATH", host.buildHint);
38
+ }
39
+ const resolved = host.resolvePath(chosen);
40
+ if (!host.existsSync(resolved)) {
41
+ throw new MissingArtifactError(`at ${resolved}`, host.buildHint);
42
+ }
43
+ return resolved;
44
+ }
45
+ //# sourceMappingURL=artifact.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"artifact.js","sourceRoot":"","sources":["../src/artifact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAgBnD;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,QAAgB;IAC7D,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,MAAM,IAAI,QAAQ,CAAC;IACrD,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,GAAG,IAAI,MAAM,CAAC;IACzE,OAAO,MAAM,IAAI,KAAK,CAAC;AACzB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,OAAO,MAAM,IAAI,OAAO,CAAC;AAC3B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,QAA4B,EAAE,IAAkB;IAC9E,MAAM,MAAM,GAAG,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC;IAC9D,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,oBAAoB,CAC5B,uEAAuE,EACvE,IAAI,CAAC,SAAS,CACf,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,oBAAoB,CAAC,MAAM,QAAQ,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACnE,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}