1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// One compile, in a worker (a Web Worker in pages, a worker_thread in Node): asc calls process.exit(1)
// when it crashes (a stack overflow on deep generics does), which here ends only this worker, and a
// compile that runs long is terminated by index.js. Gets { code, urls? }, posts { binary } or { error }.
import { ABI, ENTRY } from "./abi.js";
const FLAGS = ["--runtime", "stub", "--use", "abort=", "--optimizeLevel", "1", "--noAssert", "--maximumMemory", "1024"];
// A page's worker sees no import map: the page resolves asc and its dependencies (index.js `urls`) and
// asc's own bare imports are rewritten to them, loaded as blob modules. Node resolves them itself.
async function loadAsc(urls) {
if (!urls) return import("assemblyscript/asc");
const rewrite = async (url, to) => {
const source = (await (await fetch(url)).text()).replace(/from"(assemblyscript|binaryen|long)"/g, (all, name) => (to[name] ? `from"${to[name]}"` : all));
return URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
};
const deps = { binaryen: urls.binaryen, long: urls.long };
return import(await rewrite(urls["assemblyscript/asc"], { ...deps, assemblyscript: await rewrite(urls.assemblyscript, deps) }));
}
async function compile(code, urls) {
// the prelude's names without a line of their own, so the compiler's line numbers are the agent's
const files = { "abi.ts": ABI, "module.ts": ENTRY, "code.ts": (code.includes('"./abi"') ? "" : 'import { Json, Kind, Run, print } from "./abi"; ') + code };
const asc = await loadAsc(urls);
let binary;
let log = "";
const stream = { write: (chunk) => ((log += typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk)), true) };
const { error } = await asc.main(["module.ts", "--outFile", "module.wasm", ...FLAGS], {
stdout: stream,
stderr: stream,
// only these files and asc's own standard library: an import of anything else is a compile error
readFile: (name) => (Object.hasOwn(files, name.replace(/^\.\//, "")) ? files[name.replace(/^\.\//, "")] : null),
writeFile: (name, contents) => {
if (name.endsWith("module.wasm") && typeof contents !== "string") binary = new Uint8Array(contents);
},
listFiles: () => [],
});
if (error || !binary) return { error: (log || error?.message || "no output").trim() };
// a module that declares imports compiles; refuse it here, with its imports named (the host would refuse it too)
const imports = WebAssembly.Module.imports(await WebAssembly.compile(binary));
if (imports.length) return { error: `imports are not allowed: ${imports.map((i) => `${i.module}.${i.name}`).join(", ")}` };
return { binary };
}
const port = typeof self === "undefined" ? (await import("node:worker_threads")).parentPort : self;
const reply = async ({ code, urls }) => port.postMessage(await compile(code, urls).catch((error) => ({ error: String(error?.message ?? error) })));
if (typeof self === "undefined") port.once("message", reply);
else self.onmessage = ({ data }) => reply(data);