galley-js-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.
- package/README.md +15 -0
- package/build/builder.mjs +276 -0
- package/build/fixture.mjs +85 -0
- package/build/shim.mjs +216 -0
- package/dist/artifact.d.ts +38 -0
- package/dist/artifact.js +45 -0
- package/dist/artifact.js.map +1 -0
- package/dist/constants.d.ts +34 -0
- package/dist/constants.js +41 -0
- package/dist/constants.js.map +1 -0
- package/dist/diagnostic.d.ts +34 -0
- package/dist/diagnostic.js +28 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/errors.d.ts +22 -0
- package/dist/errors.js +38 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/names.d.ts +20 -0
- package/dist/names.js +29 -0
- package/dist/names.js.map +1 -0
- package/dist/node.d.ts +45 -0
- package/dist/node.js +137 -0
- package/dist/node.js.map +1 -0
- package/dist/port.d.ts +193 -0
- package/dist/port.js +14 -0
- package/dist/port.js.map +1 -0
- package/dist/procedures.d.ts +62 -0
- package/dist/procedures.js +154 -0
- package/dist/procedures.js.map +1 -0
- package/dist/session.d.ts +123 -0
- package/dist/session.js +599 -0
- package/dist/session.js.map +1 -0
- package/dist/text.d.ts +7 -0
- package/dist/text.js +16 -0
- package/dist/text.js.map +1 -0
- package/package.json +31 -0
- package/src/artifact.ts +62 -0
- package/src/constants.ts +47 -0
- package/src/diagnostic.ts +48 -0
- package/src/errors.ts +49 -0
- package/src/index.ts +53 -0
- package/src/node.ts +154 -0
- package/src/port.ts +213 -0
- package/src/procedures.ts +169 -0
- package/src/session.ts +747 -0
- package/src/text.ts +19 -0
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# galley-js-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 `galley-js-node`, `galley-js-bun`,
|
|
7
|
+
* `galley-js-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 = "galley-js-node";
|
|
57
|
+
/** Canonical wasm build for the wasm adapter and the universal fallback leg. */
|
|
58
|
+
export const WASM_LIBRARY_BASE = "galley-js-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 galley-js-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
|
+
* `galley-js-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;
|
package/dist/artifact.js
ADDED
|
@@ -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"}
|