Baseline: Ludic compiler + toolchain, Phase 1 syntax fixes complete
Self-hosted compiler (selfhost/*.ludic), runtime, examples, editor tooling, and docs. Phase 1 of the syntax-redesign cohesion pass has landed: edge-system fix, signature-query, when-alias, and the documentation truth-pass. Suite green (14/14), C-free bootstrap fixpoint holds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
commit
985f9ad8f2
418 changed files with 39065 additions and 0 deletions
106
tools/ludic-web/run.mjs
Normal file
106
tools/ludic-web/run.mjs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
// ============================================================================
|
||||
// tools/ludic-web/run.mjs — run a headless Ludic wasm build under Node.
|
||||
//
|
||||
// node tools/ludic-web/run.mjs build/web/snake_headless.wasm --stdin=ddss
|
||||
//
|
||||
// The point of this runner is the regression it makes possible: the same game,
|
||||
// the same input, compiled once for the host and once for wasm32, has to render
|
||||
// the same frame. Ludic is fixed-point and its RNG is seeded, so "the same" here
|
||||
// means byte-identical PPM — which is a much stronger check on the wasm backend
|
||||
// than "it started".
|
||||
//
|
||||
// It shares runtime/web/platform.js's Host: files, stdin, stdout and the clock
|
||||
// are identical services whichever host provides them. Only the window differs,
|
||||
// and a headless build has none.
|
||||
// ============================================================================
|
||||
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||
import { Host, ExitSignal } from '../../runtime/web/platform.js';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const modulePath = args.find((a) => !a.startsWith('--'));
|
||||
const stdin = (args.find((a) => a.startsWith('--stdin=')) || '--stdin=').slice(8);
|
||||
const maxFrames = Number((args.find((a) => a.startsWith('--frames=')) || '--frames=100000').slice(9));
|
||||
|
||||
if (!modulePath) {
|
||||
console.error('usage: run.mjs <module.wasm> [--stdin=keys] [--frames=n]');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
// Files, backed by the real filesystem: a headless build reads its assets and
|
||||
// writes out.ppm exactly where the native headless binary does.
|
||||
class NodeFiles {
|
||||
constructor() { this.open = new Map(); this.next = 1; }
|
||||
|
||||
fopen(path, mode) {
|
||||
const h = this.next++;
|
||||
if (mode.includes('w') || mode.includes('a')) {
|
||||
this.open.set(h, { path, write: true, chunks: [], size: 0, pos: 0 });
|
||||
return h;
|
||||
}
|
||||
if (!existsSync(path)) return 0;
|
||||
this.open.set(h, { path, write: false, data: new Uint8Array(readFileSync(path)), pos: 0 });
|
||||
return h;
|
||||
}
|
||||
|
||||
fread(h, dst, want) {
|
||||
const f = this.open.get(h);
|
||||
if (!f || f.write) return 0;
|
||||
const n = Math.max(0, Math.min(want, f.data.length - f.pos));
|
||||
dst.set(f.data.subarray(f.pos, f.pos + n));
|
||||
f.pos += n;
|
||||
return n;
|
||||
}
|
||||
|
||||
fwrite(h, src, n) {
|
||||
const f = this.open.get(h);
|
||||
if (!f || !f.write) return 0;
|
||||
f.chunks.push(src.slice(0, n));
|
||||
f.size += n; f.pos += n;
|
||||
return n;
|
||||
}
|
||||
|
||||
fclose(h) {
|
||||
const f = this.open.get(h);
|
||||
if (!f) return;
|
||||
if (f.write) {
|
||||
const all = new Uint8Array(f.size);
|
||||
let at = 0;
|
||||
for (const c of f.chunks) { all.set(c, at); at += c.length; }
|
||||
writeFileSync(f.path, all);
|
||||
}
|
||||
this.open.delete(h);
|
||||
}
|
||||
|
||||
fseek(h, off, whence) {
|
||||
const f = this.open.get(h);
|
||||
if (!f) return -1;
|
||||
const end = f.write ? f.size : f.data.length;
|
||||
const base = whence === 1 ? f.pos : whence === 2 ? end : 0;
|
||||
f.pos = Math.max(0, Math.min(end, base + off));
|
||||
return 0;
|
||||
}
|
||||
|
||||
ftell(h) { const f = this.open.get(h); return f ? f.pos : -1; }
|
||||
}
|
||||
|
||||
const host = new Host({ files: new NodeFiles(), stdin });
|
||||
const { instance } = await WebAssembly.instantiate(readFileSync(modulePath), { env: host.imports() });
|
||||
host.bind(instance.exports.memory);
|
||||
|
||||
const { ludic_boot, ludic_frame, ludic_alive, ludic_teardown } = instance.exports;
|
||||
|
||||
let frames = 0;
|
||||
try {
|
||||
ludic_boot();
|
||||
while (host.running && ludic_alive() && frames < maxFrames) { ludic_frame(); frames++; }
|
||||
ludic_teardown();
|
||||
} catch (e) {
|
||||
if (!(e instanceof ExitSignal)) throw e;
|
||||
}
|
||||
host.flush();
|
||||
|
||||
if (frames >= maxFrames) {
|
||||
console.error(`ludic-web: stopped after ${maxFrames} frames (the game never ended)`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(host.exitCode ?? 0);
|
||||
Loading…
Add table
Add a link
Reference in a new issue