import { execFile } from "node:child_process"; import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import { afterEach, expect, test, vi } from "vitest"; import { compile, compileLibrary } from "../src/index.js"; const directories: string[] = []; afterEach(async () => { vi.unstubAllEnvs(); await Promise.all(directories.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); }); async function fixture(source: string) { const outDir = await mkdtemp(join(tmpdir(), "scriptc-worker-target-")); directories.push(outDir); const entry = join(outDir, "main.ts"); await writeFile(entry, source); return { entry, outDir, outPath: join(outDir, "program.ll"), outputKind: "llvm" as const }; } const worker = 'import { Worker } from "node:worker_threads"; new Worker(new URL(import.meta.url));'; test.each(["dynamic", "wasi"])( "worker constructors refuse %s execution before code generation", async (mode) => { const request = await fixture(worker); if (mode === "wasi") { vi.stubEnv("SCRIPTC_CC", "zigcc"); vi.stubEnv("SCRIPTC_TARGET", "wasm32-wasi"); } const result = await compile(request.entry, { ...request, dynamic: mode === "dynamic" }); expect(result.ok).toBe(false); if (!result.ok) expect(result.diagnostics).toEqual( expect.arrayContaining([ expect.objectContaining({ code: "SC2020", message: expect.stringContaining("Worker execution"), }), ]), ); }, ); test("runtime worker filenames produce an actionable source diagnostic", async () => { const request = await fixture( 'import { Worker } from "node:worker_threads"; new Worker(process.argv[2]);', ); const result = await compile(request.entry, request); expect(result.ok).toBe(false); if (!result.ok) expect(result.diagnostics).toEqual( expect.arrayContaining([ expect.objectContaining({ code: "SC2020", hint: expect.stringContaining("statically compiled"), }), ]), ); }); test("libraries refuse worker roots through the library diagnostic boundary", async () => { const request = await fixture(worker + "\nexport function boot(): number { return 0; }"); const profilePath = join(request.outDir, "profile.json"); await writeFile( profilePath, JSON.stringify({ profile_format: 1, name: "worker-library", entry: "main.ts", emission: "llvm", abi: { prefix: "w_", init_symbol: "w_init", sink_register_symbol: "w_panic", collect_symbol: null, result_reset_symbol: null, }, exports: [{ export: "boot", symbol: "w_boot", params: [], returns: "f64" }], }), ); const result = await compileLibrary({ profilePath, outDir: request.outDir }); expect(result.ok).toBe(false); if (!result.ok) expect(result.diagnostics).toEqual( expect.arrayContaining([ expect.objectContaining({ code: "SC4005", message: expect.stringContaining("worker") }), ]), ); }); test("worker TLS arguments use local snapshots before native pointer expansion", async () => { const request = await fixture( worker + '\nimport { createSecureContext } from "node:tls"; const cert = Buffer.from(new SharedArrayBuffer(4)); createSecureContext({cert, key: cert});', ); const result = await compile(request.entry, request); expect(result.ok, JSON.stringify(result)).toBe(true); if (result.ok) { const llvm = await readFile(request.outPath, "utf8"); expect(llvm).toContain("call ptr @scr_bytes_local_copy("); expect(llvm).toContain("call ptr @scr_tls_create_secure_context("); } }); test("worker programs refuse foreign callbacks and guard byte-pointer FFI calls", async () => { for (const foreign of [true, false]) { const source = foreign ? "declare function nativeStart(cb: (value: number) => void): void; nativeStart(() => {});" : "declare function consume(value: Uint8Array): number; consume(new Uint8Array(new SharedArrayBuffer(4)));"; const request = await fixture(worker + source); const ffiProfilePath = join(request.outDir, "ffi.json"); await writeFile( ffiProfilePath, JSON.stringify({ ffi_format: 5, functions: [ foreign ? { name: "nativeStart", symbol: "native_start", returns: "void", params: [ { callback: { id: "tick", params: ["f64", { context: "tick" }], returns: "void", lifetime: "retained", invoke: "foreign", }, }, { context: "tick" }, ], } : { name: "consume", symbol: "consume", params: ["bytes"], returns: "f64" }, ], }), ); const result = await compile(request.entry, { ...request, ffiProfilePath }); expect(result.ok, JSON.stringify(result)).toBe(!foreign); if (result.ok) expect(await readFile(request.outPath, "utf8")).toContain( "call void @scr_bytes_require_unshared(", ); else expect(result.diagnostics).toEqual( expect.arrayContaining([ expect.objectContaining({ code: "SC2020", message: expect.stringContaining("foreign-thread"), }), ]), ); } }); test("worker programs inline the thread-local allocator and keep async state behind runtime calls", async () => { const source = worker + ` class Cell { constructor(public next: Cell | null, public value: number) {} } let head: Cell | null = null; for (let i = 0; i < 3; i++) head = new Cell(head, i); async function settle(): Promise { return head === null ? 0 : head.value; } settle().then((value) => console.log(value)); `; const request = await fixture(source); const result = await compile(request.entry, request); expect(result.ok, JSON.stringify(result)).toBe(true); if (result.ok) { const llvm = await readFile(request.outPath, "utf8"); // Worker runtimes keep one small-object allocator per script thread: // the inline paths address its state, live counts and dispose hook in // the runtime's one thread-local ScrThreadHot block. expect(llvm).toMatch(/@scr_thread_hot = external thread_local(\(initialexec\))? global /); expect(llvm).not.toMatch(/@scr_(sa|cyc_live|cyc_old_freed|weak_dispose_hook) = external/); expect(llvm).toContain("call ptr @llvm.threadlocal.address.p0(ptr @scr_thread_hot)"); expect(llvm).toMatch(/getelementptr inbounds i8, ptr %[\w.]+\.tl\.hot, i64 808/); // Worker fibers own context termination; no fiberless async frames. expect(llvm).not.toContain("@scr_async_inline_enter"); } }); test("JavaScript worker entry spellings run their TypeScript sources", async () => { const request = await fixture(`import { Worker } from "node:worker_threads"; function run(worker: Worker): Promise { return new Promise((resolve) => { worker.on("message", (text: string) => console.log(text)); worker.on("exit", () => resolve()); }); } await run(new Worker(new URL("./task.js", import.meta.url))); await run(new Worker(new URL("./module-task.mjs", import.meta.url))); `); for (const [file, text] of [ ["task.ts", "script task"], ["module-task.mts", "module task"], ]) await writeFile( join(request.outDir, file), `import { parentPort } from "node:worker_threads";\nparentPort!.postMessage("${text}");\n`, ); const outPath = join(request.outDir, "program"); const result = await compile(request.entry, { outDir: request.outDir, outPath }); expect(result.ok, JSON.stringify(result)).toBe(true); const { stdout } = await promisify(execFile)(outPath, [], { encoding: "utf8" }); expect(stdout).toBe("script task\nmodule task\n"); }); test("worker entries without a source file stay refused", async () => { const request = await fixture( 'import { Worker } from "node:worker_threads"; new Worker(new URL("./missing.js", import.meta.url));', ); const result = await compile(request.entry, request); expect(result.ok).toBe(false); if (!result.ok) expect(result.diagnostics).toEqual( expect.arrayContaining([ expect.objectContaining({ message: expect.stringContaining("missing.js") }), ]), ); }); test("worker programs poll inline and skip termination polls in bounded functions", async () => { const request = await fixture( worker + ` function mix(a: number, b: number): number { return (a * 31 + b) % 1000003; } function spin(n: number): number { return n < 2 ? mix(n, 1) : spin(n - 1) + spin(n - 2); } console.log(mix(1, 2), spin(5)); `, ); const result = await compile(request.entry, request); expect(result.ok, JSON.stringify(result)).toBe(true); if (result.ok) { const llvm = await readFile(request.outPath, "utf8"); const body = (name: string): string => { const start = llvm.search(new RegExp(`^define [^\\n]*@${name}\\(`, "m")); expect(start).toBeGreaterThanOrEqual(0); return llvm.slice(start, llvm.indexOf("\n}\n", start)); }; // A bounded helper neither polls on entry nor makes its callers poll. expect(body("sc_f_mix")).not.toContain("@scr_exc_alert"); // Recursion polls on entry: the inline test reads the process-wide // alert word (no thread-local access), and only the slow path calls into // the runtime. expect(body("sc_f_spin")).toMatch(/load atomic i32, ptr @scr_exc_alert monotonic/); expect(body("sc_f_spin")).toContain("call zeroext i1 @scr_exc_pending()"); expect(body("sc_f_spin")).not.toContain("@scr_exc_active"); expect(llvm).toMatch(/^@scr_exc_alert = external global i32, align 64$/m); } }); test.each([ ["x86_64-unknown-linux-gnu", true], ["aarch64-unknown-linux-gnu", true], ["arm64-apple-macosx14.0.0", false], ])("worker executables for %s use the executable TLS models: %s", async (target, models) => { vi.stubEnv("SCRIPTC_TARGET", target); const request = await fixture(worker + "\nlet count = 0;\ncount++;\nconsole.log(count);\n"); const result = await compile(request.entry, request); expect(result.ok, JSON.stringify(result)).toBe(true); if (result.ok) { const llvm = await readFile(request.outPath, "utf8"); // Every thread-local of a worker executable lives in its own TLS block: // ELF uses local-exec for the program's and initial-exec for the // runtime's; Darwin's thread-local variables have one model. const plain = /^@[-$._A-Za-z0-9]+ = (internal|external) thread_local global /m; if (models) { expect(llvm).not.toMatch(plain); expect(llvm).toMatch(/^@[-$._A-Za-z0-9]+ = internal thread_local\(localexec\) global /m); expect(llvm).toMatch(/^@[-$._A-Za-z0-9]+ = external thread_local\(initialexec\) global /m); } else { expect(llvm).toMatch(plain); expect(llvm).not.toContain("thread_local("); } } });