Files
scriptc/packages/cli/test/native-link-info.test.ts
T
Chris Tate f87a04fa06 Build typed 'message' listener parameters from the message
A 'message' listener with a typed parameter received each message as a
checked-dynamic value: the port decoded the message into dyn objects, the
listener's thunk matched the value against each union arm, read every
field again to build the record, and the dyn objects were then released.
For bench/channel-messages that was about a third of the delivery time.

- A closure registered with on, once, addListener, prependListener or
  prependOnceListener for 'message' gets a typed message entry when its
  parameter is a number, boolean, string, a record of those without union
  fields, or a boxed union of those arms with null or undefined. The
  entry builds the parameter from the message's nodes and calls the
  closure.
- The entry handles a message only when it has the parameter's exact
  layout: the union arm the checked conversion picks, and a record's
  declared keys in declaration order with no others. Any other message
  runs through the thunk on the decoded value, as before, so undeclared
  keys, other orders and mismatches behave as they did.
- Ports, workers and parentPort deliver a message through the entries
  when no value in it is reached twice and it holds no ports or native
  handles. Listeners without an entry share one decoded value, made when
  the first of them runs.
- The message readers are runtime entry points, so the runtime ABI is now
  27.

Corpus 7130 and 7131 check typed listeners on MessageChannel ports, a
worker and parentPort against Node: exact layouts, extra keys, shared
values, nested records, untyped listeners beside typed ones, once
listeners and a transferred port.
2026-10-22 07:08:26 -05:00

171 lines
6.3 KiB
TypeScript

import { execFile } from "node:child_process";
import { createRequire } from "node:module";
import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { release as osRelease, tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { afterEach, describe, expect, test } from "vitest";
import type { NativeLinkInfo } from "@scriptc/compiler";
const execFileAsync = promisify(execFile);
const require = createRequire(import.meta.url);
const repoRoot = join(import.meta.dirname, "../../..");
const cliEntry = join(repoRoot, "packages/cli/src/main.ts");
const tsxLoader = join(dirname(require.resolve("tsx/package.json")), "dist/loader.mjs");
const supported =
process.platform === "darwin" &&
process.arch === "arm64" &&
Number.parseInt(osRelease().split(".", 1)[0] ?? "", 10) >= 24;
const dirs: string[] = [];
afterEach(async () => {
await Promise.all(dirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});
function cli(args: string[]) {
return execFileAsync(process.execPath, ["--import", tsxLoader, cliEntry, ...args], {
maxBuffer: 16 * 1024 * 1024,
});
}
async function fixture() {
const dir = await mkdtemp(join(tmpdir(), "scriptc-native-link-info-"));
dirs.push(dir);
const entry = join(dir, "main.ts");
await writeFile(entry, 'console.log("external link");\n');
return { dir, entry, object: join(dir, "app.o") };
}
test("print option validation is explicit", async () => {
const { entry } = await fixture();
await expect(cli(["build", entry, "--print=wat"])).rejects.toMatchObject({
stderr: expect.stringContaining("unknown print kind"),
});
await expect(
cli(["build", entry, "--print=native-link-info", "--emit=llvm"]),
).rejects.toMatchObject({ stderr: expect.stringContaining("requires --emit=obj") });
await expect(cli(["run", entry, "--print=native-link-info"])).rejects.toMatchObject({
stderr: expect.stringContaining("is a build option"),
});
});
describe.runIf(supported)("macOS arm64 native link info", () => {
test("prints a stable cache-independent precompiled runtime recipe", async () => {
const { entry, object } = await fixture();
const { stdout, stderr } = await cli([
"build",
entry,
"--print=native-link-info",
"-o",
object,
]);
expect(stderr).toBe("");
const info = JSON.parse(stdout) as NativeLinkInfo;
expect(info).toMatchObject({
schema: "scriptc.native-link-info.v1",
format: 1,
object_abi: { stability: "experimental", compatibility: "exact-runtime-version" },
target: {
name: "macos-arm64",
llvm_triple: "arm64-apple-macosx14.0.0",
object_format: "macho",
minimum_os: "14.0",
},
program: { object, entry_symbol: "main" },
runtime_abi: { version: 27, marker: "scr_runtime_abi_v27" },
runtime_pack: {
kind: "precompiled",
package: "@scriptc/runtime-darwin-arm64",
path_base: "runtime_pack.root",
},
link: { system_libraries: ["System"], frameworks: [] },
});
expect(info.runtime_pack.root).not.toContain("node_modules/.cache");
expect(info.link.input_order.join("\n")).not.toContain("node_modules/.cache");
expect(info.runtime_pack.objects.map((object) => object.path)).toEqual(
expect.arrayContaining([
"artifacts/release/runtime/default/scr_console.o",
"artifacts/release/runtime/default/scr_async.o",
"artifacts/release/runtime/default/scr_cycle.o",
]),
);
await expect(readFile(object)).resolves.toBeInstanceOf(Buffer);
});
test("printing link info emits an object without invoking external tools", async () => {
const { dir, entry, object } = await fixture();
const traps = join(dir, "traps");
await execFileAsync("mkdir", [traps]);
const trapLog = join(dir, "trap.log");
for (const tool of ["clang", "cc", "gcc", "zig", "ar", "ld", "xcrun"]) {
const path = join(traps, tool);
await writeFile(path, `#!/bin/sh\nprintf '${tool}\\n' >> '${trapLog}'\nexit 97\n`);
await chmod(path, 0o755);
}
const { stdout, stderr } = await execFileAsync(
process.execPath,
["--import", tsxLoader, cliEntry, "build", entry, "--print=native-link-info", "-o", object],
{
env: {
...process.env,
PATH: `${traps}:${process.env["PATH"] ?? ""}`,
SCRIPTC_CACHE_DIR: join(dir, "cache"),
},
maxBuffer: 16 * 1024 * 1024,
},
);
expect(stderr).toBe("");
expect((JSON.parse(stdout) as NativeLinkInfo).program.object).toBe(object);
await expect(readFile(trapLog)).rejects.toMatchObject({ code: "ENOENT" });
});
test("an out-of-tree C-driver build consumes the reported object and runtime pack", async () => {
const { dir, entry, object } = await fixture();
const { stdout } = await cli(["build", entry, "--print=native-link-info", "-o", object]);
const info = JSON.parse(stdout) as NativeLinkInfo;
const executable = join(dir, "app");
await execFileAsync("clang", [
...info.link.driver_flags,
...info.link.input_order,
...info.link.system_libraries.map((name) => `-l${name}`),
"-o",
executable,
]);
await expect(execFileAsync(executable, [], { encoding: "utf8" })).resolves.toMatchObject({
stdout: "external link\n",
stderr: "",
});
}, 30_000);
test("a mismatched runtime ABI fails at link time through the versioned marker", async () => {
const { dir, entry, object } = await fixture();
await cli(["build", entry, "--emit=obj", "-o", object]);
const stub = join(dir, "wrong-runtime.c");
await writeFile(
stub,
[
"void scr_runtime_abi_v1(void) {} /* previous ABI */",
"void scr_console_log(void) {}",
"void scr_init(void) {}",
"void scr_lib_init(void) {}",
"void scr_str_release(void) {}",
"void scr_str_retain_v(void) {}",
"void scr_error_vts(void) {}",
"",
].join("\n"),
);
const error = await execFileAsync("clang", [
"-target",
"arm64-apple-macosx14.0.0",
object,
stub,
"-o",
join(dir, "bad"),
]).then(
() => null,
(failure: { stderr?: string }) => failure,
);
expect(error?.stderr).toContain("scr_runtime_abi_v27");
});
});