mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
Bootstrap a standalone native compiler (#572)
- Build static programs through the TypeScript 7 frontend, C and LLVM emitters, and native linker without a JavaScript host - Share runtime-pack verification and link planning across native and Node compiler hosts - Handle large C output and deep logical-expression validation during compiler bootstrapping
This commit is contained in:
@@ -63,6 +63,7 @@ jobs:
|
||||
--exclude=tests/harness/self-hosting-serialization.test.ts
|
||||
--exclude=tests/harness/self-hosting.test.ts
|
||||
--exclude=tests/harness/self-hosting-llvm-emitter.test.ts
|
||||
--exclude=tests/harness/self-hosting-native-driver.test.ts
|
||||
--exclude=tests/harness/fetch.test.ts
|
||||
--exclude=tests/harness/npm-static.test.ts
|
||||
--exclude=tests/harness/surface-manifest.test.ts
|
||||
@@ -82,6 +83,12 @@ jobs:
|
||||
env:
|
||||
SCRIPTC_TEST_WORKERS: "1"
|
||||
run: pnpm test tests/harness/self-hosting-llvm-emitter.test.ts
|
||||
- name: Tests (native compiler bootstrap)
|
||||
if: matrix.shard == 6
|
||||
env:
|
||||
SCRIPTC_TEST_WORKERS: "1"
|
||||
VITEST_RPC_TIMEOUT: "180000"
|
||||
run: pnpm test tests/harness/self-hosting-native-driver.test.ts
|
||||
# These long files otherwise collide in one Vitest file shard and contend
|
||||
# for the worker RPC. Spread them across jobs with one worker each.
|
||||
- name: Tests (fetch)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"scripts": {
|
||||
"build": "pnpm -r --filter \"./packages/*\" run build",
|
||||
"build:fresh": "rm -rf packages/compiler/dist packages/cli/dist node_modules/.cache/scriptc-tsc && pnpm build",
|
||||
"build:native-compiler": "node --max-old-space-size=8192 --import tsx scripts/build-native-compiler.mts",
|
||||
"bench:builds": "node scripts/bench-builds.mjs",
|
||||
"test": "vitest run",
|
||||
"test:ts7": "node scripts/test-ts7.mjs",
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { STRING, VOID, type IrModule } from "../../ir/ir.js";
|
||||
import { IR_VERSION } from "../../ir/serialize.js";
|
||||
import { emitCModule, emitCModuleChunks } from "./c-emitter.js";
|
||||
|
||||
test("chunked C emission preserves separators, Unicode and the final newline", () => {
|
||||
const loc = { file: "source-🦀.ts", start: 0, end: 1 };
|
||||
const mod: IrModule = {
|
||||
irVersion: IR_VERSION, sourceFile: loc.file, entry: "main",
|
||||
functions: [{ name: "main", params: [], locals: [], returnType: VOID, loc,
|
||||
body: Array.from({ length: 2000 }, (_, i) => ({ kind: "exprStmt" as const, loc,
|
||||
expr: { kind: "strLit" as const, value: `Unicode 🦀 ${i}`, type: STRING, loc },
|
||||
})),
|
||||
}],
|
||||
};
|
||||
const chunks = emitCModuleChunks(mod);
|
||||
expect(chunks.length).toBeGreaterThan(2);
|
||||
expect(chunks.join("")).toBe(emitCModule(mod));
|
||||
expect(Buffer.concat(chunks.map((chunk) => Buffer.from(chunk))).toString()).toBe(emitCModule(mod));
|
||||
expect(chunks.at(-1)!.endsWith("\n")).toBe(true);
|
||||
});
|
||||
@@ -87,6 +87,28 @@ export function emitCModule(
|
||||
return new CEmitter(scalarizeNumericRecords(mod), sourceText, options).emit();
|
||||
}
|
||||
|
||||
/** Output chunks avoid the host's single-string size limit when a compiler
|
||||
* contains both emitters. Keep individual emitted lines intact, including
|
||||
* UTF-16 surrogate pairs. Joining these preserves emitCModule's exact bytes. */
|
||||
export function emitCModuleChunks(
|
||||
mod: IrModule,
|
||||
sourceText?: string,
|
||||
options: CEmitOptions = {},
|
||||
): string[] {
|
||||
const lines = new CEmitter(scalarizeNumericRecords(mod), sourceText, options).emitLines();
|
||||
const chunks: string[] = [];
|
||||
let chunk = "";
|
||||
let first = true;
|
||||
for (const line of lines) {
|
||||
if (!first) chunk += "\n";
|
||||
first = false;
|
||||
chunk += line;
|
||||
if (chunk.length >= 64 * 1024) { chunks.push(chunk); chunk = ""; }
|
||||
}
|
||||
if (chunk !== "") chunks.push(chunk);
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// Box construction moved onto CEmitter (boxNewC method): obj-kind boxes now
|
||||
// also carry the payload type's trace entry point, which is type-directed
|
||||
// through the emitter's cycle analysis.
|
||||
@@ -568,7 +590,9 @@ export class CEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
emit(): string {
|
||||
emit(): string { return this.emitLines().join("\n"); }
|
||||
|
||||
emitLines(): string[] {
|
||||
const body: string[] = [];
|
||||
// Async-generator spawn wrappers need their type-directed result
|
||||
// builders even when user code only creates and drops the iterator.
|
||||
@@ -773,7 +797,7 @@ export class CEmitter {
|
||||
// loop — the profile-declared external symbols instead. Everything
|
||||
// above is unchanged (still all internal linkage).
|
||||
this.emitLibEntries(out, globals);
|
||||
return out.join("\n");
|
||||
return out;
|
||||
}
|
||||
const refGlobals = globals.filter((g) => isRefCounted(g.type));
|
||||
// Interned function-value closures are IMMORTAL (rc == SIZE_MAX), so
|
||||
@@ -1019,7 +1043,7 @@ export class CEmitter {
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
return out.join("\n");
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ── library mode ─────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/** Runtime requirements derived from the lowered program, shared by compiler hosts. */
|
||||
import {
|
||||
moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleUsesAssert, moduleUsesBigInt,
|
||||
moduleUsesCopying, moduleUsesDc, moduleUsesDgram, moduleUsesDynAsync, moduleUsesDynInvoke,
|
||||
moduleUsesEmitter, moduleUsesFetch, moduleUsesFileHandle, moduleUsesFsWatch, moduleUsesHttp2,
|
||||
moduleUsesHttpServer, moduleUsesInspect, moduleUsesLegacyTextDecoder, moduleUsesNet,
|
||||
moduleUsesNodeTest, moduleUsesParseArgs, moduleUsesProcessEvents, moduleUsesQs, moduleUsesRegex,
|
||||
moduleUsesSearchParams, moduleUsesStream, moduleUsesSymbol, moduleUsesTls, moduleUsesTlsCa,
|
||||
moduleUsesZlib, type IrModule,
|
||||
} from "../ir/ir.js";
|
||||
import { hasForeignFfiCallback } from "./ffi-callbacks.js";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
|
||||
export function executableLinkFeatures(mod: IrModule, dynamic: boolean): NativeLinkFeatures {
|
||||
return {
|
||||
dynamic,
|
||||
regex: moduleUsesRegex(mod),
|
||||
copying: moduleUsesCopying(mod),
|
||||
textDecoderLegacy: moduleUsesLegacyTextDecoder(mod),
|
||||
fileHandle: moduleUsesFileHandle(mod),
|
||||
fetch: moduleUsesFetch(mod),
|
||||
netIsland:
|
||||
moduleEmbedsBuiltin(mod, "node:http") ||
|
||||
moduleEmbedsBuiltin(mod, "node:https") ||
|
||||
moduleEmbedsBuiltin(mod, "node:net") ||
|
||||
moduleEmbedsBuiltin(mod, "node:tls"),
|
||||
zlib: moduleUsesZlib(mod) || moduleEmbedsCompressedNpm(mod),
|
||||
assert: moduleUsesAssert(mod),
|
||||
inspect: moduleUsesInspect(mod),
|
||||
dynInvoke: moduleUsesDynInvoke(mod),
|
||||
dc: moduleUsesDc(mod),
|
||||
dynAsync: moduleUsesDynAsync(mod),
|
||||
events: moduleUsesProcessEvents(mod),
|
||||
emitter: moduleUsesEmitter(mod),
|
||||
symbol: moduleUsesSymbol(mod),
|
||||
bigint: moduleUsesBigInt(mod),
|
||||
searchParams: moduleUsesSearchParams(mod),
|
||||
qs: moduleUsesQs(mod),
|
||||
parseArgs: moduleUsesParseArgs(mod),
|
||||
stream: moduleUsesStream(mod),
|
||||
net: moduleUsesNet(mod),
|
||||
http: moduleUsesHttpServer(mod),
|
||||
http2: moduleUsesHttp2(mod),
|
||||
dgram: moduleUsesDgram(mod),
|
||||
watch: moduleUsesFsWatch(mod),
|
||||
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
|
||||
nodeTest: moduleUsesNodeTest(mod),
|
||||
tls: moduleUsesTls(mod),
|
||||
tlsCa: moduleUsesTlsCa(mod),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/** Ordered link inputs shared by synchronous native and asynchronous Node hosts. */
|
||||
import {
|
||||
executableOptimizationLinkerArgs, executableStripLinkerArgs, windowsSubsystemLinkerArgs,
|
||||
type NativeTargetSpec, type WindowsSubsystem,
|
||||
} from "./targets.js";
|
||||
|
||||
export interface ExecutableLinkInputs {
|
||||
inputs: string[];
|
||||
systemLibraries: string[];
|
||||
driverFlags: string[];
|
||||
}
|
||||
|
||||
export function executableLinkInputs(options: {
|
||||
target: NativeTargetSpec;
|
||||
programObject: string;
|
||||
ffiLibraries: readonly string[];
|
||||
ffiSystemLibraries: readonly string[];
|
||||
ffiFrameworks: readonly string[];
|
||||
runtimeObjects: readonly string[];
|
||||
runtimeArchives: readonly string[];
|
||||
runtimeSystemLibraries: readonly string[];
|
||||
optimization: "release" | "dev";
|
||||
strip?: boolean;
|
||||
windowsSubsystem?: WindowsSubsystem;
|
||||
}): ExecutableLinkInputs {
|
||||
if (options.ffiFrameworks.length !== 0 && options.target.platform !== "darwin") throw new Error("FFI frameworks require a Darwin target");
|
||||
return {
|
||||
inputs: [options.programObject, ...options.ffiLibraries, ...options.runtimeObjects, ...options.runtimeArchives],
|
||||
systemLibraries: [...new Set([...options.ffiSystemLibraries, ...options.runtimeSystemLibraries])],
|
||||
driverFlags: [
|
||||
...options.ffiFrameworks.flatMap((name) => ["-framework", name]),
|
||||
...options.target.executableLinkerArgs.map((arg, index, args) =>
|
||||
index > 0 && args[index - 1] === "-target" ? options.target.linkerTargetTriple : arg
|
||||
),
|
||||
...executableOptimizationLinkerArgs(options.target.platform, options.optimization),
|
||||
...executableStripLinkerArgs(options.target.platform, options.strip ?? false),
|
||||
...windowsSubsystemLinkerArgs(options.target.platform, options.windowsSubsystem),
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -11,13 +11,8 @@ import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import type { NativeArtifactDependency } from "./native-toolchain.js";
|
||||
import { needsDarwinDebugSymbols } from "./debug-symbols.js";
|
||||
import { loadRuntimePack, type RuntimePackSelection } from "./runtime-pack.js";
|
||||
import {
|
||||
executableOptimizationLinkerArgs,
|
||||
executableStripLinkerArgs,
|
||||
windowsSubsystemLinkerArgs,
|
||||
type NativeTargetSpec,
|
||||
type WindowsSubsystem,
|
||||
} from "./targets.js";
|
||||
import { executableLinkInputs } from "./link-plan-core.js";
|
||||
import type { NativeTargetSpec, WindowsSubsystem } from "./targets.js";
|
||||
|
||||
export interface NativeLinkPlan {
|
||||
target: NativeTargetSpec;
|
||||
@@ -53,25 +48,19 @@ export async function createNativeLinkPlan(options: {
|
||||
target: options.target,
|
||||
darwinDebugSymbols: needsDarwinDebugSymbols(options.target.platform, options.optimization, options.strip),
|
||||
outputPath: options.outPath,
|
||||
inputs: [
|
||||
options.programObject,
|
||||
...(options.ffi?.libraries ?? []),
|
||||
...runtimePack.runtimeObjects,
|
||||
...runtimePack.archives,
|
||||
],
|
||||
systemLibraries: [...new Set([
|
||||
...(options.ffi?.systemLibraries ?? []),
|
||||
...runtimePack.systemLibraries,
|
||||
])],
|
||||
driverFlags: [
|
||||
...(options.ffi?.frameworks ?? []).flatMap(name => ["-framework", name]),
|
||||
...options.target.executableLinkerArgs.map((arg, index, args) =>
|
||||
index > 0 && args[index - 1] === "-target" ? options.target.linkerTargetTriple : arg
|
||||
),
|
||||
...executableOptimizationLinkerArgs(options.target.platform, options.optimization),
|
||||
...executableStripLinkerArgs(options.target.platform, options.strip ?? false),
|
||||
...windowsSubsystemLinkerArgs(options.target.platform, options.windowsSubsystem),
|
||||
],
|
||||
...executableLinkInputs({
|
||||
target: options.target,
|
||||
programObject: options.programObject,
|
||||
ffiLibraries: options.ffi?.libraries ?? [],
|
||||
ffiSystemLibraries: options.ffi?.systemLibraries ?? [],
|
||||
ffiFrameworks: options.ffi?.frameworks ?? [],
|
||||
runtimeObjects: runtimePack.runtimeObjects,
|
||||
runtimeArchives: runtimePack.archives,
|
||||
runtimeSystemLibraries: runtimePack.systemLibraries,
|
||||
optimization: options.optimization,
|
||||
...(options.strip === undefined ? {} : { strip: options.strip }),
|
||||
...(options.windowsSubsystem === undefined ? {} : { windowsSubsystem: options.windowsSubsystem }),
|
||||
}),
|
||||
dependencyPaths: [
|
||||
...runtimePack.dependencyPaths,
|
||||
...(options.ffi?.libraries ?? []),
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/** Native codegen helper protocol shared by compiler hosts. */
|
||||
import type { NativeHelperSpec, NativeTargetSpec } from "./targets.js";
|
||||
|
||||
export const NATIVE_CODEGEN_PROTOCOL_VERSION = "1";
|
||||
export const NATIVE_CODEGEN_LLVM_VERSION = "22.1.8";
|
||||
|
||||
export type NativeCodegenOutputKind = "asm" | "obj";
|
||||
|
||||
export class NativeCodegenError extends Error {
|
||||
constructor(
|
||||
readonly diagnosticCode: "SC3002" | "SC3003" | "SC3004",
|
||||
message: string,
|
||||
readonly detailCode?: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "NativeCodegenError";
|
||||
}
|
||||
}
|
||||
|
||||
export interface NativeCodegenVersion {
|
||||
ok: true;
|
||||
protocol_version: string;
|
||||
scriptc_package_version: string;
|
||||
llvm_version: string;
|
||||
host_triple: string;
|
||||
targets: string[];
|
||||
supported_targets: string[];
|
||||
default_target: string;
|
||||
data_layout: string;
|
||||
}
|
||||
|
||||
export function validateNativeCodegenVersion(
|
||||
value: Record<string, unknown>,
|
||||
target: NativeTargetSpec,
|
||||
helper: NativeHelperSpec,
|
||||
expectedPackageVersion: string,
|
||||
): NativeCodegenVersion {
|
||||
const mismatch = (field: string, expected: string): never => {
|
||||
throw new NativeCodegenError(
|
||||
"SC3003",
|
||||
`LLVM native helper is incompatible: ${field} is ${JSON.stringify(value[field])}, expected ${JSON.stringify(expected)}; reinstall scriptc so its compiler and ${helper.packageName} packages have matching versions`,
|
||||
"version_mismatch",
|
||||
);
|
||||
};
|
||||
if (value["ok"] !== true) mismatch("ok", "true");
|
||||
if (value["protocol_version"] !== NATIVE_CODEGEN_PROTOCOL_VERSION) {
|
||||
mismatch("protocol_version", NATIVE_CODEGEN_PROTOCOL_VERSION);
|
||||
}
|
||||
if (value["scriptc_package_version"] !== expectedPackageVersion) {
|
||||
mismatch("scriptc_package_version", expectedPackageVersion);
|
||||
}
|
||||
if (value["llvm_version"] !== NATIVE_CODEGEN_LLVM_VERSION) {
|
||||
mismatch("llvm_version", NATIVE_CODEGEN_LLVM_VERSION);
|
||||
}
|
||||
if (value["default_target"] !== helper.defaultTarget) {
|
||||
mismatch("default_target", helper.defaultTarget);
|
||||
}
|
||||
if (value["data_layout"] !== helper.defaultDataLayout) {
|
||||
mismatch("data_layout", helper.defaultDataLayout);
|
||||
}
|
||||
if (!Array.isArray(value["targets"]) || !value["targets"].includes(target.llvmBackend)) {
|
||||
mismatch("targets", `an array containing ${target.llvmBackend}`);
|
||||
}
|
||||
if (!Array.isArray(value["supported_targets"]) || !value["supported_targets"].includes(target.llvmTriple)) {
|
||||
mismatch("supported_targets", `an array containing ${target.llvmTriple}`);
|
||||
}
|
||||
if (typeof value["host_triple"] !== "string") mismatch("host_triple", "a string");
|
||||
return value as unknown as NativeCodegenVersion;
|
||||
}
|
||||
@@ -28,34 +28,16 @@ import {
|
||||
} from "./targets.js";
|
||||
import { compilerReleaseVersion } from "../library/sidecar.js";
|
||||
|
||||
import {
|
||||
NativeCodegenError, validateNativeCodegenVersion as validateHelperVersion,
|
||||
type NativeCodegenVersion, type NativeCodegenOutputKind,
|
||||
} from "./native-codegen-core.js";
|
||||
export {
|
||||
NATIVE_CODEGEN_PROTOCOL_VERSION, NATIVE_CODEGEN_LLVM_VERSION, NativeCodegenError,
|
||||
type NativeCodegenVersion, type NativeCodegenOutputKind,
|
||||
} from "./native-codegen-core.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
export const NATIVE_CODEGEN_PROTOCOL_VERSION = "1";
|
||||
export const NATIVE_CODEGEN_LLVM_VERSION = "22.1.8";
|
||||
|
||||
export type NativeCodegenOutputKind = "asm" | "obj";
|
||||
|
||||
export class NativeCodegenError extends Error {
|
||||
constructor(
|
||||
readonly diagnosticCode: "SC3002" | "SC3003" | "SC3004",
|
||||
message: string,
|
||||
readonly detailCode?: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = "NativeCodegenError";
|
||||
}
|
||||
}
|
||||
|
||||
export interface NativeCodegenVersion {
|
||||
ok: true;
|
||||
protocol_version: string;
|
||||
scriptc_package_version: string;
|
||||
llvm_version: string;
|
||||
host_triple: string;
|
||||
targets: string[];
|
||||
supported_targets: string[];
|
||||
default_target: string;
|
||||
data_layout: string;
|
||||
}
|
||||
|
||||
interface HelperIdentity {
|
||||
packageName: string;
|
||||
@@ -132,42 +114,9 @@ async function invoke(binaryPath: string, args: string[]): Promise<{ stdout: str
|
||||
}
|
||||
|
||||
export function validateNativeCodegenVersion(
|
||||
value: Record<string, unknown>,
|
||||
target: NativeTargetSpec,
|
||||
helper: NativeHelperSpec,
|
||||
value: Record<string, unknown>, target: NativeTargetSpec, helper: NativeHelperSpec,
|
||||
): NativeCodegenVersion {
|
||||
const expectedPackageVersion = compilerReleaseVersion();
|
||||
const mismatch = (field: string, expected: string): never => {
|
||||
throw new NativeCodegenError(
|
||||
"SC3003",
|
||||
`LLVM native helper is incompatible: ${field} is ${JSON.stringify(value[field])}, expected ${JSON.stringify(expected)}; reinstall scriptc so its compiler and ${helper.packageName} packages have matching versions`,
|
||||
"version_mismatch",
|
||||
);
|
||||
};
|
||||
if (value["ok"] !== true) mismatch("ok", "true");
|
||||
if (value["protocol_version"] !== NATIVE_CODEGEN_PROTOCOL_VERSION) {
|
||||
mismatch("protocol_version", NATIVE_CODEGEN_PROTOCOL_VERSION);
|
||||
}
|
||||
if (value["scriptc_package_version"] !== expectedPackageVersion) {
|
||||
mismatch("scriptc_package_version", expectedPackageVersion);
|
||||
}
|
||||
if (value["llvm_version"] !== NATIVE_CODEGEN_LLVM_VERSION) {
|
||||
mismatch("llvm_version", NATIVE_CODEGEN_LLVM_VERSION);
|
||||
}
|
||||
if (value["default_target"] !== helper.defaultTarget) {
|
||||
mismatch("default_target", helper.defaultTarget);
|
||||
}
|
||||
if (value["data_layout"] !== helper.defaultDataLayout) {
|
||||
mismatch("data_layout", helper.defaultDataLayout);
|
||||
}
|
||||
if (!Array.isArray(value["targets"]) || !value["targets"].includes(target.llvmBackend)) {
|
||||
mismatch("targets", `an array containing ${target.llvmBackend}`);
|
||||
}
|
||||
if (!Array.isArray(value["supported_targets"]) || !value["supported_targets"].includes(target.llvmTriple)) {
|
||||
mismatch("supported_targets", `an array containing ${target.llvmTriple}`);
|
||||
}
|
||||
if (typeof value["host_triple"] !== "string") mismatch("host_triple", "a string");
|
||||
return value as unknown as NativeCodegenVersion;
|
||||
return validateHelperVersion(value, target, helper, compilerReleaseVersion());
|
||||
}
|
||||
|
||||
async function resolveHelper(
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/** Native tool invocation without a JavaScript host or shell command construction. */
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync, statSync } from "node:fs";
|
||||
import type { NativeHelperSpec, NativeTargetSpec } from "./targets.js";
|
||||
import { validateNativeCodegenVersion } from "./native-codegen-core.js";
|
||||
|
||||
export function runNativeTool(executable: string, args: string[]): string {
|
||||
const result = spawnSync(executable, args, { encoding: "utf8" });
|
||||
if (result.error) throw new Error(`${executable}: ${result.error.message}`);
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${executable} failed (${result.signal ?? String(result.status)}): ${result.stderr.trim()}`);
|
||||
}
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
export function requireNativeArtifact(path: string): void {
|
||||
const info = statSync(path);
|
||||
if (!info.isFile() || info.size === 0) throw new Error(`native tool produced no artifact: ${path}`);
|
||||
}
|
||||
|
||||
export function emitNativeObject(options: {
|
||||
executable: string;
|
||||
packageRoot: string;
|
||||
compilerVersion: string;
|
||||
target: NativeTargetSpec;
|
||||
helper: NativeHelperSpec;
|
||||
inputPath: string;
|
||||
outputPath: string;
|
||||
sourcePath: string;
|
||||
optimization: "release" | "dev";
|
||||
}): void {
|
||||
const identity = JSON.parse(readFileSync(options.packageRoot + "/package.json", "utf8")) as { name: string; version: string };
|
||||
if (identity.name !== options.helper.packageName || identity.version !== options.compilerVersion) {
|
||||
throw new Error(`LLVM helper package mismatch: expected ${options.helper.packageName}@${options.compilerVersion}`);
|
||||
}
|
||||
const version = JSON.parse(runNativeTool(options.executable, ["version", "--format=json"])) as Record<string, unknown>;
|
||||
validateNativeCodegenVersion(version, options.target, options.helper, options.compilerVersion);
|
||||
runNativeTool(options.executable, [
|
||||
"emit", "--input", options.inputPath, "--output", options.outputPath, "--filetype", "obj",
|
||||
"--target", options.target.llvmTriple, "--opt-level", options.optimization === "dev" ? "0" : "2",
|
||||
"--relocation-model", options.target.relocationModel, "--diagnostic-format", "json",
|
||||
"--source-path", options.sourcePath,
|
||||
]);
|
||||
requireNativeArtifact(options.outputPath);
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/** Runtime-pack schema and selection shared by compiler hosts. Filesystem
|
||||
* discovery, artifact verification and staging belong to the calling host. */
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import { RUNTIME_ABI_MARKER, RUNTIME_ABI_VERSION } from "./runtime-abi.js";
|
||||
import type { NativeTargetSpec } from "./targets.js";
|
||||
|
||||
export const RUNTIME_PACK_SCHEMA = "scriptc.runtime-pack.v1" as const;
|
||||
export const RUNTIME_PACK_FORMAT = 1 as const;
|
||||
|
||||
export type RuntimePredicate =
|
||||
| boolean
|
||||
| string
|
||||
| { all?: string[]; any?: string[]; not?: string[] };
|
||||
|
||||
export interface RuntimePackArtifact {
|
||||
path: string;
|
||||
sha256: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface RuntimePackVariant extends RuntimePackArtifact {
|
||||
id: string;
|
||||
when: Record<string, boolean>;
|
||||
defines: string[];
|
||||
}
|
||||
|
||||
interface RuntimePackUnit {
|
||||
source: string;
|
||||
predicate: RuntimePredicate;
|
||||
variants: RuntimePackVariant[];
|
||||
}
|
||||
|
||||
interface RuntimePackArchive extends RuntimePackArtifact {
|
||||
id: "quickjs" | "libregexp" | "zlib" | "mbedtls";
|
||||
predicate: RuntimePredicate;
|
||||
}
|
||||
|
||||
export interface RuntimePackManifest {
|
||||
schema: typeof RUNTIME_PACK_SCHEMA;
|
||||
format: typeof RUNTIME_PACK_FORMAT;
|
||||
package: string;
|
||||
version: string;
|
||||
target: {
|
||||
name: NativeTargetSpec["name"];
|
||||
llvm_triple: NativeTargetSpec["llvmTriple"];
|
||||
architecture: NativeTargetSpec["architecture"];
|
||||
object_format: NativeTargetSpec["objectFormat"];
|
||||
minimum_os: NativeTargetSpec["minimumOs"];
|
||||
};
|
||||
runtime_abi: { version: number; marker: string };
|
||||
compiler: { command: string; identity: string; target: string };
|
||||
macros: {
|
||||
executable: string[];
|
||||
excluded: string[];
|
||||
sanitizer: "external-toolchain-required";
|
||||
};
|
||||
flavors: Record<"release" | "dev", {
|
||||
optimization: "-O2" | "-O0";
|
||||
runtime_units: RuntimePackUnit[];
|
||||
}>;
|
||||
archives: RuntimePackArchive[];
|
||||
system_libraries: { name: string; predicate: RuntimePredicate }[];
|
||||
licenses: { path: string; license: string }[];
|
||||
}
|
||||
|
||||
export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
nativeFetch: boolean;
|
||||
netIslandEffective: boolean;
|
||||
netEffective: boolean;
|
||||
httpEffective: boolean;
|
||||
tlsEffective: boolean;
|
||||
tlsCaEffective: boolean;
|
||||
zlibEffective: boolean;
|
||||
}
|
||||
|
||||
export class RuntimePackError extends Error {
|
||||
constructor(message: string, readonly code: "missing" | "invalid" | "unsupported") {
|
||||
super(message);
|
||||
this.name = "RuntimePackError";
|
||||
}
|
||||
}
|
||||
|
||||
function object(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
function validDigest(value: unknown): value is string {
|
||||
return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
||||
}
|
||||
|
||||
function validPredicate(value: unknown): value is RuntimePredicate {
|
||||
if (typeof value === "boolean" || typeof value === "string") return true;
|
||||
const item = object(value);
|
||||
if (item === null) return false;
|
||||
const keys = Object.keys(item);
|
||||
if (keys.some((key) => key !== "all" && key !== "any" && key !== "not")) return false;
|
||||
return keys.length > 0 && keys.every((key) =>
|
||||
Array.isArray(item[key]) && (item[key] as unknown[]).every((feature) => typeof feature === "string")
|
||||
);
|
||||
}
|
||||
|
||||
function validArtifact(value: unknown): value is RuntimePackArtifact {
|
||||
const item = object(value);
|
||||
return item !== null && typeof item.path === "string" && !item.path.startsWith("/") &&
|
||||
!item.path.split(/[\\/]/).includes("..") && validDigest(item.sha256) &&
|
||||
typeof item.size === "number" && Number.isInteger(item.size) && item.size >= 0;
|
||||
}
|
||||
|
||||
export function parseRuntimePackManifest(value: unknown): RuntimePackManifest {
|
||||
const manifest = object(value);
|
||||
const target = object(manifest?.target);
|
||||
const abi = object(manifest?.runtime_abi);
|
||||
const compiler = object(manifest?.compiler);
|
||||
const macros = object(manifest?.macros);
|
||||
const flavors = object(manifest?.flavors);
|
||||
const validFlavor = (value: unknown, optimization: string): boolean => {
|
||||
const flavor = object(value);
|
||||
return flavor?.optimization === optimization && Array.isArray(flavor.runtime_units) &&
|
||||
flavor.runtime_units.every((raw) => {
|
||||
const unit = object(raw);
|
||||
return typeof unit?.source === "string" && validPredicate(unit.predicate) &&
|
||||
Array.isArray(unit.variants) && unit.variants.length > 0 && unit.variants.every((variantRaw) => {
|
||||
const variant = object(variantRaw);
|
||||
const when = object(variant?.when);
|
||||
return validArtifact(variantRaw) && typeof variant?.id === "string" && when !== null &&
|
||||
Object.values(when).every((entry) => typeof entry === "boolean") &&
|
||||
Array.isArray(variant.defines) && variant.defines.every((entry) => typeof entry === "string");
|
||||
});
|
||||
});
|
||||
};
|
||||
if (
|
||||
manifest?.schema !== RUNTIME_PACK_SCHEMA || manifest.format !== RUNTIME_PACK_FORMAT ||
|
||||
typeof manifest.package !== "string" || typeof manifest.version !== "string" ||
|
||||
!isRuntimePackTarget(target) ||
|
||||
abi?.version !== RUNTIME_ABI_VERSION || abi.marker !== RUNTIME_ABI_MARKER ||
|
||||
typeof compiler?.command !== "string" || typeof compiler.identity !== "string" ||
|
||||
compiler.target !== (target?.llvm_triple) ||
|
||||
!Array.isArray(macros?.executable) || !macros.executable.every((entry) => typeof entry === "string") ||
|
||||
!Array.isArray(macros.excluded) || !macros.excluded.every((entry) => typeof entry === "string") ||
|
||||
macros.sanitizer !== "external-toolchain-required" ||
|
||||
flavors === null || !validFlavor(flavors.release, "-O2") || !validFlavor(flavors.dev, "-O0") ||
|
||||
!Array.isArray(manifest.archives) || !manifest.archives.every((raw) => {
|
||||
const archive = object(raw);
|
||||
return validArtifact(raw) && typeof archive?.id === "string" && validPredicate(archive.predicate);
|
||||
}) ||
|
||||
!Array.isArray(manifest.system_libraries) || !manifest.system_libraries.every((raw) => {
|
||||
const library = object(raw);
|
||||
return typeof library?.name === "string" && validPredicate(library.predicate);
|
||||
}) ||
|
||||
!Array.isArray(manifest.licenses) || !manifest.licenses.every((raw) => {
|
||||
const license = object(raw);
|
||||
return typeof license?.path === "string" && typeof license.license === "string";
|
||||
})
|
||||
) throw new RuntimePackError("installed runtime-pack.json is malformed or incompatible", "invalid");
|
||||
return manifest as unknown as RuntimePackManifest;
|
||||
}
|
||||
|
||||
function isRuntimePackTarget(target: Record<string, unknown> | null): boolean {
|
||||
return target !== null &&
|
||||
typeof target.name === "string" &&
|
||||
typeof target.llvm_triple === "string" &&
|
||||
(target.architecture === "arm64" || target.architecture === "x64" || target.architecture === "wasm32") &&
|
||||
(target.object_format === "macho" || target.object_format === "elf" || target.object_format === "coff" || target.object_format === "wasm") &&
|
||||
typeof target.minimum_os === "string";
|
||||
}
|
||||
|
||||
export function effectiveRuntimeFeatures(
|
||||
features: NativeLinkFeatures,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): RuntimeFeatureSet {
|
||||
const curlFetch = features.dynamic && features.fetch && env["SCRIPTC_FETCH_CURL"] === "1";
|
||||
if (curlFetch) {
|
||||
throw new RuntimePackError(
|
||||
"SCRIPTC_FETCH_CURL=1 is an external developer-toolchain comparison mode and is not available with precompiled runtime packs",
|
||||
"unsupported",
|
||||
);
|
||||
}
|
||||
const nativeFetch = features.fetch;
|
||||
const netIslandEffective = features.dynamic && (features.netIsland || nativeFetch);
|
||||
const netEffective = features.net || nativeFetch || netIslandEffective;
|
||||
const httpEffective = features.http || nativeFetch || netIslandEffective;
|
||||
const tlsEffective = features.tls || nativeFetch || netIslandEffective;
|
||||
const tlsCaEffective = features.tlsCa || tlsEffective;
|
||||
return {
|
||||
...features,
|
||||
nativeFetch,
|
||||
netIslandEffective,
|
||||
netEffective,
|
||||
httpEffective,
|
||||
tlsEffective,
|
||||
tlsCaEffective,
|
||||
zlibEffective: features.zlib || nativeFetch,
|
||||
};
|
||||
}
|
||||
|
||||
export function evaluateRuntimePredicate(
|
||||
predicate: RuntimePredicate,
|
||||
features: object,
|
||||
): boolean {
|
||||
const values = features as Record<string, boolean>;
|
||||
if (typeof predicate === "boolean") return predicate;
|
||||
if (typeof predicate === "string") return values[predicate] === true;
|
||||
return (predicate.all?.every((name) => values[name] === true) ?? true) &&
|
||||
(predicate.any?.some((name) => values[name] === true) ?? true) &&
|
||||
(predicate.not?.every((name) => values[name] !== true) ?? true);
|
||||
}
|
||||
|
||||
function selectVariant(unit: RuntimePackUnit, features: RuntimeFeatureSet): RuntimePackVariant {
|
||||
const matches = unit.variants.filter((variant) =>
|
||||
Object.entries(variant.when).every(([name, expected]) => features[name as keyof RuntimeFeatureSet] === expected)
|
||||
);
|
||||
matches.sort((a, b) => Object.keys(b.when).length - Object.keys(a.when).length || a.id.localeCompare(b.id));
|
||||
const selected = matches[0];
|
||||
if (selected === undefined) {
|
||||
throw new RuntimePackError(`runtime pack has no variant for ${unit.source}`, "invalid");
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
/** Validate package and target identity before selecting any native input. */
|
||||
export function validateRuntimePackIdentity(
|
||||
manifest: RuntimePackManifest,
|
||||
packageName: string | undefined,
|
||||
packageVersion: string | undefined,
|
||||
target: NativeTargetSpec,
|
||||
compilerVersion: string,
|
||||
): void {
|
||||
if (
|
||||
packageName !== target.runtimePackPackage || manifest.package !== target.runtimePackPackage ||
|
||||
packageVersion !== compilerVersion || manifest.version !== compilerVersion
|
||||
) {
|
||||
throw new RuntimePackError(
|
||||
`runtime pack version mismatch: expected ${target.runtimePackPackage}@${compilerVersion}, found ${packageName}@${packageVersion}`,
|
||||
"invalid",
|
||||
);
|
||||
}
|
||||
if (
|
||||
manifest.target.name !== target.name || manifest.target.llvm_triple !== target.llvmTriple ||
|
||||
manifest.target.architecture !== target.architecture || manifest.target.object_format !== target.objectFormat ||
|
||||
manifest.target.minimum_os !== target.minimumOs
|
||||
) throw new RuntimePackError(`runtime pack does not support target ${target.name}`, "invalid");
|
||||
}
|
||||
|
||||
export interface RuntimePackArtifacts {
|
||||
features: RuntimeFeatureSet;
|
||||
runtime: RuntimePackArtifact[];
|
||||
archives: RuntimePackArtifact[];
|
||||
systemLibraries: string[];
|
||||
}
|
||||
|
||||
/** Selection is deterministic and independent of installation paths. Both
|
||||
* hosts verify and privately stage exactly these artifacts before linking. */
|
||||
export function selectRuntimePackArtifacts(
|
||||
manifest: RuntimePackManifest,
|
||||
requested: NativeLinkFeatures,
|
||||
flavor: "release" | "dev",
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): RuntimePackArtifacts {
|
||||
const features = effectiveRuntimeFeatures(requested, env);
|
||||
return {
|
||||
features,
|
||||
runtime: manifest.flavors[flavor].runtime_units
|
||||
.filter((unit) => evaluateRuntimePredicate(unit.predicate, features))
|
||||
.map((unit) => selectVariant(unit, features)),
|
||||
archives: manifest.archives.filter((archive) => evaluateRuntimePredicate(archive.predicate, features)),
|
||||
systemLibraries: manifest.system_libraries
|
||||
.filter((entry) => evaluateRuntimePredicate(entry.predicate, features))
|
||||
.map((entry) => entry.name),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/** Synchronous runtime-pack host for the statically compiled driver. Each
|
||||
* selected artifact is hashed and copied from the same read into private
|
||||
* storage. This host has no persistent cache or mutable installed link inputs. */
|
||||
import { createHash } from "node:crypto";
|
||||
import { closeSync, constants, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import type { NativeLinkFeatures } from "./native-link-info.js";
|
||||
import type { NativeTargetSpec } from "./targets.js";
|
||||
import {
|
||||
RuntimePackError, parseRuntimePackManifest, selectRuntimePackArtifacts, validateRuntimePackIdentity,
|
||||
type RuntimePackArtifact,
|
||||
} from "./runtime-pack-core.js";
|
||||
|
||||
export interface NativeRuntimePack {
|
||||
runtimeObjects: string[];
|
||||
archives: string[];
|
||||
systemLibraries: string[];
|
||||
}
|
||||
|
||||
function stageArtifact(root: string, stage: string, artifact: RuntimePackArtifact): string {
|
||||
const source = join(root, artifact.path);
|
||||
const destination = join(stage, artifact.path);
|
||||
let bytes: Buffer;
|
||||
try { bytes = readFileSync(source); }
|
||||
catch { throw new RuntimePackError(`runtime pack artifact is missing: ${artifact.path}`, "invalid"); }
|
||||
if (bytes.length !== artifact.size || createHash("sha256").update(bytes).digest("hex") !== artifact.sha256) {
|
||||
throw new RuntimePackError(`runtime pack artifact hash mismatch: ${artifact.path}`, "invalid");
|
||||
}
|
||||
mkdirSync(dirname(destination), { recursive: true });
|
||||
const fd = openSync(destination, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o400);
|
||||
try {
|
||||
let offset = 0;
|
||||
while (offset < bytes.length) {
|
||||
const written = writeSync(fd, bytes, offset, bytes.length - offset);
|
||||
if (written === 0) throw new RuntimePackError(`could not stage runtime pack artifact: ${artifact.path}`, "invalid");
|
||||
offset += written;
|
||||
}
|
||||
} finally { closeSync(fd); }
|
||||
return destination;
|
||||
}
|
||||
|
||||
/** stageRoot must be a new private directory owned by the caller. The caller
|
||||
* removes it on success or failure, after all native tools have finished. */
|
||||
export function stageNativeRuntimePack(
|
||||
root: string,
|
||||
stageRoot: string,
|
||||
target: NativeTargetSpec,
|
||||
compilerVersion: string,
|
||||
features: NativeLinkFeatures,
|
||||
flavor: "release" | "dev",
|
||||
): NativeRuntimePack {
|
||||
const packagePath = join(root, "package.json");
|
||||
const manifestPath = join(root, "runtime-pack.json");
|
||||
const packageText = readFileSync(packagePath, "utf8");
|
||||
const manifestText = readFileSync(manifestPath, "utf8");
|
||||
const identity = JSON.parse(packageText) as { name?: string; version?: string };
|
||||
const manifest = parseRuntimePackManifest(JSON.parse(manifestText));
|
||||
validateRuntimePackIdentity(manifest, identity.name, identity.version, target, compilerVersion);
|
||||
const selected = selectRuntimePackArtifacts(manifest, features, flavor);
|
||||
const runtimeObjects = selected.runtime.map((artifact) => stageArtifact(root, stageRoot, artifact));
|
||||
const archives = selected.archives.map((artifact) => stageArtifact(root, stageRoot, artifact));
|
||||
for (const license of manifest.licenses) {
|
||||
try { readFileSync(join(root, license.path)); }
|
||||
catch { throw new RuntimePackError("runtime pack license payload is incomplete", "invalid"); }
|
||||
}
|
||||
if (readFileSync(packagePath, "utf8") !== packageText || readFileSync(manifestPath, "utf8") !== manifestText) {
|
||||
throw new RuntimePackError("runtime pack changed while staging verified artifacts", "invalid");
|
||||
}
|
||||
return { runtimeObjects, archives, systemLibraries: selected.systemLibraries };
|
||||
}
|
||||
@@ -9,77 +9,17 @@ import {
|
||||
nativeArtifactDependenciesStillMatch,
|
||||
type NativeArtifactDependency,
|
||||
} from "./native-toolchain.js";
|
||||
import { RUNTIME_ABI_MARKER, RUNTIME_ABI_VERSION } from "./runtime-abi.js";
|
||||
import type { NativeTargetSpec } from "./targets.js";
|
||||
|
||||
export const RUNTIME_PACK_SCHEMA = "scriptc.runtime-pack.v1" as const;
|
||||
export const RUNTIME_PACK_FORMAT = 1 as const;
|
||||
|
||||
export type RuntimePredicate =
|
||||
| boolean
|
||||
| string
|
||||
| { all?: string[]; any?: string[]; not?: string[] };
|
||||
|
||||
interface RuntimePackArtifact {
|
||||
path: string;
|
||||
sha256: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface RuntimePackVariant extends RuntimePackArtifact {
|
||||
id: string;
|
||||
when: Record<string, boolean>;
|
||||
defines: string[];
|
||||
}
|
||||
|
||||
interface RuntimePackUnit {
|
||||
source: string;
|
||||
predicate: RuntimePredicate;
|
||||
variants: RuntimePackVariant[];
|
||||
}
|
||||
|
||||
interface RuntimePackArchive extends RuntimePackArtifact {
|
||||
id: "quickjs" | "libregexp" | "zlib" | "mbedtls";
|
||||
predicate: RuntimePredicate;
|
||||
}
|
||||
|
||||
export interface RuntimePackManifest {
|
||||
schema: typeof RUNTIME_PACK_SCHEMA;
|
||||
format: typeof RUNTIME_PACK_FORMAT;
|
||||
package: string;
|
||||
version: string;
|
||||
target: {
|
||||
name: NativeTargetSpec["name"];
|
||||
llvm_triple: NativeTargetSpec["llvmTriple"];
|
||||
architecture: NativeTargetSpec["architecture"];
|
||||
object_format: NativeTargetSpec["objectFormat"];
|
||||
minimum_os: NativeTargetSpec["minimumOs"];
|
||||
};
|
||||
runtime_abi: { version: number; marker: string };
|
||||
compiler: { command: string; identity: string; target: string };
|
||||
macros: {
|
||||
executable: string[];
|
||||
excluded: string[];
|
||||
sanitizer: "external-toolchain-required";
|
||||
};
|
||||
flavors: Record<"release" | "dev", {
|
||||
optimization: "-O2" | "-O0";
|
||||
runtime_units: RuntimePackUnit[];
|
||||
}>;
|
||||
archives: RuntimePackArchive[];
|
||||
system_libraries: { name: string; predicate: RuntimePredicate }[];
|
||||
licenses: { path: string; license: string }[];
|
||||
}
|
||||
|
||||
export interface RuntimeFeatureSet extends NativeLinkFeatures {
|
||||
nativeFetch: boolean;
|
||||
netIslandEffective: boolean;
|
||||
netEffective: boolean;
|
||||
httpEffective: boolean;
|
||||
tlsEffective: boolean;
|
||||
tlsCaEffective: boolean;
|
||||
zlibEffective: boolean;
|
||||
}
|
||||
import {
|
||||
RuntimePackError, parseRuntimePackManifest, selectRuntimePackArtifacts, validateRuntimePackIdentity,
|
||||
type RuntimePackArtifact, type RuntimePackManifest, type RuntimeFeatureSet,
|
||||
} from "./runtime-pack-core.js";
|
||||
export {
|
||||
RUNTIME_PACK_SCHEMA, RUNTIME_PACK_FORMAT, RuntimePackError, parseRuntimePackManifest,
|
||||
effectiveRuntimeFeatures, evaluateRuntimePredicate,
|
||||
type RuntimePackManifest, type RuntimePredicate, type RuntimeFeatureSet,
|
||||
} from "./runtime-pack-core.js";
|
||||
|
||||
export interface RuntimePackSelection {
|
||||
root: string;
|
||||
@@ -97,152 +37,6 @@ export interface RuntimePackSelection {
|
||||
selectedArchiveArtifacts: RuntimePackArtifact[];
|
||||
}
|
||||
|
||||
export class RuntimePackError extends Error {
|
||||
constructor(message: string, readonly code: "missing" | "invalid" | "unsupported") {
|
||||
super(message);
|
||||
this.name = "RuntimePackError";
|
||||
}
|
||||
}
|
||||
|
||||
function object(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
function validDigest(value: unknown): value is string {
|
||||
return typeof value === "string" && /^[0-9a-f]{64}$/.test(value);
|
||||
}
|
||||
|
||||
function validPredicate(value: unknown): value is RuntimePredicate {
|
||||
if (typeof value === "boolean" || typeof value === "string") return true;
|
||||
const item = object(value);
|
||||
if (item === null) return false;
|
||||
const keys = Object.keys(item);
|
||||
if (keys.some((key) => key !== "all" && key !== "any" && key !== "not")) return false;
|
||||
return keys.length > 0 && keys.every((key) =>
|
||||
Array.isArray(item[key]) && (item[key] as unknown[]).every((feature) => typeof feature === "string")
|
||||
);
|
||||
}
|
||||
|
||||
function validArtifact(value: unknown): value is RuntimePackArtifact {
|
||||
const item = object(value);
|
||||
return item !== null && typeof item.path === "string" && !item.path.startsWith("/") &&
|
||||
!item.path.split(/[\\/]/).includes("..") && validDigest(item.sha256) &&
|
||||
typeof item.size === "number" && Number.isInteger(item.size) && item.size >= 0;
|
||||
}
|
||||
|
||||
export function parseRuntimePackManifest(value: unknown): RuntimePackManifest {
|
||||
const manifest = object(value);
|
||||
const target = object(manifest?.target);
|
||||
const abi = object(manifest?.runtime_abi);
|
||||
const compiler = object(manifest?.compiler);
|
||||
const macros = object(manifest?.macros);
|
||||
const flavors = object(manifest?.flavors);
|
||||
const validFlavor = (value: unknown, optimization: string): boolean => {
|
||||
const flavor = object(value);
|
||||
return flavor?.optimization === optimization && Array.isArray(flavor.runtime_units) &&
|
||||
flavor.runtime_units.every((raw) => {
|
||||
const unit = object(raw);
|
||||
return typeof unit?.source === "string" && validPredicate(unit.predicate) &&
|
||||
Array.isArray(unit.variants) && unit.variants.length > 0 && unit.variants.every((variantRaw) => {
|
||||
const variant = object(variantRaw);
|
||||
const when = object(variant?.when);
|
||||
return validArtifact(variantRaw) && typeof variant?.id === "string" && when !== null &&
|
||||
Object.values(when).every((entry) => typeof entry === "boolean") &&
|
||||
Array.isArray(variant.defines) && variant.defines.every((entry) => typeof entry === "string");
|
||||
});
|
||||
});
|
||||
};
|
||||
if (
|
||||
manifest?.schema !== RUNTIME_PACK_SCHEMA || manifest.format !== RUNTIME_PACK_FORMAT ||
|
||||
typeof manifest.package !== "string" || typeof manifest.version !== "string" ||
|
||||
!isRuntimePackTarget(target) ||
|
||||
abi?.version !== RUNTIME_ABI_VERSION || abi.marker !== RUNTIME_ABI_MARKER ||
|
||||
typeof compiler?.command !== "string" || typeof compiler.identity !== "string" ||
|
||||
compiler.target !== (target?.llvm_triple) ||
|
||||
!Array.isArray(macros?.executable) || !macros.executable.every((entry) => typeof entry === "string") ||
|
||||
!Array.isArray(macros.excluded) || !macros.excluded.every((entry) => typeof entry === "string") ||
|
||||
macros.sanitizer !== "external-toolchain-required" ||
|
||||
flavors === null || !validFlavor(flavors.release, "-O2") || !validFlavor(flavors.dev, "-O0") ||
|
||||
!Array.isArray(manifest.archives) || !manifest.archives.every((raw) => {
|
||||
const archive = object(raw);
|
||||
return validArtifact(raw) && typeof archive?.id === "string" && validPredicate(archive.predicate);
|
||||
}) ||
|
||||
!Array.isArray(manifest.system_libraries) || !manifest.system_libraries.every((raw) => {
|
||||
const library = object(raw);
|
||||
return typeof library?.name === "string" && validPredicate(library.predicate);
|
||||
}) ||
|
||||
!Array.isArray(manifest.licenses) || !manifest.licenses.every((raw) => {
|
||||
const license = object(raw);
|
||||
return typeof license?.path === "string" && typeof license.license === "string";
|
||||
})
|
||||
) throw new RuntimePackError("installed runtime-pack.json is malformed or incompatible", "invalid");
|
||||
return manifest as unknown as RuntimePackManifest;
|
||||
}
|
||||
|
||||
function isRuntimePackTarget(target: Record<string, unknown> | null): boolean {
|
||||
return target !== null &&
|
||||
typeof target.name === "string" &&
|
||||
typeof target.llvm_triple === "string" &&
|
||||
(target.architecture === "arm64" || target.architecture === "x64" || target.architecture === "wasm32") &&
|
||||
(target.object_format === "macho" || target.object_format === "elf" || target.object_format === "coff" || target.object_format === "wasm") &&
|
||||
typeof target.minimum_os === "string";
|
||||
}
|
||||
|
||||
export function effectiveRuntimeFeatures(
|
||||
features: NativeLinkFeatures,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): RuntimeFeatureSet {
|
||||
const curlFetch = features.dynamic && features.fetch && env["SCRIPTC_FETCH_CURL"] === "1";
|
||||
if (curlFetch) {
|
||||
throw new RuntimePackError(
|
||||
"SCRIPTC_FETCH_CURL=1 is an external developer-toolchain comparison mode and is not available with precompiled runtime packs",
|
||||
"unsupported",
|
||||
);
|
||||
}
|
||||
const nativeFetch = features.fetch;
|
||||
const netIslandEffective = features.dynamic && (features.netIsland || nativeFetch);
|
||||
const netEffective = features.net || nativeFetch || netIslandEffective;
|
||||
const httpEffective = features.http || nativeFetch || netIslandEffective;
|
||||
const tlsEffective = features.tls || nativeFetch || netIslandEffective;
|
||||
const tlsCaEffective = features.tlsCa || tlsEffective;
|
||||
return {
|
||||
...features,
|
||||
nativeFetch,
|
||||
netIslandEffective,
|
||||
netEffective,
|
||||
httpEffective,
|
||||
tlsEffective,
|
||||
tlsCaEffective,
|
||||
zlibEffective: features.zlib || nativeFetch,
|
||||
};
|
||||
}
|
||||
|
||||
export function evaluateRuntimePredicate(
|
||||
predicate: RuntimePredicate,
|
||||
features: object,
|
||||
): boolean {
|
||||
const values = features as Record<string, boolean>;
|
||||
if (typeof predicate === "boolean") return predicate;
|
||||
if (typeof predicate === "string") return values[predicate] === true;
|
||||
return (predicate.all?.every((name) => values[name] === true) ?? true) &&
|
||||
(predicate.any?.some((name) => values[name] === true) ?? true) &&
|
||||
(predicate.not?.every((name) => values[name] !== true) ?? true);
|
||||
}
|
||||
|
||||
function selectVariant(unit: RuntimePackUnit, features: RuntimeFeatureSet): RuntimePackVariant {
|
||||
const matches = unit.variants.filter((variant) =>
|
||||
Object.entries(variant.when).every(([name, expected]) => features[name as keyof RuntimeFeatureSet] === expected)
|
||||
);
|
||||
matches.sort((a, b) => Object.keys(b.when).length - Object.keys(a.when).length || a.id.localeCompare(b.id));
|
||||
const selected = matches[0];
|
||||
if (selected === undefined) {
|
||||
throw new RuntimePackError(`runtime pack has no variant for ${unit.source}`, "invalid");
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
async function verifyArtifact(root: string, artifact: RuntimePackArtifact): Promise<string> {
|
||||
const path = join(root, artifact.path);
|
||||
let bytes: Buffer;
|
||||
@@ -362,29 +156,12 @@ export async function loadRuntimePack(options: {
|
||||
if (error instanceof RuntimePackError) throw error;
|
||||
throw new RuntimePackError(`could not read ${packageName}/runtime-pack.json`, "invalid");
|
||||
}
|
||||
if (
|
||||
packageManifest.name !== packageName || manifest.package !== packageName ||
|
||||
packageManifest.version !== compilerReleaseVersion() || manifest.version !== compilerReleaseVersion()
|
||||
) {
|
||||
throw new RuntimePackError(
|
||||
`runtime pack version mismatch: expected ${packageName}@${compilerReleaseVersion()}, found ${packageManifest.name}@${packageManifest.version}`,
|
||||
"invalid",
|
||||
);
|
||||
}
|
||||
if (
|
||||
manifest.target.name !== options.target.name ||
|
||||
manifest.target.llvm_triple !== options.target.llvmTriple ||
|
||||
manifest.target.architecture !== options.target.architecture ||
|
||||
manifest.target.object_format !== options.target.objectFormat ||
|
||||
manifest.target.minimum_os !== options.target.minimumOs
|
||||
) throw new RuntimePackError(`runtime pack does not support target ${options.target.name}`, "invalid");
|
||||
const features = effectiveRuntimeFeatures(options.features, options.env);
|
||||
validateRuntimePackIdentity(manifest, packageManifest.name, packageManifest.version, options.target, compilerReleaseVersion());
|
||||
const flavor = options.optimization;
|
||||
const selectedUnits = manifest.flavors[flavor].runtime_units
|
||||
.filter((unit) => evaluateRuntimePredicate(unit.predicate, features));
|
||||
const selectedVariants = selectedUnits.map((unit) => selectVariant(unit, features));
|
||||
const selectedArchives = manifest.archives
|
||||
.filter((archive) => evaluateRuntimePredicate(archive.predicate, features));
|
||||
const selected = selectRuntimePackArtifacts(manifest, options.features, flavor, options.env);
|
||||
const features = selected.features;
|
||||
const selectedVariants = selected.runtime;
|
||||
const selectedArchives = selected.archives;
|
||||
const selectedArtifactPaths = [
|
||||
...selectedVariants,
|
||||
...selectedArchives,
|
||||
@@ -414,9 +191,7 @@ export async function loadRuntimePack(options: {
|
||||
features,
|
||||
runtimeObjects,
|
||||
archives,
|
||||
systemLibraries: manifest.system_libraries
|
||||
.filter((entry) => evaluateRuntimePredicate(entry.predicate, features))
|
||||
.map((entry) => entry.name),
|
||||
systemLibraries: selected.systemLibraries,
|
||||
dependencyPaths: [packagePath, manifestPath, ...runtimeObjects, ...archives],
|
||||
sourceDependencies,
|
||||
selectedRuntimeArtifacts: selectedVariants,
|
||||
|
||||
@@ -23,7 +23,7 @@ import type { Lowerer } from "../lowerer.js";
|
||||
import { lowerIndexMergeHelper } from "../lower-containers.js";
|
||||
import type { IndexMergeContributor } from "../lower-containers.js";
|
||||
import { isGenericCallableMemberType } from "../../type-mapper.js";
|
||||
import { numLit, varRef } from "../../../ir/build.js";
|
||||
import { numLit, strLit, varRef } from "../../../ir/build.js";
|
||||
import { isSafeToRepeat } from "./evaluation-safety.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { fenceSymbolFieldCopy } from "../symbol-fields.js";
|
||||
@@ -211,7 +211,7 @@ export function lowerDynObjectLiteral(
|
||||
const descriptor: IrExpr = { kind: "dynObjLit", type: DYN, loc, fields: [
|
||||
{ key: { kind: "strLit", value: "value", type: STRING, loc }, value: field.value },
|
||||
...["writable", "enumerable", "configurable"].map(name => ({
|
||||
key: { kind: "strLit", value: name, type: STRING, loc } as IrExpr,
|
||||
key: strLit(name, loc),
|
||||
value: { kind: "dynFrom", value: { kind: "boolLit", value: true, type: BOOL, loc }, type: DYN, loc } as IrExpr,
|
||||
})),
|
||||
] };
|
||||
@@ -278,7 +278,7 @@ export function lowerDynObjectLiteral(
|
||||
kind: "dynObjLit", type: DYN, loc: locOf(prop), fields: [
|
||||
{ key: { kind: "strLit", value: ts.isGetAccessorDeclaration(prop) ? "get" : "set", type: STRING, loc }, value: lowerer.coerceToExpected(fn, DYN) },
|
||||
...["enumerable", "configurable"].map(name => ({
|
||||
key: { kind: "strLit", value: name, type: STRING, loc } as IrExpr,
|
||||
key: strLit(name, loc),
|
||||
value: { kind: "dynFrom", value: { kind: "boolLit", value: true, type: BOOL, loc }, type: DYN, loc } as IrExpr,
|
||||
})),
|
||||
],
|
||||
|
||||
@@ -29,7 +29,7 @@ export function lowerFfiMemoryModule(lowerer: Lowerer, loc: SrcLoc): IrExpr {
|
||||
: [{ kind: "return", value: { kind: "dynFrom", value: call, type: DYN, loc }, loc }], loc,
|
||||
});
|
||||
const closure: IrExpr = { kind: "closure", fnName: name, captures: [], type: funcOf(locals.map(() => DYN), resultType.kind === "void" ? VOID : DYN), loc };
|
||||
const callback = entry.params.find(isFfiCallbackParam)?.callback;
|
||||
const callback = entry.params.find(param => isFfiCallbackParam(param))?.callback;
|
||||
return object({
|
||||
library: text(entry.library!), name: text(entry.name),
|
||||
operation: text(entry.callbackOperation ?? "call"), target: text(entry.callbackTarget ?? ""),
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
resolveCc,
|
||||
targetPlatform,
|
||||
} from "./backend/external-c.js";
|
||||
import { emitCModule } from "./backend/c/c-emitter.js";
|
||||
import { emitCModule, emitCModuleChunks } from "./backend/c/c-emitter.js";
|
||||
import { emitLlvmModule, LlvmUnsupportedError } from "./backend/llvm/emitter.js";
|
||||
import { emitNativeArtifact, NativeCodegenError } from "./backend/native-codegen.js";
|
||||
import { privateSiblingPath } from "./backend/build-cache.js";
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
import { validateSidecar } from "./library/sidecar-validate.js";
|
||||
import type { EntryExportInfo } from "./frontend/lib-exports.js";
|
||||
import type { ContractFacts } from "./frontend/lib-contract.js";
|
||||
import { moduleLibAsyncSurface, moduleLibNondeterministicSurface, moduleEmbedsBuiltin, moduleEmbedsCompressedNpm, moduleUsesAssert, moduleUsesCopying, moduleUsesDc, moduleUsesDgram, moduleUsesDynAsync, moduleUsesDynInvoke, moduleUsesEmitter, moduleUsesFetch, moduleUsesFileHandle, moduleUsesFsWatch, moduleUsesHttp2, moduleUsesHttpServer, moduleUsesInspect, moduleUsesLegacyTextDecoder, moduleUsesNet, moduleUsesNodeTest, moduleUsesParseArgs, moduleUsesProcessEvents, moduleUsesQs, moduleUsesRegex, moduleUsesSearchParams, moduleUsesStream, moduleUsesSymbol, moduleUsesTls, moduleUsesTlsCa, moduleUsesZlib, type IrFfiImport, type IrLibSection, type IrModule, type IrRecordShape, type IrType, type SrcLoc } from "./ir/ir.js";
|
||||
import { moduleLibAsyncSurface, moduleLibNondeterministicSurface, moduleEmbedsBuiltin, moduleUsesAssert, moduleUsesCopying, moduleUsesEmitter, moduleUsesFetch, moduleUsesInspect, moduleUsesLegacyTextDecoder, moduleUsesRegex, moduleUsesSearchParams, moduleUsesSymbol, moduleUsesZlib, type IrFfiImport, type IrLibSection, type IrModule, type IrRecordShape, type IrType, type SrcLoc } from "./ir/ir.js";
|
||||
import { moduleUsesBigInt } from "./ir/ir.js";
|
||||
import { serializeModule } from "./ir/serialize.js";
|
||||
import { validateModule } from "./ir/validate.js";
|
||||
@@ -64,7 +64,7 @@ import { clearResolveCaches } from "./frontend/resolve.js";
|
||||
import type { LowerResult } from "./frontend/lowering/lowerer.js";
|
||||
import type { CoverageInput } from "./coverage/report.js";
|
||||
import { loadFfiProfile, type FfiProfile } from "./ffi/ffi-manifest.js";
|
||||
import { hasForeignFfiCallback } from "./backend/ffi-callbacks.js";
|
||||
import { executableLinkFeatures } from "./backend/executable-features.js";
|
||||
import { FrontendInputTracker, trackedReadFile } from "./frontend/input-tracker.js";
|
||||
import { libraryFrontendImplementationFingerprint, publishEarlyLibraryCache, readEarlyLibraryCache, readSemanticLibraryCache, type EarlyLibraryCacheOptions, type EarlyLibraryCachePublish, type EarlyLibraryNativeFeatures, type SemanticLibraryCacheHit } from "./library/library-cache.js";
|
||||
import { createSourceLineRebaser } from "./library/semantic-source.js";
|
||||
@@ -95,6 +95,7 @@ export {
|
||||
export { ANDROID_MIN_API, IPHONEOS_MIN_VERSION, isAndroidTarget, isIosTarget, isMobileTarget, mobileLibraryTarget, mobileTargetRefusal } from "./backend/external-c.js";
|
||||
export {
|
||||
emitCModule,
|
||||
emitCModuleChunks,
|
||||
emitCModule as emitModule,
|
||||
type CEmitOptions,
|
||||
} from "./backend/c/c-emitter.js";
|
||||
@@ -654,40 +655,7 @@ function executableNativeFeatures(
|
||||
backend,
|
||||
...(optimization === "dev" ? { optimization: "dev" as const } : {}),
|
||||
...(llvmRefusal === undefined ? {} : { llvmRefusal }),
|
||||
dynamic,
|
||||
regex: moduleUsesRegex(mod),
|
||||
copying: moduleUsesCopying(mod),
|
||||
textDecoderLegacy: moduleUsesLegacyTextDecoder(mod),
|
||||
fileHandle: moduleUsesFileHandle(mod),
|
||||
fetch: moduleUsesFetch(mod),
|
||||
netIsland:
|
||||
moduleEmbedsBuiltin(mod, "node:http") ||
|
||||
moduleEmbedsBuiltin(mod, "node:https") ||
|
||||
moduleEmbedsBuiltin(mod, "node:net") ||
|
||||
moduleEmbedsBuiltin(mod, "node:tls"),
|
||||
zlib: moduleUsesZlib(mod) || moduleEmbedsCompressedNpm(mod),
|
||||
assert: moduleUsesAssert(mod),
|
||||
inspect: moduleUsesInspect(mod),
|
||||
dynInvoke: moduleUsesDynInvoke(mod),
|
||||
dc: moduleUsesDc(mod),
|
||||
dynAsync: moduleUsesDynAsync(mod),
|
||||
events: moduleUsesProcessEvents(mod),
|
||||
emitter: moduleUsesEmitter(mod),
|
||||
symbol: moduleUsesSymbol(mod),
|
||||
bigint: moduleUsesBigInt(mod),
|
||||
searchParams: moduleUsesSearchParams(mod),
|
||||
qs: moduleUsesQs(mod),
|
||||
parseArgs: moduleUsesParseArgs(mod),
|
||||
stream: moduleUsesStream(mod),
|
||||
net: moduleUsesNet(mod),
|
||||
http: moduleUsesHttpServer(mod),
|
||||
http2: moduleUsesHttp2(mod),
|
||||
dgram: moduleUsesDgram(mod),
|
||||
watch: moduleUsesFsWatch(mod),
|
||||
foreignFfi: hasForeignFfiCallback(mod.ffiImports ?? []),
|
||||
nodeTest: moduleUsesNodeTest(mod),
|
||||
tls: moduleUsesTls(mod),
|
||||
tlsCa: moduleUsesTlsCa(mod),
|
||||
...executableLinkFeatures(mod, dynamic),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1255,7 +1223,7 @@ async function compileTracked(
|
||||
|
||||
if (outputKind === "c") {
|
||||
await mkdir(dirname(opts.outPath), { recursive: true });
|
||||
await writeFile(opts.outPath, emitCModule(lowered.module, entryText, debugOptions));
|
||||
await writeFile(opts.outPath, emitCModuleChunks(lowered.module, entryText, debugOptions));
|
||||
return { ok: true, artifact: { kind: "c", path: opts.outPath } };
|
||||
}
|
||||
|
||||
@@ -1359,7 +1327,7 @@ async function compileTracked(
|
||||
}
|
||||
}
|
||||
if (backend === "c") {
|
||||
await writeFile(cPath, emitCModule(lowered.module!, entryText, debugOptions));
|
||||
await writeFile(cPath, emitCModuleChunks(lowered.module!, entryText, debugOptions));
|
||||
}
|
||||
let irPath: string | undefined;
|
||||
if (opts.emitIr) {
|
||||
|
||||
@@ -122,6 +122,26 @@ test("nullish chains retain child-before-parent diagnostic order", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test("logical trees retain left/right/parent diagnostic order", () => {
|
||||
const at = (start: number) => ({ ...loc, start });
|
||||
const expr: IrExpr = {
|
||||
kind: "logical", op: "&&", type: BOOL, loc: at(4),
|
||||
left: {
|
||||
kind: "logical", op: "||", type: STRING, loc: at(2),
|
||||
left: { kind: "numLit", value: 0, type: STRING, loc: at(0) },
|
||||
right: { kind: "boolLit", value: true, type: F64, loc: at(1) },
|
||||
},
|
||||
right: { kind: "strLit", value: "wrong", type: BOOL, loc: at(3) },
|
||||
};
|
||||
expect(validateModule(expressionModule(expr, [])).map((error) => [error.loc.start, error.message])).toEqual([
|
||||
[0, "in main: numLit must be f64"],
|
||||
[1, "in main: boolLit must be bool"],
|
||||
[1, "in main: logical || right: expected string, got f64"],
|
||||
[3, "in main: strLit must be string"],
|
||||
[2, "in main: logical && left: expected bool, got string"],
|
||||
]);
|
||||
});
|
||||
|
||||
test.each(["callValue", "dynCall"] as const)("%s requires a checked-value receiver and preserves it in serialization", (kind) => {
|
||||
const funcType: IrType = { kind: "func", params: [], ret: DYN };
|
||||
const closure: IrExpr = { kind: "closure", fnName: "callback", captures: [], type: funcType, loc };
|
||||
|
||||
@@ -2261,6 +2261,33 @@ function validateFunction(
|
||||
}
|
||||
}
|
||||
|
||||
function checkLogicalTree(root: IrExpr & { kind: "logical" }): void {
|
||||
// Long predicates can associate in either direction. Keep their
|
||||
// left/right/parent diagnostic order with bounded native stack use.
|
||||
const pending: { expr: IrExpr; visited: boolean }[] = [{ expr: root, visited: false }];
|
||||
while (pending.length !== 0) {
|
||||
const task = pending.pop()!;
|
||||
const e = task.expr;
|
||||
if (e.kind !== "logical") {
|
||||
checkExpr(e);
|
||||
} else if (!task.visited) {
|
||||
pending.push({ expr: e, visited: true });
|
||||
pending.push({ expr: e.right, visited: false });
|
||||
pending.push({ expr: e.left, visited: false });
|
||||
} else {
|
||||
if (
|
||||
e.type.kind !== "f64" && e.type.kind !== "string" && e.type.kind !== "bool" &&
|
||||
e.type.kind !== "jsval" && e.type.kind !== "union" && e.type.kind !== "dyn"
|
||||
) {
|
||||
err(`logical ${e.op} must be f64|string|bool|jsval|union|dyn, got ${e.type.kind}`, e.loc);
|
||||
}
|
||||
if (e.type.kind === "union") checkTruthyUnion(e.type.unionId, e.loc);
|
||||
expectType(e.left, e.type, `logical ${e.op} left`);
|
||||
expectType(e.right, e.type, `logical ${e.op} right`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkExpr(e: IrExpr): void {
|
||||
switch (e.kind) {
|
||||
case "numLit":
|
||||
@@ -2474,17 +2501,7 @@ function validateFunction(
|
||||
if (e.type.kind !== "bool") err("toBool must be bool", e.loc);
|
||||
break;
|
||||
case "logical":
|
||||
checkExpr(e.left);
|
||||
checkExpr(e.right);
|
||||
if (
|
||||
e.type.kind !== "f64" && e.type.kind !== "string" && e.type.kind !== "bool" &&
|
||||
e.type.kind !== "jsval" && e.type.kind !== "union" && e.type.kind !== "dyn"
|
||||
) {
|
||||
err(`logical ${e.op} must be f64|string|bool|jsval|union|dyn, got ${e.type.kind}`, e.loc);
|
||||
}
|
||||
if (e.type.kind === "union") checkTruthyUnion(e.type.unionId, e.loc);
|
||||
expectType(e.left, e.type, `logical ${e.op} left`);
|
||||
expectType(e.right, e.type, `logical ${e.op} right`);
|
||||
checkLogicalTree(e);
|
||||
break;
|
||||
case "unionEq": {
|
||||
checkExpr(e.left);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { parseNativeArguments } from "./arguments.js";
|
||||
|
||||
test("native build arguments preserve paths and select explicit output modes", () => {
|
||||
expect(parseNativeArguments([
|
||||
"build", "source with spaces.ts", "-o", "result with spaces", "--backend=c", "--emit=obj",
|
||||
"--toolchain", "/installed/compiler.json", "--dev", "--strip", "--ffi=bindings.json", "--npm-static=one,@scope/two",
|
||||
], "default.json")).toEqual({
|
||||
help: false, toolchainPath: "/installed/compiler.json",
|
||||
build: {
|
||||
entryPath: "source with spaces.ts", outputPath: "result with spaces", backend: "c", outputKind: "obj",
|
||||
optimization: "dev", strip: true, ffiProfilePath: "bindings.json", npmStatic: ["one", "@scope/two"],
|
||||
},
|
||||
});
|
||||
expect(parseNativeArguments(["--out=result", "--npm-static", "auto", "--", "-entry.ts"], "default.json")).toMatchObject({
|
||||
toolchainPath: "default.json", build: { entryPath: "-entry.ts", outputKind: "exe", backend: "llvm", npmStatic: "auto" },
|
||||
});
|
||||
expect(parseNativeArguments(["--help"], "default.json").help).toBe(true);
|
||||
});
|
||||
|
||||
test("native build rejects unknown, missing and ambiguous arguments", () => {
|
||||
for (const [args, message] of [
|
||||
[[], "entry source"],
|
||||
[["main.ts"], "output path"],
|
||||
[["main.ts", "-o"], "requires a value"],
|
||||
[["main.ts", "-o", "--dev"], "requires a value"],
|
||||
[["main.ts", "-o=x", "extra.ts"], "unexpected argument"],
|
||||
[["main.ts", "-o=x", "--dynamic"], "unknown native compiler option"],
|
||||
[["main.ts", "-o=x", "--backend=other"], "--backend must"],
|
||||
[["main.ts", "-o=x", "--emit=other"], "--emit must"],
|
||||
] as const) expect(() => parseNativeArguments(args, "toolchain.json")).toThrow(message);
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { NativeBuildOptions } from "./driver.js";
|
||||
|
||||
export interface NativeArguments {
|
||||
help: boolean;
|
||||
toolchainPath: string;
|
||||
build: NativeBuildOptions;
|
||||
}
|
||||
|
||||
export function parseNativeArguments(args: readonly string[], defaultToolchain: string): NativeArguments {
|
||||
let toolchainPath = defaultToolchain;
|
||||
let entryPath = "";
|
||||
let outputPath = "";
|
||||
let backend: "c" | "llvm" = "llvm";
|
||||
let outputKind: "exe" | "obj" | "c" | "llvm" = "exe";
|
||||
let optimization: "release" | "dev" = "release";
|
||||
let strip = false;
|
||||
let help = false;
|
||||
let ffiProfilePath: string | undefined;
|
||||
let npmStatic: string[] | "auto" | undefined;
|
||||
let positional = false;
|
||||
for (let index = 0; index < args.length; index++) {
|
||||
const arg = args[index]!;
|
||||
if (!positional && (arg === "--help" || arg === "-h")) { help = true; continue; }
|
||||
if (!positional && arg === "--") { positional = true; continue; }
|
||||
if (!positional && arg === "--dev") { optimization = "dev"; continue; }
|
||||
if (!positional && arg === "--strip") { strip = true; continue; }
|
||||
if (!positional && arg.startsWith("-")) {
|
||||
const equals = arg.indexOf("=");
|
||||
const name = equals < 0 ? arg : arg.slice(0, equals);
|
||||
if (!["-o", "--out", "--toolchain", "--backend", "--emit", "--ffi", "--npm-static"].includes(name)) {
|
||||
throw new Error(`unknown native compiler option: ${name}`);
|
||||
}
|
||||
const value = equals < 0 ? args[++index] : arg.slice(equals + 1);
|
||||
if (value === undefined || value === "" || (equals < 0 && value.startsWith("--"))) throw new Error(`${name} requires a value`);
|
||||
if (name === "-o" || name === "--out") outputPath = value;
|
||||
else if (name === "--toolchain") toolchainPath = value;
|
||||
else if (name === "--ffi") ffiProfilePath = value;
|
||||
else if (name === "--npm-static") npmStatic = value === "auto" ? "auto" : value.split(",");
|
||||
else if (name === "--backend") {
|
||||
if (value !== "c" && value !== "llvm") throw new Error("--backend must be c or llvm");
|
||||
backend = value;
|
||||
} else if (name === "--emit") {
|
||||
if (value !== "exe" && value !== "obj" && value !== "c" && value !== "llvm") throw new Error("--emit must be exe, obj, c, or llvm");
|
||||
outputKind = value;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!positional && index === 0 && arg === "build") continue;
|
||||
if (entryPath !== "") throw new Error(`unexpected argument: ${arg}`);
|
||||
entryPath = arg;
|
||||
}
|
||||
if (!help && entryPath === "") throw new Error("a TypeScript entry source is required");
|
||||
if (!help && outputPath === "") throw new Error("an output path is required (-o <path>)");
|
||||
return {
|
||||
help, toolchainPath,
|
||||
build: {
|
||||
entryPath, outputPath, backend, outputKind, optimization, strip,
|
||||
...(ffiProfilePath === undefined ? {} : { ffiProfilePath }),
|
||||
...(npmStatic === undefined ? {} : { npmStatic }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const NATIVE_HELP = `Usage: scriptc-native build <entry.ts> -o <output> [options]
|
||||
|
||||
--backend <llvm|c> Code generation backend (default: llvm)
|
||||
--emit <exe|obj|c|llvm> Output artifact (default: exe)
|
||||
--dev Disable optimization and include debug information
|
||||
--strip Strip executable symbols
|
||||
--ffi <profile.json> Native FFI bindings and link inputs
|
||||
--npm-static <packages> Compile comma-separated packages, or auto
|
||||
--toolchain <file> Native toolchain manifest (default: beside executable)
|
||||
--help Show this help
|
||||
|
||||
The native compiler builds static programs; unsupported statements are errors.`;
|
||||
@@ -0,0 +1,148 @@
|
||||
/** Static executable compiler. The TS7 parser/checker and native toolchain
|
||||
* are external native processes; lowering, validation and both emitters run
|
||||
* inside this binary. Installed paths are supplied by the distribution. */
|
||||
import { appendFileSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { basename, dirname, join, resolve } from "node:path";
|
||||
import { emitCModuleChunks } from "../backend/c/c-emitter.js";
|
||||
import { emitLlvmModule } from "../backend/llvm/emitter.js";
|
||||
import { executableLinkFeatures } from "../backend/executable-features.js";
|
||||
import { executableLinkInputs } from "../backend/link-plan-core.js";
|
||||
import { stageNativeRuntimePack } from "../backend/runtime-pack-native.js";
|
||||
import { emitNativeObject, requireNativeArtifact, runNativeTool } from "../backend/native-tools.js";
|
||||
import type { NativeHelperSpec, NativeTargetSpec } from "../backend/targets.js";
|
||||
import { loadFfiProfile, type FfiProfile } from "../ffi/ffi-manifest.js";
|
||||
import { runNativeFrontend } from "../frontend/pipeline-native.js";
|
||||
import type { LowerStats } from "../frontend/lowering/lowerer.js";
|
||||
import { validateModule } from "../ir/validate.js";
|
||||
|
||||
export interface NativeToolchain {
|
||||
compilerVersion: string;
|
||||
ts7Executable: string;
|
||||
target: NativeTargetSpec;
|
||||
helper: NativeHelperSpec;
|
||||
helperExecutable: string;
|
||||
helperPackageRoot: string;
|
||||
runtimePackRoot: string;
|
||||
runtimeIncludeDirectory: string;
|
||||
cCompiler: string;
|
||||
cCompilerArgs: string[];
|
||||
linker: string;
|
||||
linkerArgs: string[];
|
||||
dsymutil: string;
|
||||
}
|
||||
|
||||
export interface NativeBuildOptions {
|
||||
entryPath: string;
|
||||
outputPath: string;
|
||||
backend: "c" | "llvm";
|
||||
outputKind: "exe" | "obj" | "c" | "llvm";
|
||||
optimization: "release" | "dev";
|
||||
strip: boolean;
|
||||
ffiProfilePath?: string;
|
||||
npmStatic?: readonly string[] | "auto";
|
||||
}
|
||||
|
||||
export interface NativeBuildResult {
|
||||
outputPath: string;
|
||||
stats: LowerStats;
|
||||
}
|
||||
|
||||
export function buildNative(options: NativeBuildOptions, toolchain: NativeToolchain): NativeBuildResult {
|
||||
const entry = resolve(options.entryPath);
|
||||
const output = resolve(options.outputPath);
|
||||
if (entry === output) throw new Error("output path must differ from the entry source");
|
||||
let ffi: FfiProfile | null = null;
|
||||
if (options.ffiProfilePath !== undefined) {
|
||||
const loaded = loadFfiProfile(options.ffiProfilePath);
|
||||
if (!loaded.ok) throw new Error(JSON.stringify(loaded.diagnostics));
|
||||
ffi = loaded.profile;
|
||||
}
|
||||
const frontend = runNativeFrontend(entry, toolchain.ts7Executable, options.npmStatic);
|
||||
try {
|
||||
if (frontend.preflight.length !== 0) throw new Error(JSON.stringify(frontend.preflight));
|
||||
const lowered = frontend.lower({ dynamic: false, targetPlatform: toolchain.target.platform, ffiImports: ffi?.functions ?? [] });
|
||||
if (lowered.module === null || lowered.stats.statementsFailed !== 0 || lowered.stats.statementsIsland !== 0 || lowered.stats.functionsSkipped !== 0) {
|
||||
throw new Error(JSON.stringify(lowered.diagnostics));
|
||||
}
|
||||
const module = lowered.module;
|
||||
const errors = validateModule(module);
|
||||
if (errors.length !== 0) throw new Error(JSON.stringify(errors));
|
||||
const features = executableLinkFeatures(module, false);
|
||||
const sourceOutput = options.outputKind === "c" || options.outputKind === "llvm";
|
||||
const backend = sourceOutput ? options.outputKind : options.backend;
|
||||
const debug = options.optimization === "dev" && !options.strip;
|
||||
const debugSources = debug ? frontend.sourceTexts() : new Map<string, string>();
|
||||
const chunks = backend === "c"
|
||||
? emitCModuleChunks(module, undefined, { debugSources })
|
||||
: [emitLlvmModule(module, {
|
||||
targetTriple: toolchain.target.llvmTriple,
|
||||
pointerBits: toolchain.target.pointerBits,
|
||||
wasi: toolchain.target.platform === "wasi",
|
||||
runtimeAbiMarker: true,
|
||||
...(debug ? { debugSources } : {}),
|
||||
})];
|
||||
const writeSource = (path: string): void => {
|
||||
writeFileSync(path, "");
|
||||
for (const chunk of chunks) appendFileSync(path, chunk);
|
||||
};
|
||||
mkdirSync(dirname(output), { recursive: true });
|
||||
// A sibling temporary directory keeps installation on the same volume
|
||||
// and preserves the basename used by Mach-O's ad-hoc signature.
|
||||
const stage = mkdtempSync(join(dirname(output), ".scriptc-native-"));
|
||||
try {
|
||||
const outputDirectory = join(stage, "output");
|
||||
const inputDirectory = join(stage, "input");
|
||||
mkdirSync(outputDirectory);
|
||||
mkdirSync(inputDirectory);
|
||||
const stagedOutput = join(outputDirectory, basename(output));
|
||||
if (sourceOutput) {
|
||||
writeSource(stagedOutput);
|
||||
} else {
|
||||
const input = join(inputDirectory, backend === "c" ? "program.c" : "program.ll");
|
||||
const object = options.outputKind === "obj" ? stagedOutput : join(inputDirectory, "program" + toolchain.target.outputSuffixes.obj);
|
||||
writeSource(input);
|
||||
if (backend === "llvm") {
|
||||
emitNativeObject({
|
||||
executable: toolchain.helperExecutable, packageRoot: toolchain.helperPackageRoot,
|
||||
compilerVersion: toolchain.compilerVersion, target: toolchain.target, helper: toolchain.helper,
|
||||
inputPath: input, outputPath: object, sourcePath: entry, optimization: options.optimization,
|
||||
});
|
||||
} else {
|
||||
runNativeTool(toolchain.cCompiler, [
|
||||
...toolchain.cCompilerArgs, "-std=c11", "-target", toolchain.target.linkerTargetTriple,
|
||||
"-fno-strict-aliasing", options.optimization === "dev" ? "-O0" : "-O2",
|
||||
...(debug ? ["-gline-tables-only", "-gno-column-info"] : []),
|
||||
"-I", toolchain.runtimeIncludeDirectory, "-c", input, "-o", object,
|
||||
]);
|
||||
requireNativeArtifact(object);
|
||||
}
|
||||
if (options.outputKind === "exe") {
|
||||
const runtime = stageNativeRuntimePack(toolchain.runtimePackRoot, join(stage, "runtime"),
|
||||
toolchain.target, toolchain.compilerVersion, features, options.optimization);
|
||||
const plan = executableLinkInputs({
|
||||
target: toolchain.target, programObject: object,
|
||||
ffiLibraries: ffi?.libraries ?? [], ffiSystemLibraries: ffi?.systemLibraries ?? [],
|
||||
ffiFrameworks: ffi?.frameworks ?? [],
|
||||
runtimeObjects: runtime.runtimeObjects, runtimeArchives: runtime.archives,
|
||||
runtimeSystemLibraries: runtime.systemLibraries, optimization: options.optimization, strip: options.strip,
|
||||
});
|
||||
runNativeTool(toolchain.linker, [
|
||||
...toolchain.linkerArgs, ...plan.driverFlags, ...plan.inputs,
|
||||
...plan.systemLibraries.map((name) => `-l${name}`), "-o", stagedOutput,
|
||||
]);
|
||||
requireNativeArtifact(stagedOutput);
|
||||
if (toolchain.target.platform === "darwin" && options.optimization === "dev" && !options.strip) {
|
||||
runNativeTool(toolchain.dsymutil, [stagedOutput, "-o", stagedOutput + ".dSYM"]);
|
||||
rmSync(output + ".dSYM", { recursive: true, force: true });
|
||||
renameSync(stagedOutput + ".dSYM", output + ".dSYM");
|
||||
}
|
||||
}
|
||||
}
|
||||
renameSync(stagedOutput, output);
|
||||
if (options.outputKind === "exe" && toolchain.target.platform === "darwin" && (options.optimization !== "dev" || options.strip)) {
|
||||
rmSync(output + ".dSYM", { recursive: true, force: true });
|
||||
}
|
||||
} finally { rmSync(stage, { recursive: true, force: true }); }
|
||||
return { outputPath: output, stats: lowered.stats };
|
||||
} finally { frontend.dispose(); }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { buildNative } from "./driver.js";
|
||||
import { loadNativeToolchain } from "./toolchain.js";
|
||||
import { NATIVE_HELP, parseNativeArguments } from "./arguments.js";
|
||||
|
||||
try {
|
||||
const args = parseNativeArguments(process.argv.slice(2), process.execPath + ".json");
|
||||
if (args.help) console.log(NATIVE_HELP);
|
||||
else {
|
||||
const result = buildNative(args.build, loadNativeToolchain(args.toolchainPath));
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { loadNativeToolchain, type NativeToolchainManifest } from "./toolchain.js";
|
||||
|
||||
test("native toolchain installation paths relocate with the manifest", () => {
|
||||
const root = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-tools-"));
|
||||
const path = join(root, "toolchain.json");
|
||||
const manifest: NativeToolchainManifest = {
|
||||
schema: "scriptc.native-toolchain.v1", compiler_version: "1.2.3", target: "macos-arm64",
|
||||
ts7: "typescript/tsc", llvm_package: "llvm", runtime_pack: "runtime", runtime_headers: "headers",
|
||||
c_compiler: "clang", c_compiler_args: [], linker: "tools/linker", linker_args: ["cc"], dsymutil: "dsymutil",
|
||||
};
|
||||
try {
|
||||
writeFileSync(path, JSON.stringify(manifest));
|
||||
const toolchain = loadNativeToolchain(path);
|
||||
expect(toolchain.ts7Executable).toBe(join(root, "typescript/tsc"));
|
||||
expect(toolchain.helperExecutable).toBe(join(root, "llvm/bin/scriptc-llvm-codegen"));
|
||||
expect(toolchain.runtimePackRoot).toBe(join(root, "runtime"));
|
||||
expect(toolchain.linker).toBe(join(root, "tools/linker"));
|
||||
expect(toolchain.cCompiler).toBe("clang");
|
||||
expect(toolchain.linkerArgs).toEqual(["cc"]);
|
||||
expect(toolchain.target.name).toBe("macos-arm64");
|
||||
for (const invalid of [null, [], {}, { ...manifest, schema: "other" }, { ...manifest, target: "other" },
|
||||
{ ...manifest, ts7: "" }, { ...manifest, linker_args: [1] }]) {
|
||||
writeFileSync(path, JSON.stringify(invalid));
|
||||
expect(() => loadNativeToolchain(path)).toThrow();
|
||||
}
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, resolve } from "node:path";
|
||||
import { NATIVE_TARGETS, type NativeTargetSpec } from "../backend/targets.js";
|
||||
import type { NativeToolchain } from "./driver.js";
|
||||
|
||||
/** Distribution paths are relative to this manifest, so moving an installed
|
||||
* compiler does not bake the seed machine's paths into the native binary. */
|
||||
export interface NativeToolchainManifest {
|
||||
schema: "scriptc.native-toolchain.v1";
|
||||
compiler_version: string;
|
||||
target: string;
|
||||
ts7: string;
|
||||
llvm_package: string;
|
||||
runtime_pack: string;
|
||||
runtime_headers: string;
|
||||
c_compiler: string;
|
||||
c_compiler_args: string[];
|
||||
linker: string;
|
||||
linker_args: string[];
|
||||
dsymutil: string;
|
||||
}
|
||||
|
||||
function pathFrom(root: string, value: string): string {
|
||||
return isAbsolute(value) ? value : resolve(root, value);
|
||||
}
|
||||
|
||||
function commandFrom(root: string, value: string): string {
|
||||
return value.includes("/") || value.includes("\\") ? pathFrom(root, value) : value;
|
||||
}
|
||||
|
||||
export function loadNativeToolchain(path: string): NativeToolchain {
|
||||
const raw: unknown = JSON.parse(readFileSync(path, "utf8"));
|
||||
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("invalid native toolchain manifest");
|
||||
const value = raw as Record<string, unknown>;
|
||||
if (value.schema !== "scriptc.native-toolchain.v1") throw new Error("unsupported native toolchain manifest schema");
|
||||
for (const name of ["compiler_version", "target", "ts7", "llvm_package", "runtime_pack", "runtime_headers", "c_compiler", "linker", "dsymutil"]) {
|
||||
if (typeof value[name] !== "string" || value[name] === "") throw new Error(`native toolchain requires ${name}`);
|
||||
}
|
||||
for (const name of ["c_compiler_args", "linker_args"]) {
|
||||
if (!Array.isArray(value[name]) || !(value[name] as unknown[]).every((arg) => typeof arg === "string")) {
|
||||
throw new Error(`native toolchain requires a string array for ${name}`);
|
||||
}
|
||||
}
|
||||
const manifest = raw as NativeToolchainManifest;
|
||||
const targets: readonly NativeTargetSpec[] = NATIVE_TARGETS;
|
||||
const target = targets.find((item) => item.name === manifest.target);
|
||||
if (target === undefined || target.platform === "wasi") throw new Error(`unsupported native compiler target: ${manifest.target}`);
|
||||
const root = dirname(resolve(path));
|
||||
const helperPackageRoot = pathFrom(root, manifest.llvm_package);
|
||||
return {
|
||||
compilerVersion: manifest.compiler_version,
|
||||
ts7Executable: pathFrom(root, manifest.ts7),
|
||||
target, helper: target.helper, helperPackageRoot,
|
||||
helperExecutable: join(helperPackageRoot, "bin", target.platform === "win32" ? "scriptc-llvm-codegen.exe" : "scriptc-llvm-codegen"),
|
||||
runtimePackRoot: pathFrom(root, manifest.runtime_pack),
|
||||
runtimeIncludeDirectory: pathFrom(root, manifest.runtime_headers),
|
||||
cCompiler: commandFrom(root, manifest.c_compiler), cCompilerArgs: manifest.c_compiler_args,
|
||||
linker: commandFrom(root, manifest.linker), linkerArgs: manifest.linker_args,
|
||||
dsymutil: commandFrom(root, manifest.dsymutil),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/** Seed a native compiler using the Node-hosted compiler. Subsequent builds
|
||||
* can invoke the resulting executable with the emitted FFI profile. */
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, relative, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { compile } from "../packages/compiler/src/index.js";
|
||||
import { nativeCodegenTarget } from "../packages/compiler/src/backend/targets.js";
|
||||
import { ts7Executable } from "../packages/compiler/src/frontend/ts7/rpc-api.js";
|
||||
import type { NativeToolchainManifest } from "../packages/compiler/src/native/toolchain.js";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const output = resolve(process.argv[2] ?? join(root, ".scriptc/native"));
|
||||
const target = nativeCodegenTarget();
|
||||
if (target === null || target.platform === "wasi") throw new Error("a supported native host is required to build the compiler");
|
||||
mkdirSync(output, { recursive: true });
|
||||
const executable = join(output, "scriptc-native" + target.outputSuffixes.exe);
|
||||
const object = join(output, "ts7-process" + target.outputSuffixes.obj);
|
||||
const ffi = join(output, "ts7-process.ffi.json");
|
||||
const nativeSources = join(root, "packages/compiler/native");
|
||||
const compiler = process.env["SCRIPTC_CC"] ?? target.defaultLinker;
|
||||
const compilerArgs = process.env["SCRIPTC_CC"] === undefined ? [...target.defaultLinkerArgs] : [];
|
||||
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
||||
// This object and profile are reused by native rebuilds with packaged runtimes.
|
||||
// Sanitizer instrumentation belongs to the seed compiler's own build below.
|
||||
execFileSync(compiler, [
|
||||
...compilerArgs, "-std=c11", "-Wall", "-Wextra", "-Werror", "-target", target.linkerTargetTriple,
|
||||
"-c", join(nativeSources, "ts7-process.c"), "-o", object,
|
||||
], { stdio: "inherit" });
|
||||
writeFileSync(ffi, JSON.stringify({
|
||||
...JSON.parse(readFileSync(join(nativeSources, "ts7-process.ffi.json"), "utf8")), libraries: [object],
|
||||
}, null, 2) + "\n");
|
||||
const result = await compile(join(root, "packages/compiler/src/native/main.ts"), {
|
||||
outDir: output, outPath: executable, backend: "c", optimization: "dev", dynamic: false, ffiProfilePath: ffi, sanitize,
|
||||
});
|
||||
if (!result.ok) throw new Error(result.diagnostics.map((item) =>
|
||||
`${item.loc ? `${item.loc.file}:${item.loc.start}: ` : ""}${item.code}: ${item.message}`,
|
||||
).join("\n"));
|
||||
|
||||
// The executable path can be canonical even when the build directory was
|
||||
// reached through a symlink (for example /tmp on macOS).
|
||||
const relativeRoot = realpathSync(output);
|
||||
const packagePath = (name: string) => relative(relativeRoot, join(root, "packages", name.replace("@scriptc/", "")));
|
||||
const manifest: NativeToolchainManifest = {
|
||||
schema: "scriptc.native-toolchain.v1",
|
||||
compiler_version: (JSON.parse(readFileSync(join(root, "packages/compiler/package.json"), "utf8")) as { version: string }).version,
|
||||
target: target.name, ts7: relative(relativeRoot, ts7Executable()),
|
||||
llvm_package: packagePath(target.helper.packageName), runtime_pack: packagePath(target.runtimePackPackage),
|
||||
runtime_headers: relative(relativeRoot, join(root, "packages/runtime/src")),
|
||||
c_compiler: compiler, c_compiler_args: compilerArgs,
|
||||
linker: process.env["SCRIPTC_LINKER"] ?? target.defaultLinker,
|
||||
linker_args: process.env["SCRIPTC_LINKER"] === undefined ? [...target.defaultLinkerArgs] : [],
|
||||
dsymutil: process.env["SCRIPTC_DSYMUTIL"] ?? "dsymutil",
|
||||
};
|
||||
writeFileSync(executable + ".json", JSON.stringify(manifest, null, 2) + "\n");
|
||||
console.log(executable);
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { relative } from "node:path";
|
||||
import { loadNativeToolchain } from "../../../packages/compiler/src/native/toolchain.js";
|
||||
import { stageNativeRuntimePack } from "../../../packages/compiler/src/backend/runtime-pack-native.js";
|
||||
import { validateNativeCodegenVersion } from "../../../packages/compiler/src/backend/native-codegen-core.js";
|
||||
import type { NativeLinkFeatures } from "../../../packages/compiler/src/backend/native-link-info.js";
|
||||
|
||||
const request = JSON.parse(readFileSync(process.argv[2]!, "utf8")) as {
|
||||
toolchain: string;
|
||||
stage: string;
|
||||
features: NativeLinkFeatures;
|
||||
action: "stage" | "helper";
|
||||
version: Record<string, unknown>;
|
||||
};
|
||||
try {
|
||||
const toolchain = loadNativeToolchain(request.toolchain);
|
||||
if (request.action === "helper") {
|
||||
const version = validateNativeCodegenVersion(request.version, toolchain.target, toolchain.helper, toolchain.compilerVersion);
|
||||
console.log(version.protocol_version, version.scriptc_package_version, version.llvm_version, version.default_target);
|
||||
} else {
|
||||
const pack = stageNativeRuntimePack(toolchain.runtimePackRoot, request.stage, toolchain.target,
|
||||
toolchain.compilerVersion, request.features, "release");
|
||||
console.log(JSON.stringify({
|
||||
runtime: pack.runtimeObjects.map((path) => relative(request.stage, path)),
|
||||
archives: pack.archives.map((path) => relative(request.stage, path)),
|
||||
libraries: pack.systemLibraries,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import {
|
||||
parseRuntimePackManifest, selectRuntimePackArtifacts, validateRuntimePackIdentity,
|
||||
} from "../../../packages/compiler/src/backend/runtime-pack-core.js";
|
||||
import type { NativeLinkFeatures } from "../../../packages/compiler/src/backend/native-link-info.js";
|
||||
import type { NativeTargetSpec } from "../../../packages/compiler/src/backend/targets.js";
|
||||
|
||||
const request = JSON.parse(readFileSync(process.argv[2]!, "utf8")) as {
|
||||
manifest: unknown;
|
||||
target: NativeTargetSpec;
|
||||
packageName: string;
|
||||
packageVersion: string;
|
||||
compilerVersion: string;
|
||||
features: NativeLinkFeatures;
|
||||
flavor: "release" | "dev";
|
||||
};
|
||||
try {
|
||||
const manifest = parseRuntimePackManifest(request.manifest);
|
||||
validateRuntimePackIdentity(manifest, request.packageName, request.packageVersion, request.target, request.compilerVersion);
|
||||
const selected = selectRuntimePackArtifacts(manifest, request.features, request.flavor, {});
|
||||
console.log(JSON.stringify({
|
||||
features: selected.features,
|
||||
runtime: selected.runtime.map((item) => ({ path: item.path, sha256: item.sha256, size: item.size })),
|
||||
archives: selected.archives.map((item) => ({ path: item.path, sha256: item.sha256, size: item.size })),
|
||||
systemLibraries: selected.systemLibraries,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.log(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { execFile, spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { delimiter, join, resolve } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import { expect, test } from "vitest";
|
||||
import type { NativeToolchainManifest } from "../../packages/compiler/src/native/toolchain.js";
|
||||
|
||||
const root = join(import.meta.dirname, "../..");
|
||||
const exec = promisify(execFile);
|
||||
|
||||
function absoluteCommand(command: string): string {
|
||||
if (command.includes("/") || command.includes("\\")) return resolve(command);
|
||||
const extensions = process.platform === "win32" ? ["", ".exe", ".cmd"] : [""];
|
||||
for (const directory of (process.env["PATH"] ?? "").split(delimiter)) {
|
||||
for (const extension of extensions) {
|
||||
const path = join(directory, command + extension);
|
||||
if (existsSync(path)) return resolve(path);
|
||||
}
|
||||
}
|
||||
throw new Error(`native tool is not on PATH: ${command}`);
|
||||
}
|
||||
|
||||
test("the standalone compiler builds programs and rebuilds itself with Node unavailable", async () => {
|
||||
const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-bootstrap-"));
|
||||
const executable = (name: string) => join(directory, name + (process.platform === "win32" ? ".exe" : ""));
|
||||
const options = { cwd: root, timeout: 1_800_000, maxBuffer: 16 * 1024 * 1024 };
|
||||
try {
|
||||
await exec(process.execPath, ["--max-old-space-size=8192", "--import", "tsx", join(root, "scripts/build-native-compiler.mts"), directory], options);
|
||||
const seed = executable("scriptc-native");
|
||||
const manifestPath = seed + ".json";
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as NativeToolchainManifest;
|
||||
manifest.c_compiler = absoluteCommand(manifest.c_compiler);
|
||||
manifest.linker = absoluteCommand(manifest.linker);
|
||||
if (process.platform === "darwin") manifest.dsymutil = absoluteCommand(manifest.dsymutil);
|
||||
writeFileSync(manifestPath, JSON.stringify(manifest));
|
||||
const nativeOptions = { ...options, env: { ...process.env, PATH: "" } };
|
||||
const invoke = async (compiler: string, args: string[]) => {
|
||||
const result = await exec(compiler, [...args, "--toolchain", manifestPath], nativeOptions).catch((error: unknown) => {
|
||||
const failure = error as Error & { code?: string | number; signal?: string; stdout?: string; stderr?: string };
|
||||
throw new Error(`${failure.message}\ncode=${failure.code} signal=${failure.signal}\n${failure.stderr ?? ""}\n${failure.stdout ?? ""}`, { cause: error });
|
||||
});
|
||||
expect(result.stderr).toBe("");
|
||||
const built = JSON.parse(result.stdout) as { outputPath: string; stats: {
|
||||
statementsTotal: number; statementsFailed: number; statementsIsland: number; functionsSkipped: number;
|
||||
} };
|
||||
expect(built.stats.statementsFailed).toBe(0);
|
||||
expect(built.stats.statementsIsland).toBe(0);
|
||||
expect(built.stats.functionsSkipped).toBe(0);
|
||||
return built;
|
||||
};
|
||||
expect((await exec(seed, ["--help"], nativeOptions)).stdout).toContain("Usage: scriptc-native");
|
||||
const checkProgram = async (compiler: string, source: string, backend: string) => {
|
||||
// This basename formerly collided with the driver's temporary object.
|
||||
const output = executable("program.o");
|
||||
const built = await invoke(compiler, ["build", source, "-o", output, "--backend", backend, "--dev", "--strip"]);
|
||||
expect(built.outputPath).toBe(output);
|
||||
const oracle = spawnSync(process.execPath, [source], options);
|
||||
const actual = spawnSync(output, [], nativeOptions);
|
||||
for (const result of [oracle, actual]) {
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.signal).toBeNull();
|
||||
expect(result.status, result.stderr.toString()).toBe(0);
|
||||
}
|
||||
expect(actual.stdout).toEqual(oracle.stdout);
|
||||
expect(actual.stderr).toEqual(oracle.stderr);
|
||||
};
|
||||
const sample = join(root, "tests/corpus/class-array-optional-return.ts");
|
||||
await checkProgram(seed, sample, "c");
|
||||
await checkProgram(seed, sample, "llvm");
|
||||
|
||||
const badSource = join(directory, "bad.ts");
|
||||
writeFileSync(badSource, 'const value: number = "wrong"; console.log(value);\n');
|
||||
const retained = executable("retained");
|
||||
writeFileSync(retained, "existing output");
|
||||
const failed = spawnSync(seed, [badSource, "-o", retained, "--toolchain", manifestPath], nativeOptions);
|
||||
expect(failed.status).toBe(1);
|
||||
expect(failed.stderr.toString()).toContain("not assignable");
|
||||
expect(readFileSync(retained, "utf8")).toBe("existing output");
|
||||
|
||||
const entry = join(root, "packages/compiler/src/native/main.ts");
|
||||
const profile = join(directory, "ts7-process.ffi.json");
|
||||
const rebuilt = executable("scriptc-rebuilt");
|
||||
const self = await invoke(seed, [entry, "-o", rebuilt, "--backend=c", "--dev", "--strip", "--ffi", profile]);
|
||||
expect(self.stats.statementsTotal).toBeGreaterThan(10_000);
|
||||
console.log("native self-build", self.stats);
|
||||
await checkProgram(rebuilt, join(root, "tests/corpus/nullish-long-chain.ts"), "c");
|
||||
await checkProgram(rebuilt, sample, "llvm");
|
||||
|
||||
// Both generations lower and emit the complete driver. Comparing C
|
||||
// avoids the >512MB JSON string required by its expanded IR graph.
|
||||
const seedC = join(directory, "seed.c");
|
||||
const rebuiltC = join(directory, "rebuilt.c");
|
||||
await invoke(seed, [entry, "--emit=c", "-o", seedC, "--ffi", profile]);
|
||||
await invoke(rebuilt, [entry, "--emit=c", "-o", rebuiltC, "--ffi", profile]);
|
||||
expect(readFileSync(seedC).equals(readFileSync(rebuiltC)), "native compiler generations must emit identical C").toBe(true);
|
||||
} finally { rmSync(directory, { recursive: true, force: true }); }
|
||||
}, 5_400_000);
|
||||
@@ -0,0 +1,119 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { analyze, compile } from "@scriptc/compiler";
|
||||
import { nativeCodegenTarget } from "../../packages/compiler/src/backend/targets.js";
|
||||
import type { NativeLinkFeatures } from "../../packages/compiler/src/backend/native-link-info.js";
|
||||
import type { RuntimePackManifest } from "../../packages/compiler/src/backend/runtime-pack-core.js";
|
||||
import type { NativeToolchainManifest } from "../../packages/compiler/src/native/toolchain.js";
|
||||
|
||||
const root = join(import.meta.dirname, "../..");
|
||||
const entry = join(root, "tests/fixtures/self-hosting/native-toolchain.ts");
|
||||
const features: NativeLinkFeatures = {
|
||||
dynamic: false, regex: false, copying: false, textDecoderLegacy: false, fileHandle: false,
|
||||
fetch: false, netIsland: false, zlib: false, assert: false, inspect: false, dynInvoke: false,
|
||||
dc: false, dynAsync: false, events: false, emitter: false, symbol: false, bigint: false,
|
||||
searchParams: false, qs: false, parseArgs: false, stream: false, net: false, http: false,
|
||||
http2: false, dgram: false, watch: false, foreignFfi: false, nodeTest: false, tls: false, tlsCa: false,
|
||||
};
|
||||
|
||||
test("native toolchain configuration, staging and helper validation are static", () => {
|
||||
const { coverage } = analyze(entry, { dynamic: false });
|
||||
expect(coverage.preflightFailed).toBe(false);
|
||||
expect(coverage.stats.statementsFailed).toBe(0);
|
||||
expect(coverage.stats.statementsIsland).toBe(0);
|
||||
expect(coverage.stats.functionsSkipped).toBe(0);
|
||||
});
|
||||
|
||||
for (const backend of ["c", "llvm"] as const) {
|
||||
test(`native toolchain integrity and failure behavior match Node (${backend})`, async () => {
|
||||
const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-native-integrity-"));
|
||||
const target = nativeCodegenTarget()!;
|
||||
try {
|
||||
const built = await compile(entry, {
|
||||
outDir: directory, outPath: join(directory, "toolchain" + target.outputSuffixes.exe),
|
||||
backend, optimization: "dev", dynamic: false, sanitize: process.env["SCRIPTC_SAN"] === "1",
|
||||
});
|
||||
if (!built.ok) throw new Error(JSON.stringify(built.diagnostics));
|
||||
expect(built.backend).toBe(backend);
|
||||
const pack = join(directory, "pack");
|
||||
mkdirSync(pack);
|
||||
const bytes = Buffer.from([0, 255, 1, 128, 10, 0]);
|
||||
const artifact = { path: "objects/base.o", size: bytes.length, sha256: createHash("sha256").update(bytes).digest("hex") };
|
||||
mkdirSync(join(pack, "objects"));
|
||||
writeFileSync(join(pack, artifact.path), bytes);
|
||||
writeFileSync(join(pack, "license.txt"), "license");
|
||||
const units = [{ source: "base.c", predicate: true, variants: [{ id: "default", when: {}, defines: [], ...artifact }] }];
|
||||
const manifest: RuntimePackManifest = {
|
||||
schema: "scriptc.runtime-pack.v1", format: 1, package: target.runtimePackPackage, version: "1.2.3",
|
||||
target: { name: target.name, llvm_triple: target.llvmTriple, architecture: target.architecture,
|
||||
object_format: target.objectFormat, minimum_os: target.minimumOs },
|
||||
runtime_abi: { version: 4, marker: "scr_runtime_abi_v4" },
|
||||
compiler: { command: "fixture", identity: "fixture", target: target.llvmTriple },
|
||||
macros: { executable: [], excluded: [], sanitizer: "external-toolchain-required" },
|
||||
flavors: { release: { optimization: "-O2", runtime_units: units }, dev: { optimization: "-O0", runtime_units: units } },
|
||||
archives: [], system_libraries: [{ name: "fixture", predicate: true }], licenses: [{ path: "license.txt", license: "fixture" }],
|
||||
};
|
||||
const manifestPath = join(pack, "runtime-pack.json");
|
||||
writeFileSync(manifestPath, JSON.stringify(manifest));
|
||||
writeFileSync(join(pack, "package.json"), JSON.stringify({ name: manifest.package, version: manifest.version }));
|
||||
const toolchain = join(directory, "toolchain.json");
|
||||
const config: NativeToolchainManifest = {
|
||||
schema: "scriptc.native-toolchain.v1", compiler_version: manifest.version, target: target.name,
|
||||
ts7: "ts7", llvm_package: "llvm", runtime_pack: "pack", runtime_headers: "headers",
|
||||
c_compiler: "cc", c_compiler_args: [], linker: "ld", linker_args: [], dsymutil: "dsymutil",
|
||||
};
|
||||
writeFileSync(toolchain, JSON.stringify(config));
|
||||
const input = join(directory, "request.json");
|
||||
const stage = join(directory, "stage");
|
||||
const version = {
|
||||
ok: true, protocol_version: "1", scriptc_package_version: manifest.version, llvm_version: "22.1.8",
|
||||
host_triple: target.llvmTriple, targets: [target.llvmBackend], supported_targets: [target.llvmTriple],
|
||||
default_target: target.helper.defaultTarget, data_layout: target.helper.defaultDataLayout,
|
||||
};
|
||||
const check = (status: number, message: string, action = "stage", helperVersion: object = version) => {
|
||||
writeFileSync(input, JSON.stringify({ toolchain, stage, features, action, version: helperVersion }));
|
||||
const options = { cwd: root, timeout: 30_000, maxBuffer: 1024 * 1024 };
|
||||
const results = [];
|
||||
for (const native of [false, true]) {
|
||||
rmSync(stage, { recursive: true, force: true });
|
||||
const result = spawnSync(native ? built.binaryPath : process.execPath,
|
||||
native ? [input] : ["--import", "tsx", entry, input],
|
||||
native ? { ...options, env: { ...process.env, PATH: "" } } : options);
|
||||
expect(result.error).toBeUndefined();
|
||||
expect(result.signal).toBeNull();
|
||||
expect(result.status, result.stderr.toString()).toBe(status);
|
||||
expect(result.stdout.toString()).toContain(message);
|
||||
results.push(result);
|
||||
}
|
||||
expect(results[1]!.stdout).toEqual(results[0]!.stdout);
|
||||
expect(results[1]!.stderr).toEqual(results[0]!.stderr);
|
||||
};
|
||||
check(0, "base.o");
|
||||
expect(readFileSync(join(stage, artifact.path))).toEqual(bytes);
|
||||
// A later replacement of the installed input cannot change staged bytes.
|
||||
writeFileSync(join(pack, artifact.path), Buffer.from("changed"));
|
||||
expect(readFileSync(join(stage, artifact.path))).toEqual(bytes);
|
||||
check(1, "hash mismatch");
|
||||
expect(existsSync(join(stage, artifact.path))).toBe(false);
|
||||
writeFileSync(join(pack, artifact.path), bytes);
|
||||
rmSync(join(pack, "license.txt"));
|
||||
check(1, "license payload is incomplete");
|
||||
writeFileSync(join(pack, "license.txt"), "license");
|
||||
rmSync(join(pack, artifact.path));
|
||||
check(1, "artifact is missing");
|
||||
writeFileSync(join(pack, artifact.path), bytes);
|
||||
writeFileSync(toolchain, JSON.stringify({ ...config, compiler_version: "other" }));
|
||||
check(1, "version mismatch");
|
||||
writeFileSync(toolchain, JSON.stringify(config));
|
||||
check(0, "1 1.2.3 22.1.8", "helper");
|
||||
for (const [field, value] of Object.entries({ protocol_version: "2", llvm_version: "0", scriptc_package_version: "other",
|
||||
targets: [], supported_targets: [], default_target: "other", data_layout: "other", host_triple: null })) {
|
||||
check(1, field, "helper", { ...version, [field]: value });
|
||||
}
|
||||
} finally { rmSync(directory, { recursive: true, force: true }); }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { analyze, compile } from "@scriptc/compiler";
|
||||
import { NATIVE_TARGETS, nativeCodegenTarget } from "../../packages/compiler/src/backend/targets.js";
|
||||
import type { NativeLinkFeatures } from "../../packages/compiler/src/backend/native-link-info.js";
|
||||
import type { RuntimePackManifest } from "../../packages/compiler/src/backend/runtime-pack-core.js";
|
||||
|
||||
const root = join(import.meta.dirname, "../..");
|
||||
const entry = join(root, "tests/fixtures/self-hosting/runtime-pack.ts");
|
||||
const base: NativeLinkFeatures = {
|
||||
dynamic: false, regex: false, copying: false, textDecoderLegacy: false, fileHandle: false,
|
||||
fetch: false, netIsland: false, zlib: false, assert: false, inspect: false, dynInvoke: false,
|
||||
dc: false, dynAsync: false, events: false, emitter: false, symbol: false, bigint: false,
|
||||
searchParams: false, qs: false, parseArgs: false, stream: false, net: false, http: false,
|
||||
http2: false, dgram: false, watch: false, foreignFfi: false, nodeTest: false, tls: false, tlsCa: false,
|
||||
};
|
||||
|
||||
test("runtime manifest validation and selection are entirely static", () => {
|
||||
const { coverage } = analyze(entry, { dynamic: false });
|
||||
expect(coverage.preflightFailed).toBe(false);
|
||||
expect(coverage.stats.statementsTotal).toBeGreaterThan(50);
|
||||
expect(coverage.stats.statementsFailed).toBe(0);
|
||||
expect(coverage.stats.statementsIsland).toBe(0);
|
||||
expect(coverage.stats.functionsSkipped).toBe(0);
|
||||
});
|
||||
|
||||
for (const backend of ["c", "llvm"] as const) {
|
||||
test(`native runtime manifest selection matches Node (${backend})`, async () => {
|
||||
const directory = mkdtempSync(join(process.platform === "win32" ? tmpdir() : "/tmp", "scriptc-pack-selection-"));
|
||||
try {
|
||||
const built = await compile(entry, {
|
||||
outDir: directory, outPath: join(directory, "selection" + (process.platform === "win32" ? ".exe" : "")),
|
||||
backend, dynamic: false, optimization: "dev", sanitize: process.env["SCRIPTC_SAN"] === "1",
|
||||
});
|
||||
if (!built.ok) throw new Error(JSON.stringify(built.diagnostics));
|
||||
expect(built.backend).toBe(backend);
|
||||
const input = join(directory, "request.json");
|
||||
const check = (request: object, status: number, name: string) => {
|
||||
writeFileSync(input, JSON.stringify(request));
|
||||
const options = { cwd: root, timeout: 30_000, maxBuffer: 16 * 1024 * 1024 };
|
||||
const oracle = spawnSync(process.execPath, ["--import", "tsx", entry, input], options);
|
||||
const native = spawnSync(built.binaryPath, [input], { ...options, env: { ...process.env, PATH: "" } });
|
||||
for (const result of [oracle, native]) {
|
||||
expect(result.error, name).toBeUndefined();
|
||||
expect(result.signal, name).toBeNull();
|
||||
expect(result.status, `${name}: ${result.stderr}`).toBe(status);
|
||||
}
|
||||
if (status === 0) expect(JSON.parse(native.stdout.toString()), name).toEqual(JSON.parse(oracle.stdout.toString()));
|
||||
else expect(native.stdout.toString(), name).toBe(oracle.stdout.toString());
|
||||
expect(native.stderr, name).toEqual(oracle.stderr);
|
||||
return native.stdout.toString();
|
||||
};
|
||||
// The installed manifest exercises the actual predicates and variants.
|
||||
// Substitute each target identity to exercise validation on every host.
|
||||
const hostTarget = nativeCodegenTarget()!;
|
||||
const packageDir = join(root, "packages", hostTarget.runtimePackPackage.replace("@scriptc/", ""));
|
||||
const installed = JSON.parse(readFileSync(join(packageDir, "runtime-pack.json"), "utf8")) as RuntimePackManifest;
|
||||
for (const target of NATIVE_TARGETS) {
|
||||
const manifest: RuntimePackManifest = {
|
||||
...installed, package: target.runtimePackPackage,
|
||||
target: { name: target.name, llvm_triple: target.llvmTriple, architecture: target.architecture,
|
||||
object_format: target.objectFormat, minimum_os: target.minimumOs },
|
||||
compiler: { ...installed.compiler, target: target.llvmTriple },
|
||||
};
|
||||
const request = {
|
||||
manifest, target, packageName: manifest.package, packageVersion: manifest.version,
|
||||
compilerVersion: manifest.version, features: base, flavor: "release",
|
||||
};
|
||||
check(request, 0, target.name + " base");
|
||||
if (target.name === hostTarget.name) {
|
||||
for (const name of Object.keys(base)) {
|
||||
check({ ...request, features: { ...base, [name]: true } }, 0, target.name + " " + name);
|
||||
}
|
||||
const all = Object.fromEntries(Object.keys(base).map((name) => [name, true]));
|
||||
check({ ...request, features: all, flavor: "dev" }, 0, target.name + " all");
|
||||
}
|
||||
check({ ...request, packageVersion: "mismatch" }, 1, "package version");
|
||||
check({ ...request, manifest: { ...manifest, target: { ...manifest.target, minimum_os: "other" } } }, 1, "target identity");
|
||||
check({ ...request, manifest: { ...manifest, runtime_abi: { version: 0, marker: "wrong" } } }, 1, "runtime ABI");
|
||||
check({ ...request, manifest: { ...manifest, flavors: {} } }, 1, "missing flavor");
|
||||
check({ ...request, manifest: { ...manifest, archives: [{ path: "../escape", sha256: "0".repeat(64), size: 1, id: "zlib", predicate: true }] } }, 1, "artifact traversal");
|
||||
check({ ...request, manifest: { ...manifest, system_libraries: [{ name: "m", predicate: { any: [1] } }] } }, 1, "invalid predicate");
|
||||
}
|
||||
} finally { rmSync(directory, { recursive: true, force: true }); }
|
||||
});
|
||||
}
|
||||
@@ -26,6 +26,20 @@ export function validatorCases(): ValidatorCase[] {
|
||||
cases.push({ name, module, ...(diagnostic ? { diagnostic } : {}) });
|
||||
};
|
||||
add("empty module", () => {});
|
||||
for (const direction of ["left", "right", "alternating"]) {
|
||||
for (const invalid of [false, true]) {
|
||||
add(`deep logical ${direction} ${invalid ? "invalid" : "valid"}`, (m) => {
|
||||
let tree: IrExpr = invalid ? { kind: "boolLit", value: true, type: F64, loc } : boolLit(true, loc);
|
||||
for (let depth = 0; depth < 128; depth++) {
|
||||
const leaf = boolLit(depth % 2 === 0, loc);
|
||||
const left = direction === "left" || (direction === "alternating" && depth % 2 === 0);
|
||||
tree = { kind: "logical", op: depth % 2 === 0 ? "&&" : "||",
|
||||
left: left ? tree : leaf, right: left ? leaf : tree, type: BOOL, loc };
|
||||
}
|
||||
m.functions[0]!.body = [expression(tree)];
|
||||
}, invalid ? "boolLit must be bool" : undefined);
|
||||
}
|
||||
}
|
||||
add("duplicate function", (m) => { m.functions.push(structuredClone(m.functions[0]!)); }, "duplicate function");
|
||||
add("unknown local", (m) => { m.functions[0]!.body = [expression(varRef("missing", F64, loc))]; }, "missing");
|
||||
add("parameter without local", (m) => {
|
||||
|
||||
Reference in New Issue
Block a user