chore: import upstream snapshot with attribution
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
This commit is contained in:
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$root"
|
||||
|
||||
native_shards="${ZERO_AGENT_NATIVE_TEST_SHARDS:-4}"
|
||||
if ! [[ "$native_shards" =~ ^[0-9]+$ ]] || (( native_shards < 1 )); then
|
||||
echo "ZERO_AGENT_NATIVE_TEST_SHARDS must be a positive integer" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dry_run="${ZERO_AGENT_CHECK_DRY_RUN:-0}"
|
||||
work_root="${ZERO_AGENT_CHECK_WORK_ROOT:-/tmp/zero-agent-checks-$$}"
|
||||
log_dir="${ZERO_AGENT_CHECK_LOG_DIR:-$root/.zero/agent-checks}"
|
||||
mkdir -p "$work_root" "$log_dir" "$root/.zero/toolchains"
|
||||
|
||||
cleanup() {
|
||||
if [[ "${ZERO_AGENT_CHECK_KEEP_WORKDIR:-}" != "1" ]]; then
|
||||
rm -rf "$work_root"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
zig_platform() {
|
||||
case "$(uname -s):$(uname -m)" in
|
||||
Linux:x86_64) printf "linux-x64" ;;
|
||||
Darwin:arm64 | Darwin:aarch64) printf "macos-aarch64" ;;
|
||||
Darwin:x86_64) printf "macos-x86_64" ;;
|
||||
*)
|
||||
echo "unsupported host for pinned Zig installer: $(uname -s):$(uname -m)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
zig_dir="$root/.zero/toolchains/zig-0.14.1-$(zig_platform)"
|
||||
if [[ "$dry_run" != "1" ]]; then
|
||||
bash scripts/install-zig.sh >/dev/null
|
||||
fi
|
||||
export PATH="$zig_dir:$PATH"
|
||||
|
||||
prepare_job_dir() {
|
||||
local name="$1"
|
||||
local dir="$work_root/$name"
|
||||
mkdir -p "$dir"
|
||||
(
|
||||
cd "$root"
|
||||
tar \
|
||||
--exclude='./.git' \
|
||||
--exclude='./.zero' \
|
||||
--exclude='./node_modules' \
|
||||
--exclude='./docs/.next' \
|
||||
--exclude='./docs/node_modules' \
|
||||
-cf - .
|
||||
) | (
|
||||
cd "$dir"
|
||||
tar -xf -
|
||||
)
|
||||
printf "%s" "$dir"
|
||||
}
|
||||
|
||||
pids=()
|
||||
names=()
|
||||
logs=()
|
||||
|
||||
run_job() {
|
||||
local name="$1"
|
||||
shift
|
||||
if [[ "$dry_run" == "1" ]]; then
|
||||
echo "would start: $name: $*"
|
||||
return
|
||||
fi
|
||||
|
||||
local dir
|
||||
dir="$(prepare_job_dir "$name")"
|
||||
local log="$log_dir/$name.log"
|
||||
names+=("$name")
|
||||
logs+=("$log")
|
||||
(
|
||||
cd "$dir"
|
||||
export PATH="$PATH"
|
||||
pnpm install --frozen-lockfile --prefer-offline
|
||||
"$@"
|
||||
) >"$log" 2>&1 &
|
||||
pids+=("$!")
|
||||
echo "started: $name"
|
||||
}
|
||||
|
||||
run_job conformance pnpm run conformance:local
|
||||
|
||||
for shard in $(seq 1 "$native_shards"); do
|
||||
run_job "native-tests-${shard}-of-${native_shards}" env ZERO_NATIVE_TEST_SHARD="${shard}/${native_shards}" pnpm run native:test:local
|
||||
done
|
||||
|
||||
run_job native-sanitizer-smoke pnpm run native:sanitize
|
||||
run_job command-contract-snapshots pnpm run command-contracts:local
|
||||
run_job workspace-checks bash scripts/workspace-checks.sh
|
||||
|
||||
if [[ "$dry_run" == "1" ]]; then
|
||||
echo "agent checks plan ok"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
failed=0
|
||||
for i in "${!pids[@]}"; do
|
||||
if wait "${pids[$i]}"; then
|
||||
echo "ok: ${names[$i]}"
|
||||
else
|
||||
echo "failed: ${names[$i]}"
|
||||
echo "log: ${logs[$i]}"
|
||||
failed=1
|
||||
fi
|
||||
done
|
||||
|
||||
if (( failed != 0 )); then
|
||||
echo
|
||||
echo "failed job logs:"
|
||||
for i in "${!pids[@]}"; do
|
||||
if [[ -f "${logs[$i]}" ]]; then
|
||||
echo "--- ${names[$i]} (${logs[$i]}) ---"
|
||||
tail -n 80 "${logs[$i]}"
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "agent checks ok"
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const outDir = ".zero/agent-repair-demo";
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const brokenSource = `pub fn main(world: World) -> Void raises {
|
||||
let src: [4]u8 = [1, 2, 3, 4]
|
||||
let dst: [4]u8 = [0, 0, 0, 0]
|
||||
let copied: usize = std.mem.copy(dst, src)
|
||||
if copied == 4 {
|
||||
check world.out.write("copied\\n")
|
||||
}
|
||||
}
|
||||
`;
|
||||
const workFile = join(outDir, "main.0");
|
||||
const workGraph = join(outDir, "main.graph");
|
||||
writeFileSync(workFile, brokenSource);
|
||||
|
||||
function zeroJson(args, allowFailure = false) {
|
||||
try {
|
||||
return JSON.parse(execFileSync("bin/zero", args, { encoding: "utf8" }));
|
||||
} catch (error) {
|
||||
if (!allowFailure) throw error;
|
||||
return JSON.parse(error.stdout.toString());
|
||||
}
|
||||
}
|
||||
|
||||
const importPlan = zeroJson(["import", "--json", "--format", "binary", "--out", workGraph, workFile], true);
|
||||
assert.equal(importPlan.ok, false);
|
||||
assert.equal(importPlan.diagnostics[0].code, "TYP009");
|
||||
assert.equal(importPlan.diagnostics[0].repair.id, "make-binding-mutable");
|
||||
|
||||
const explain = zeroJson(["explain", "--json", "TYP009"]);
|
||||
assert.equal(explain.code, "TYP009");
|
||||
assert.equal(explain.repair.id, "make-binding-mutable");
|
||||
|
||||
const repair = importPlan.diagnostics[0].repair;
|
||||
assert.equal(repair.id, "make-binding-mutable");
|
||||
|
||||
const fixedSource = brokenSource
|
||||
.replace(" let dst: [4]u8", " var dst: [4]u8")
|
||||
.replace(" let _copied: i32", " let _copied: usize");
|
||||
writeFileSync(workFile, fixedSource);
|
||||
const fixedImport = zeroJson(["import", "--json", "--format", "binary", "--out", workGraph, workFile]);
|
||||
assert.equal(fixedImport.ok, true);
|
||||
|
||||
const fixed = zeroJson(["check", "--json", workGraph]);
|
||||
assert.equal(fixed.ok, true);
|
||||
assert.equal(fixed.diagnostics.length, 0);
|
||||
|
||||
const projectDir = join(outDir, "project");
|
||||
rmSync(projectDir, { recursive: true, force: true });
|
||||
execFileSync("bin/zero", ["new", "package", projectDir], { stdio: ["ignore", "ignore", "pipe"] });
|
||||
|
||||
function assertNoC(body) {
|
||||
assert.equal(body.generatedCBytes ?? 0, 0);
|
||||
assert.equal(body.cBridgeFallback ?? body.selfHostRouting?.cBridge?.required ?? false, false);
|
||||
assert.notEqual(body.legacy, true);
|
||||
}
|
||||
|
||||
const projectCheck = zeroJson(["check", "--json", projectDir]);
|
||||
assert.equal(projectCheck.ok, true);
|
||||
assertNoC(projectCheck);
|
||||
|
||||
const projectTest = zeroJson(["test", "--json", projectDir]);
|
||||
assert.equal(projectTest.ok, true);
|
||||
assert.equal(projectTest.testBackend, "direct-program-graph");
|
||||
assertNoC(projectTest);
|
||||
|
||||
const projectMain = join(projectDir, "src", "main.0");
|
||||
const projectMainSource = readFileSync(projectMain, "utf8");
|
||||
writeFileSync(projectMain, projectMainSource.split('\n\ntest "package import works"')[0] + "\n");
|
||||
|
||||
const projectGraph = zeroJson(["inspect", "--json", projectDir]);
|
||||
assert.equal(projectGraph.selfHostRouting.cBridge.required, false);
|
||||
|
||||
const projectDoc = zeroJson(["doc", "--json", projectDir]);
|
||||
assertNoC(projectDoc);
|
||||
|
||||
const projectSize = zeroJson(["size", "--json", projectDir]);
|
||||
assertNoC(projectSize);
|
||||
|
||||
const projectMem = zeroJson(["mem", "--json", projectDir]);
|
||||
assertNoC(projectMem);
|
||||
|
||||
const releaseOut = join(outDir, "project-linux-release");
|
||||
const projectRelease = zeroJson(["build", "--json", "--release", "tiny", "--target", "linux-musl-x64", projectDir, "--out", releaseOut]);
|
||||
assert.equal(projectRelease.emit, "exe");
|
||||
assert.equal(projectRelease.compiler, "zero-elf64");
|
||||
assertNoC(projectRelease);
|
||||
|
||||
console.log("agent repair demo ok");
|
||||
@@ -0,0 +1,82 @@
|
||||
import nodeAssert from "node:assert/strict";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
const methods = ["deepEqual", "doesNotMatch", "equal", "match", "notEqual", "ok"];
|
||||
|
||||
function formatError(error) {
|
||||
if (!(error instanceof Error)) return String(error);
|
||||
return error.stack || error.message;
|
||||
}
|
||||
|
||||
function formatFailure(failure, index) {
|
||||
const header = `${index + 1}. ${failure.method}: ${failure.message}`;
|
||||
return failure.stack ? `${header}\n${failure.stack}` : header;
|
||||
}
|
||||
|
||||
export function createAggregateAssert(options = {}) {
|
||||
const failFast = process.env.ZERO_VALIDATION_FAIL_FAST === "1" ||
|
||||
process.env.ZERO_ASSERT_FAIL_FAST === "1" ||
|
||||
options.failFast === true;
|
||||
const failures = [];
|
||||
|
||||
function record(method, error) {
|
||||
if (failFast) throw error;
|
||||
failures.push({
|
||||
method,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : "",
|
||||
});
|
||||
}
|
||||
|
||||
function aggregateAssert(value, message) {
|
||||
try {
|
||||
nodeAssert(value, message);
|
||||
} catch (error) {
|
||||
record("assert", error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const method of methods) {
|
||||
aggregateAssert[method] = (...args) => {
|
||||
try {
|
||||
nodeAssert[method](...args);
|
||||
} catch (error) {
|
||||
record(method, error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
aggregateAssert.fail = (...args) => {
|
||||
try {
|
||||
nodeAssert.fail(...args);
|
||||
} catch (error) {
|
||||
record("fail", error);
|
||||
}
|
||||
};
|
||||
|
||||
aggregateAssert.failures = failures;
|
||||
return aggregateAssert;
|
||||
}
|
||||
|
||||
export function finishAggregateAssert(assert, options = {}) {
|
||||
const failures = assert?.failures ?? [];
|
||||
if (failures.length === 0) return;
|
||||
|
||||
const suite = options.suite || "validation";
|
||||
const reportPath = options.reportPath;
|
||||
const summary = failures.map(formatFailure).join("\n\n");
|
||||
const text = `${suite} collected ${failures.length} assertion failure(s):\n\n${summary}\n`;
|
||||
|
||||
if (reportPath) {
|
||||
mkdirSync(dirname(reportPath), { recursive: true });
|
||||
writeFileSync(reportPath, `${JSON.stringify({ suite, ok: false, failures }, null, 2)}\n`);
|
||||
}
|
||||
|
||||
process.stderr.write(text);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
export function describeFailure(error) {
|
||||
return formatError(error);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, rm, stat, symlink, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const root = process.cwd();
|
||||
const zero = "bin/zero";
|
||||
const fixture = "examples/hello.graph";
|
||||
const tempDir = await mkdtemp("/tmp/zero-artifact-finalization-");
|
||||
const outDir = join(tempDir, "out");
|
||||
const exe = join(outDir, "hello");
|
||||
const dirOutput = join(outDir, "as-directory");
|
||||
const symlinkTarget = join(outDir, "symlink-target");
|
||||
const symlinkOutput = join(outDir, "as-symlink");
|
||||
|
||||
type CommandFailure = {
|
||||
status: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
async function runZero(args: string[]) {
|
||||
return execFileAsync(zero, args, { cwd: root, timeout: 20000 });
|
||||
}
|
||||
|
||||
async function expectZeroFailure(args: string[]): Promise<CommandFailure> {
|
||||
try {
|
||||
await runZero(args);
|
||||
} catch (error) {
|
||||
const failure = error as CommandFailure;
|
||||
assert.notEqual(failure.status, 0);
|
||||
return {
|
||||
status: failure.status ?? null,
|
||||
stdout: failure.stdout ?? "",
|
||||
stderr: failure.stderr ?? "",
|
||||
};
|
||||
}
|
||||
assert.fail(`expected zero ${args.join(" ")} to fail`);
|
||||
}
|
||||
|
||||
function assertMentionsPathRejection(failure: CommandFailure) {
|
||||
const text = `${failure.stdout}\n${failure.stderr}`;
|
||||
assert.match(text, /regular|directory|symlink|artifact|output/i);
|
||||
}
|
||||
|
||||
async function assertExecutable(path: string) {
|
||||
const info = await stat(path);
|
||||
assert.equal(info.isFile(), true);
|
||||
assert.ok(info.size > 0, "executable output must be non-empty");
|
||||
if (process.platform !== "win32") {
|
||||
assert.notEqual(info.mode & 0o111, 0, "executable output must have an execute bit");
|
||||
}
|
||||
}
|
||||
|
||||
async function assertRunnable(path: string) {
|
||||
const { stdout, stderr } = await execFileAsync(path, [], { timeout: 5000 });
|
||||
assert.equal(stderr, "");
|
||||
assert.equal(stdout.trim(), "hello from zero");
|
||||
}
|
||||
|
||||
try {
|
||||
await mkdir(outDir, { recursive: true });
|
||||
|
||||
await runZero(["build", "--out", exe, fixture]);
|
||||
await assertExecutable(exe);
|
||||
await assertRunnable(exe);
|
||||
|
||||
await mkdir(dirOutput);
|
||||
const directoryFailure = await expectZeroFailure(["build", "--out", dirOutput, fixture]);
|
||||
assertMentionsPathRejection(directoryFailure);
|
||||
|
||||
if (process.platform !== "win32") {
|
||||
await writeFile(symlinkTarget, "not an executable\n");
|
||||
await symlink(symlinkTarget, symlinkOutput);
|
||||
const symlinkFailure = await expectZeroFailure(["build", "--out", symlinkOutput, fixture]);
|
||||
assertMentionsPathRejection(symlinkFailure);
|
||||
}
|
||||
|
||||
console.log("artifact finalization smoke ok");
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
Executable
+1115
File diff suppressed because it is too large
Load Diff
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$root"
|
||||
|
||||
mode="${ZERO_BENCH_MODE:-local}"
|
||||
if [[ " $* " == *" --mode local "* ]]; then
|
||||
mode="local"
|
||||
fi
|
||||
|
||||
if [[ "$mode" == "local" ]]; then
|
||||
make -C native/zero-c >/dev/null
|
||||
fi
|
||||
|
||||
if [[ " $* " == *" --mode "* ]]; then
|
||||
scripts/bench.mts "$@"
|
||||
else
|
||||
scripts/bench.mts --mode "$mode" "$@"
|
||||
fi
|
||||
@@ -0,0 +1,54 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { readdir, rm } from "node:fs/promises";
|
||||
import { execFile } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const cc = process.env.CC ?? "cc";
|
||||
const out = `/tmp/zero-canonical-text-smoke-${process.pid}`;
|
||||
|
||||
async function fixtureArgs(root: string) {
|
||||
const canonicalDir = path.join(root, "fixtures/canonical");
|
||||
const rejectDir = path.join(root, "fixtures/reject");
|
||||
const args: string[] = [];
|
||||
if (existsSync(canonicalDir)) {
|
||||
for (const name of (await readdir(canonicalDir)).filter((item) => item.endsWith(".canonical.0")).sort()) {
|
||||
args.push("--accept", path.join(canonicalDir, name));
|
||||
}
|
||||
}
|
||||
if (existsSync(rejectDir)) {
|
||||
for (const name of (await readdir(rejectDir)).filter((item) => item.endsWith(".0")).sort()) {
|
||||
args.push("--reject", path.join(rejectDir, name));
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
try {
|
||||
const nativeSources = (await readdir("native/zero-c/src"))
|
||||
.filter((name) => name.endsWith(".c") && name !== "main.c")
|
||||
.sort()
|
||||
.map((name) => path.join("native/zero-c/src", name));
|
||||
await execFileAsync(cc, [
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wpedantic",
|
||||
"-I",
|
||||
"native/zero-c/include",
|
||||
"-I",
|
||||
"native/zero-c/src",
|
||||
...nativeSources,
|
||||
"native/zero-c/tests/canonical_text_smoke.c",
|
||||
"-o",
|
||||
out,
|
||||
]);
|
||||
const args = process.env.ZERO_CANONICAL_TEXT_FIXTURES ? await fixtureArgs(process.env.ZERO_CANONICAL_TEXT_FIXTURES) : [];
|
||||
const result = await execFileAsync(out, args, { maxBuffer: 1024 * 1024 * 8 });
|
||||
if (!result.stdout.includes("canonical text smoke ok")) {
|
||||
throw new Error(`unexpected canonical text smoke output: ${result.stdout}`);
|
||||
}
|
||||
} finally {
|
||||
await rm(out, { force: true });
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const zeroBin = path.join(repoRoot, "bin/zero");
|
||||
const stdRoot = path.join(repoRoot, "std");
|
||||
const tmpRoot = path.join(os.tmpdir(), `zero-stdlib-binary-graphs-${process.pid}`);
|
||||
|
||||
function runZero(args: string[]) {
|
||||
const result = spawnSync(zeroBin, args, {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
const command = ["bin/zero", ...args].join(" ");
|
||||
const output = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
||||
throw new Error(`${command} failed\n${output}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.mkdirSync(tmpRoot, { recursive: true });
|
||||
|
||||
const graphs = fs.readdirSync(stdRoot)
|
||||
.filter((name) => name.endsWith(".graph"))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
for (const name of graphs) {
|
||||
const relative = `std/${name}`;
|
||||
const tmp = path.join(tmpRoot, name);
|
||||
runZero(["validate", "--format", "binary", "--out", tmp, relative]);
|
||||
const magic = fs.readFileSync(tmp).subarray(0, 8).toString("latin1");
|
||||
if (magic !== "ZRGBIN1\0") throw new Error(`${relative}: generated graph is not a binary graph store`);
|
||||
fs.renameSync(tmp, path.join(repoRoot, relative));
|
||||
}
|
||||
|
||||
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
||||
console.log(`converted ${graphs.length} std graph store(s) to binary`);
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const outPath = path.join(repoRoot, "native/zero-c/src/embedded_runtime_sources.inc");
|
||||
const inputs = [
|
||||
["zero_embedded_zero_runtime_h", "native/zero-c/include/zero_runtime.h"],
|
||||
["zero_embedded_zero_runtime_c", "native/zero-c/runtime/zero_runtime.c"],
|
||||
["zero_embedded_zero_http_curl_c", "native/zero-c/runtime/zero_http_curl.c"],
|
||||
];
|
||||
|
||||
function chunkText(text) {
|
||||
const chunks = [];
|
||||
let current = "";
|
||||
for (const line of text.match(/[^\n]*\n|[^\n]+$/g) || []) {
|
||||
if (current && JSON.stringify(current + line).length > 3000) {
|
||||
chunks.push(current);
|
||||
current = "";
|
||||
}
|
||||
if (JSON.stringify(line).length <= 3000) {
|
||||
current += line;
|
||||
continue;
|
||||
}
|
||||
for (let index = 0; index < line.length; index += 1400) {
|
||||
if (current) {
|
||||
chunks.push(current);
|
||||
current = "";
|
||||
}
|
||||
chunks.push(line.slice(index, index + 1400));
|
||||
}
|
||||
}
|
||||
if (current) chunks.push(current);
|
||||
return chunks;
|
||||
}
|
||||
|
||||
const out = [];
|
||||
out.push("/* Generated from Zero runtime sources. Run node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/embed-runtime-sources.mts to refresh. */");
|
||||
out.push("#ifndef ZERO_EMBEDDED_RUNTIME_SOURCES_INC");
|
||||
out.push("#define ZERO_EMBEDDED_RUNTIME_SOURCES_INC");
|
||||
out.push("");
|
||||
|
||||
for (const [name, relativePath] of inputs) {
|
||||
const text = fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
|
||||
out.push(`static const char *const ${name}[] = {`);
|
||||
for (const chunk of chunkText(text)) {
|
||||
out.push(` ${JSON.stringify(chunk)},`);
|
||||
}
|
||||
out.push(" NULL");
|
||||
out.push("};");
|
||||
out.push("");
|
||||
}
|
||||
|
||||
out.push("#endif");
|
||||
out.push("");
|
||||
|
||||
fs.writeFileSync(outPath, out.join("\n"));
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const outPath = path.join(repoRoot, "native/zero-c/src/embedded_skills.inc");
|
||||
const inputs = [
|
||||
"skills/zero/SKILL.md",
|
||||
...fs.readdirSync(path.join(repoRoot, "skill-data"))
|
||||
.filter((name) => name.endsWith(".md"))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((name) => `skill-data/${name}`),
|
||||
];
|
||||
|
||||
type EmbeddedSkill = {
|
||||
name: string;
|
||||
description: string;
|
||||
hidden: boolean;
|
||||
relativePath: string;
|
||||
text: string;
|
||||
ident?: string;
|
||||
};
|
||||
|
||||
function parseFrontmatter(text, relativePath): Omit<EmbeddedSkill, "relativePath" | "text" | "ident"> {
|
||||
const match = text.trimStart().match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
||||
if (!match) throw new Error(`${relativePath}: missing skill frontmatter`);
|
||||
|
||||
const lines = match[1].split(/\r?\n/);
|
||||
let name = null;
|
||||
let description = "";
|
||||
let hidden = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
if (line.startsWith("name:")) {
|
||||
name = line.slice("name:".length).trim();
|
||||
} else if (line.startsWith("description:")) {
|
||||
const parts = [line.slice("description:".length).trim()];
|
||||
while (i + 1 < lines.length && /^( |\t)/.test(lines[i + 1])) {
|
||||
parts.push(lines[++i].trim());
|
||||
}
|
||||
description = parts.join(" ");
|
||||
} else if (line.startsWith("hidden:")) {
|
||||
hidden = ["true", "yes"].includes(line.slice("hidden:".length).trim());
|
||||
}
|
||||
}
|
||||
|
||||
if (!name) throw new Error(`${relativePath}: missing skill name`);
|
||||
return { name, description, hidden };
|
||||
}
|
||||
|
||||
function chunkText(text) {
|
||||
const chunks = [];
|
||||
let current = "";
|
||||
for (const line of text.match(/[^\n]*\n|[^\n]+$/g) || []) {
|
||||
if (current && JSON.stringify(current + line).length > 3000) {
|
||||
chunks.push(current);
|
||||
current = "";
|
||||
}
|
||||
if (JSON.stringify(line).length <= 3000) {
|
||||
current += line;
|
||||
continue;
|
||||
}
|
||||
for (let index = 0; index < line.length; index += 1400) {
|
||||
if (current) {
|
||||
chunks.push(current);
|
||||
current = "";
|
||||
}
|
||||
chunks.push(line.slice(index, index + 1400));
|
||||
}
|
||||
}
|
||||
if (current) chunks.push(current);
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function cIdent(text) {
|
||||
return text.replace(/[^A-Za-z0-9_]/g, "_");
|
||||
}
|
||||
|
||||
const skills: EmbeddedSkill[] = inputs.map((relativePath) => {
|
||||
const text = fs.readFileSync(path.join(repoRoot, relativePath), "utf8");
|
||||
return { ...parseFrontmatter(text, relativePath), relativePath, text };
|
||||
}).sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const out = [];
|
||||
out.push("/* Generated from Zero skill data. Run node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/embed-skill-data.mts to refresh. */");
|
||||
out.push("#ifndef ZERO_EMBEDDED_SKILLS_INC");
|
||||
out.push("#define ZERO_EMBEDDED_SKILLS_INC");
|
||||
out.push("");
|
||||
out.push("typedef struct {");
|
||||
out.push(" const char *name;");
|
||||
out.push(" const char *description;");
|
||||
out.push(" bool hidden;");
|
||||
out.push(" const char *const *content;");
|
||||
out.push("} ZeroEmbeddedSkill;");
|
||||
out.push("");
|
||||
|
||||
for (const skill of skills) {
|
||||
const ident = `zero_embedded_skill_${cIdent(skill.name)}_chunks`;
|
||||
skill.ident = ident;
|
||||
out.push(`static const char *const ${ident}[] = {`);
|
||||
for (const chunk of chunkText(skill.text)) {
|
||||
out.push(` ${JSON.stringify(chunk)},`);
|
||||
}
|
||||
out.push(" NULL");
|
||||
out.push("};");
|
||||
out.push("");
|
||||
}
|
||||
|
||||
out.push("static const ZeroEmbeddedSkill zero_embedded_skills[] = {");
|
||||
for (const skill of skills) {
|
||||
out.push(` {${JSON.stringify(skill.name)}, ${JSON.stringify(skill.description)}, ${skill.hidden ? "true" : "false"}, ${skill.ident}},`);
|
||||
}
|
||||
out.push("};");
|
||||
out.push("static const size_t zero_embedded_skill_count = sizeof(zero_embedded_skills) / sizeof(zero_embedded_skills[0]);");
|
||||
out.push("");
|
||||
out.push("#endif");
|
||||
out.push("");
|
||||
|
||||
fs.writeFileSync(outPath, out.join("\n"));
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const stdRoot = path.join(repoRoot, "std");
|
||||
const outPath = path.join(repoRoot, "native/zero-c/src/embedded_stdlib_graph.inc");
|
||||
const outDir = path.join(repoRoot, "native/zero-c/src/embedded_stdlib_graph");
|
||||
|
||||
function cIdent(text: string): string {
|
||||
return text.replace(/[^A-Za-z0-9_]/g, "_");
|
||||
}
|
||||
|
||||
function appendByteArray(out: string[], ident: string, bytes: Buffer) {
|
||||
out.push(`static const unsigned char ${ident}[] = {`);
|
||||
const rowWidth = 64;
|
||||
for (let offset = 0; offset < bytes.length; offset += rowWidth) {
|
||||
const row = [...bytes.subarray(offset, Math.min(offset + rowWidth, bytes.length))]
|
||||
.map((byte) => `0x${byte.toString(16).padStart(2, "0")}`)
|
||||
.join(", ");
|
||||
out.push(` ${row},`);
|
||||
}
|
||||
out.push("};");
|
||||
}
|
||||
|
||||
const fnvPrime = 1099511628211n;
|
||||
const fnvMask = (1n << 64n) - 1n;
|
||||
|
||||
function fnvMul(hash: bigint): bigint {
|
||||
return (hash * fnvPrime) & fnvMask;
|
||||
}
|
||||
|
||||
function hashText(hash: bigint, text: string): bigint {
|
||||
for (const byte of Buffer.from(text)) {
|
||||
hash ^= BigInt(byte);
|
||||
hash = fnvMul(hash);
|
||||
}
|
||||
hash ^= 0xffn;
|
||||
return fnvMul(hash);
|
||||
}
|
||||
|
||||
function hashBytes(hash: bigint, bytes: Buffer): bigint {
|
||||
hash ^= BigInt(bytes.length);
|
||||
hash = fnvMul(hash);
|
||||
for (const byte of bytes) {
|
||||
hash ^= BigInt(byte);
|
||||
hash = fnvMul(hash);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
const inputs = fs.existsSync(stdRoot)
|
||||
? fs.readdirSync(stdRoot)
|
||||
.filter((name) => name.endsWith(".graph"))
|
||||
.sort((a, b) => a.localeCompare(b))
|
||||
.map((name) => `std/${name}`)
|
||||
: [];
|
||||
|
||||
const out: string[] = [];
|
||||
out.push("/* Generated from Zero standard-library binary ProgramGraphs. Run node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/embed-stdlib-graphs.mts to refresh. */");
|
||||
out.push("#ifndef ZERO_EMBEDDED_STDLIB_GRAPH_INC");
|
||||
out.push("#define ZERO_EMBEDDED_STDLIB_GRAPH_INC");
|
||||
out.push("");
|
||||
|
||||
fs.rmSync(outDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
|
||||
let fingerprint = hashText(1469598103934665603n, "zero-embedded-stdlib-graph-v1");
|
||||
|
||||
for (const relativePath of inputs) {
|
||||
const ident = `zero_embedded_stdlib_graph_${cIdent(relativePath)}_bytes`;
|
||||
const partName = `${cIdent(relativePath)}.inc`;
|
||||
const part: string[] = [];
|
||||
const bytes = fs.readFileSync(path.join(repoRoot, relativePath));
|
||||
const moduleName = relativePath.slice(0, -".graph".length).replace("/", ".");
|
||||
const modulePath = relativePath.slice(0, -".graph".length) + ".0";
|
||||
fingerprint = hashText(fingerprint, moduleName);
|
||||
fingerprint = hashText(fingerprint, modulePath);
|
||||
fingerprint = hashBytes(fingerprint, bytes);
|
||||
part.push(`/* Generated from ${relativePath}. */`);
|
||||
appendByteArray(part, ident, bytes);
|
||||
part.push("");
|
||||
fs.writeFileSync(path.join(outDir, partName), part.join("\n"));
|
||||
out.push(`#include "embedded_stdlib_graph/${partName}"`);
|
||||
}
|
||||
|
||||
out.push("");
|
||||
out.push(`#define ZERO_EMBEDDED_STDLIB_GRAPH_FINGERPRINT 0x${fingerprint.toString(16).padStart(16, "0")}ull`);
|
||||
out.push("");
|
||||
out.push("#endif");
|
||||
out.push("");
|
||||
|
||||
fs.writeFileSync(outPath, out.join("\n"));
|
||||
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readdirSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
// Every example package with a committed zero.graph must pass `zero check`,
|
||||
// build an executable for the canonical linux-musl-x64 target, and `zero run`
|
||||
// cleanly. This closes the gap where a stale example keeps passing check while
|
||||
// run-compilation fails. Host-backend gaps (for example fs programs on the
|
||||
// Mach-O direct backend) are tolerated only after the linux build proves the
|
||||
// package is not stale; hosts whose backend supports the package still run it.
|
||||
|
||||
const zeroBin = resolve("bin/zero");
|
||||
const examplesDir = resolve("examples");
|
||||
const buildTarget = "linux-musl-x64";
|
||||
const runTimeoutMs = 120_000;
|
||||
const buildTimeoutMs = 300_000;
|
||||
|
||||
type Expectation = {
|
||||
exitCode?: number;
|
||||
output?: string;
|
||||
server?: { readyText: string };
|
||||
};
|
||||
|
||||
const expectations: Record<string, Expectation> = {
|
||||
"batch3-cli": { output: "batch3 cli ok" },
|
||||
"direct-package-arrays": { exitCode: 13 },
|
||||
"direct-package-call-order": { exitCode: 27 },
|
||||
"memory-package": { output: "memory package ok" },
|
||||
"ping-pong-api": { server: { readyText: "listening on " } },
|
||||
"readall-cli": { output: "readall cli ok" },
|
||||
"resource-cli": { output: "resource cli ok" },
|
||||
"systems-package": { output: "systems package" },
|
||||
"zero-hash": { output: "zero-hash ok" },
|
||||
};
|
||||
|
||||
const packages = readdirSync(examplesDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && existsSync(join(examplesDir, entry.name, "zero.graph")))
|
||||
.map((entry) => entry.name)
|
||||
.sort();
|
||||
|
||||
if (packages.length === 0) {
|
||||
console.error("examples gate found no example packages with committed zero.graph stores");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const staleExpectations = Object.keys(expectations).filter((name) => !packages.includes(name));
|
||||
if (staleExpectations.length > 0) {
|
||||
console.error(`examples gate expectations name missing packages: ${staleExpectations.join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function runZero(args: string[], cwd: string, timeoutMs = runTimeoutMs) {
|
||||
const result = spawnSync(zeroBin, args, {
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
timeout: timeoutMs,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
return {
|
||||
code: result.status,
|
||||
signal: result.signal,
|
||||
stdout: result.stdout ?? "",
|
||||
stderr: result.stderr ?? "",
|
||||
error: result.error?.message ?? "",
|
||||
timedOut: result.error !== undefined && "code" in result.error && result.error.code === "ETIMEDOUT",
|
||||
timeoutMs,
|
||||
};
|
||||
}
|
||||
|
||||
function zeroFailure(action: string, result: ReturnType<typeof runZero>) {
|
||||
const detail = result.stderr || result.stdout || result.error;
|
||||
if (result.timedOut) {
|
||||
return `${action} timed out after ${result.timeoutMs}ms${detail ? `\n${detail}` : ""}`;
|
||||
}
|
||||
return `${action} failed (exit ${result.code}${result.signal ? ` signal ${result.signal}` : ""})${detail ? `\n${detail}` : ""}`;
|
||||
}
|
||||
|
||||
function isHostBackendGap(text: string) {
|
||||
return text.includes("BLD004") && text.includes("executable buildability subset");
|
||||
}
|
||||
|
||||
async function probeServer(packagePath: string, readyText: string, workDir: string) {
|
||||
return await new Promise<{ ok: boolean; detail: string }>((settle) => {
|
||||
const child = spawn(zeroBin, ["run", packagePath], { cwd: workDir, detached: true, stdio: ["ignore", "pipe", "pipe"] });
|
||||
let output = "";
|
||||
let done = false;
|
||||
const finish = (ok: boolean, detail: string) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
clearTimeout(timer);
|
||||
if (child.pid !== undefined && child.exitCode === null) {
|
||||
try {
|
||||
process.kill(-child.pid, "SIGKILL");
|
||||
} catch {
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
}
|
||||
settle({ ok, detail });
|
||||
};
|
||||
const timer = setTimeout(() => finish(false, `server never printed '${readyText}'\n${output}`), runTimeoutMs);
|
||||
const onChunk = (chunk: Buffer) => {
|
||||
output += chunk.toString();
|
||||
if (output.includes(readyText)) finish(true, "server ready");
|
||||
};
|
||||
child.stdout.on("data", onChunk);
|
||||
child.stderr.on("data", onChunk);
|
||||
child.on("error", (error) => finish(false, error.message));
|
||||
child.on("exit", (code, signal) => finish(false, `server exited before readiness: code ${code} signal ${signal}\n${output}`));
|
||||
});
|
||||
}
|
||||
|
||||
const failures: string[] = [];
|
||||
const startedAt = Date.now();
|
||||
|
||||
for (const name of packages) {
|
||||
const packagePath = join(examplesDir, name);
|
||||
const expectation = expectations[name] ?? {};
|
||||
const workDir = mkdtempSync(join(tmpdir(), `zero-examples-gate-${name}-`));
|
||||
const steps: string[] = [];
|
||||
let failure = "";
|
||||
|
||||
const check = runZero(["check", packagePath], workDir);
|
||||
if (check.code === 0) {
|
||||
steps.push("check");
|
||||
} else {
|
||||
failure = zeroFailure("zero check", check);
|
||||
}
|
||||
|
||||
let linuxBuildOk = false;
|
||||
if (!failure && !expectation.server) {
|
||||
const build = runZero(["build", "--emit", "exe", "--target", buildTarget, packagePath, "--out", join(workDir, `${name}-${buildTarget}`)], workDir, buildTimeoutMs);
|
||||
if (build.code === 0) {
|
||||
linuxBuildOk = true;
|
||||
steps.push(`build ${buildTarget}`);
|
||||
} else {
|
||||
failure = zeroFailure(`zero build --target ${buildTarget}`, build);
|
||||
}
|
||||
}
|
||||
|
||||
if (!failure && expectation.server) {
|
||||
const probe = await probeServer(packagePath, expectation.server.readyText, workDir);
|
||||
if (probe.ok) {
|
||||
steps.push("run server-ready");
|
||||
} else {
|
||||
failure = `zero run server probe failed: ${probe.detail}`;
|
||||
}
|
||||
} else if (!failure) {
|
||||
const expectedExit = expectation.exitCode ?? 0;
|
||||
// Arguments after the input belong to the program (zero run [input] -- <args>),
|
||||
// so --out must precede the package path or it lands in the program argv.
|
||||
const run = runZero(["run", "--out", join(workDir, `${name}-exe`), packagePath], workDir);
|
||||
const runText = `${run.stdout}${run.stderr}`;
|
||||
if (run.code !== expectedExit && linuxBuildOk && isHostBackendGap(runText)) {
|
||||
steps.push("run host-backend-gap");
|
||||
} else if (run.code !== expectedExit) {
|
||||
failure = `zero run exited ${run.code}${run.signal ? ` signal ${run.signal}` : ""}, expected ${expectedExit}\n${runText}`;
|
||||
} else if (expectation.output && !run.stdout.includes(expectation.output)) {
|
||||
failure = `zero run output missing '${expectation.output}'\n${runText}`;
|
||||
} else {
|
||||
steps.push("run");
|
||||
}
|
||||
}
|
||||
|
||||
// Retry the teardown: macOS indexing can briefly repopulate the temp tree
|
||||
// while rm walks it, surfacing as ENOTEMPTY on an otherwise clean removal.
|
||||
rmSync(workDir, { force: true, recursive: true, maxRetries: 5, retryDelay: 100 });
|
||||
if (failure) {
|
||||
failures.push(`${name}: ${failure.trimEnd()}`);
|
||||
console.error(`examples gate FAIL: ${name}`);
|
||||
} else {
|
||||
console.log(`examples gate ok: ${name} (${steps.join(", ")})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`\nexamples gate failed for ${failures.length} package(s):\n`);
|
||||
for (const failure of failures) console.error(`${failure}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`examples gate ok: ${packages.length} packages in ${((Date.now() - startedAt) / 1000).toFixed(1)}s`);
|
||||
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const root = process.cwd();
|
||||
const tempDir = await mkdtemp("/tmp/zero-fs-runtime-");
|
||||
const src = join(tempDir, "fs-runtime-smoke.c");
|
||||
const exe = join(tempDir, "fs-runtime-smoke");
|
||||
const input = join(tempDir, "input.txt");
|
||||
const large = join(tempDir, "large.bin");
|
||||
const nestedDir = join(tempDir, "nested");
|
||||
const missing = join(tempDir, "missing.txt");
|
||||
|
||||
const harness = String.raw`
|
||||
#include "zero_runtime.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static ZeroByteView bytes(const char *text) {
|
||||
return (ZeroByteView){(const unsigned char *)text, strlen(text)};
|
||||
}
|
||||
|
||||
static ZeroMutByteView mut_bytes(unsigned char *ptr, size_t len) {
|
||||
return (ZeroMutByteView){ptr, len};
|
||||
}
|
||||
|
||||
static int fail(const char *name) {
|
||||
fprintf(stderr, "fs runtime smoke failed: %s\n", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int expect_true(int ok, const char *name) {
|
||||
return ok ? 0 : fail(name);
|
||||
}
|
||||
|
||||
static int expect_none(ZeroMaybeUsize result, const char *name) {
|
||||
return expect_true(result.has == 0 && result.value == 0, name);
|
||||
}
|
||||
|
||||
static int expect_small_read(const char *path) {
|
||||
unsigned char buffer[64];
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
ZeroMaybeUsize result = zero_fs_read_bytes(bytes(path), mut_bytes(buffer, sizeof(buffer)));
|
||||
return expect_true(
|
||||
result.has == 1 &&
|
||||
result.value == 14 &&
|
||||
memcmp(buffer, "hello runtime\n", 14) == 0 &&
|
||||
buffer[14] == 0,
|
||||
"read regular file"
|
||||
);
|
||||
}
|
||||
|
||||
static int expect_partial_read(const char *path) {
|
||||
unsigned char buffer[5];
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
ZeroMaybeUsize result = zero_fs_read_bytes(bytes(path), mut_bytes(buffer, sizeof(buffer)));
|
||||
return expect_true(
|
||||
result.has == 1 &&
|
||||
result.value == 14 &&
|
||||
memcmp(buffer, "hello", sizeof(buffer)) == 0,
|
||||
"read partial buffer reports total size"
|
||||
);
|
||||
}
|
||||
|
||||
static int expect_zero_len_read(const char *path) {
|
||||
unsigned char buffer[1] = {0xaa};
|
||||
ZeroMaybeUsize result = zero_fs_read_bytes(bytes(path), mut_bytes(buffer, 0));
|
||||
return expect_true(result.has == 1 && result.value == 14 && buffer[0] == 0xaa, "read zero-length buffer reports total size");
|
||||
}
|
||||
|
||||
static int expect_large_read(const char *path) {
|
||||
const size_t len = 1048576u + 17u;
|
||||
unsigned char *buffer = (unsigned char *)malloc(len);
|
||||
if (!buffer) return fail("allocate large read buffer");
|
||||
memset(buffer, 0, len);
|
||||
ZeroMaybeUsize result = zero_fs_read_bytes(bytes(path), mut_bytes(buffer, len));
|
||||
int ok = result.has == 1 && result.value == len;
|
||||
for (size_t i = 0; ok && i < len; i++) {
|
||||
ok = buffer[i] == 'z';
|
||||
}
|
||||
free(buffer);
|
||||
return expect_true(ok, "read large file");
|
||||
}
|
||||
|
||||
static int expect_rejected_inputs(const char *directory, const char *missing) {
|
||||
unsigned char buffer[16];
|
||||
memset(buffer, 0, sizeof(buffer));
|
||||
|
||||
int status = 0;
|
||||
status |= expect_none(zero_fs_read_bytes(bytes(directory), mut_bytes(buffer, sizeof(buffer))), "reject directory");
|
||||
status |= expect_none(zero_fs_read_bytes(bytes(missing), mut_bytes(buffer, sizeof(buffer))), "reject missing file");
|
||||
status |= expect_none(zero_fs_read_bytes((ZeroByteView){NULL, 1}, mut_bytes(buffer, sizeof(buffer))), "reject null path");
|
||||
status |= expect_none(zero_fs_read_bytes((ZeroByteView){(const unsigned char *)"", 0}, mut_bytes(buffer, sizeof(buffer))), "reject empty path");
|
||||
status |= expect_none(zero_fs_read_bytes(bytes(missing), (ZeroMutByteView){NULL, sizeof(buffer)}), "reject null buffer");
|
||||
status |= expect_none(zero_fs_read_bytes(bytes(missing), (ZeroMutByteView){NULL, 0}), "reject null zero-length buffer");
|
||||
|
||||
char long_path[4097];
|
||||
memset(long_path, 'a', 4096);
|
||||
long_path[4096] = '\0';
|
||||
status |= expect_none(
|
||||
zero_fs_read_bytes((ZeroByteView){(const unsigned char *)long_path, 4096}, mut_bytes(buffer, sizeof(buffer))),
|
||||
"reject path at runtime limit"
|
||||
);
|
||||
return status;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 5) return fail("expected input, large, directory, and missing paths");
|
||||
int status = 0;
|
||||
status |= expect_small_read(argv[1]);
|
||||
status |= expect_partial_read(argv[1]);
|
||||
status |= expect_zero_len_read(argv[1]);
|
||||
status |= expect_large_read(argv[2]);
|
||||
status |= expect_rejected_inputs(argv[3], argv[4]);
|
||||
return status;
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
await mkdir(nestedDir);
|
||||
await writeFile(input, "hello runtime\n");
|
||||
await writeFile(large, Buffer.alloc(1048576 + 17, "z"));
|
||||
await writeFile(src, harness);
|
||||
await execFileAsync("cc", [
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wpedantic",
|
||||
"-Inative/zero-c/include",
|
||||
"native/zero-c/runtime/zero_runtime.c",
|
||||
src,
|
||||
"-o",
|
||||
exe,
|
||||
], { cwd: root });
|
||||
const { stdout, stderr } = await execFileAsync(exe, [input, large, nestedDir, missing], { timeout: 5000 });
|
||||
assert.equal(stderr, "");
|
||||
assert.equal(stdout, "");
|
||||
console.log("fs runtime smoke ok");
|
||||
} finally {
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,428 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const zero = process.env.ZERO_BIN || "bin/zero";
|
||||
const target = process.env.ZERO_GRAPH_BUILD_PERF_TARGET || "linux-musl-x64";
|
||||
const outRoot = process.env.ZERO_GRAPH_BUILD_PERF_DIR || `/tmp/zero-graph-build-perf-${process.pid}`;
|
||||
const maxBuffer = 128 * 1024 * 1024;
|
||||
|
||||
type Fixture = {
|
||||
name: string;
|
||||
input: string;
|
||||
target?: string;
|
||||
checkTarget?: string;
|
||||
};
|
||||
|
||||
type CommandRun = {
|
||||
ok: boolean;
|
||||
command: string[];
|
||||
wallMs: number;
|
||||
body: any;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
type PerformanceBudget = {
|
||||
name: string;
|
||||
fixture: string;
|
||||
pass: "cold" | "warm";
|
||||
command: "build" | "check";
|
||||
metric: string;
|
||||
max: number;
|
||||
unit: "ms" | "bytes";
|
||||
};
|
||||
|
||||
const fixtures: Fixture[] = [
|
||||
{ name: "hello-artifact", input: "examples/hello.graph" },
|
||||
{ name: "stdlib-heavy-artifact", input: "conformance/native/pass/stdlib-target-neutral.graph" },
|
||||
{ name: "crm-api-package", input: "examples/crm-api" },
|
||||
];
|
||||
|
||||
const budgetMode = process.env.ZERO_GRAPH_BUILD_PERF_BUDGET || "warn";
|
||||
|
||||
function envNumber(name: string, fallback: number) {
|
||||
const raw = process.env[name];
|
||||
if (!raw) return fallback;
|
||||
const parsed = Number(raw);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
const performanceBudgets: PerformanceBudget[] = [
|
||||
{
|
||||
name: "simple graph cold build",
|
||||
fixture: "hello-artifact",
|
||||
pass: "cold",
|
||||
command: "build",
|
||||
metric: "compilerElapsedMs",
|
||||
max: envNumber("ZERO_GRAPH_BUILD_PERF_SIMPLE_COLD_MAX_MS", 5000),
|
||||
unit: "ms",
|
||||
},
|
||||
{
|
||||
name: "simple graph warm build",
|
||||
fixture: "hello-artifact",
|
||||
pass: "warm",
|
||||
command: "build",
|
||||
metric: "compilerElapsedMs",
|
||||
max: envNumber("ZERO_GRAPH_BUILD_PERF_SIMPLE_WARM_MAX_MS", 3000),
|
||||
unit: "ms",
|
||||
},
|
||||
{
|
||||
name: "simple graph artifact size",
|
||||
fixture: "hello-artifact",
|
||||
pass: "warm",
|
||||
command: "build",
|
||||
metric: "artifactBytes",
|
||||
max: envNumber("ZERO_GRAPH_BUILD_PERF_SIMPLE_ARTIFACT_MAX_BYTES", 8192),
|
||||
unit: "bytes",
|
||||
},
|
||||
{
|
||||
name: "simple graph lowered IR size",
|
||||
fixture: "hello-artifact",
|
||||
pass: "warm",
|
||||
command: "build",
|
||||
metric: "loweredIrBytes",
|
||||
max: envNumber("ZERO_GRAPH_BUILD_PERF_SIMPLE_LOWERED_IR_MAX_BYTES", 16384),
|
||||
unit: "bytes",
|
||||
},
|
||||
{
|
||||
name: "stdlib-heavy cold build",
|
||||
fixture: "stdlib-heavy-artifact",
|
||||
pass: "cold",
|
||||
command: "build",
|
||||
metric: "compilerElapsedMs",
|
||||
max: envNumber("ZERO_GRAPH_BUILD_PERF_STDLIB_HEAVY_COLD_MAX_MS", 120000),
|
||||
unit: "ms",
|
||||
},
|
||||
{
|
||||
name: "stdlib-heavy warm build",
|
||||
fixture: "stdlib-heavy-artifact",
|
||||
pass: "warm",
|
||||
command: "build",
|
||||
metric: "compilerElapsedMs",
|
||||
max: envNumber("ZERO_GRAPH_BUILD_PERF_STDLIB_HEAVY_WARM_MAX_MS", 120000),
|
||||
unit: "ms",
|
||||
},
|
||||
{
|
||||
name: "stdlib-heavy warm graph load",
|
||||
fixture: "stdlib-heavy-artifact",
|
||||
pass: "warm",
|
||||
command: "build",
|
||||
metric: "timings.graphLoadMs",
|
||||
max: envNumber("ZERO_GRAPH_BUILD_PERF_STDLIB_HEAVY_GRAPH_LOAD_MAX_MS", 30000),
|
||||
unit: "ms",
|
||||
},
|
||||
{
|
||||
name: "stdlib-heavy warm stdlib merge",
|
||||
fixture: "stdlib-heavy-artifact",
|
||||
pass: "warm",
|
||||
command: "build",
|
||||
metric: "timings.stdlibMergeMs",
|
||||
max: envNumber("ZERO_GRAPH_BUILD_PERF_STDLIB_HEAVY_STDLIB_MERGE_MAX_MS", 70000),
|
||||
unit: "ms",
|
||||
},
|
||||
{
|
||||
name: "stdlib-heavy artifact size",
|
||||
fixture: "stdlib-heavy-artifact",
|
||||
pass: "warm",
|
||||
command: "build",
|
||||
metric: "artifactBytes",
|
||||
max: envNumber("ZERO_GRAPH_BUILD_PERF_STDLIB_HEAVY_ARTIFACT_MAX_BYTES", 262144),
|
||||
unit: "bytes",
|
||||
},
|
||||
{
|
||||
name: "stdlib-heavy lowered IR size",
|
||||
fixture: "stdlib-heavy-artifact",
|
||||
pass: "warm",
|
||||
command: "build",
|
||||
metric: "loweredIrBytes",
|
||||
max: envNumber("ZERO_GRAPH_BUILD_PERF_STDLIB_HEAVY_LOWERED_IR_MAX_BYTES", 4194304),
|
||||
unit: "bytes",
|
||||
},
|
||||
];
|
||||
|
||||
function selectedFixtures() {
|
||||
const raw = process.env.ZERO_GRAPH_BUILD_PERF_CASES;
|
||||
if (!raw) return fixtures;
|
||||
const wanted = new Set(raw.split(",").map((part) => part.trim()).filter(Boolean));
|
||||
return fixtures.filter((fixture) => wanted.has(fixture.name) || wanted.has(fixture.input));
|
||||
}
|
||||
|
||||
function phaseMs(body: any, name: string) {
|
||||
const phase = Array.isArray(body?.compilerPhases)
|
||||
? body.compilerPhases.find((item: any) => item?.name === name)
|
||||
: null;
|
||||
return typeof phase?.elapsedMs === "number" ? phase.elapsedMs : null;
|
||||
}
|
||||
|
||||
function graphBuildTimings(body: any) {
|
||||
const timings = body?.graphBuild?.timings || {};
|
||||
return {
|
||||
graphLoadMs: timings.graphLoadMs ?? null,
|
||||
stdlibMergeMs: timings.stdlibMergeMs ?? null,
|
||||
stdlibReferenceScanMs: timings.stdlibReferenceScanMs ?? null,
|
||||
stdlibCleanupMs: timings.stdlibCleanupMs ?? null,
|
||||
stdlibModuleLoadMs: timings.stdlibModuleLoadMs ?? null,
|
||||
stdlibNodeMergeMs: timings.stdlibNodeMergeMs ?? null,
|
||||
stdlibEdgeMergeMs: timings.stdlibEdgeMergeMs ?? null,
|
||||
stdlibFinalizeMs: timings.stdlibFinalizeMs ?? null,
|
||||
readinessCheckMs: timings.readinessCheckMs ?? null,
|
||||
mirCacheLoadMs: timings.mirCacheLoadMs ?? null,
|
||||
mirLowerMs: timings.mirLowerMs ?? null,
|
||||
mirCacheWriteMs: timings.mirCacheWriteMs ?? null,
|
||||
mirCacheReloadMs: timings.mirCacheReloadMs ?? null,
|
||||
mirLoadOrLowerMs: timings.mirLoadOrLowerMs ?? null,
|
||||
lowerPhaseMs: timings.lowerPhaseMs ?? phaseMs(body, "lower"),
|
||||
codegenMs: timings.codegenMs ?? phaseMs(body, "codegen"),
|
||||
objectMs: timings.objectMs ?? phaseMs(body, "object"),
|
||||
linkMs: timings.linkMs ?? phaseMs(body, "link"),
|
||||
};
|
||||
}
|
||||
|
||||
function graphBuildStdlibMergeFacts(body: any) {
|
||||
const merge = body?.graphBuild?.stdlibMerge || {};
|
||||
return {
|
||||
modulesMerged: merge.modulesMerged ?? null,
|
||||
nodesMerged: merge.nodesMerged ?? null,
|
||||
edgesMerged: merge.edgesMerged ?? null,
|
||||
cacheHit: merge.cacheHit ?? null,
|
||||
cacheStored: merge.cacheStored ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function graphCheckTimings(body: any) {
|
||||
const timings = body?.graphCompiler?.timings || {};
|
||||
const resolveMs = timings.resolveMs ?? phaseMs(body, "resolve");
|
||||
const checkMs = timings.checkMs ?? phaseMs(body, "check");
|
||||
const lowerMs = timings.lowerMs ?? phaseMs(body, "lower");
|
||||
return {
|
||||
graphLoadMs: timings.loadMs ?? null,
|
||||
resolveMs: resolveMs ?? null,
|
||||
checkMs: checkMs ?? null,
|
||||
readinessCheckMs: typeof resolveMs === "number" && typeof checkMs === "number" ? resolveMs + checkMs : null,
|
||||
readinessLowerMs: lowerMs ?? null,
|
||||
cacheMs: timings.cacheMs ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function cacheFacts(body: any) {
|
||||
const caches = Array.isArray(body?.compilerCaches) ? body.compilerCaches : [];
|
||||
const mapped = caches.find((cache: any) => cache?.name === "mappedFinalMir") || body?.graphBuild?.mappedFinalMir || null;
|
||||
return {
|
||||
hits: caches.filter((cache: any) => cache?.hit === true).length,
|
||||
misses: caches.filter((cache: any) => cache?.hit === false).length,
|
||||
mappedFinalMir: mapped ? {
|
||||
hit: mapped.hit ?? null,
|
||||
written: mapped.written ?? null,
|
||||
byteLength: mapped.byteLength ?? null,
|
||||
memoryMapped: mapped.memoryMapped ?? null,
|
||||
codegenImmediate: mapped.codegenImmediate ?? null,
|
||||
programReconstructed: mapped.programReconstructed ?? null,
|
||||
} : null,
|
||||
};
|
||||
}
|
||||
|
||||
function valueAtPath(value: any, path: string) {
|
||||
return path.split(".").reduce((current, part) => current?.[part], value);
|
||||
}
|
||||
|
||||
function evaluateBudgets(results: any[]) {
|
||||
const mode = budgetMode === "fail" || budgetMode === "warn" || budgetMode === "off" ? budgetMode : "warn";
|
||||
if (mode === "off") {
|
||||
return { schemaVersion: 1, mode, status: "off", ok: true, overCount: 0, skippedCount: 0, items: [] };
|
||||
}
|
||||
const items = performanceBudgets.map((budget) => {
|
||||
const fixture = results.find((item) => item.name === budget.fixture);
|
||||
const run = fixture?.[budget.pass]?.[budget.command];
|
||||
const actual = valueAtPath(run, budget.metric);
|
||||
if (!fixture) {
|
||||
return { ...budget, status: "skipped", actual: null, reason: "fixture not selected" };
|
||||
}
|
||||
if (typeof actual !== "number") {
|
||||
return { ...budget, status: "skipped", actual: null, reason: "metric missing" };
|
||||
}
|
||||
const overBy = Number(Math.max(0, actual - budget.max).toFixed(3));
|
||||
const unitSuffix = budget.unit === "ms" ? "Ms" : "Bytes";
|
||||
return {
|
||||
...budget,
|
||||
status: actual <= budget.max ? "ok" : "over",
|
||||
actual,
|
||||
max: budget.max,
|
||||
[`actual${unitSuffix}`]: actual,
|
||||
[`max${unitSuffix}`]: budget.max,
|
||||
[`overBy${unitSuffix}`]: overBy,
|
||||
ratio: Number((actual / budget.max).toFixed(3)),
|
||||
};
|
||||
});
|
||||
const overCount = items.filter((item) => item.status === "over").length;
|
||||
const skippedCount = items.filter((item) => item.status === "skipped").length;
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
mode,
|
||||
status: overCount === 0 ? "ok" : mode === "fail" ? "fail" : "warn",
|
||||
ok: overCount === 0,
|
||||
overCount,
|
||||
skippedCount,
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
function writeBudgetMessages(budget: any) {
|
||||
if (!budget || budget.mode === "off") return;
|
||||
for (const item of budget.items || []) {
|
||||
if (item.status === "over") {
|
||||
process.stderr.write(
|
||||
`graph perf budget ${budget.status}: ${item.name} ${item.actual} ${item.unit} > ${item.max} ${item.unit} (${item.metric})\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runJson(args: string[], env: Record<string, string | undefined>): Promise<CommandRun> {
|
||||
const started = performance.now();
|
||||
try {
|
||||
const result = await execFileAsync(zero, args, {
|
||||
env: { ...process.env, ...env },
|
||||
maxBuffer,
|
||||
});
|
||||
const wallMs = Number((performance.now() - started).toFixed(3));
|
||||
return { ok: true, command: [zero, ...args], wallMs, body: JSON.parse(result.stdout), stderr: result.stderr };
|
||||
} catch (error: any) {
|
||||
const wallMs = Number((performance.now() - started).toFixed(3));
|
||||
let body: any = null;
|
||||
try {
|
||||
body = JSON.parse(error.stdout?.toString() ?? "");
|
||||
} catch {
|
||||
body = null;
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
command: [zero, ...args],
|
||||
wallMs,
|
||||
body,
|
||||
stderr: error.stderr?.toString() ?? error.message ?? "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function buildCompiler() {
|
||||
if (process.env.ZERO_GRAPH_BUILD_PERF_SKIP_BUILD === "1") return null;
|
||||
const started = performance.now();
|
||||
await execFileAsync("make", ["-C", "native/zero-c"], { maxBuffer });
|
||||
return Number((performance.now() - started).toFixed(3));
|
||||
}
|
||||
|
||||
async function zeroVersion() {
|
||||
const result = await runJson(["--version", "--json"], {});
|
||||
if (!result.ok) throw new Error(`zero version failed: ${result.stderr}`);
|
||||
return result.body;
|
||||
}
|
||||
|
||||
function requireOk(run: CommandRun) {
|
||||
if (run.ok && run.body?.ok !== false) return;
|
||||
const diagnostic = run.body?.diagnostics?.[0];
|
||||
const message = diagnostic ? `${diagnostic.code}: ${diagnostic.message}` : run.stderr || "command failed";
|
||||
throw new Error(`${run.command.join(" ")} failed: ${message}`);
|
||||
}
|
||||
|
||||
function normalizeBuild(run: CommandRun) {
|
||||
requireOk(run);
|
||||
return {
|
||||
ok: true,
|
||||
wallMs: run.wallMs,
|
||||
compilerElapsedMs: run.body?.elapsedMs ?? null,
|
||||
graphHash: run.body?.graph?.graphHash ?? null,
|
||||
lowering: run.body?.graph?.lowering ?? null,
|
||||
artifactBytes: run.body?.artifactBytes ?? null,
|
||||
loweredIrBytes: run.body?.loweredIrBytes ?? null,
|
||||
timings: graphBuildTimings(run.body),
|
||||
stdlibMerge: graphBuildStdlibMergeFacts(run.body),
|
||||
caches: cacheFacts(run.body),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCheck(run: CommandRun) {
|
||||
requireOk(run);
|
||||
return {
|
||||
ok: true,
|
||||
wallMs: run.wallMs,
|
||||
graphHash: run.body?.graph?.graphHash ?? run.body?.graphHash ?? null,
|
||||
targetReady: run.body?.targetReadiness?.ok ?? null,
|
||||
timings: graphCheckTimings(run.body),
|
||||
caches: cacheFacts(run.body),
|
||||
};
|
||||
}
|
||||
|
||||
async function runFixture(fixture: Fixture) {
|
||||
const fixtureTarget = fixture.target || target;
|
||||
const checkTarget = fixture.checkTarget || process.env.ZERO_GRAPH_BUILD_PERF_CHECK_TARGET || "";
|
||||
const fixtureRoot = join(outRoot, fixture.name);
|
||||
const buildCache = join(fixtureRoot, "build-cache");
|
||||
const checkCache = join(fixtureRoot, "check-cache");
|
||||
await mkdir(fixtureRoot, { recursive: true });
|
||||
|
||||
async function runPass(pass: "cold" | "warm") {
|
||||
const artifactPath = join(fixtureRoot, `${fixture.name}-${pass}.o`);
|
||||
process.stderr.write(`graph perf ${fixture.name} ${pass} build\n`);
|
||||
const build = await runJson(["build", "--json", "--emit", "obj", "--target", fixtureTarget, fixture.input, "--out", artifactPath], {
|
||||
ZERO_CACHE_DIR: buildCache,
|
||||
});
|
||||
process.stderr.write(`graph perf ${fixture.name} ${pass} check\n`);
|
||||
const checkArgs = ["check", "--json"];
|
||||
if (checkTarget) checkArgs.push("--target", checkTarget);
|
||||
checkArgs.push(fixture.input);
|
||||
const check = await runJson(checkArgs, {
|
||||
ZERO_CACHE_DIR: checkCache,
|
||||
});
|
||||
return {
|
||||
build: normalizeBuild(build),
|
||||
check: normalizeCheck(check),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: fixture.name,
|
||||
input: fixture.input,
|
||||
target: fixtureTarget,
|
||||
checkTarget: checkTarget || "host-default",
|
||||
cold: await runPass("cold"),
|
||||
warm: await runPass("warm"),
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await rm(outRoot, { recursive: true, force: true });
|
||||
await mkdir(outRoot, { recursive: true });
|
||||
const nativeBuildMs = await buildCompiler();
|
||||
const version = await zeroVersion();
|
||||
const cases = selectedFixtures();
|
||||
if (cases.length === 0) throw new Error("ZERO_GRAPH_BUILD_PERF_CASES selected no fixtures");
|
||||
const started = performance.now();
|
||||
const results = [];
|
||||
for (const fixture of cases) results.push(await runFixture(fixture));
|
||||
const elapsedMs = Number((performance.now() - started).toFixed(3));
|
||||
const budgets = evaluateBudgets(results);
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
kind: "zero-graph-build-perf",
|
||||
generatedAt: new Date().toISOString(),
|
||||
zero: version,
|
||||
target,
|
||||
outDir: outRoot,
|
||||
nativeBuildMs,
|
||||
elapsedMs,
|
||||
budgets,
|
||||
cases: results,
|
||||
};
|
||||
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
||||
writeBudgetMessages(budgets);
|
||||
if (budgets.status === "fail") process.exit(1);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const files = [
|
||||
"tests/zero-cli.test.ts",
|
||||
"scripts/snapshot-command-contracts.mts",
|
||||
"scripts/program-graph-parity.mts",
|
||||
"scripts/reliability-smoke.mts",
|
||||
];
|
||||
|
||||
const compilerInputCommands = new Set(["check", "build", "run", "test", "size", "mem", "doc", "dev", "time", "fix"]);
|
||||
const compilerInputValueFlags = new Set(["--backend", "--emit", "--filter", "--out", "--profile", "--release", "--target"]);
|
||||
const abiInputSubcommands = new Set(["check", "dump"]);
|
||||
|
||||
function lineForOffset(text: string, offset: number) {
|
||||
return text.slice(0, offset).split("\n").length;
|
||||
}
|
||||
|
||||
function findArrayEnd(text: string, start: number) {
|
||||
let depth = 0;
|
||||
let quote = "";
|
||||
let escaped = false;
|
||||
for (let i = start; i < text.length; i++) {
|
||||
const ch = text[i];
|
||||
if (quote) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
} else if (ch === "\\") {
|
||||
escaped = true;
|
||||
} else if (ch === quote) {
|
||||
quote = "";
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch === "\"" || ch === "'" || ch === "`") {
|
||||
quote = ch;
|
||||
} else if (ch === "[") {
|
||||
depth++;
|
||||
} else if (ch === "]") {
|
||||
depth--;
|
||||
if (depth === 0) return i + 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function stringLiterals(segment: string) {
|
||||
const out: string[] = [];
|
||||
const re = /"((?:\\.|[^"\\])*)"/g;
|
||||
let match;
|
||||
while ((match = re.exec(segment))) {
|
||||
out.push(match[1].replace(/\\"/g, "\"").replace(/\\\\/g, "\\"));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isCompilerInputCommand(strings: string[]) {
|
||||
if (strings.length === 0) return false;
|
||||
if (compilerInputCommands.has(strings[0])) return true;
|
||||
return strings[0] === "abi" && abiInputSubcommands.has(strings[1]);
|
||||
}
|
||||
|
||||
function projectionInputViolations(strings: string[]) {
|
||||
const violations: string[] = [];
|
||||
let skipNext = false;
|
||||
for (const value of strings.slice(1)) {
|
||||
if (value === "--") break;
|
||||
if (skipNext) {
|
||||
skipNext = false;
|
||||
continue;
|
||||
}
|
||||
if (compilerInputValueFlags.has(value)) {
|
||||
skipNext = true;
|
||||
continue;
|
||||
}
|
||||
if (value.endsWith(".0")) violations.push(value);
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
const failures: string[] = [];
|
||||
for (const file of files) {
|
||||
const text = readFileSync(file, "utf8");
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text[i] !== "[") continue;
|
||||
const end = findArrayEnd(text, i);
|
||||
if (end === -1) break;
|
||||
const segment = text.slice(i, end);
|
||||
const strings = stringLiterals(segment);
|
||||
if (isCompilerInputCommand(strings)) {
|
||||
for (const input of projectionInputViolations(strings)) {
|
||||
failures.push(`${file}:${lineForOffset(text, i)}: compiler command ${strings[0]} uses projection input ${input}`);
|
||||
}
|
||||
}
|
||||
i = end - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error("direct .0 compiler inputs are not allowed in command contracts/tests:");
|
||||
for (const failure of failures) console.error(`- ${failure}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("graph input policy ok");
|
||||
Executable
+976
@@ -0,0 +1,976 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { createServer as createHttpServer, request as createHttpRequest } from "node:http";
|
||||
import { createServer as createHttpsServer } from "node:https";
|
||||
import { connect as connectTcp, createServer as createTcpServer } from "node:net";
|
||||
import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { basename } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const zero = "bin/zero";
|
||||
const outDir = ".zero/native-test";
|
||||
const zeroRunTimeoutMs = 20000;
|
||||
const target =
|
||||
process.platform === "darwin" && process.arch === "arm64" ? "darwin-arm64" :
|
||||
process.platform === "linux" && process.arch === "x64" ? "linux-x64" :
|
||||
null;
|
||||
|
||||
type RawZeroListenerRequestOptions = {
|
||||
end?: boolean;
|
||||
timeoutMs?: number;
|
||||
};
|
||||
|
||||
function zeroArray(count) {
|
||||
return `0_u8; ${count}`;
|
||||
}
|
||||
|
||||
async function canLinkCurl() {
|
||||
const src = `/tmp/zero-http-curl-link-${process.pid}.c`;
|
||||
const exe = `/tmp/zero-http-curl-link-${process.pid}`;
|
||||
await writeFile(src, "#include <curl/curl.h>\nint main(void) { return curl_global_init(CURL_GLOBAL_DEFAULT) == CURLE_OK ? 0 : 1; }\n");
|
||||
try {
|
||||
await execFileAsync("cc", [src, "-lcurl", "-o", exe]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
await rm(src, { force: true });
|
||||
await rm(exe, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function canRunOpenSsl() {
|
||||
try {
|
||||
await execFileAsync("openssl", ["version"]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function assertProviderUnavailableRuntime() {
|
||||
const src = `/tmp/zero-http-provider-unavailable-${process.pid}.c`;
|
||||
const exe = `/tmp/zero-http-provider-unavailable-${process.pid}`;
|
||||
await writeFile(src, `#include "zero_runtime.h"
|
||||
|
||||
int main(void) {
|
||||
const unsigned char request[] = "GET http://127.0.0.1/\\n\\n";
|
||||
unsigned char response[64] = {0};
|
||||
uint64_t result = zero_http_fetch_result(
|
||||
(ZeroByteView){request, sizeof(request) - 1},
|
||||
(ZeroMutByteView){response, sizeof(response)},
|
||||
10000000
|
||||
);
|
||||
return zero_http_result_ok(result) == 0 &&
|
||||
zero_http_result_status(result) == 0 &&
|
||||
zero_http_result_body_len(result) == 0 &&
|
||||
zero_http_result_error(result) == ZERO_HTTP_PROVIDER_UNAVAILABLE &&
|
||||
zero_http_response_len((ZeroByteView){response, sizeof(response)}) == 0 &&
|
||||
zero_http_response_body_offset((ZeroByteView){response, sizeof(response)}) == ZERO_HTTP_RESPONSE_META_BYTES ? 0 : 1;
|
||||
}
|
||||
`);
|
||||
try {
|
||||
await execFileAsync("cc", [
|
||||
"-DZERO_RUNTIME_NO_CURL",
|
||||
"-Inative/zero-c/include",
|
||||
"native/zero-c/runtime/zero_runtime.c",
|
||||
"native/zero-c/runtime/zero_http_curl.c",
|
||||
src,
|
||||
"-o",
|
||||
exe,
|
||||
]);
|
||||
await execFileAsync(exe, [], { timeout: 5000 });
|
||||
} finally {
|
||||
await rm(src, { force: true });
|
||||
await rm(exe, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function createTlsFixture() {
|
||||
const dir = await mkdtemp("/tmp/zero-http-tls-");
|
||||
const caKey = `${dir}/ca.key`;
|
||||
const caCert = `${dir}/ca.pem`;
|
||||
const serverKey = `${dir}/server.key`;
|
||||
const serverCsr = `${dir}/server.csr`;
|
||||
const serverCert = `${dir}/server.pem`;
|
||||
const serverConf = `${dir}/server.cnf`;
|
||||
await writeFile(serverConf, `[req]
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
prompt = no
|
||||
[req_distinguished_name]
|
||||
CN = localhost
|
||||
[v3_req]
|
||||
subjectAltName = @alt_names
|
||||
[alt_names]
|
||||
DNS.1 = localhost
|
||||
IP.1 = 127.0.0.1
|
||||
`);
|
||||
try {
|
||||
await execFileAsync("openssl", ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "1", "-keyout", caKey, "-out", caCert, "-subj", "/CN=Zero Test CA"]);
|
||||
await execFileAsync("openssl", ["req", "-newkey", "rsa:2048", "-nodes", "-keyout", serverKey, "-out", serverCsr, "-subj", "/CN=localhost", "-config", serverConf]);
|
||||
await execFileAsync("openssl", ["x509", "-req", "-in", serverCsr, "-CA", caCert, "-CAkey", caKey, "-CAcreateserial", "-out", serverCert, "-days", "1", "-sha256", "-extfile", serverConf, "-extensions", "v3_req"]);
|
||||
return { dir, caCert, serverKey, serverCert };
|
||||
} catch {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function listen(server) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
server.off("error", reject);
|
||||
resolve(server.address().port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function listenOn(server, port) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const onError = (error) => {
|
||||
server.off("listening", onListening);
|
||||
reject(error);
|
||||
};
|
||||
const onListening = () => {
|
||||
server.off("error", onError);
|
||||
resolve();
|
||||
};
|
||||
server.once("error", onError);
|
||||
server.once("listening", onListening);
|
||||
server.listen(port, "127.0.0.1");
|
||||
});
|
||||
}
|
||||
|
||||
function close(server) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => error ? reject(error) : resolve());
|
||||
});
|
||||
}
|
||||
|
||||
function waitForZeroListener(child, timeoutMs = 30000) {
|
||||
return new Promise<{ port: number; stdout: string; stderr: string }>((resolve, reject) => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
const maybeReady = () => {
|
||||
const match = `${stdout}\n${stderr}`.match(/listening on http:\/\/127\.0\.0\.1:(\d+)/);
|
||||
if (match) {
|
||||
clearTimeout(timer);
|
||||
resolve({ port: Number(match[1]), stdout, stderr });
|
||||
}
|
||||
};
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error(`timed out waiting for zero listener\nstdout:\n${stdout}\nstderr:\n${stderr}`));
|
||||
}, timeoutMs);
|
||||
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk.toString("utf8");
|
||||
maybeReady();
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk.toString("utf8");
|
||||
maybeReady();
|
||||
});
|
||||
child.once("exit", (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
reject(new Error(`zero listener exited before ready: code=${code} signal=${signal}\nstdout:\n${stdout}\nstderr:\n${stderr}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function stopZeroListener(child) {
|
||||
return new Promise<void>((resolve) => {
|
||||
if (child.exitCode !== null || child.signalCode !== null) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const killTimer = setTimeout(() => {
|
||||
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||
}, 1000);
|
||||
child.once("exit", () => {
|
||||
clearTimeout(killTimer);
|
||||
resolve();
|
||||
});
|
||||
child.kill("SIGTERM");
|
||||
});
|
||||
}
|
||||
|
||||
function requestZeroListener(port, method, path, body = "", headers = {}) {
|
||||
const bodyBuffer = Buffer.from(body);
|
||||
return new Promise<{ status: number; body: string }>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
request.destroy(new Error(`${method} ${path} timed out`));
|
||||
}, 5000);
|
||||
const request = createHttpRequest({
|
||||
host: "127.0.0.1",
|
||||
port,
|
||||
path,
|
||||
method,
|
||||
headers: {
|
||||
...headers,
|
||||
...(bodyBuffer.length > 0 ? { "content-length": String(bodyBuffer.length) } : {}),
|
||||
},
|
||||
}, (response) => {
|
||||
const chunks = [];
|
||||
response.on("data", (chunk) => chunks.push(chunk));
|
||||
response.on("end", () => {
|
||||
clearTimeout(timeout);
|
||||
resolve({
|
||||
status: response.statusCode ?? 0,
|
||||
body: Buffer.concat(chunks).toString("utf8"),
|
||||
});
|
||||
});
|
||||
});
|
||||
request.on("error", (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(new Error(`${method} ${path} failed: ${error.message}`));
|
||||
});
|
||||
if (bodyBuffer.length > 0) request.write(bodyBuffer);
|
||||
request.end();
|
||||
});
|
||||
}
|
||||
|
||||
function rawZeroListenerRequest(port, payload, options: RawZeroListenerRequestOptions = {}) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const chunks = [];
|
||||
let settled = false;
|
||||
const socket = connectTcp({ host: "127.0.0.1", port }, () => {
|
||||
if (options.end === false) socket.write(payload);
|
||||
else socket.end(payload);
|
||||
});
|
||||
const timeout = setTimeout(() => {
|
||||
finish(new Error("raw HTTP request timed out"));
|
||||
}, options.timeoutMs ?? 5000);
|
||||
function finish(error = null) {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
socket.destroy();
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(Buffer.concat(chunks).toString("utf8"));
|
||||
}
|
||||
socket.on("data", (chunk) => chunks.push(chunk));
|
||||
socket.on("end", () => finish());
|
||||
socket.on("close", () => {
|
||||
if (!settled && chunks.length > 0) finish();
|
||||
});
|
||||
socket.on("error", finish);
|
||||
});
|
||||
}
|
||||
|
||||
async function assertExplicitPortConflict() {
|
||||
const dir = await mkdtemp("/tmp/zero-http-listen-explicit-");
|
||||
const packageDir = `${dir}/ping-pong-api`;
|
||||
const patchPath = `${dir}/explicit-port.patch`;
|
||||
try {
|
||||
await cp("examples/ping-pong-api", packageDir, { recursive: true });
|
||||
await writeFile(patchPath, `zero-program-graph-patch v1
|
||||
replaceFunctionBody main
|
||||
check std.http.listen(world, 3000_u16)
|
||||
end
|
||||
`);
|
||||
await execFileAsync(zero, ["patch", packageDir, patchPath], { timeout: zeroRunTimeoutMs });
|
||||
const result = await execFileAsync(zero, ["run", packageDir], { timeout: zeroRunTimeoutMs }).catch((error) => error);
|
||||
assert.notEqual(result.code, 0);
|
||||
assert.match(`${result.stdout ?? ""}\n${result.stderr ?? ""}`, /std\.http\.listen could not bind the requested port|Address already in use/);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
async function runHttpListenExample() {
|
||||
const tempDirsBefore = new Set((await readdir("/tmp")).filter((entry) => entry.startsWith("zero-listen-")));
|
||||
let reserved3000 = null;
|
||||
let port3000Occupied = false;
|
||||
const devPortGuard = createTcpServer();
|
||||
try {
|
||||
await listenOn(devPortGuard, 3000);
|
||||
reserved3000 = devPortGuard;
|
||||
port3000Occupied = true;
|
||||
} catch (error) {
|
||||
try {
|
||||
devPortGuard.close();
|
||||
} catch {
|
||||
// The guard may never have started if another local service owns 3000.
|
||||
}
|
||||
if (error && error.code === "EADDRINUSE") {
|
||||
port3000Occupied = true;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const child = spawn(zero, ["run", "examples/ping-pong-api"], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
try {
|
||||
const { port } = await waitForZeroListener(child);
|
||||
if (port3000Occupied) {
|
||||
assert.notEqual(port, 3000);
|
||||
}
|
||||
assert.deepEqual(await requestZeroListener(port, "GET", "/ping"), {
|
||||
status: 200,
|
||||
body: "{\"message\":\"pong\"}",
|
||||
});
|
||||
assert.deepEqual(await requestZeroListener(port, "GET", "/health"), {
|
||||
status: 200,
|
||||
body: "{\"ok\":true}",
|
||||
});
|
||||
assert.deepEqual(await requestZeroListener(port, "GET", "/missing"), {
|
||||
status: 404,
|
||||
body: "{\"error\":\"not_found\"}",
|
||||
});
|
||||
assert.deepEqual(await requestZeroListener(port, "POST", "/echo", "{\"ping\":1}", { "content-type": "application/json" }), {
|
||||
status: 201,
|
||||
body: "{\"ping\":1}",
|
||||
});
|
||||
assert.deepEqual(await requestZeroListener(port, "POST", "/echo", "not-json"), {
|
||||
status: 400,
|
||||
body: "{\"error\":\"bad_request\"}",
|
||||
});
|
||||
const badRequest = await rawZeroListenerRequest(port, "POST /echo\r\ncontent-length: nope\r\n\r\nx");
|
||||
assert.match(badRequest, /^HTTP\/1\.1 400 Bad Request/);
|
||||
assert.match(badRequest, /\r\nconnection: close\r\n/i);
|
||||
assert.match(badRequest, /\{"error":"bad_request"\}/);
|
||||
const tooLarge = await rawZeroListenerRequest(port, "POST /echo\r\ncontent-length: 999999\r\n\r\n");
|
||||
assert.match(tooLarge, /^HTTP\/1\.1 413 Payload Too Large/);
|
||||
assert.match(tooLarge, /\{"error":"payload_too_large"\}/);
|
||||
const incomplete = await rawZeroListenerRequest(port, "POST /echo\r\ncontent-length: 4\r\n\r\nx", { end: false, timeoutMs: 8000 });
|
||||
assert.match(incomplete, /^HTTP\/1\.1 408 Request Timeout/);
|
||||
assert.match(incomplete, /\{"error":"request_timeout"\}/);
|
||||
if (port3000Occupied) await assertExplicitPortConflict();
|
||||
} finally {
|
||||
await stopZeroListener(child);
|
||||
if (reserved3000) await close(reserved3000);
|
||||
const tempDirsAfter = (await readdir("/tmp")).filter((entry) => entry.startsWith("zero-listen-"));
|
||||
for (const entry of tempDirsAfter) {
|
||||
assert(tempDirsBefore.has(entry), `std.http.listen leaked temporary directory /tmp/${entry}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runExitCode(path, env = {}) {
|
||||
try {
|
||||
await execFileAsync(path, [], { timeout: 5000, env: { ...process.env, ...env } });
|
||||
return 0;
|
||||
} catch (error) {
|
||||
if (typeof error.code === "number") return error.code;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function importGraphInput(sourcePath) {
|
||||
const graphPath = `/tmp/zero-http-runtime-${process.pid}-${basename(sourcePath, ".0")}.graph`;
|
||||
const projectionSidecar = `${sourcePath.slice(0, -2)}.graph`;
|
||||
await rm(projectionSidecar, { force: true });
|
||||
await rm(graphPath, { force: true });
|
||||
await execFileAsync(zero, ["import", "--format", "text", "--out", graphPath, sourcePath]);
|
||||
return graphPath;
|
||||
}
|
||||
|
||||
function assertRuntimeReport(report, targetName) {
|
||||
assert.equal(report.generatedCBytes, 0);
|
||||
assert.equal(report.objectBackend?.linking?.targetLibraries, "zero-runtime,curl");
|
||||
assert.equal(report.objectBackend?.linking?.externalToolchain, "cc");
|
||||
assert.equal(report.objectBackend?.httpRuntime?.status, "supported");
|
||||
assert.equal(report.objectBackend?.httpRuntime?.provider, "curl");
|
||||
assert.equal(report.objectBackend?.httpRuntime?.tlsVerification, true);
|
||||
assert.equal(report.objectBackend?.httpRuntime?.customCa?.env, "ZERO_HTTP_TEST_CA_BUNDLE");
|
||||
assert(report.objectBackend?.linkerPlan?.staticLibraries?.includes("zero_runtime.o"));
|
||||
assert(report.objectBackend?.linkerPlan?.staticLibraries?.includes("zero_http_curl.o"));
|
||||
assert(report.objectBackend?.linkerPlan?.systemLibraries?.includes("curl"));
|
||||
assert.equal(report.objectBackend?.directFacts?.runtimeHelperCount, 2);
|
||||
if (targetName === "darwin-arm64") {
|
||||
assert.equal(report.objectBackend?.objectEmission?.path, "direct-macho64-object");
|
||||
} else {
|
||||
assert.equal(report.objectBackend?.objectEmission?.path, "direct-elf64-object");
|
||||
}
|
||||
}
|
||||
|
||||
async function buildAndRun(name, source, expectedCode, env = {}) {
|
||||
const src = `/tmp/zero-http-runtime-${process.pid}-${name}.0`;
|
||||
const exe = `${outDir}/${name}`;
|
||||
const jsonPath = `${exe}.json`;
|
||||
await writeFile(src, source);
|
||||
const input = await importGraphInput(src);
|
||||
const build = await execFileAsync(zero, ["build", "--json", "--emit", "exe", "--target", target, input, "--out", exe], { maxBuffer: 1024 * 1024 });
|
||||
await writeFile(jsonPath, build.stdout);
|
||||
assertRuntimeReport(JSON.parse(build.stdout), target);
|
||||
const code = await runExitCode(exe, env);
|
||||
assert.equal(code, expectedCode);
|
||||
}
|
||||
|
||||
async function runHttpJsonExample(baseUrl) {
|
||||
const exe = `${outDir}/std-http-json-example`;
|
||||
const run = await execFileAsync(zero, ["run", "--out", exe, "examples/std-http-json.graph", "--", `GET ${baseUrl}/ok\n\n`], { timeout: zeroRunTimeoutMs });
|
||||
assert.equal(run.stdout, "http json ok\n");
|
||||
}
|
||||
|
||||
async function runHttpRequestExample(baseUrl) {
|
||||
const exe = `${outDir}/std-http-request-example`;
|
||||
const request = `POST ${baseUrl}/echo\ncontent-type: application/json\nx-zero-test: yes\n\n{"ping":1}`;
|
||||
const run = await execFileAsync(zero, ["run", "--out", exe, "examples/std-http-request.graph", "--", request], { timeout: zeroRunTimeoutMs });
|
||||
assert.equal(run.stdout, "http request ok\n");
|
||||
}
|
||||
|
||||
async function runHttpHeadersExample(baseUrl) {
|
||||
const exe = `${outDir}/std-http-headers-example`;
|
||||
const run = await execFileAsync(zero, ["run", "--out", exe, "examples/std-http-headers.graph", "--", `GET ${baseUrl}/headers\n\n`, "connection"], { timeout: zeroRunTimeoutMs });
|
||||
assert.equal(run.stdout, "http header found\n");
|
||||
}
|
||||
|
||||
async function runJsonApiClientExample(baseUrl) {
|
||||
const exe = `${outDir}/json-api-client-example`;
|
||||
const run = await execFileAsync(zero, ["run", "--out", exe, "examples/json-api-client.graph", "--", `${baseUrl}/client`], { timeout: zeroRunTimeoutMs });
|
||||
assert.equal(run.stdout, "json api client ok\n");
|
||||
}
|
||||
|
||||
async function runJsonApiRouterExample() {
|
||||
const exe = `${outDir}/json-api-router-example`;
|
||||
const request = "POST /users?tenant=demo\ncontent-type: application/json\n\n{\"id\":7}";
|
||||
const run = await execFileAsync(zero, ["run", "--out", exe, "examples/json-api-router.graph", "--", request], { timeout: zeroRunTimeoutMs });
|
||||
assert.equal(run.stdout, "json api router ok\n");
|
||||
const corsExe = `${outDir}/json-api-router-cors-example`;
|
||||
const preflight = "OPTIONS /users\naccess-control-request-method: POST\n\n";
|
||||
const cors = await execFileAsync(zero, ["run", "--out", corsExe, "examples/json-api-router.graph", "--", preflight], { timeout: zeroRunTimeoutMs });
|
||||
assert.equal(cors.stdout, "json api router ok\n");
|
||||
}
|
||||
|
||||
function okSource(baseUrl) {
|
||||
return `export c fn main() -> i32 {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var response: [512]u8 = [${zeroArray(512)}]
|
||||
let result: HttpResult = std.http.fetch(client, std.mem.span("GET ${baseUrl}/ok\\n\\n"), response, std.time.ms(1000))
|
||||
let body_offset: usize = std.http.responseBodyOffset(response)
|
||||
let body_len: usize = std.http.resultBodyLen(result)
|
||||
if std.http.resultOk(result) == false {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultStatus(result) != 200 {
|
||||
return 99
|
||||
}
|
||||
if body_len != 8 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultError(result) != std.http.errorNone() {
|
||||
return 99
|
||||
}
|
||||
if std.http.responseLen(response) < body_len {
|
||||
return 99
|
||||
}
|
||||
if response[body_offset] != 123_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[body_offset + 1] != 34_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[body_offset + 2] != 111_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[body_offset + 3] != 107_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[body_offset + 4] != 34_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[body_offset + 5] != 58_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[body_offset + 6] != 49_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[body_offset + 7] != 125_u8 {
|
||||
return 99
|
||||
}
|
||||
return 8
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function fetchRequestSource(baseUrl, method, path, body, expectedStatus, expectedBodyLen, expectedCode) {
|
||||
return `export c fn main() -> i32 {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
let request: Span<u8> = std.mem.span("${method} ${baseUrl}${path}\\ncontent-type: application/json\\nx-zero-test: yes\\n\\n${body}")
|
||||
var response: [512]u8 = [${zeroArray(512)}]
|
||||
let result: HttpResult = std.http.fetch(client, request, response, std.time.ms(1000))
|
||||
let body_offset: usize = std.http.responseBodyOffset(response)
|
||||
if std.http.resultOk(result) == false {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultStatus(result) != ${expectedStatus} {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultBodyLen(result) != ${expectedBodyLen} {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultError(result) != std.http.errorNone() {
|
||||
return 99
|
||||
}
|
||||
if response[body_offset] != 123_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[body_offset + 2] == 0_u8 {
|
||||
return 99
|
||||
}
|
||||
return ${expectedCode}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function resultFailureSource(baseUrl, path, responseSize, timeoutMs, expectedStatus, expectedLen, expectedError, expectedCode) {
|
||||
return `export c fn main() -> i32 {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var response: [${responseSize}]u8 = [${zeroArray(responseSize)}]
|
||||
let result: HttpResult = std.http.fetch(client, std.mem.span("GET ${baseUrl}${path}\\n\\n"), response, std.time.ms(${timeoutMs}))
|
||||
if std.http.resultOk(result) != false {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultStatus(result) != ${expectedStatus} {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultBodyLen(result) != ${expectedLen} {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultError(result) != ${expectedError} {
|
||||
return 99
|
||||
}
|
||||
return ${expectedCode}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function headersSource(baseUrl) {
|
||||
return `export c fn main() -> i32 {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var response: [512]u8 = [${zeroArray(512)}]
|
||||
let result: HttpResult = std.http.fetch(client, std.mem.span("GET ${baseUrl}/ok\\n\\n"), response, std.time.ms(1000))
|
||||
let reply: HttpHeaderValue = std.http.headerValue(response, std.mem.span("x-zero-reply"))
|
||||
let reply_bytes: Maybe<Span<u8>> = std.http.headerBytes(response, reply)
|
||||
if std.http.resultOk(result) == false {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultStatus(result) != 200 {
|
||||
return 99
|
||||
}
|
||||
if std.http.responseHeadersLen(response) <= 16 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultError(result) != std.http.errorNone() {
|
||||
return 99
|
||||
}
|
||||
if std.http.headerFound(reply) == false {
|
||||
return 99
|
||||
}
|
||||
if std.http.headerLen(reply) != 3 {
|
||||
return 99
|
||||
}
|
||||
if !reply_bytes.has {
|
||||
return 99
|
||||
}
|
||||
if reply_bytes.value[0] != 121_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[24] != 72_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[25] != 84_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[26] != 84_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[27] != 80_u8 {
|
||||
return 99
|
||||
}
|
||||
return 44
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function interimHeadersSource(baseUrl) {
|
||||
return `export c fn main() -> i32 {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var response: [512]u8 = [${zeroArray(512)}]
|
||||
let result: HttpResult = std.http.fetch(client, std.mem.span("GET ${baseUrl}/interim\\n\\n"), response, std.time.ms(1000))
|
||||
let reply: HttpHeaderValue = std.http.headerValue(response, std.mem.span("x-zero-reply"))
|
||||
let reply_offset: usize = std.http.headerOffset(reply)
|
||||
let body_offset: usize = std.http.responseBodyOffset(response)
|
||||
if std.http.resultOk(result) == false {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultStatus(result) != 200 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultBodyLen(result) != 8 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultError(result) != std.http.errorNone() {
|
||||
return 99
|
||||
}
|
||||
if std.http.headerFound(reply) == false {
|
||||
return 99
|
||||
}
|
||||
if std.http.headerLen(reply) != 5 {
|
||||
return 99
|
||||
}
|
||||
if response[reply_offset] != 102_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[33] != 50_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[34] != 48_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[35] != 48_u8 {
|
||||
return 99
|
||||
}
|
||||
if response[body_offset] != 123_u8 {
|
||||
return 99
|
||||
}
|
||||
return 46
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function headSource(baseUrl) {
|
||||
return `export c fn main() -> i32 {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
let request: Span<u8> = std.mem.span("HEAD ${baseUrl}/ok\\nx-zero-test: yes\\n\\n")
|
||||
var response: [512]u8 = [${zeroArray(512)}]
|
||||
let result: HttpResult = std.http.fetch(client, request, response, std.time.ms(1000))
|
||||
let reply: HttpHeaderValue = std.http.headerValue(response, std.mem.span("x-zero-reply"))
|
||||
if std.http.resultOk(result) == false {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultStatus(result) != 200 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultBodyLen(result) != 0 {
|
||||
return 99
|
||||
}
|
||||
if std.http.responseHeadersLen(response) <= 16 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultError(result) != std.http.errorNone() {
|
||||
return 99
|
||||
}
|
||||
if std.http.headerFound(reply) == false {
|
||||
return 99
|
||||
}
|
||||
if std.http.headerLen(reply) != 3 {
|
||||
return 99
|
||||
}
|
||||
return 45
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function jsonResultSource(baseUrl) {
|
||||
return `export c fn main() -> i32 {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var response: [512]u8 = [${zeroArray(512)}]
|
||||
let result: HttpResult = std.http.fetch(client, std.mem.span("GET ${baseUrl}/ok\\n\\n"), response, std.time.ms(1000))
|
||||
let bytes: Maybe<Span<u8>> = std.http.responseBody(response, result)
|
||||
var arena_buf: [16]u8 = [${zeroArray(16)}]
|
||||
var arena: FixedBufAlloc = std.mem.fixedBufAlloc(arena_buf)
|
||||
var parsed: Maybe<JsonDoc> = null
|
||||
if bytes.has {
|
||||
parsed = std.json.parseBytes(arena, bytes.value)
|
||||
}
|
||||
if std.http.resultOk(result) == false {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultStatus(result) != 200 {
|
||||
return 99
|
||||
}
|
||||
if !bytes.has {
|
||||
return 99
|
||||
}
|
||||
if std.mem.len(bytes.value) != 8 {
|
||||
return 99
|
||||
}
|
||||
if parsed.has == false {
|
||||
return 99
|
||||
}
|
||||
if std.json.validateBytes(bytes.value) == false {
|
||||
return 99
|
||||
}
|
||||
if std.json.streamTokensBytes(bytes.value) != 3 {
|
||||
return 99
|
||||
}
|
||||
return 37
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function invalidUrlSource() {
|
||||
return `export c fn main() -> i32 {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var response: [64]u8 = [${zeroArray(64)}]
|
||||
let result: HttpResult = std.http.fetch(client, std.mem.span("GET http://\\n\\n"), response, std.time.ms(1000))
|
||||
if std.http.resultOk(result) != false {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultStatus(result) != 0 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultBodyLen(result) != 0 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultError(result) != std.http.errorInvalidUrl() {
|
||||
return 99
|
||||
}
|
||||
return 36
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function shorthandRejectedSource(baseUrl) {
|
||||
return `export c fn main() -> i32 {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var response: [64]u8 = [${zeroArray(64)}]
|
||||
let result: HttpResult = std.http.fetch(client, std.mem.span("${baseUrl}/ok"), response, std.time.ms(1000))
|
||||
if std.http.resultOk(result) != false {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultStatus(result) != 0 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultBodyLen(result) != 0 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultError(result) != std.http.errorInvalidRequest() {
|
||||
return 99
|
||||
}
|
||||
return 48
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function unterminatedEnvelopeSource(baseUrl) {
|
||||
return `export c fn main() -> i32 {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var response: [64]u8 = [${zeroArray(64)}]
|
||||
let result: HttpResult = std.http.fetch(client, std.mem.span("GET ${baseUrl}/ok"), response, std.time.ms(1000))
|
||||
if std.http.resultOk(result) != false {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultStatus(result) != 0 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultBodyLen(result) != 0 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultError(result) != std.http.errorInvalidRequest() {
|
||||
return 99
|
||||
}
|
||||
return 49
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
function invalidRequestSource(baseUrl) {
|
||||
return `export c fn main() -> i32 {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var response: [128]u8 = [${zeroArray(128)}]
|
||||
let request: Span<u8> = std.mem.span("POST ${baseUrl}/echo\\nbad-header\\n\\n")
|
||||
let result: HttpResult = std.http.fetch(client, request, response, std.time.ms(1000))
|
||||
if std.http.resultOk(result) != false {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultStatus(result) != 0 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultBodyLen(result) != 0 {
|
||||
return 99
|
||||
}
|
||||
if std.http.resultError(result) != std.http.errorInvalidRequest() {
|
||||
return 99
|
||||
}
|
||||
return 43
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
process.stdout.write("http runtime smoke skipped: unsupported host target\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!(await canLinkCurl())) {
|
||||
process.stdout.write("http runtime smoke skipped: cc cannot link libcurl\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await mkdir(outDir, { recursive: true });
|
||||
await assertProviderUnavailableRuntime();
|
||||
await runHttpListenExample();
|
||||
|
||||
function handleRequest(request, response) {
|
||||
if (request.url === "/headers") {
|
||||
response.sendDate = false;
|
||||
response.writeHead(204, { "connection": "close" });
|
||||
response.end();
|
||||
} else if (request.url === "/ok") {
|
||||
response.writeHead(200, { "content-type": "application/json", "x-zero-reply": "yes" });
|
||||
response.end("{\"ok\":1}");
|
||||
} else if (request.url === "/echo") {
|
||||
const chunks = [];
|
||||
request.on("data", (chunk) => chunks.push(chunk));
|
||||
request.on("end", () => {
|
||||
const body = Buffer.concat(chunks).toString("utf8");
|
||||
if (request.method === "POST" &&
|
||||
request.headers["x-zero-test"] === "yes" &&
|
||||
request.headers["content-type"] === "application/json" &&
|
||||
body === "{\"ping\":1}") {
|
||||
response.writeHead(201, { "content-type": "application/json", "x-zero-reply": "yes" });
|
||||
response.end("{\"echo\":1}");
|
||||
} else {
|
||||
response.writeHead(400, { "content-type": "text/plain" });
|
||||
response.end("bad request");
|
||||
}
|
||||
});
|
||||
} else if (request.url === "/client") {
|
||||
const chunks = [];
|
||||
request.on("data", (chunk) => chunks.push(chunk));
|
||||
request.on("end", () => {
|
||||
const body = Buffer.concat(chunks).toString("utf8");
|
||||
if (request.method === "POST" &&
|
||||
request.headers["content-type"] === "application/json" &&
|
||||
body === "{\"ping\":1}") {
|
||||
response.writeHead(201, { "content-type": "application/json", "x-zero-reply": "yes" });
|
||||
response.end("{\"echo\":1}");
|
||||
} else {
|
||||
response.writeHead(400, { "content-type": "text/plain" });
|
||||
response.end("bad request");
|
||||
}
|
||||
});
|
||||
} else if (request.url === "/replace") {
|
||||
const chunks = [];
|
||||
request.on("data", (chunk) => chunks.push(chunk));
|
||||
request.on("end", () => {
|
||||
const body = Buffer.concat(chunks).toString("utf8");
|
||||
if (request.method === "PUT" &&
|
||||
request.headers["x-zero-test"] === "yes" &&
|
||||
request.headers["content-type"] === "application/json" &&
|
||||
body === "{\"value\":2}") {
|
||||
response.writeHead(202, { "content-type": "application/json", "x-zero-reply": "yes" });
|
||||
response.end("{\"put\":1}");
|
||||
} else {
|
||||
response.writeHead(400, { "content-type": "text/plain" });
|
||||
response.end("bad request");
|
||||
}
|
||||
});
|
||||
} else if (request.url === "/missing") {
|
||||
response.writeHead(404, { "content-type": "text/plain" });
|
||||
response.end("missing");
|
||||
} else if (request.url === "/large") {
|
||||
response.writeHead(200, { "content-type": "text/plain" });
|
||||
response.end("0123456789abcdef0123456789abcdef");
|
||||
} else if (request.url === "/slow") {
|
||||
setTimeout(() => {
|
||||
response.writeHead(200, { "content-type": "text/plain" });
|
||||
response.end("slow");
|
||||
}, 250);
|
||||
} else {
|
||||
response.writeHead(500, { "content-type": "text/plain" });
|
||||
response.end("unexpected");
|
||||
}
|
||||
}
|
||||
|
||||
function createInterimHeaderServer() {
|
||||
return createTcpServer((socket) => {
|
||||
socket.once("data", () => {
|
||||
socket.end([
|
||||
"HTTP/1.1 103 Early Hints\r\n",
|
||||
"Link: </zero.css>; rel=preload; as=style\r\n",
|
||||
"\r\n",
|
||||
"HTTP/1.1 200 OK\r\n",
|
||||
"Content-Type: application/json\r\n",
|
||||
"X-Zero-Reply: final\r\n",
|
||||
"Connection: close\r\n",
|
||||
"Content-Length: 8\r\n",
|
||||
"\r\n",
|
||||
"{\"ok\":1}",
|
||||
].join(""));
|
||||
});
|
||||
socket.on("error", () => {});
|
||||
});
|
||||
}
|
||||
|
||||
const server = createHttpServer(handleRequest);
|
||||
const port = await listen(server);
|
||||
const baseUrl = `http://127.0.0.1:${port}`;
|
||||
const interimServer = createInterimHeaderServer();
|
||||
const interimPort = await listen(interimServer);
|
||||
const interimBaseUrl = `http://127.0.0.1:${interimPort}`;
|
||||
|
||||
try {
|
||||
await buildAndRun("http-runtime-ok", okSource(baseUrl), 8);
|
||||
await buildAndRun("http-runtime-404", resultFailureSource(baseUrl, "/missing", 512, 1000, 404, 7, "std.http.errorNone()", 32), 32);
|
||||
await buildAndRun("http-runtime-overflow", resultFailureSource(baseUrl, "/large", 32, 1000, 200, 0, "std.http.errorTooLarge()", 33), 33);
|
||||
await buildAndRun("http-runtime-timeout", resultFailureSource(baseUrl, "/slow", 512, 25, 0, 0, "std.http.errorTimeout()", 34), 34);
|
||||
await buildAndRun("http-runtime-post", fetchRequestSource(baseUrl, "POST", "/echo", "{\\\"ping\\\":1}", 201, 10, 42), 42);
|
||||
await buildAndRun("http-runtime-put", fetchRequestSource(baseUrl, "PUT", "/replace", "{\\\"value\\\":2}", 202, 9, 47), 47);
|
||||
await buildAndRun("http-runtime-headers", headersSource(baseUrl), 44);
|
||||
await buildAndRun("http-runtime-interim-headers", interimHeadersSource(interimBaseUrl), 46);
|
||||
await buildAndRun("http-runtime-head", headSource(baseUrl), 45);
|
||||
await buildAndRun("http-runtime-result-json", jsonResultSource(baseUrl), 37);
|
||||
await runHttpJsonExample(baseUrl);
|
||||
await runHttpRequestExample(baseUrl);
|
||||
await runHttpHeadersExample(baseUrl);
|
||||
await runJsonApiClientExample(baseUrl);
|
||||
await runJsonApiRouterExample();
|
||||
await buildAndRun("http-runtime-shorthand-rejected", shorthandRejectedSource(baseUrl), 48);
|
||||
await buildAndRun("http-runtime-unterminated-envelope", unterminatedEnvelopeSource(baseUrl), 49);
|
||||
await buildAndRun("http-runtime-invalid-url", invalidUrlSource(), 36);
|
||||
await buildAndRun("http-runtime-invalid-request", invalidRequestSource(baseUrl), 43);
|
||||
} finally {
|
||||
await close(server);
|
||||
await close(interimServer);
|
||||
}
|
||||
|
||||
if (await canRunOpenSsl()) {
|
||||
const tls = await createTlsFixture();
|
||||
if (tls) {
|
||||
const httpsServer = createHttpsServer({
|
||||
key: await readFile(tls.serverKey),
|
||||
cert: await readFile(tls.serverCert),
|
||||
}, handleRequest);
|
||||
const httpsPort = await listen(httpsServer);
|
||||
const httpsBaseUrl = `https://127.0.0.1:${httpsPort}`;
|
||||
try {
|
||||
await buildAndRun("http-runtime-https-ok", okSource(httpsBaseUrl), 8, { ZERO_HTTP_TEST_CA_BUNDLE: tls.caCert });
|
||||
await buildAndRun("http-runtime-https-untrusted", resultFailureSource(httpsBaseUrl, "/ok", 512, 1000, 0, 0, "std.http.errorTls()", 35), 35);
|
||||
} finally {
|
||||
await close(httpsServer);
|
||||
await rm(tls.dir, { recursive: true, force: true });
|
||||
}
|
||||
} else {
|
||||
process.stdout.write("https runtime smoke skipped: openssl fixture generation failed\n");
|
||||
}
|
||||
} else {
|
||||
process.stdout.write("https runtime smoke skipped: openssl unavailable\n");
|
||||
}
|
||||
|
||||
const okReport = JSON.parse(await readFile(`${outDir}/http-runtime-ok.json`, "utf8"));
|
||||
assertRuntimeReport(okReport, target);
|
||||
process.stdout.write(`http runtime smoke ok (${target})\n`);
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
version="${ZERO_ZIG_VERSION:-0.14.1}"
|
||||
release_tag="${ZERO_ZIG_RELEASE_TAG:-toolchain-zig-${version}}"
|
||||
mirror_base="${ZERO_ZIG_MIRROR_BASE:-https://github.com/vercel-labs/zerolang/releases/download/${release_tag}}"
|
||||
cache_root="${ZERO_ZIG_CACHE_DIR:-${PWD}/.zero/toolchains}"
|
||||
|
||||
host_os="$(uname -s)"
|
||||
host_arch="$(uname -m)"
|
||||
|
||||
case "${host_os}:${host_arch}" in
|
||||
Linux:x86_64)
|
||||
platform="x86_64-linux"
|
||||
archive_name="zig-x86_64-linux-${version}.tar.xz"
|
||||
archive_sha="24aeeec8af16c381934a6cd7d95c807a8cb2cf7df9fa40d359aa884195c4716c"
|
||||
;;
|
||||
Darwin:arm64 | Darwin:aarch64)
|
||||
platform="aarch64-macos"
|
||||
archive_name="zig-aarch64-macos-${version}.tar.xz"
|
||||
archive_sha="39f3dc5e79c22088ce878edc821dedb4ca5a1cd9f5ef915e9b3cc3053e8faefa"
|
||||
;;
|
||||
Darwin:x86_64)
|
||||
platform="x86_64-macos"
|
||||
archive_name="zig-x86_64-macos-${version}.tar.xz"
|
||||
archive_sha="b0f8bdfb9035783db58dd6c19d7dea89892acc3814421853e5752fe4573e5f43"
|
||||
;;
|
||||
*)
|
||||
echo "unsupported Zig host for pinned installer: ${host_os}:${host_arch}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
install_dir="${ZERO_ZIG_INSTALL_DIR:-${cache_root}/zig-${version}-${platform}}"
|
||||
archive_path="${cache_root}/${archive_name}"
|
||||
archive_url="${mirror_base}/${archive_name}"
|
||||
|
||||
check_sha256() {
|
||||
expected="$1"
|
||||
path="$2"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
printf '%s %s\n' "$expected" "$path" | sha256sum -c -
|
||||
else
|
||||
printf '%s %s\n' "$expected" "$path" | shasum -a 256 -c -
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -x "${install_dir}/zig" ] && [ "$("${install_dir}/zig" version)" = "$version" ]; then
|
||||
echo "Zig ${version} already installed at ${install_dir}"
|
||||
else
|
||||
mkdir -p "$cache_root"
|
||||
if [ ! -f "$archive_path" ] || ! check_sha256 "$archive_sha" "$archive_path" >/dev/null 2>&1; then
|
||||
echo "Downloading ${archive_name} from repo mirror"
|
||||
curl -fsSL --retry 3 --retry-delay 2 --output "$archive_path" "$archive_url"
|
||||
fi
|
||||
check_sha256 "$archive_sha" "$archive_path"
|
||||
|
||||
tmp_dir="${install_dir}.tmp.$$"
|
||||
rm -rf "$tmp_dir"
|
||||
mkdir -p "$tmp_dir"
|
||||
tar -xf "$archive_path" -C "$tmp_dir" --strip-components=1
|
||||
rm -rf "$install_dir"
|
||||
mv "$tmp_dir" "$install_dir"
|
||||
fi
|
||||
|
||||
if [ "$("${install_dir}/zig" version)" != "$version" ]; then
|
||||
echo "installed Zig version does not match ${version}: $("${install_dir}/zig" version)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${GITHUB_PATH:-}" ]; then
|
||||
printf '%s\n' "$install_dir" >> "$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
echo "Zig ${version} installed at ${install_dir}"
|
||||
@@ -0,0 +1,158 @@
|
||||
export function jsonByteTokenCount(bytes) {
|
||||
if (!bytes || bytes.length === 0) return -1n;
|
||||
const scanner = { bytes, pos: 0, tokens: 0 };
|
||||
const len = () => scanner.bytes.length;
|
||||
const skipWs = () => {
|
||||
while (scanner.pos < len()) {
|
||||
const ch = scanner.bytes[scanner.pos];
|
||||
if (ch !== 0x20 && ch !== 0x0a && ch !== 0x0d && ch !== 0x09) return;
|
||||
scanner.pos += 1;
|
||||
}
|
||||
};
|
||||
const parseString = () => {
|
||||
if (scanner.pos >= len() || scanner.bytes[scanner.pos] !== 0x22) return false;
|
||||
scanner.pos += 1;
|
||||
while (scanner.pos < len()) {
|
||||
const ch = scanner.bytes[scanner.pos++];
|
||||
if (ch === 0x22) return true;
|
||||
if (ch < 0x20) return false;
|
||||
if (ch !== 0x5c) continue;
|
||||
if (scanner.pos >= len()) return false;
|
||||
const esc = scanner.bytes[scanner.pos++];
|
||||
if (esc === 0x22 || esc === 0x5c || esc === 0x2f || esc === 0x62 || esc === 0x66 || esc === 0x6e || esc === 0x72 || esc === 0x74) continue;
|
||||
if (esc !== 0x75 || scanner.pos + 4 > len()) return false;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const hex = scanner.bytes[scanner.pos++];
|
||||
const ok = (hex >= 0x30 && hex <= 0x39) || (hex >= 0x61 && hex <= 0x66) || (hex >= 0x41 && hex <= 0x46);
|
||||
if (!ok) return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const matchLiteral = (literal) => {
|
||||
const start = scanner.pos;
|
||||
for (let i = 0; i < literal.length; i++) {
|
||||
if (scanner.pos >= len() || scanner.bytes[scanner.pos] !== literal.charCodeAt(i)) {
|
||||
scanner.pos = start;
|
||||
return false;
|
||||
}
|
||||
scanner.pos += 1;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const parseNumber = () => {
|
||||
const start = scanner.pos;
|
||||
if (scanner.pos < len() && scanner.bytes[scanner.pos] === 0x2d) scanner.pos += 1;
|
||||
if (scanner.pos >= len()) {
|
||||
scanner.pos = start;
|
||||
return false;
|
||||
}
|
||||
if (scanner.bytes[scanner.pos] === 0x30) {
|
||||
scanner.pos += 1;
|
||||
} else if (scanner.bytes[scanner.pos] >= 0x31 && scanner.bytes[scanner.pos] <= 0x39) {
|
||||
while (scanner.pos < len() && scanner.bytes[scanner.pos] >= 0x30 && scanner.bytes[scanner.pos] <= 0x39) scanner.pos += 1;
|
||||
} else {
|
||||
scanner.pos = start;
|
||||
return false;
|
||||
}
|
||||
if (scanner.pos < len() && scanner.bytes[scanner.pos] === 0x2e) {
|
||||
scanner.pos += 1;
|
||||
const digits = scanner.pos;
|
||||
while (scanner.pos < len() && scanner.bytes[scanner.pos] >= 0x30 && scanner.bytes[scanner.pos] <= 0x39) scanner.pos += 1;
|
||||
if (scanner.pos === digits) {
|
||||
scanner.pos = start;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (scanner.pos < len() && (scanner.bytes[scanner.pos] === 0x65 || scanner.bytes[scanner.pos] === 0x45)) {
|
||||
scanner.pos += 1;
|
||||
if (scanner.pos < len() && (scanner.bytes[scanner.pos] === 0x2b || scanner.bytes[scanner.pos] === 0x2d)) scanner.pos += 1;
|
||||
const digits = scanner.pos;
|
||||
while (scanner.pos < len() && scanner.bytes[scanner.pos] >= 0x30 && scanner.bytes[scanner.pos] <= 0x39) scanner.pos += 1;
|
||||
if (scanner.pos === digits) {
|
||||
scanner.pos = start;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const parseValue = (depth) => {
|
||||
if (depth > 64) return false;
|
||||
skipWs();
|
||||
if (scanner.pos >= len()) return false;
|
||||
const ch = scanner.bytes[scanner.pos];
|
||||
if (ch === 0x7b) return parseObject(depth);
|
||||
if (ch === 0x5b) return parseArray(depth);
|
||||
if (ch === 0x22) {
|
||||
scanner.tokens += 1;
|
||||
return parseString();
|
||||
}
|
||||
if (ch === 0x74) {
|
||||
scanner.tokens += 1;
|
||||
return matchLiteral("true");
|
||||
}
|
||||
if (ch === 0x66) {
|
||||
scanner.tokens += 1;
|
||||
return matchLiteral("false");
|
||||
}
|
||||
if (ch === 0x6e) {
|
||||
scanner.tokens += 1;
|
||||
return matchLiteral("null");
|
||||
}
|
||||
if (ch === 0x2d || (ch >= 0x30 && ch <= 0x39)) {
|
||||
scanner.tokens += 1;
|
||||
return parseNumber();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const parseArray = (depth) => {
|
||||
if (scanner.pos >= len() || scanner.bytes[scanner.pos] !== 0x5b) return false;
|
||||
scanner.tokens += 1;
|
||||
scanner.pos += 1;
|
||||
skipWs();
|
||||
if (scanner.pos < len() && scanner.bytes[scanner.pos] === 0x5d) {
|
||||
scanner.pos += 1;
|
||||
return true;
|
||||
}
|
||||
for (;;) {
|
||||
if (!parseValue(depth + 1)) return false;
|
||||
skipWs();
|
||||
if (scanner.pos < len() && scanner.bytes[scanner.pos] === 0x5d) {
|
||||
scanner.pos += 1;
|
||||
return true;
|
||||
}
|
||||
if (scanner.pos >= len() || scanner.bytes[scanner.pos] !== 0x2c) return false;
|
||||
scanner.pos += 1;
|
||||
skipWs();
|
||||
}
|
||||
};
|
||||
const parseObject = (depth) => {
|
||||
if (scanner.pos >= len() || scanner.bytes[scanner.pos] !== 0x7b) return false;
|
||||
scanner.tokens += 1;
|
||||
scanner.pos += 1;
|
||||
skipWs();
|
||||
if (scanner.pos < len() && scanner.bytes[scanner.pos] === 0x7d) {
|
||||
scanner.pos += 1;
|
||||
return true;
|
||||
}
|
||||
for (;;) {
|
||||
if (!parseString()) return false;
|
||||
scanner.tokens += 1;
|
||||
skipWs();
|
||||
if (scanner.pos >= len() || scanner.bytes[scanner.pos] !== 0x3a) return false;
|
||||
scanner.pos += 1;
|
||||
if (!parseValue(depth + 1)) return false;
|
||||
skipWs();
|
||||
if (scanner.pos < len() && scanner.bytes[scanner.pos] === 0x7d) {
|
||||
scanner.pos += 1;
|
||||
return true;
|
||||
}
|
||||
if (scanner.pos >= len() || scanner.bytes[scanner.pos] !== 0x2c) return false;
|
||||
scanner.pos += 1;
|
||||
skipWs();
|
||||
}
|
||||
};
|
||||
if (!parseValue(0)) return -1n;
|
||||
skipWs();
|
||||
return scanner.pos === len() ? BigInt(scanner.tokens) : -1n;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const zero = "bin/zero";
|
||||
const outDir = ".zero/llvm-profile";
|
||||
const addInput = "examples/add.graph";
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
function runJson(args: string[], options: { allowFailure?: boolean } = {}) {
|
||||
try {
|
||||
const stdout = execFileSync(zero, args, { encoding: "utf8" });
|
||||
return { code: 0, body: JSON.parse(stdout) };
|
||||
} catch (error: any) {
|
||||
if (!options.allowFailure) throw error;
|
||||
return {
|
||||
code: error.status ?? error.code ?? 1,
|
||||
body: JSON.parse(error.stdout?.toString() ?? "{}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function llvmOptimizationLevel(profile: string) {
|
||||
if (profile === "debug" || profile === "dev") return "-O0";
|
||||
if (profile === "fast" || profile === "release-fast") return "-O2";
|
||||
if (profile === "audit") return "-O1";
|
||||
return "-Oz";
|
||||
}
|
||||
|
||||
function buildRow(id: string, backend: string, profile: string, emit: string, out: string, allowFailure = false) {
|
||||
const args = ["build", "--json", "--backend", backend, "--profile", profile, "--emit", emit, addInput, "--out", out];
|
||||
const result = runJson(args, { allowFailure });
|
||||
if (result.code !== 0) {
|
||||
return {
|
||||
id,
|
||||
backendFamily: backend,
|
||||
profile,
|
||||
emit,
|
||||
status: "blocked",
|
||||
diagnostic: result.body.diagnostics?.[0]?.code ?? "unknown",
|
||||
backendBlocker: result.body.diagnostics?.[0]?.backendBlocker ?? null,
|
||||
};
|
||||
}
|
||||
assert.equal(result.body.generatedCBytes, 0);
|
||||
assert.equal(result.body.cBridgeFallback ?? false, false);
|
||||
assert.equal(result.body.artifactBytes, statSync(result.body.artifactPath).size);
|
||||
return {
|
||||
id,
|
||||
backendFamily: backend,
|
||||
profile,
|
||||
emit,
|
||||
status: "measured",
|
||||
elapsedMs: result.body.elapsedMs,
|
||||
artifactBytes: result.body.artifactBytes,
|
||||
optimizationLevel: backend === "llvm" && emit === "exe"
|
||||
? llvmOptimizationLevel(profile)
|
||||
: result.body.profileSemantics?.codegenOptimization ?? null,
|
||||
target: result.body.target,
|
||||
};
|
||||
}
|
||||
|
||||
const size = runJson(["size", "--json", "--backend", "llvm", addInput]).body;
|
||||
assert.equal(size.targetSupport.backendFamily, "llvm");
|
||||
assert.equal(size.backendProfile.backendFamily, "llvm");
|
||||
assert.equal(size.backendProfile.fallbackPolicy, "none");
|
||||
assert.equal(size.backendProfile.targetTriple, size.targetSupport.llvmTargetTriple);
|
||||
assert.equal(size.backendProfile.optimizationLevel, "-Oz");
|
||||
assert.equal(size.objectBackend.backendFamily, "llvm");
|
||||
assert.equal(size.backendComparison.rows.length, 4);
|
||||
|
||||
const check = runJson(["check", "--json", "--backend", "llvm", addInput]).body;
|
||||
const llvmHostReady = check.targetReadiness.ok === true;
|
||||
|
||||
const rows = [
|
||||
buildRow("direct-debug", "direct", "debug", "exe", join(outDir, "add-direct-debug")),
|
||||
buildRow("direct-small", "direct", "small", "exe", join(outDir, "add-direct-small")),
|
||||
buildRow("llvm-ir-no-opt", "llvm", "debug", "llvm-ir", join(outDir, "add-debug.ll")),
|
||||
buildRow("llvm-ir-optimized-profile", "llvm", "small", "llvm-ir", join(outDir, "add-small.ll")),
|
||||
];
|
||||
|
||||
if (llvmHostReady) {
|
||||
rows.push(buildRow("llvm-native-no-opt", "llvm", "debug", "exe", join(outDir, "add-llvm-debug")));
|
||||
rows.push(buildRow("llvm-native-optimized", "llvm", "small", "exe", join(outDir, "add-llvm-small")));
|
||||
} else {
|
||||
rows.push(buildRow("llvm-native-no-opt", "llvm", "debug", "exe", join(outDir, "add-llvm-debug"), true));
|
||||
rows.push(buildRow("llvm-native-optimized", "llvm", "small", "exe", join(outDir, "add-llvm-small"), true));
|
||||
}
|
||||
|
||||
assert(rows.some((row) => row.id === "direct-debug" && row.status === "measured"));
|
||||
assert(rows.some((row) => row.id === "direct-small" && row.status === "measured"));
|
||||
assert(rows.some((row) => row.id === "llvm-ir-no-opt" && row.status === "measured"));
|
||||
assert(rows.some((row) => row.id === "llvm-ir-optimized-profile" && row.status === "measured"));
|
||||
if (llvmHostReady) {
|
||||
assert(rows.some((row) => row.id === "llvm-native-no-opt" && row.status === "measured"));
|
||||
assert(rows.some((row) => row.id === "llvm-native-optimized" && row.status === "measured"));
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
kind: "zero-llvm-profile-smoke",
|
||||
graphInput: addInput,
|
||||
llvmHostReady,
|
||||
rows,
|
||||
}, null, 2));
|
||||
@@ -0,0 +1,31 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const cc = process.env.CC ?? "cc";
|
||||
const out = `/tmp/zero-mir-verifier-smoke-${process.pid}`;
|
||||
|
||||
try {
|
||||
await execFileAsync(cc, [
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wpedantic",
|
||||
"-I",
|
||||
"native/zero-c/include",
|
||||
"-I",
|
||||
"native/zero-c/src",
|
||||
"native/zero-c/src/mir_verify.c",
|
||||
"native/zero-c/tests/mir_verify_smoke.c",
|
||||
"-o",
|
||||
out,
|
||||
]);
|
||||
const result = await execFileAsync(out);
|
||||
if (!result.stdout.includes("mir verifier smoke ok")) {
|
||||
throw new Error(`unexpected MIR verifier smoke output: ${result.stdout}`);
|
||||
}
|
||||
} finally {
|
||||
await rm(out, { force: true });
|
||||
}
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
version="${ZERO_ZIG_VERSION:-0.14.1}"
|
||||
release_tag="${ZERO_ZIG_RELEASE_TAG:-toolchain-zig-${version}}"
|
||||
upstream_base="${ZERO_ZIG_UPSTREAM_BASE:-https://ziglang.org/download/${version}}"
|
||||
work_dir="${ZERO_ZIG_MIRROR_WORK_DIR:-/tmp/zero-zig-mirror-${version}}"
|
||||
|
||||
assets=(
|
||||
"zig-x86_64-linux-${version}.tar.xz:24aeeec8af16c381934a6cd7d95c807a8cb2cf7df9fa40d359aa884195c4716c"
|
||||
"zig-aarch64-macos-${version}.tar.xz:39f3dc5e79c22088ce878edc821dedb4ca5a1cd9f5ef915e9b3cc3053e8faefa"
|
||||
"zig-x86_64-macos-${version}.tar.xz:b0f8bdfb9035783db58dd6c19d7dea89892acc3814421853e5752fe4573e5f43"
|
||||
)
|
||||
|
||||
check_sha256() {
|
||||
expected="$1"
|
||||
path="$2"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
printf '%s %s\n' "$expected" "$path" | sha256sum -c -
|
||||
else
|
||||
printf '%s %s\n' "$expected" "$path" | shasum -a 256 -c -
|
||||
fi
|
||||
}
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "gh is required to upload mirrored Zig assets" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -rf "$work_dir"
|
||||
mkdir -p "$work_dir"
|
||||
|
||||
: > "${work_dir}/CHECKSUMS.txt"
|
||||
for asset in "${assets[@]}"; do
|
||||
name="${asset%%:*}"
|
||||
sha="${asset##*:}"
|
||||
path="${work_dir}/${name}"
|
||||
echo "Downloading ${name}"
|
||||
curl -fsSL --retry 3 --retry-delay 2 --output "$path" "${upstream_base}/${name}"
|
||||
check_sha256 "$sha" "$path"
|
||||
printf '%s %s\n' "$sha" "$name" >> "${work_dir}/CHECKSUMS.txt"
|
||||
done
|
||||
|
||||
if ! gh release view "$release_tag" >/dev/null 2>&1; then
|
||||
gh release create "$release_tag" \
|
||||
--title "Zig ${version} mirror" \
|
||||
--notes "Repo-owned mirror of pinned Zig ${version} archives used by CI and release workflows." \
|
||||
--latest=false
|
||||
fi
|
||||
|
||||
for asset in "${assets[@]}"; do
|
||||
name="${asset%%:*}"
|
||||
gh release upload "$release_tag" "${work_dir}/${name}" --clobber
|
||||
done
|
||||
gh release upload "$release_tag" "${work_dir}/CHECKSUMS.txt" --clobber
|
||||
|
||||
echo "Mirrored Zig ${version} assets to GitHub release ${release_tag}"
|
||||
@@ -0,0 +1,100 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
const headerPath = "native/zero-c/include/zero.h";
|
||||
const contractsHeaderPath = "native/zero-c/include/zero_contracts.h";
|
||||
const accessEffectPattern = /\bZ_(?:IN|OUT|INOUT|SINK)\b/;
|
||||
const returnOwnershipPattern = /\bZ_RET_(?:OWNED|BORROWED)\b/;
|
||||
const returnOptionalPattern = /\bZ_RET_OPTIONAL\b/;
|
||||
const contractMacros = ["Z_IN", "Z_OUT", "Z_INOUT", "Z_SINK", "Z_OPTIONAL", "Z_RET_OWNED", "Z_RET_BORROWED", "Z_RET_OPTIONAL"];
|
||||
|
||||
type Violation = {
|
||||
kind: string;
|
||||
line?: number;
|
||||
declaration?: string;
|
||||
parameter?: string;
|
||||
macro?: string;
|
||||
};
|
||||
|
||||
function stripLineComment(line: string): string {
|
||||
const index = line.indexOf("//");
|
||||
return index >= 0 ? line.slice(0, index) : line;
|
||||
}
|
||||
|
||||
function pointerParameters(declaration: string): string[] {
|
||||
const open = declaration.indexOf("(");
|
||||
const close = declaration.lastIndexOf(")");
|
||||
if (open < 0 || close < open) return [];
|
||||
const params = declaration.slice(open + 1, close).trim();
|
||||
if (params === "" || params === "void") return [];
|
||||
return params
|
||||
.split(",")
|
||||
.map((param) => param.trim())
|
||||
.filter((param) => param !== "..." && param.includes("*"));
|
||||
}
|
||||
|
||||
function pointerReturn(declaration: string): string | null {
|
||||
const beforeParams = declaration.slice(0, declaration.indexOf("(")).trim();
|
||||
if (!beforeParams.includes("*")) return null;
|
||||
return beforeParams.replace(/\s+z_[A-Za-z0-9_]+$/, "").trim();
|
||||
}
|
||||
|
||||
function exportedDeclarations(header: string): { line: number; declaration: string }[] {
|
||||
return header
|
||||
.split("\n")
|
||||
.map((line, index) => ({ line: index + 1, declaration: stripLineComment(line).trim() }))
|
||||
.filter(({ declaration }) => /\bz_[A-Za-z0-9_]+\s*\([^;]*\);$/.test(declaration));
|
||||
}
|
||||
|
||||
function contractViolations(header: string, contractsHeader: string): Violation[] {
|
||||
const violations: Violation[] = [];
|
||||
for (const macro of contractMacros) {
|
||||
if (!new RegExp(`#define\\s+${macro}\\b`).test(contractsHeader)) {
|
||||
violations.push({ kind: "missing-contract-macro", macro });
|
||||
}
|
||||
}
|
||||
for (const { line, declaration } of exportedDeclarations(header)) {
|
||||
const returned = pointerReturn(declaration);
|
||||
if (returned) {
|
||||
if (!returnOwnershipPattern.test(returned)) {
|
||||
violations.push({ kind: "missing-pointer-return-ownership", line, declaration, parameter: returned });
|
||||
}
|
||||
if (returnOptionalPattern.test(returned) && !returnOwnershipPattern.test(returned)) {
|
||||
violations.push({ kind: "optional-return-without-ownership", line, declaration, parameter: returned });
|
||||
}
|
||||
}
|
||||
for (const parameter of pointerParameters(declaration)) {
|
||||
if (!accessEffectPattern.test(parameter)) {
|
||||
violations.push({ kind: "missing-pointer-access-effect", line, declaration, parameter });
|
||||
}
|
||||
if (/\bZ_OPTIONAL\b/.test(parameter) && !accessEffectPattern.test(parameter)) {
|
||||
violations.push({ kind: "optional-without-access-effect", line, declaration, parameter });
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations;
|
||||
}
|
||||
|
||||
const header = await readFile(headerPath, "utf8");
|
||||
const contractsHeader = await readFile(contractsHeaderPath, "utf8");
|
||||
const declarations = exportedDeclarations(header);
|
||||
const pointerParamCount = declarations.reduce((count, { declaration }) => count + pointerParameters(declaration).length, 0);
|
||||
const pointerReturnCount = declarations.reduce((count, { declaration }) => count + (pointerReturn(declaration) ? 1 : 0), 0);
|
||||
const violations = contractViolations(header, contractsHeader);
|
||||
|
||||
const result = {
|
||||
schema: 1,
|
||||
ok: violations.length === 0,
|
||||
header: headerPath,
|
||||
declarations: declarations.length,
|
||||
pointerParameters: pointerParamCount,
|
||||
pointerReturns: pointerReturnCount,
|
||||
contractMacros,
|
||||
violations,
|
||||
};
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error(JSON.stringify(result, null, 2));
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync } from "node:fs";
|
||||
|
||||
const targetByHost = {
|
||||
"darwin:arm64": "darwin-arm64",
|
||||
"darwin:x64": "darwin-x64",
|
||||
"linux:arm64": "linux-musl-arm64",
|
||||
"linux:x64": "linux-musl-x64",
|
||||
"win32:arm64": "win32-arm64.exe",
|
||||
"win32:x64": "win32-x64.exe",
|
||||
};
|
||||
|
||||
function run(command, args, options = {}) {
|
||||
execFileSync(command, args, { stdio: "inherit", ...options });
|
||||
}
|
||||
|
||||
const target = targetByHost[`${process.platform}:${process.arch}`];
|
||||
if (!target) {
|
||||
console.error(`native smoke does not know a runnable target for ${process.platform}/${process.arch}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const out = ".zero/out/add-native";
|
||||
const exe = target.startsWith("win32-") ? `${out}.exe` : out;
|
||||
|
||||
mkdirSync(".zero/out", { recursive: true });
|
||||
run("make", ["-C", "native/zero-c"]);
|
||||
run("bin/zero", ["check", "examples/hello.graph"]);
|
||||
run("bin/zero", ["build", "--emit", "exe", "--target", target, "examples/add.graph", "--out", out]);
|
||||
run(exe, []);
|
||||
Executable
+320
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const root = process.cwd();
|
||||
const outDir = join(root, ".zero", "native-test-sandbox");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
loadDotEnvFiles([".env", ".env.local"]);
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.includes("--help") || args.includes("-h")) {
|
||||
console.log(`Run native test commands inside Vercel Sandbox.
|
||||
|
||||
Usage:
|
||||
pnpm run native:test:sandbox
|
||||
node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/native-test-sandbox.mts -- pnpm run conformance
|
||||
|
||||
Environment:
|
||||
ZERO_NATIVE_TEST_SANDBOX_SNAPSHOT_ID Reuse a prepared native-test snapshot
|
||||
ZERO_BENCH_SANDBOX_SNAPSHOT_ID Fallback reusable snapshot id from benchmark setup
|
||||
ZERO_NATIVE_TEST_SANDBOX_REFRESH=1 Ignore the snapshot id and rebuild
|
||||
ZERO_NATIVE_TEST_SANDBOX_PROJECT_DIR Remote project directory
|
||||
ZERO_NATIVE_TEST_SANDBOX_RUNTIME Vercel Sandbox runtime, defaults to node24
|
||||
ZERO_NATIVE_TEST_SANDBOX_TIMEOUT_MS Sandbox timeout, defaults to 600000
|
||||
ZERO_NATIVE_TEST_SANDBOX_VCPUS Sandbox vCPU count, defaults to 16
|
||||
ZERO_SANDBOX_COMMAND Command to run when no -- command is passed
|
||||
|
||||
The runner uploads a source archive that excludes generated build output and never copies native test binaries back to the local machine.`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) return value;
|
||||
return `'${value.replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
function sandboxCommand() {
|
||||
const separator = args.indexOf("--");
|
||||
if (separator !== -1 && separator + 1 < args.length) {
|
||||
return args.slice(separator + 1).map(shellQuote).join(" ");
|
||||
}
|
||||
return process.env.ZERO_SANDBOX_COMMAND?.trim() || "bash scripts/test-native.sh";
|
||||
}
|
||||
|
||||
function loadDotEnvFiles(paths) {
|
||||
for (const path of paths) {
|
||||
const fullPath = join(root, path);
|
||||
if (!existsSync(fullPath)) continue;
|
||||
const lines = readFileSync(fullPath, "utf8").split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
||||
const match = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(trimmed);
|
||||
if (!match || process.env[match[1]] !== undefined) continue;
|
||||
process.env[match[1]] = parseDotEnvValue(match[2]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseDotEnvValue(value) {
|
||||
const trimmed = value.trim();
|
||||
const quote = trimmed[0];
|
||||
if ((quote === "\"" || quote === "'") && trimmed.endsWith(quote)) {
|
||||
const inner = trimmed.slice(1, -1);
|
||||
return quote === "\"" ? inner.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, "\t").replace(/\\"/g, "\"").replace(/\\\\/g, "\\") : inner;
|
||||
}
|
||||
const commentStart = trimmed.search(/\s#/);
|
||||
return commentStart === -1 ? trimmed : trimmed.slice(0, commentStart).trimEnd();
|
||||
}
|
||||
|
||||
function parsePositiveInt(value, fallback) {
|
||||
const parsed = Number.parseInt(value ?? "", 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function parseOptionalNonNegativeInt(value) {
|
||||
if (value === undefined || value === "") return undefined;
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (!Number.isFinite(parsed) || parsed < 0) throw new Error(`expected a non-negative integer, got ${value}`);
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function sandboxProjectDir() {
|
||||
return process.env.ZERO_NATIVE_TEST_SANDBOX_PROJECT_DIR ||
|
||||
process.env.ZERO_BENCH_SANDBOX_PROJECT_DIR ||
|
||||
"/vercel/sandbox/zero-lang";
|
||||
}
|
||||
|
||||
function sandboxRuntime() {
|
||||
return process.env.ZERO_NATIVE_TEST_SANDBOX_RUNTIME ||
|
||||
process.env.ZERO_BENCH_SANDBOX_RUNTIME ||
|
||||
"node24";
|
||||
}
|
||||
|
||||
function sandboxTimeout() {
|
||||
return parsePositiveInt(
|
||||
process.env.ZERO_NATIVE_TEST_SANDBOX_TIMEOUT_MS ?? process.env.ZERO_BENCH_SANDBOX_TIMEOUT_MS,
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
function sandboxVcpus() {
|
||||
return parsePositiveInt(
|
||||
process.env.ZERO_NATIVE_TEST_SANDBOX_VCPUS ?? process.env.ZERO_BENCH_SANDBOX_VCPUS,
|
||||
16,
|
||||
);
|
||||
}
|
||||
|
||||
function snapshotFromEnv() {
|
||||
const refresh = (process.env.ZERO_NATIVE_TEST_SANDBOX_REFRESH ?? "").toLowerCase();
|
||||
if (["1", "true", "yes"].includes(refresh)) return null;
|
||||
const snapshotId = process.env.ZERO_NATIVE_TEST_SANDBOX_SNAPSHOT_ID?.trim() ||
|
||||
process.env.ZERO_BENCH_SANDBOX_SNAPSHOT_ID?.trim();
|
||||
if (!snapshotId) return null;
|
||||
return {
|
||||
snapshotId,
|
||||
projectDir: sandboxProjectDir(),
|
||||
timeout: sandboxTimeout(),
|
||||
vcpus: sandboxVcpus(),
|
||||
reused: true,
|
||||
};
|
||||
}
|
||||
|
||||
function ensureVercelAuthEnv() {
|
||||
if (process.env.VERCEL_OIDC_TOKEN?.trim()) return;
|
||||
throw new Error(
|
||||
"native:test:sandbox requires VERCEL_OIDC_TOKEN. Put it in .env or .env.local, matching the benchmark sandbox runner, or export it before running.",
|
||||
);
|
||||
}
|
||||
|
||||
function writeSnapshotEnv(snapshot) {
|
||||
const snapshotPath = join(outDir, "snapshot.env");
|
||||
const lines = [
|
||||
`ZERO_NATIVE_TEST_SANDBOX_SNAPSHOT_ID=${snapshot.snapshotId}`,
|
||||
`ZERO_NATIVE_TEST_SANDBOX_PROJECT_DIR=${snapshot.projectDir}`,
|
||||
"",
|
||||
];
|
||||
writeFileSync(snapshotPath, lines.join("\n"));
|
||||
process.stderr.write(`Created Vercel Sandbox native-test snapshot: ${snapshot.snapshotId}\n`);
|
||||
process.stderr.write(`Saved reusable snapshot env to ${snapshotPath}\n`);
|
||||
process.stderr.write("Add ZERO_NATIVE_TEST_SANDBOX_SNAPSHOT_ID to .env to reuse it, or set ZERO_NATIVE_TEST_SANDBOX_REFRESH=1 to rebuild it.\n");
|
||||
}
|
||||
|
||||
function createSourceArchive() {
|
||||
const archivePath = join(outDir, "source.tar.gz");
|
||||
rmSync(archivePath, { force: true });
|
||||
const excludes = [
|
||||
".git",
|
||||
".cursor",
|
||||
".env",
|
||||
".env.*",
|
||||
".vercel",
|
||||
".zero",
|
||||
".next",
|
||||
"dist",
|
||||
"coverage",
|
||||
"node_modules",
|
||||
"docs/.next",
|
||||
"docs/out",
|
||||
"extensions/*/node_modules",
|
||||
"docs/node_modules",
|
||||
];
|
||||
const excludeArgs = excludes.flatMap((exclude) => [`--exclude=${exclude}`, `--exclude=./${exclude}`]);
|
||||
const metadataArgs = process.platform === "darwin" ? ["--no-xattrs"] : [];
|
||||
const result = spawnSync("tar", [...metadataArgs, ...excludeArgs, "-czf", archivePath, "."], {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, COPYFILE_DISABLE: "1" },
|
||||
});
|
||||
if (result.status !== 0) throw new Error(`failed to create native-test source archive\n${result.stderr.trim()}`);
|
||||
return archivePath;
|
||||
}
|
||||
|
||||
async function runSandboxCommand(sandbox, params, label) {
|
||||
const result = await sandbox.runCommand(params);
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
for await (const log of result.logs()) {
|
||||
if (log.stream === "stdout") {
|
||||
stdout += log.data;
|
||||
process.stdout.write(log.data);
|
||||
} else {
|
||||
stderr += log.data;
|
||||
process.stderr.write(log.data);
|
||||
}
|
||||
}
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`${label} failed with exit code ${result.exitCode}`);
|
||||
}
|
||||
return { stdout, stderr };
|
||||
}
|
||||
|
||||
function describeError(error) {
|
||||
if (!(error instanceof Error)) return String(error);
|
||||
const parts = [error.message];
|
||||
if ("json" in error && error.json) parts.push(JSON.stringify(error.json));
|
||||
if ("text" in error && error.text) parts.push(String(error.text));
|
||||
return parts.filter(Boolean).join("\n");
|
||||
}
|
||||
|
||||
async function createPreparedSnapshot(Sandbox) {
|
||||
const runtime = sandboxRuntime();
|
||||
const timeout = sandboxTimeout();
|
||||
const vcpus = sandboxVcpus();
|
||||
const snapshotExpiration = parseOptionalNonNegativeInt(process.env.ZERO_NATIVE_TEST_SANDBOX_SNAPSHOT_EXPIRATION_MS);
|
||||
const archivePath = createSourceArchive();
|
||||
const sandbox = await Sandbox.create({ runtime, timeout, resources: { vcpus } });
|
||||
const projectDir = sandboxProjectDir();
|
||||
|
||||
try {
|
||||
await sandbox.writeFiles([{ path: "/tmp/zero-lang-source.tar.gz", content: readFileSync(archivePath) }]);
|
||||
await runSandboxCommand(
|
||||
sandbox,
|
||||
{
|
||||
cmd: "bash",
|
||||
args: [
|
||||
"-lc",
|
||||
[
|
||||
"set -euo pipefail",
|
||||
"as_root() { if command -v sudo >/dev/null 2>&1; then sudo \"$@\"; else \"$@\"; fi; }",
|
||||
"install_build_tools() {",
|
||||
" if command -v make >/dev/null 2>&1 && { command -v cc >/dev/null 2>&1 || command -v gcc >/dev/null 2>&1 || command -v clang >/dev/null 2>&1; }; then return; fi",
|
||||
" if command -v apt-get >/dev/null 2>&1; then",
|
||||
" as_root apt-get update",
|
||||
" as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential ca-certificates git gzip make tar",
|
||||
" elif command -v apk >/dev/null 2>&1; then",
|
||||
" as_root apk add --no-cache build-base ca-certificates git gzip make tar",
|
||||
" elif command -v dnf >/dev/null 2>&1; then",
|
||||
" as_root dnf install -y ca-certificates gcc git glibc-devel gzip make tar",
|
||||
" elif command -v yum >/dev/null 2>&1; then",
|
||||
" as_root yum install -y ca-certificates gcc git glibc-devel gzip make tar",
|
||||
" else",
|
||||
" echo 'no supported package manager found to install make and a C compiler' >&2",
|
||||
" exit 127",
|
||||
" fi",
|
||||
"}",
|
||||
"install_build_tools",
|
||||
"command -v make >/dev/null 2>&1",
|
||||
"command -v cc >/dev/null 2>&1 || command -v gcc >/dev/null 2>&1 || command -v clang >/dev/null 2>&1",
|
||||
`rm -rf ${projectDir}`,
|
||||
`mkdir -p ${projectDir}`,
|
||||
`tar -xzf /tmp/zero-lang-source.tar.gz -C ${projectDir}`,
|
||||
`cd ${projectDir}`,
|
||||
"corepack enable",
|
||||
"corepack prepare pnpm@11.1.3 --activate",
|
||||
"pnpm install --frozen-lockfile",
|
||||
"make -C native/zero-c",
|
||||
"node --version",
|
||||
].join("\n"),
|
||||
],
|
||||
},
|
||||
"native-test sandbox setup",
|
||||
);
|
||||
const snapshot = await sandbox.snapshot(snapshotExpiration === undefined ? undefined : { expiration: snapshotExpiration });
|
||||
const preparedSnapshot = { snapshotId: snapshot.snapshotId, projectDir, timeout, vcpus, reused: false };
|
||||
writeSnapshotEnv(preparedSnapshot);
|
||||
return preparedSnapshot;
|
||||
} catch (error) {
|
||||
await sandbox.stop({ blocking: true }).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function runSandboxTask(Sandbox, snapshot, command) {
|
||||
const archivePath = createSourceArchive();
|
||||
const sandbox = await Sandbox.create({
|
||||
source: { type: "snapshot", snapshotId: snapshot.snapshotId },
|
||||
timeout: snapshot.timeout,
|
||||
resources: { vcpus: snapshot.vcpus },
|
||||
});
|
||||
try {
|
||||
await sandbox.writeFiles([{ path: "/tmp/zero-lang-source-current.tar.gz", content: readFileSync(archivePath) }]);
|
||||
await runSandboxCommand(
|
||||
sandbox,
|
||||
{
|
||||
cmd: "bash",
|
||||
args: [
|
||||
"-lc",
|
||||
[
|
||||
"set -euxo pipefail",
|
||||
`cd ${snapshot.projectDir}`,
|
||||
"find . -mindepth 1 -maxdepth 1 ! -name node_modules -exec rm -rf {} +",
|
||||
`tar -xzf /tmp/zero-lang-source-current.tar.gz -C ${snapshot.projectDir}`,
|
||||
"corepack enable",
|
||||
"corepack prepare pnpm@11.1.3 --activate",
|
||||
"pnpm install --frozen-lockfile",
|
||||
"make -C native/zero-c",
|
||||
command,
|
||||
].join("\n"),
|
||||
],
|
||||
cwd: snapshot.projectDir,
|
||||
env: { ZERO_NATIVE_TEST_SANDBOX: "1" },
|
||||
},
|
||||
`${command} in Vercel Sandbox`,
|
||||
);
|
||||
} finally {
|
||||
await sandbox.stop({ blocking: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
ensureVercelAuthEnv();
|
||||
const { Sandbox } = await import("@vercel/sandbox").catch((error) => {
|
||||
throw new Error(`native:test:sandbox requires @vercel/sandbox. Run pnpm install first.\n${error.message}`);
|
||||
});
|
||||
|
||||
const reusableSnapshot = snapshotFromEnv();
|
||||
const snapshot = reusableSnapshot ?? (await createPreparedSnapshot(Sandbox));
|
||||
const reuseText = snapshot.reused ? "reused" : "fresh";
|
||||
const command = sandboxCommand();
|
||||
process.stderr.write(`Running ${command} in Vercel Sandbox using ${reuseText} snapshot ${snapshot.snapshotId}...\n`);
|
||||
await runSandboxTask(Sandbox, snapshot, command);
|
||||
process.stderr.write(`sandbox command ok: ${command}\n`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(describeError(error));
|
||||
process.exit(1);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,350 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import assert from "node:assert/strict";
|
||||
import { existsSync } from "node:fs";
|
||||
import { lstat, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
const cc = process.env.CC ?? "cc";
|
||||
const out = `/tmp/zero-program-graph-smoke-${process.pid}`;
|
||||
|
||||
try {
|
||||
await execFileAsync(cc, [
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wpedantic",
|
||||
"-I",
|
||||
"native/zero-c/include",
|
||||
"-I",
|
||||
"native/zero-c/src",
|
||||
"native/zero-c/src/program_graph.c",
|
||||
"native/zero-c/src/program_graph_adjacency.c",
|
||||
"native/zero-c/src/program_graph_clone.c",
|
||||
"native/zero-c/src/program_graph_identity.c",
|
||||
"native/zero-c/src/program_graph_import.c",
|
||||
"native/zero-c/src/program_graph_lower.c",
|
||||
"native/zero-c/src/program_graph_node_id.c",
|
||||
"native/zero-c/src/program_graph_order.c",
|
||||
"native/zero-c/src/program_graph_format.c",
|
||||
"native/zero-c/src/program_graph_handle.c",
|
||||
"native/zero-c/src/program_graph_compare.c",
|
||||
"native/zero-c/src/program_graph_projection_path.c",
|
||||
"native/zero-c/src/program_graph_projection_fast.c",
|
||||
"native/zero-c/src/program_graph_reconcile.c",
|
||||
"native/zero-c/src/program_graph_reconcile_apply.c",
|
||||
"native/zero-c/src/program_graph_resolve.c",
|
||||
"native/zero-c/src/program_graph_semantics.c",
|
||||
"native/zero-c/src/program_graph_source_map.c",
|
||||
"native/zero-c/src/program_graph_store.c",
|
||||
"native/zero-c/src/program_graph_store_binary.c",
|
||||
"native/zero-c/src/program_graph_store_prune.c",
|
||||
"native/zero-c/src/program_graph_store_tables.c",
|
||||
"native/zero-c/src/program_graph_std_deps.c",
|
||||
"native/zero-c/src/program_graph_std_merge.c",
|
||||
"native/zero-c/src/program_graph_std_prune.c",
|
||||
"native/zero-c/src/program_graph_string_map.c",
|
||||
"native/zero-c/src/program_graph_validate.c",
|
||||
"native/zero-c/src/program_graph_view.c",
|
||||
"native/zero-c/src/c_import.c",
|
||||
"native/zero-c/src/canonical_text.c",
|
||||
"native/zero-c/src/canonical_text_format.c",
|
||||
"native/zero-c/src/canonical_text_program.c",
|
||||
"native/zero-c/src/canonical_text_write.c",
|
||||
"native/zero-c/src/fs_read.c",
|
||||
"native/zero-c/src/std_sig.c",
|
||||
"native/zero-c/src/std_source.c",
|
||||
"native/zero-c/src/ast.c",
|
||||
"native/zero-c/tests/program_graph_smoke_stubs.c",
|
||||
"native/zero-c/tests/program_graph_smoke.c",
|
||||
"-o",
|
||||
out,
|
||||
]);
|
||||
const result = await execFileAsync(out);
|
||||
if (!result.stdout.includes("program graph smoke ok")) {
|
||||
throw new Error(`unexpected ProgramGraph smoke output: ${result.stdout}`);
|
||||
}
|
||||
} finally {
|
||||
await rm(out, { force: true });
|
||||
}
|
||||
|
||||
const zero = process.env.ZERO_BIN || (existsSync(".zero/bin/zero") ? ".zero/bin/zero" : "bin/zero");
|
||||
const checkedInBinaryRoot = "examples/binary-graph-store";
|
||||
const checkedInBinaryCrmRoot = "examples/crm-api";
|
||||
const binaryRoot = `/tmp/zero-program-graph-binary-store-${process.pid}`;
|
||||
|
||||
async function zeroRun(args: string[]) {
|
||||
return execFileAsync(zero, args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
|
||||
}
|
||||
|
||||
async function zeroRunMaybe(args: string[]) {
|
||||
try {
|
||||
const result = await zeroRun(args);
|
||||
return { code: 0, stdout: result.stdout, stderr: result.stderr };
|
||||
} catch (error: any) {
|
||||
return {
|
||||
code: error.code ?? error.status ?? 1,
|
||||
stdout: error.stdout ?? "",
|
||||
stderr: error.stderr ?? "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function findCheckedInGraphStores(root: string, stores: string[] = []) {
|
||||
if (!existsSync(root)) return stores;
|
||||
for (const entry of await readdir(root, { withFileTypes: true })) {
|
||||
const path = `${root}/${entry.name}`;
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === ".zero" || entry.name === "node_modules") continue;
|
||||
await findCheckedInGraphStores(path, stores);
|
||||
} else if (entry.isFile() && entry.name === "zero.graph") {
|
||||
stores.push(path);
|
||||
} else if (entry.isSymbolicLink()) {
|
||||
const stat = await lstat(path);
|
||||
if (stat.isFile() && entry.name === "zero.graph") stores.push(path);
|
||||
}
|
||||
}
|
||||
return stores;
|
||||
}
|
||||
|
||||
async function findCheckedInFiles(root: string, suffix: string, files: string[] = []) {
|
||||
if (!existsSync(root)) return files;
|
||||
for (const entry of await readdir(root, { withFileTypes: true })) {
|
||||
const path = `${root}/${entry.name}`;
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === ".zero" || entry.name === "node_modules") continue;
|
||||
await findCheckedInFiles(path, suffix, files);
|
||||
} else if (entry.isFile() && entry.name.endsWith(suffix)) {
|
||||
files.push(path);
|
||||
} else if (entry.isSymbolicLink()) {
|
||||
const stat = await lstat(path);
|
||||
if (stat.isFile() && entry.name.endsWith(suffix)) files.push(path);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function findAtomicWriteTempFiles(root: string, files: string[] = []) {
|
||||
if (!existsSync(root)) return files;
|
||||
for (const entry of await readdir(root, { withFileTypes: true })) {
|
||||
const path = `${root}/${entry.name}`;
|
||||
if (entry.name.includes(".zero-tmp-")) {
|
||||
files.push(path);
|
||||
continue;
|
||||
}
|
||||
if (entry.isDirectory()) {
|
||||
await findAtomicWriteTempFiles(path, files);
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
async function assertNoAtomicWriteTempFiles(root: string) {
|
||||
assert.deepEqual(await findAtomicWriteTempFiles(root), [], `${root}: atomic graph writes must clean up temporary files`);
|
||||
}
|
||||
|
||||
function projectionHasGraphAuthority(path: string) {
|
||||
if (existsSync(path.replace(/\.0$/, ".graph"))) return true;
|
||||
for (let current = dirname(path); current !== "." && current !== "/"; current = dirname(current)) {
|
||||
if (existsSync(join(current, "zero.graph"))) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function assertBinaryGraphStore(path: string, bytes: Buffer) {
|
||||
assert.equal(bytes.subarray(0, 8).toString("binary"), "ZRGBIN1\0", `${path} should be a binary repository graph store`);
|
||||
}
|
||||
|
||||
const BINARY_NODE_COUNT_OFFSET = 40;
|
||||
const BINARY_EDGE_COUNT_OFFSET = 48;
|
||||
const BINARY_STRING_BYTES_OFFSET = 64;
|
||||
const BINARY_MODULE_IDENTITY_REF_OFFSET = 72;
|
||||
|
||||
function corruptBinaryStoreModuleIdentity(bytes: Buffer) {
|
||||
const copy = Buffer.from(bytes);
|
||||
const stringBytes = Number(copy.readBigUInt64LE(BINARY_STRING_BYTES_OFFSET));
|
||||
const moduleIdentityOffset = Number(copy.readBigUInt64LE(BINARY_MODULE_IDENTITY_REF_OFFSET));
|
||||
const moduleIdentityLen = Number(copy.readBigUInt64LE(BINARY_MODULE_IDENTITY_REF_OFFSET + 8));
|
||||
assert(stringBytes <= copy.length, "binary store fixture has invalid string table size");
|
||||
assert(moduleIdentityLen > 2, "binary store fixture module identity is too short to corrupt");
|
||||
const stringsOffset = copy.length - stringBytes;
|
||||
assert(moduleIdentityOffset + 1 < stringBytes, "binary store fixture module identity offset is out of range");
|
||||
copy[stringsOffset + moduleIdentityOffset + 1] = 0;
|
||||
return copy;
|
||||
}
|
||||
|
||||
function corruptBinaryStoreNodeCount(bytes: Buffer) {
|
||||
const copy = Buffer.from(bytes);
|
||||
copy.writeBigUInt64LE(1_000_001n, BINARY_NODE_COUNT_OFFSET);
|
||||
return copy;
|
||||
}
|
||||
|
||||
function corruptBinaryStoreEdgeCount(bytes: Buffer) {
|
||||
const copy = Buffer.from(bytes);
|
||||
copy.writeBigUInt64LE(4_000_001n, BINARY_EDGE_COUNT_OFFSET);
|
||||
return copy;
|
||||
}
|
||||
|
||||
async function assertInvalidBinaryStoreRejected(root: string, bytes: Buffer, label: string) {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
await mkdir(root, { recursive: true });
|
||||
await writeFile(
|
||||
`${root}/zero.toml`,
|
||||
'[package]\nname = "corrupt-binary-store"\nversion = "0.1.0"\n\n[targets.cli]\nkind = "exe"\nmain = "main.0"\n',
|
||||
);
|
||||
await writeFile(`${root}/main.0`, "pub fn main() -> i32 { return 0 }\n");
|
||||
await writeFile(`${root}/zero.graph`, bytes);
|
||||
const status = await zeroRunMaybe(["status", "--json", root]);
|
||||
assert.notEqual(status.code, 0, `${label}: corrupt binary store status should fail`);
|
||||
const body = JSON.parse(status.stdout);
|
||||
assert.equal(body.repositoryGraph.storePresent, true);
|
||||
assert.equal(body.repositoryGraph.storeValid, false);
|
||||
assert.equal(body.repositoryGraph.projectionState, "store-invalid");
|
||||
assert.equal(body.diagnostics[0].code, "RGP003");
|
||||
assert.match(body.diagnostics[0].message, /repository graph store is invalid|invalid repository graph store|invalid binary repository graph store/);
|
||||
}
|
||||
|
||||
const checkedInStores = [
|
||||
...(await findCheckedInGraphStores("examples")),
|
||||
...(await findCheckedInGraphStores("conformance")),
|
||||
...(await findCheckedInGraphStores("benchmarks")),
|
||||
].sort();
|
||||
assert(checkedInStores.length > 0, "expected checked-in repository graph stores");
|
||||
for (const store of checkedInStores) {
|
||||
assertBinaryGraphStore(store, await readFile(store));
|
||||
}
|
||||
|
||||
const checkedInGraphArtifacts = [
|
||||
...(await findCheckedInFiles("examples", ".graph")),
|
||||
...(await findCheckedInFiles("conformance", ".graph")),
|
||||
...(await findCheckedInFiles("benchmarks", ".graph")),
|
||||
...(await findCheckedInFiles("std", ".graph")),
|
||||
].sort();
|
||||
assert(checkedInGraphArtifacts.length > 0, "expected checked-in graph artifacts");
|
||||
for (const graph of checkedInGraphArtifacts) {
|
||||
assertBinaryGraphStore(graph, await readFile(graph));
|
||||
}
|
||||
|
||||
const checkedInProjections = [
|
||||
...(await findCheckedInFiles("examples", ".0")),
|
||||
...(await findCheckedInFiles("conformance", ".0")),
|
||||
...(await findCheckedInFiles("benchmarks", ".0")),
|
||||
...(await findCheckedInFiles("std", ".0")),
|
||||
].sort();
|
||||
assert(checkedInProjections.length > 0, "expected checked-in source projections");
|
||||
for (const projection of checkedInProjections) {
|
||||
assert.equal(projectionHasGraphAuthority(projection), true, `${projection} must have a graph authority`);
|
||||
}
|
||||
|
||||
for (const store of checkedInStores) {
|
||||
const root = dirname(store);
|
||||
const status = JSON.parse((await zeroRun(["status", "--json", root])).stdout);
|
||||
assert.equal(status.store.encoding, "binary", `${root}: checked-in repository graph store must be binary`);
|
||||
assert.equal(status.repositoryGraph.projectionValidity, "clean", `${root}: checked-in source projections must be clean`);
|
||||
assert.equal((await zeroRun(["verify-projection", root])).stdout, "repository graph verify-projection ok\n", `${root}: verify-projection`);
|
||||
}
|
||||
|
||||
const checkedInBinaryStore = await readFile(`${checkedInBinaryRoot}/zero.graph`);
|
||||
assertBinaryGraphStore(`${checkedInBinaryRoot}/zero.graph`, checkedInBinaryStore);
|
||||
const checkedInBinaryStatus = JSON.parse((await zeroRun(["status", "--json", checkedInBinaryRoot])).stdout);
|
||||
assert.equal(checkedInBinaryStatus.store.encoding, "binary");
|
||||
assert.equal(checkedInBinaryStatus.repositoryGraph.projectionValidity, "clean");
|
||||
assert.equal((await zeroRun(["check", checkedInBinaryRoot])).stdout, "ok\n");
|
||||
assert.equal((await zeroRun(["test", checkedInBinaryRoot])).stdout, "1 test(s) ok\n");
|
||||
assert.equal((await zeroRun(["run", checkedInBinaryRoot])).stdout, "binary graph store example\n");
|
||||
assert.equal((await zeroRun(["verify-projection", checkedInBinaryRoot])).stdout, "repository graph verify-projection ok\n");
|
||||
|
||||
const checkedInBinaryCrmStore = await readFile(`${checkedInBinaryCrmRoot}/zero.graph`);
|
||||
assertBinaryGraphStore(`${checkedInBinaryCrmRoot}/zero.graph`, checkedInBinaryCrmStore);
|
||||
const checkedInBinaryCrmStatus = JSON.parse((await zeroRun(["status", "--json", checkedInBinaryCrmRoot])).stdout);
|
||||
assert.equal(checkedInBinaryCrmStatus.store.encoding, "binary");
|
||||
assert.equal(checkedInBinaryCrmStatus.repositoryGraph.projectionValidity, "clean");
|
||||
assert.equal((await zeroRun(["check", checkedInBinaryCrmRoot])).stdout, "ok\n");
|
||||
assert.equal((await zeroRun(["verify-projection", checkedInBinaryCrmRoot])).stdout, "repository graph verify-projection ok\n");
|
||||
|
||||
await rm(binaryRoot, { recursive: true, force: true });
|
||||
await mkdir(binaryRoot, { recursive: true });
|
||||
await zeroRun(["init", binaryRoot]);
|
||||
await assertNoAtomicWriteTempFiles(binaryRoot);
|
||||
let binaryStore = await readFile(`${binaryRoot}/zero.graph`);
|
||||
assert.equal(binaryStore.subarray(0, 8).toString("binary"), "ZRGBIN1\0");
|
||||
|
||||
await zeroRun(["patch", binaryRoot, "--op", "addMain", "--op", 'addCheckWrite fn="main" text="hello binary\\n"']);
|
||||
await assertNoAtomicWriteTempFiles(binaryRoot);
|
||||
binaryStore = await readFile(`${binaryRoot}/zero.graph`);
|
||||
assert.equal(binaryStore.subarray(0, 8).toString("binary"), "ZRGBIN1\0");
|
||||
await assertInvalidBinaryStoreRejected(
|
||||
`/tmp/zero-program-graph-binary-corrupt-nul-${process.pid}`,
|
||||
corruptBinaryStoreModuleIdentity(binaryStore),
|
||||
"embedded nul",
|
||||
);
|
||||
await assertInvalidBinaryStoreRejected(
|
||||
`/tmp/zero-program-graph-binary-corrupt-count-${process.pid}`,
|
||||
corruptBinaryStoreNodeCount(binaryStore),
|
||||
"node count",
|
||||
);
|
||||
await assertInvalidBinaryStoreRejected(
|
||||
`/tmp/zero-program-graph-binary-corrupt-edge-count-${process.pid}`,
|
||||
corruptBinaryStoreEdgeCount(binaryStore),
|
||||
"edge count",
|
||||
);
|
||||
|
||||
const binaryStatus = JSON.parse((await zeroRun(["status", "--json", binaryRoot])).stdout);
|
||||
assert.equal(binaryStatus.store.encoding, "binary");
|
||||
assert.equal(binaryStatus.storage.encoding, "single-file-binary");
|
||||
assert.equal(binaryStatus.storage.defaultEncoding, "binary");
|
||||
assert.equal(binaryStatus.storage.binaryAvailable, true);
|
||||
|
||||
assert.equal((await zeroRun(["check", binaryRoot])).stdout, "ok\n");
|
||||
assert.equal((await zeroRun(["run", binaryRoot])).stdout, "hello binary\n");
|
||||
const cleanRunStore = await readFile(`${binaryRoot}/zero.graph`);
|
||||
await writeFile(`${binaryRoot}/zero.graph`, corruptBinaryStoreNodeCount(cleanRunStore));
|
||||
const corruptCachedRun = await zeroRunMaybe(["run", binaryRoot]);
|
||||
assert.notEqual(corruptCachedRun.code, 0, "corrupt binary store run should fail before using the linked executable cache");
|
||||
assert.notEqual(corruptCachedRun.stdout, "hello binary\n", "corrupt binary store run must not execute the cached program");
|
||||
assert.match(
|
||||
`${corruptCachedRun.stdout}\n${corruptCachedRun.stderr}`,
|
||||
/repository graph store is invalid|invalid repository graph store|invalid binary repository graph store/,
|
||||
);
|
||||
await writeFile(`${binaryRoot}/zero.graph`, cleanRunStore);
|
||||
assert.match((await zeroRun(["export", binaryRoot])).stdout, /repository graph export ok/);
|
||||
await assertNoAtomicWriteTempFiles(binaryRoot);
|
||||
assert.equal((await zeroRun(["verify-projection", binaryRoot])).stdout, "repository graph verify-projection ok\n");
|
||||
|
||||
const textRoot = `/tmp/zero-program-graph-binary-convert-${process.pid}`;
|
||||
await rm(textRoot, { recursive: true, force: true });
|
||||
await mkdir(textRoot, { recursive: true });
|
||||
await zeroRun(["init", "--format", "text", textRoot]);
|
||||
await zeroRun(["patch", textRoot, "--op", "addMain", "--op", 'addCheckWrite fn="main" text="convert me\\n"']);
|
||||
await assertNoAtomicWriteTempFiles(textRoot);
|
||||
assert.match((await readFile(`${textRoot}/zero.graph`, "utf8")).slice(0, 64), /^zero-repository-graph v1/);
|
||||
await zeroRun(["export", textRoot]);
|
||||
await zeroRun(["import", "--format", "binary", textRoot]);
|
||||
await assertNoAtomicWriteTempFiles(textRoot);
|
||||
const convertedStore = await readFile(`${textRoot}/zero.graph`);
|
||||
assert.equal(convertedStore.subarray(0, 8).toString("binary"), "ZRGBIN1\0");
|
||||
assert.equal((await zeroRun(["run", textRoot])).stdout, "convert me\n");
|
||||
assert.equal((await zeroRun(["verify-projection", textRoot])).stdout, "repository graph verify-projection ok\n");
|
||||
|
||||
const projectionDefaultRoot = `/tmp/zero-program-graph-projection-default-${process.pid}`;
|
||||
await rm(projectionDefaultRoot, { recursive: true, force: true });
|
||||
await mkdir(projectionDefaultRoot, { recursive: true });
|
||||
await writeFile(
|
||||
`${projectionDefaultRoot}/main.0`,
|
||||
'pub fn main(world: World) -> Void raises {\n check world.out.write("projection default\\n")\n}\n',
|
||||
);
|
||||
await writeFile(
|
||||
`${projectionDefaultRoot}/zero.toml`,
|
||||
'[package]\nname = "projection-default"\nversion = "0.1.0"\n\n[targets.cli]\nkind = "exe"\nmain = "main.0"\n',
|
||||
);
|
||||
await zeroRun(["import", projectionDefaultRoot]);
|
||||
await assertNoAtomicWriteTempFiles(projectionDefaultRoot);
|
||||
const projectionDefaultStore = await readFile(`${projectionDefaultRoot}/zero.graph`);
|
||||
assert.equal(projectionDefaultStore.subarray(0, 8).toString("binary"), "ZRGBIN1\0");
|
||||
assert.equal((await zeroRun(["run", projectionDefaultRoot])).stdout, "projection default\n");
|
||||
|
||||
const invalidFormat = await zeroRun(["import", "--format", "pickle", textRoot]).catch((error) => error);
|
||||
assert.notEqual(invalidFormat.code ?? invalidFormat.status, 0);
|
||||
assert.match(invalidFormat.stderr, /repository graph store format is not supported/);
|
||||
Executable
+664
@@ -0,0 +1,664 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, "..");
|
||||
const checkerPath = path.join(repoRoot, "native/zero-c/src/checker.c");
|
||||
const callResolvePath = path.join(repoRoot, "native/zero-c/src/call_resolve.h");
|
||||
const stdSigPath = path.join(repoRoot, "native/zero-c/src/std_sig.h");
|
||||
const matrixPath = path.join(repoRoot, "conformance/provenance-surface.json");
|
||||
const conformancePath = path.join(repoRoot, "conformance/run.mjs");
|
||||
|
||||
const checker = readFileSync(checkerPath, "utf8");
|
||||
const callResolve = readFileSync(callResolvePath, "utf8");
|
||||
const stdSig = readFileSync(stdSigPath, "utf8");
|
||||
const surfaceSpec = JSON.parse(readFileSync(matrixPath, "utf8"));
|
||||
const conformance = readFileSync(conformancePath, "utf8");
|
||||
|
||||
const failures = [];
|
||||
|
||||
function fail(message) {
|
||||
failures.push(message);
|
||||
}
|
||||
|
||||
function assertIncludes(label, text, needle) {
|
||||
if (!text.includes(needle)) fail(`${label}: missing ${needle}`);
|
||||
}
|
||||
|
||||
function assertNotIncludes(label, text, needle) {
|
||||
if (text.includes(needle)) fail(`${label}: unexpected ${needle}`);
|
||||
}
|
||||
|
||||
function assertBefore(label, text, first, second) {
|
||||
const firstIndex = text.indexOf(first);
|
||||
const secondIndex = text.indexOf(second);
|
||||
if (firstIndex < 0) fail(`${label}: missing ${first}`);
|
||||
if (secondIndex < 0) fail(`${label}: missing ${second}`);
|
||||
if (firstIndex >= 0 && secondIndex >= 0 && firstIndex > secondIndex) fail(`${label}: ${first} must appear before ${second}`);
|
||||
}
|
||||
|
||||
function assertMatches(label, text, pattern) {
|
||||
if (!pattern.test(text)) fail(`${label}: missing ${pattern}`);
|
||||
}
|
||||
|
||||
function assertFixtureCoverage(label, fixture) {
|
||||
const fullPath = path.join(repoRoot, fixture);
|
||||
if (!existsSync(fullPath)) {
|
||||
fail(`${label}: fixture '${fixture}' does not exist`);
|
||||
return;
|
||||
}
|
||||
const basename = path.basename(fixture);
|
||||
if (!conformance.includes(fixture) && !conformance.includes(basename)) {
|
||||
fail(`${label}: fixture '${fixture}' is not run by conformance/run.mjs`);
|
||||
}
|
||||
}
|
||||
|
||||
function sliceBetween(text, start, end) {
|
||||
const startIndex = text.indexOf(start);
|
||||
if (startIndex < 0) return "";
|
||||
const endIndex = text.indexOf(end, startIndex + start.length);
|
||||
if (endIndex < 0) return text.slice(startIndex);
|
||||
return text.slice(startIndex, endIndex);
|
||||
}
|
||||
|
||||
function checkerFunctionNames(source) {
|
||||
const names = new Set();
|
||||
for (const line of source.split("\n")) {
|
||||
if (line.trim().endsWith(";")) continue;
|
||||
const match = line.match(/^static\b[^{;]*?\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
||||
if (match) names.add(match[1]);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
function currentFunctionByLine(source) {
|
||||
const current = [];
|
||||
let name = "<top-level>";
|
||||
const lines = source.split("\n");
|
||||
for (const line of lines) {
|
||||
if (!line.trim().endsWith(";")) {
|
||||
const match = line.match(/^static\b[^{;]*?\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
||||
if (match) name = match[1];
|
||||
}
|
||||
current.push(name);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function surfaceFixturePaths(fixtures) {
|
||||
if (Array.isArray(fixtures)) return fixtures;
|
||||
if (!fixtures || typeof fixtures !== "object") return [];
|
||||
const paths = [];
|
||||
for (const group of ["fail", "pass", "other"]) {
|
||||
if (fixtures[group] === undefined) continue;
|
||||
if (!Array.isArray(fixtures[group])) {
|
||||
fail(`provenance surface spec: fixtures.${group} must be an array`);
|
||||
continue;
|
||||
}
|
||||
paths.push(...fixtures[group]);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
function parseSurfaceRows(spec) {
|
||||
const rows = [];
|
||||
if (!spec || typeof spec !== "object") {
|
||||
fail("provenance surface spec: expected JSON object");
|
||||
return rows;
|
||||
}
|
||||
if (spec.schemaVersion !== 1) fail("provenance surface spec: schemaVersion must be 1");
|
||||
if (spec.kind !== "zero-provenance-surface-matrix") fail("provenance surface spec: unexpected kind");
|
||||
if (!Array.isArray(spec.surfaces)) {
|
||||
fail("provenance surface spec: surfaces must be an array");
|
||||
return rows;
|
||||
}
|
||||
const seen = new Set();
|
||||
for (let index = 0; index < spec.surfaces.length; index++) {
|
||||
const row = spec.surfaces[index];
|
||||
if (!row || typeof row !== "object") {
|
||||
fail(`provenance surface spec: surface ${index + 1} must be an object`);
|
||||
continue;
|
||||
}
|
||||
if (!row.surface || typeof row.surface !== "string") fail(`provenance surface spec: surface ${index + 1} has no name`);
|
||||
if (!row.action || typeof row.action !== "string") fail(`provenance surface spec: '${row.surface ?? index + 1}' has no action`);
|
||||
if (!Array.isArray(row.owners)) fail(`provenance surface spec: '${row.surface ?? index + 1}' owners must be an array`);
|
||||
if (row.surface && seen.has(row.surface)) fail(`provenance surface spec: duplicate surface '${row.surface}'`);
|
||||
if (row.surface) seen.add(row.surface);
|
||||
rows.push({
|
||||
surface: row.surface,
|
||||
action: row.action,
|
||||
owners: Array.isArray(row.owners) ? row.owners : [],
|
||||
fixtures: surfaceFixturePaths(row.fixtures),
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
const functionNames = checkerFunctionNames(checker);
|
||||
const rows = parseSurfaceRows(surfaceSpec);
|
||||
const rowsBySurface = new Map(rows.map((row) => [row.surface, row]));
|
||||
|
||||
const requiredSurfaces = [
|
||||
"Identifier reads",
|
||||
"Field reads",
|
||||
"Index reads",
|
||||
"Direct borrows",
|
||||
"Mutable borrows",
|
||||
"Shape literals",
|
||||
"Array literals",
|
||||
"Maybe<T> values",
|
||||
"Choice payloads",
|
||||
"check unwraps",
|
||||
"rescue values",
|
||||
"Primitive casts",
|
||||
"Plain calls",
|
||||
"Generic calls",
|
||||
"Shape namespace calls",
|
||||
"Receiver calls",
|
||||
"Static interface calls",
|
||||
"Incomplete summaries",
|
||||
"Let bindings",
|
||||
"Assignments",
|
||||
"Field assignments",
|
||||
"Index assignments",
|
||||
"Returns",
|
||||
"If joins",
|
||||
"Match joins",
|
||||
"Loop-carried values",
|
||||
"Short-circuit expressions",
|
||||
"Early returns",
|
||||
"Mutref aliases",
|
||||
];
|
||||
|
||||
for (const surface of requiredSurfaces) {
|
||||
if (!rowsBySurface.has(surface)) fail(`provenance matrix: missing surface row '${surface}'`);
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.owners.length === 0) fail(`provenance matrix: '${row.surface}' has no checker owner`);
|
||||
for (const owner of row.owners) {
|
||||
if (!functionNames.has(owner)) fail(`provenance matrix: owner '${owner}' for '${row.surface}' is not a checker function`);
|
||||
}
|
||||
for (const fixture of row.fixtures) {
|
||||
assertFixtureCoverage(`provenance matrix '${row.surface}'`, fixture);
|
||||
}
|
||||
}
|
||||
|
||||
const positivePrecisionClasses = [
|
||||
["direct same-origin mutable reassignment", "conformance/native/pass/borrow-aggregate-reassignment-same-origin.0"],
|
||||
["alias same-origin mutable reassignment", "conformance/native/pass/mutref-alias-assignment-same-origin.0"],
|
||||
["field overwrite clears old origin", "conformance/native/pass/shape-field-reference-reassignment-clears-origin.0"],
|
||||
["disjoint field assignment", "conformance/native/pass/borrow-field-independent-assignment.0"],
|
||||
["caller-owned return ref", "conformance/native/pass/borrow-return-param-ref.0"],
|
||||
["caller-owned field return ref", "conformance/native/pass/borrow-return-param-field-subpath.0"],
|
||||
["plain mutref stores caller-owned ref", "conformance/native/pass/function-mutref-reference-store.0"],
|
||||
["generic mutref stores caller-owned ref", "conformance/native/pass/generic-mutref-reference-store.0"],
|
||||
["receiver stores caller-owned ref", "conformance/native/pass/receiver-method-reference-store.0"],
|
||||
["static interface stores caller-owned ref", "conformance/native/pass/static-interface-mutref-reference-store.0"],
|
||||
["static interface returns caller-owned ref", "conformance/native/pass/static-interface-return-reference-origin.0"],
|
||||
["choice payload returns caller-owned ref", "conformance/native/pass/choice-payload-reference-return.0"],
|
||||
["choice match payload preserves caller-owned ref", "conformance/native/pass/choice-match-payload-reference-origin.0"],
|
||||
["choice match payload returns caller-owned ref", "conformance/native/pass/choice-match-payload-return-origin.0"],
|
||||
["branches merge same origin", "conformance/native/pass/borrow-branch-reassignment.0"],
|
||||
["branches overwrite away unsafe origin", "conformance/native/pass/branch-overwrite-away-reference-origin.0"],
|
||||
["indexed assignment clears overwritten origin", "conformance/native/pass/index-reference-assignment-clears-origin.0"],
|
||||
];
|
||||
|
||||
for (const [label, fixture] of positivePrecisionClasses) {
|
||||
assertFixtureCoverage(`positive provenance class '${label}'`, fixture);
|
||||
}
|
||||
|
||||
const canonicalPlaceClasses = [
|
||||
["direct field write", "conformance/native/pass/function-mutref-reference-store.0"],
|
||||
["mutref alias field write", "conformance/native/pass/mutref-alias-assignment-clears-old-origin.0"],
|
||||
["receiver self field write", "conformance/native/pass/receiver-method-reference-store.0"],
|
||||
["generic mutref write", "conformance/native/pass/generic-mutref-reference-store.0"],
|
||||
["constrained interface mutref write", "conformance/native/pass/static-interface-mutref-reference-store.0"],
|
||||
["precise indexed write clears target", "conformance/native/pass/index-reference-assignment-clears-origin.0"],
|
||||
];
|
||||
|
||||
for (const [label, fixture] of canonicalPlaceClasses) {
|
||||
if (existsSync(path.join(repoRoot, fixture))) assertFixtureCoverage(`canonical place class '${label}'`, fixture);
|
||||
}
|
||||
|
||||
const requiredFunctions = [
|
||||
"expr_value_provenance",
|
||||
"expr_reference_provenance",
|
||||
"resolve_provenance_call",
|
||||
"call_result_value_provenance",
|
||||
"function_provenance_summary",
|
||||
"function_return_value_provenance",
|
||||
"function_storage_effect_summary",
|
||||
"choice_constructor_value_provenance",
|
||||
"register_match_payload_binding_provenance",
|
||||
"apply_checked_call_storage_effects",
|
||||
"check_call_expr_expected",
|
||||
"check_named_function_call_expected",
|
||||
"check_stdlib_table_arg_range_expected",
|
||||
"check_receiver_shape_call_expected",
|
||||
"fallible_callee_in_context",
|
||||
"resolve_stdlib_fallible_call",
|
||||
"stdlib_call_error_sets_covered",
|
||||
"function_error_sets_include_stdlib_resolution",
|
||||
"find_shape_owning_method",
|
||||
"finish_shape_method_provenance_call",
|
||||
"resolve_shape_namespace_provenance_call",
|
||||
"resolve_concrete_constrained_shape_provenance_call",
|
||||
"resolve_named_provenance_call",
|
||||
"resolve_constrained_interface_provenance_call",
|
||||
"resolve_receiver_shape_provenance_call",
|
||||
"bind_type_args_to_bindings",
|
||||
"checked_call_type_args",
|
||||
"generic_bindings_from_type_args",
|
||||
"checked_call_bindings_from_recorded_type_args",
|
||||
"shape_method_bindings_from_recorded_type_args",
|
||||
"interface_method_bindings_from_recorded_type_args",
|
||||
"generic_call_bindings_from_checked_call",
|
||||
"call_resolution_record_bindings",
|
||||
"call_resolution_record_param_facts",
|
||||
"call_resolution_param_type_text",
|
||||
"resolved_call_param_type_text",
|
||||
"apply_provenance_storage_effect",
|
||||
"stdlib_install_item_write_provenance",
|
||||
"collect_effect_target_places",
|
||||
"collect_assignment_target_places",
|
||||
"expr_static_index_segment",
|
||||
"origin_path_is_definitely_within",
|
||||
"update_borrow_assignment",
|
||||
"assignment_provenance_snapshot_clear",
|
||||
"provenance_scope_snapshot_restore_union",
|
||||
];
|
||||
|
||||
for (const name of requiredFunctions) {
|
||||
if (!functionNames.has(name)) fail(`checker provenance foundation: missing function '${name}'`);
|
||||
}
|
||||
|
||||
for (const kind of [
|
||||
"Z_CALL_FUNCTION",
|
||||
"Z_CALL_STDLIB",
|
||||
"Z_CALL_SHAPE_NAMESPACE",
|
||||
"Z_CALL_RECEIVER",
|
||||
"Z_CALL_CONSTRAINED_INTERFACE",
|
||||
"Z_CALL_CONCRETE_CONSTRAINED_SHAPE",
|
||||
"Z_CALL_CHOICE_CONSTRUCTOR",
|
||||
]) {
|
||||
assertIncludes("shared call resolver", callResolve, kind);
|
||||
assertIncludes("checker provenance call resolver", checker, kind);
|
||||
}
|
||||
|
||||
for (const needle of [
|
||||
"ZCallArgument",
|
||||
"ZCallBinding",
|
||||
"ZCallError",
|
||||
"z_call_resolution_add_arg",
|
||||
"z_call_resolution_expected_arg_count",
|
||||
"z_call_resolution_param_type",
|
||||
"z_call_resolution_add_binding",
|
||||
"z_call_resolution_binding_type",
|
||||
"z_call_resolution_add_error",
|
||||
"z_call_resolution_error_set_text",
|
||||
]) {
|
||||
assertIncludes("shared call argument facts", callResolve, needle);
|
||||
}
|
||||
|
||||
assertIncludes("shared stdlib error facts", stdSig, "Z_STD_HELPER_MAX_ERRORS");
|
||||
assertIncludes("shared stdlib error facts", stdSig, "error_names");
|
||||
assertIncludes("shared stdlib error facts", stdSig, "z_std_helper_error_name");
|
||||
assertIncludes("shared stdlib error facts", stdSig, "z_std_helper_error_set_text");
|
||||
assertIncludes("shared stdlib helper classification", stdSig, "ZStdHelperKind");
|
||||
assertIncludes("shared stdlib helper classification", stdSig, "z_std_helper_kind");
|
||||
assertNotIncludes("shared stdlib fallibility facts", checker, "is_builtin_fallible_call");
|
||||
assertNotIncludes("shared stdlib fallibility facts", checker, "builtin_fallible_return_type");
|
||||
|
||||
const callResultBody = sliceBetween(checker, "static bool call_result_value_provenance", "static bool expr_reference_provenance");
|
||||
assertIncludes("call result provenance", callResultBody, "resolve_provenance_call");
|
||||
assertIncludes("call result provenance", callResultBody, "function_return_value_provenance");
|
||||
assertIncludes("call result provenance", callResultBody, "instantiate_call_provenance_entry");
|
||||
assertIncludes("call result provenance", callResultBody, "resolved_call_param_type_text");
|
||||
|
||||
const shapeMethodProvenanceBody = sliceBetween(checker, "static bool finish_shape_method_provenance_call", "static bool resolve_shape_namespace_provenance_call");
|
||||
assertIncludes("shape method provenance facts", shapeMethodProvenanceBody, "shape_method_bindings_from_recorded_type_args");
|
||||
assertIncludes("shape method provenance facts", shapeMethodProvenanceBody, "build_shape_method_bindings");
|
||||
assertIncludes("shape method provenance facts", shapeMethodProvenanceBody, "provenance_context_substitute_bindings");
|
||||
assertIncludes("shape method provenance facts", shapeMethodProvenanceBody, "call_resolution_record_bindings");
|
||||
assertIncludes("shape method provenance facts", shapeMethodProvenanceBody, "call_resolution_record_param_facts");
|
||||
assertBefore("shape method provenance binding source order", shapeMethodProvenanceBody, "shape_method_bindings_from_recorded_type_args", "build_shape_method_bindings");
|
||||
|
||||
const shapeNamespaceProvenanceBody = sliceBetween(checker, "static bool resolve_shape_namespace_provenance_call", "static bool resolve_concrete_constrained_shape_provenance_call");
|
||||
assertIncludes("shape namespace provenance resolver", shapeNamespaceProvenanceBody, "resolve_shape_namespace_call");
|
||||
assertIncludes("shape namespace provenance resolver", shapeNamespaceProvenanceBody, "finish_shape_method_provenance_call");
|
||||
|
||||
const concreteConstrainedShapeProvenanceBody = sliceBetween(checker, "static bool resolve_concrete_constrained_shape_provenance_call", "static bool resolve_named_provenance_call");
|
||||
assertIncludes("concrete constrained-shape provenance resolver", concreteConstrainedShapeProvenanceBody, "resolve_concrete_constrained_shape_call");
|
||||
assertIncludes("concrete constrained-shape provenance resolver", concreteConstrainedShapeProvenanceBody, "finish_shape_method_provenance_call");
|
||||
|
||||
const namedProvenanceBody = sliceBetween(checker, "static bool resolve_named_provenance_call", "static bool resolve_constrained_interface_provenance_call");
|
||||
assertIncludes("named provenance resolver", namedProvenanceBody, "resolve_named_function_call");
|
||||
assertIncludes("named provenance resolver", namedProvenanceBody, "generic_call_bindings_from_checked_call");
|
||||
assertIncludes("named provenance resolver", namedProvenanceBody, "provenance_context_substitute_bindings");
|
||||
assertIncludes("named provenance resolver", namedProvenanceBody, "call_resolution_record_bindings");
|
||||
assertIncludes("named provenance resolver", namedProvenanceBody, "call_resolution_record_param_facts");
|
||||
|
||||
const checkedCallTypeArgsBody = sliceBetween(checker, "static const TypeArgVec *checked_call_type_args", "static bool generic_bindings_from_type_args");
|
||||
assertIncludes("checked call type args", checkedCallTypeArgsBody, "checked_type_args");
|
||||
|
||||
const checkedCallBindingsBody = sliceBetween(
|
||||
checker,
|
||||
"static bool checked_call_bindings_from_recorded_type_args(CheckContext *ctx, const Program *program, const Function *callee, const Expr *call, GenericBinding **out_bindings, size_t *out_len, bool *out_recorded) {",
|
||||
"static bool generic_call_bindings_from_checked_call"
|
||||
);
|
||||
assertIncludes("checked call bindings", checkedCallBindingsBody, "checked_call_type_args");
|
||||
assertIncludes("checked call bindings", checkedCallBindingsBody, "generic_bindings_from_type_args");
|
||||
|
||||
const genericCallBindingsBody = sliceBetween(
|
||||
checker,
|
||||
"static bool generic_call_bindings_from_checked_call(CheckContext *ctx, const Program *program, const Function *callee, const Expr *call, Scope *scope, const char *return_type, GenericBinding **out_bindings, size_t *out_len) {",
|
||||
"static char *call_param_type_text"
|
||||
);
|
||||
assertIncludes("generic call bindings", genericCallBindingsBody, "checked_call_bindings_from_recorded_type_args");
|
||||
assertIncludes("generic call bindings", genericCallBindingsBody, "call_type_args(call)");
|
||||
assertBefore("generic call binding source order", genericCallBindingsBody, "checked_call_bindings_from_recorded_type_args", "call_type_args(call)");
|
||||
|
||||
const constrainedInterfaceProvenanceBody = sliceBetween(checker, "static bool resolve_constrained_interface_provenance_call", "static bool resolve_receiver_shape_provenance_call");
|
||||
assertIncludes("constrained interface provenance resolver", constrainedInterfaceProvenanceBody, "resolve_constrained_interface_call");
|
||||
assertIncludes("constrained interface provenance resolver", constrainedInterfaceProvenanceBody, "interface_method_bindings_from_recorded_type_args");
|
||||
assertIncludes("constrained interface provenance resolver", constrainedInterfaceProvenanceBody, "build_constrained_interface_method_bindings");
|
||||
assertIncludes("constrained interface provenance resolver", constrainedInterfaceProvenanceBody, "call_resolution_record_bindings");
|
||||
assertIncludes("constrained interface provenance resolver", constrainedInterfaceProvenanceBody, "call_resolution_record_param_facts");
|
||||
assertBefore("constrained interface provenance binding source order", constrainedInterfaceProvenanceBody, "interface_method_bindings_from_recorded_type_args", "build_constrained_interface_method_bindings");
|
||||
|
||||
const receiverProvenanceBody = sliceBetween(checker, "static bool resolve_receiver_shape_provenance_call", "static bool resolve_provenance_call");
|
||||
assertIncludes("receiver provenance resolver", receiverProvenanceBody, "resolve_receiver_shape_call");
|
||||
assertIncludes("receiver provenance resolver", receiverProvenanceBody, "shape_method_bindings_from_recorded_type_args");
|
||||
assertIncludes("receiver provenance resolver", receiverProvenanceBody, "build_receiver_shape_method_bindings");
|
||||
assertIncludes("receiver provenance resolver", receiverProvenanceBody, "call_resolution_record_bindings");
|
||||
assertIncludes("receiver provenance resolver", receiverProvenanceBody, "call_resolution_record_param_facts");
|
||||
assertBefore("receiver provenance binding source order", receiverProvenanceBody, "shape_method_bindings_from_recorded_type_args", "build_receiver_shape_method_bindings");
|
||||
|
||||
const provenanceCallBody = sliceBetween(checker, "static bool resolve_provenance_call", "static bool function_return_value_provenance");
|
||||
assertIncludes("provenance call dispatcher", provenanceCallBody, "resolve_named_provenance_call");
|
||||
assertIncludes("provenance call dispatcher", provenanceCallBody, "resolve_shape_namespace_provenance_call");
|
||||
assertIncludes("provenance call dispatcher", provenanceCallBody, "resolve_concrete_constrained_shape_provenance_call");
|
||||
assertIncludes("provenance call dispatcher", provenanceCallBody, "resolve_constrained_interface_provenance_call");
|
||||
assertIncludes("provenance call dispatcher", provenanceCallBody, "resolve_receiver_shape_provenance_call");
|
||||
assertBefore("provenance call resolver order", provenanceCallBody, "resolve_concrete_constrained_shape_provenance_call", "resolve_constrained_interface_provenance_call");
|
||||
|
||||
const callResolutionFactsResolverBody = sliceBetween(checker, "static bool call_facts_resolve_call", "static void call_facts_collect_expr");
|
||||
assertIncludes("call-resolution graph facts", callResolutionFactsResolverBody, "resolve_stdlib_call");
|
||||
assertIncludes("call-resolution graph facts", callResolutionFactsResolverBody, "resolve_choice_constructor_call");
|
||||
assertIncludes("call-resolution graph facts", callResolutionFactsResolverBody, "resolve_provenance_call");
|
||||
assertIncludes("call-resolution graph facts", callResolutionFactsResolverBody, "ResolvedProvenanceCall");
|
||||
|
||||
const callResolutionFactsJsonBody = sliceBetween(checker, "void z_append_call_resolution_facts_json", "static bool function_return_value_provenance");
|
||||
assertIncludes("call-resolution graph facts", callResolutionFactsJsonBody, "Z_CALL_FUNCTION");
|
||||
assertIncludes("call-resolution graph facts", callResolutionFactsJsonBody, "Z_CALL_CHOICE_CONSTRUCTOR");
|
||||
assertIncludes("call-resolution graph facts", callResolutionFactsJsonBody, "call_facts_collect_function");
|
||||
assertIncludes("call-resolution graph fixture", conformance, "conformance/check/pass/call-resolution-inspection.0");
|
||||
assertIncludes("call-resolution graph fixture", conformance, "callResolution");
|
||||
|
||||
const exprCallResolverBody = sliceBetween(checker, "static bool resolve_expr_call_for_type", "static const char *expr_call_return_type");
|
||||
assertIncludes("expr call return resolver", exprCallResolverBody, "resolve_named_function_call");
|
||||
assertIncludes("expr call return resolver", exprCallResolverBody, "resolve_shape_namespace_call");
|
||||
assertIncludes("expr call return resolver", exprCallResolverBody, "resolve_concrete_constrained_shape_call");
|
||||
assertIncludes("expr call return resolver", exprCallResolverBody, "resolve_constrained_interface_call");
|
||||
assertIncludes("expr call return resolver", exprCallResolverBody, "resolve_receiver_shape_call");
|
||||
assertIncludes("expr call return resolver", exprCallResolverBody, "resolve_choice_constructor_call");
|
||||
assertIncludes("expr call return resolver", exprCallResolverBody, "resolve_stdlib_call");
|
||||
|
||||
const exprCallReturnBody = sliceBetween(checker, "static const char *expr_call_return_type", "static const char *expr_type");
|
||||
assertIncludes("expr call return facts", exprCallReturnBody, "resolve_expr_call_for_type");
|
||||
assertIncludes("expr call return facts", exprCallReturnBody, "resolution.return_type");
|
||||
assertIncludes("expr call return facts", exprCallReturnBody, "Z_CALL_FUNCTION");
|
||||
assertIncludes("expr call return facts", exprCallReturnBody, "Z_CALL_SHAPE_NAMESPACE");
|
||||
assertIncludes("expr call return facts", exprCallReturnBody, "Z_CALL_CONSTRAINED_INTERFACE");
|
||||
assertIncludes("expr call return facts", exprCallReturnBody, "Z_CALL_RECEIVER");
|
||||
assertIncludes("expr call return facts", exprCallReturnBody, "generic_call_bindings_from_checked_call");
|
||||
assertIncludes("expr call return facts", exprCallReturnBody, "shape_method_bindings_from_recorded_type_args");
|
||||
assertIncludes("expr call return facts", exprCallReturnBody, "interface_method_bindings_from_recorded_type_args");
|
||||
assertNotIncludes("expr call return facts", exprCallReturnBody, "build_generic_bindings");
|
||||
assertBefore("expr call return shape binding source order", exprCallReturnBody, "shape_method_bindings_from_recorded_type_args", "build_shape_method_bindings");
|
||||
assertBefore("expr call return interface binding source order", exprCallReturnBody, "interface_method_bindings_from_recorded_type_args", "build_constrained_interface_method_bindings");
|
||||
assertBefore("expr call return receiver binding source order", exprCallReturnBody, "shape_method_bindings_from_recorded_type_args", "build_receiver_shape_method_bindings");
|
||||
|
||||
const exprTypeCallBody = sliceBetween(
|
||||
checker,
|
||||
"static const char *expr_type(CheckContext *ctx, const Program *program, const Expr *expr, Scope *scope) {",
|
||||
"case EXPR_INDEX:"
|
||||
);
|
||||
assertIncludes("expr_type call return dispatch", exprTypeCallBody, "expr_call_return_type");
|
||||
assertNotIncludes("expr_type call return dispatch", exprTypeCallBody, "resolve_named_function_call");
|
||||
assertNotIncludes("expr_type call return dispatch", exprTypeCallBody, "resolve_shape_namespace_call");
|
||||
assertNotIncludes("expr_type call return dispatch", exprTypeCallBody, "resolve_stdlib_call");
|
||||
|
||||
const namedCallBody = sliceBetween(checker, "static bool build_named_call_bindings_expected", "static bool check_stdlib_table_arg_range_expected");
|
||||
assertIncludes("named call checking argument facts", namedCallBody, "resolve_named_function_call");
|
||||
assertIncludes("named call checking argument facts", namedCallBody, "build_named_call_bindings_expected");
|
||||
assertIncludes("named call checking argument facts", namedCallBody, "check_named_call_args_expected");
|
||||
assertIncludes("named call checking argument facts", namedCallBody, "check_call_resolution_args_expected");
|
||||
assertIncludes("named call checking argument facts", namedCallBody, "check_named_call_fallibility_expected");
|
||||
assertIncludes("named call checking argument facts", namedCallBody, "prepare_named_call_return_and_storage_expected");
|
||||
assertIncludes("named call checking argument facts", namedCallBody, "finish_named_call_return_expected");
|
||||
assertIncludes("named call checking argument facts", namedCallBody, "z_call_resolution_expected_arg_count");
|
||||
assertIncludes("named call checking argument facts", namedCallBody, "call_resolution_record_param_facts");
|
||||
assertIncludes("named call checking argument facts", namedCallBody, "call_resolution_param_type_text");
|
||||
assertIncludes("named call checking storage effects", namedCallBody, "apply_checked_call_storage_effects");
|
||||
|
||||
const stdlibTableCallBody = sliceBetween(checker, "static bool check_stdlib_table_arg_range_expected", "static bool check_stdlib_allocator_arg");
|
||||
assertIncludes("stdlib table call checking argument facts", stdlibTableCallBody, "z_call_resolution_add_arg");
|
||||
assertIncludes("stdlib table call checking argument facts", stdlibTableCallBody, "call_resolution_param_type_text");
|
||||
assertIncludes("stdlib table call checking argument facts", stdlibTableCallBody, "z_call_resolution_param_type");
|
||||
assertNotIncludes("stdlib table call checking argument facts", stdlibTableCallBody, "std_call_arg_type");
|
||||
|
||||
const stdlibTableDispatchBody = sliceBetween(checker, "static bool check_stdlib_table_call_expected", "static bool check_stdlib_known_call_expected");
|
||||
assertIncludes("stdlib table call checking argument facts", stdlibTableDispatchBody, "check_stdlib_table_arg_range_expected");
|
||||
|
||||
const stdlibKnownCallBody = sliceBetween(checker, "static bool check_stdlib_known_call_expected", "static bool check_stdlib_call_expected");
|
||||
assertIncludes("stdlib known call checking argument facts", stdlibKnownCallBody, "ZCallResolution *resolution");
|
||||
assertIncludes("stdlib known call helper classification", stdlibKnownCallBody, "z_std_helper_kind");
|
||||
assertIncludes("stdlib known call helper classification", stdlibKnownCallBody, "switch (kind)");
|
||||
assertIncludes("stdlib known call checking argument facts", stdlibKnownCallBody, "check_stdlib_mem_get_call_expected");
|
||||
assertIncludes("stdlib known call checking argument facts", stdlibKnownCallBody, "check_stdlib_table_call_expected");
|
||||
assertNotIncludes("stdlib known call helper classification", stdlibKnownCallBody, "strcmp(");
|
||||
|
||||
const choiceCallBody = sliceBetween(checker, "static bool check_choice_constructor_call_expected", "static bool check_shape_namespace_call_expected");
|
||||
assertIncludes("choice call checking argument facts", choiceCallBody, "resolve_choice_constructor_call");
|
||||
assertIncludes("choice call checking argument facts", choiceCallBody, "z_call_resolution_add_arg(&choice_resolution");
|
||||
assertIncludes("choice call checking argument facts", choiceCallBody, "call_resolution_param_type_text(&choice_resolution");
|
||||
assertIncludes("choice call checking argument facts", choiceCallBody, "z_call_resolution_expected_arg_count");
|
||||
assertBefore("choice call resolution order", choiceCallBody, "resolve_choice_constructor_call", "find_choice(program");
|
||||
|
||||
const shapeNamespaceCallBody = sliceBetween(checker, "static bool check_shape_namespace_call_expected", "static bool receiver_member_call_should_resolve");
|
||||
assertIncludes("shape namespace call checking argument facts", shapeNamespaceCallBody, "call_resolution_record_param_facts");
|
||||
assertIncludes("shape namespace call checking argument facts", shapeNamespaceCallBody, "check_call_resolution_args_expected");
|
||||
assertIncludes("shape namespace call checking argument facts", shapeNamespaceCallBody, "z_call_resolution_expected_arg_count");
|
||||
assertIncludes("shape namespace call checking argument facts", shapeNamespaceCallBody, "set_expr_checked_type_args");
|
||||
assertIncludes("shape namespace call checking storage effects", shapeNamespaceCallBody, "apply_checked_call_storage_effects");
|
||||
|
||||
const receiverCallBody = sliceBetween(checker, "static bool check_receiver_method_receiver_access", "static bool check_constrained_interface_call_expected");
|
||||
assertIncludes("receiver call checking argument facts", receiverCallBody, "resolve_receiver_shape_call");
|
||||
assertIncludes("receiver call checking argument facts", receiverCallBody, "call_resolution_record_param_facts");
|
||||
assertIncludes("receiver call checking argument facts", receiverCallBody, "check_call_resolution_args_expected");
|
||||
assertIncludes("receiver call checking argument facts", receiverCallBody, "z_call_resolution_expected_arg_count");
|
||||
assertIncludes("receiver call checking argument facts", receiverCallBody, "set_expr_checked_type_args");
|
||||
assertIncludes("receiver call checking storage effects", receiverCallBody, "apply_checked_call_storage_effects");
|
||||
|
||||
const constrainedInterfaceCallBody = sliceBetween(checker, "static bool check_constrained_interface_call_expected", "static bool check_world_stream_write_call_expected");
|
||||
assertIncludes("constrained interface call checking argument facts", constrainedInterfaceCallBody, "call_resolution_record_param_facts");
|
||||
assertIncludes("constrained interface call checking argument facts", constrainedInterfaceCallBody, "check_call_resolution_args_expected");
|
||||
assertIncludes("constrained interface call checking argument facts", constrainedInterfaceCallBody, "z_call_resolution_expected_arg_count");
|
||||
assertIncludes("constrained interface call checking argument facts", constrainedInterfaceCallBody, "set_expr_checked_type_args");
|
||||
assertIncludes("constrained interface call checking storage effects", constrainedInterfaceCallBody, "apply_checked_call_storage_effects");
|
||||
|
||||
const callExprBody = sliceBetween(checker, "static bool check_call_expr_expected", "static bool check_expr_expected");
|
||||
assertIncludes("call expression checking dispatch", callExprBody, "check_choice_constructor_call_expected");
|
||||
assertIncludes("call expression checking dispatch", callExprBody, "check_shape_namespace_call_expected");
|
||||
assertIncludes("call expression checking dispatch", callExprBody, "check_receiver_shape_call_expected");
|
||||
assertIncludes("call expression checking dispatch", callExprBody, "check_constrained_interface_call_expected");
|
||||
assertIncludes("call expression checking dispatch", callExprBody, "check_named_function_call_expected");
|
||||
assertIncludes("call expression checking dispatch", callExprBody, "check_stdlib_call_expected");
|
||||
assertBefore("stdlib call resolution before fallback argument checks", callExprBody, "check_stdlib_call_expected", "for (size_t i = 0; i < expr->args.len; i++)");
|
||||
|
||||
const stdlibCallBody = sliceBetween(checker, "static bool check_stdlib_call_expected", "static bool check_choice_constructor_call_expected");
|
||||
assertIncludes("stdlib call checking dispatch", stdlibCallBody, "resolve_stdlib_call");
|
||||
assertIncludes("stdlib call checking dispatch", stdlibCallBody, "check_stdlib_known_call_expected");
|
||||
assertIncludes("stdlib call checking fallibility", stdlibCallBody, "check_stdlib_call_fallibility_expected");
|
||||
assertIncludes("stdlib call checking dispatch", stdlibCallBody, "z_call_resolution_free");
|
||||
assertIncludes("stdlib call checking dispatch", stdlibCallBody, "z_call_resolution_expected_arg_count");
|
||||
assertNotIncludes("stdlib call checking dispatch", stdlibCallBody, "std_call_arg_count");
|
||||
|
||||
const stdlibResolverBody = sliceBetween(checker, "static bool resolve_stdlib_callee", "static bool resolve_choice_constructor_call");
|
||||
assertIncludes("stdlib call resolver facts", stdlibResolverBody, "z_std_helper_find");
|
||||
assertIncludes("stdlib call resolver facts", stdlibResolverBody, "z_call_resolution_set_return_type(out, helper->return_type)");
|
||||
assertIncludes("stdlib call resolver facts", stdlibResolverBody, "helper->arg_types");
|
||||
assertIncludes("stdlib call resolver facts", stdlibResolverBody, "z_call_resolution_add_arg");
|
||||
assertIncludes("stdlib call resolver facts", stdlibResolverBody, "z_std_helper_error_name");
|
||||
assertIncludes("stdlib call resolver facts", stdlibResolverBody, "z_call_resolution_add_error");
|
||||
assertIncludes("stdlib call resolver facts", stdlibResolverBody, "resolve_stdlib_fallible_call");
|
||||
assertNotIncludes("stdlib call resolver facts", stdlibResolverBody, "std_call_arg_count");
|
||||
|
||||
const callFunctionContextBody = sliceBetween(checker, "static const Function *resolve_call_function_in_context", "static bool expr_call_has_error_flow");
|
||||
assertIncludes("fallible call function context resolver", callFunctionContextBody, "resolve_named_function_call");
|
||||
assertIncludes("fallible call function context resolver", callFunctionContextBody, "resolve_shape_namespace_call");
|
||||
assertIncludes("fallible call function context resolver", callFunctionContextBody, "resolve_concrete_constrained_shape_call");
|
||||
assertIncludes("fallible call function context resolver", callFunctionContextBody, "resolve_constrained_interface_call");
|
||||
assertIncludes("fallible call function context resolver", callFunctionContextBody, "resolve_receiver_shape_call");
|
||||
assertNotIncludes("fallible call function context resolver", callFunctionContextBody, "find_shape_method_decl");
|
||||
|
||||
const memberCallSkipBody = sliceBetween(checker, "static bool member_call_skips_callee_expr_check", "static bool check_call_callee");
|
||||
assertIncludes("member callee precheck", memberCallSkipBody, "member_root_ident_is");
|
||||
assertNotIncludes("member callee precheck", memberCallSkipBody, "member_name_buf");
|
||||
|
||||
const functionErrorFlowBody = sliceBetween(
|
||||
checker,
|
||||
"static bool function_has_error_flow_inner(CheckContext *ctx, const Program *program, const Function *fun, size_t depth) {",
|
||||
"static bool function_has_error_flow(CheckContext"
|
||||
);
|
||||
assertIncludes("fallible function body context", functionErrorFlowBody, "find_shape_owning_method");
|
||||
assertIncludes("fallible function body context", functionErrorFlowBody, "fun_ctx.shape");
|
||||
|
||||
const uncheckedFallibleCallBody = sliceBetween(checker, "static bool check_unchecked_fallible_call_expected", "static bool check_call_expr_expected");
|
||||
assertIncludes("unchecked fallible call resolver", uncheckedFallibleCallBody, "fallible_callee_in_context");
|
||||
assertNotIncludes("unchecked fallible call resolver", uncheckedFallibleCallBody, "resolve_stdlib_fallible_call");
|
||||
assertNotIncludes("unchecked fallible call resolver", uncheckedFallibleCallBody, "fallible_callee(ctx");
|
||||
|
||||
const callCalleeBody = sliceBetween(checker, "static bool check_call_callee", "static bool build_named_call_bindings_expected");
|
||||
assertIncludes("call callee precheck", callCalleeBody, "member_call_skips_callee_expr_check");
|
||||
assertNotIncludes("call callee precheck", callCalleeBody, "resolve_shape_namespace_call");
|
||||
assertNotIncludes("call callee precheck", callCalleeBody, "resolve_constrained_interface_call");
|
||||
assertNotIncludes("call callee precheck", callCalleeBody, "resolve_stdlib_call");
|
||||
|
||||
const receiverShouldResolveBody = sliceBetween(checker, "static bool receiver_member_call_should_resolve", "static bool check_receiver_method_receiver_access");
|
||||
assertIncludes("receiver resolution gate", receiverShouldResolveBody, "member_root_ident_is");
|
||||
assertIncludes("receiver resolution gate", receiverShouldResolveBody, "resolve_stdlib_call");
|
||||
assertNotIncludes("receiver resolution gate", receiverShouldResolveBody, "member_name_buf");
|
||||
|
||||
const checkCallBody = sliceBetween(
|
||||
checker,
|
||||
"static bool check_expr_expected(CheckContext *ctx, const Program *program, const Expr *expr, Scope *scope, ZDiag *diag, const char *expected) {",
|
||||
"case EXPR_CAST:"
|
||||
);
|
||||
assertIncludes("call checking dispatch", checkCallBody, "check_call_expr_expected");
|
||||
|
||||
const checkedCallBody = sliceBetween(checker, "static bool apply_checked_call_storage_effects", "static bool apply_resolved_call_storage_effects");
|
||||
assertIncludes("checked call storage effects", checkedCallBody, "resolve_provenance_call");
|
||||
assertIncludes("checked call storage effects", checkedCallBody, "apply_provenance_call_storage_effects");
|
||||
|
||||
const stdMemGetProvenanceBody = sliceBetween(checker, "static bool std_mem_get_value_provenance", "static bool value_provenance_add_actual_place");
|
||||
assertIncludes("std.mem.get provenance resolver", stdMemGetProvenanceBody, "resolve_stdlib_call");
|
||||
assertIncludes("std.mem.get provenance resolver", stdMemGetProvenanceBody, "z_std_helper_kind");
|
||||
assertIncludes("std.mem.get provenance resolver", stdMemGetProvenanceBody, "z_call_resolution_add_arg");
|
||||
assertNotIncludes("std.mem.get provenance resolver", stdMemGetProvenanceBody, "member_name_buf");
|
||||
assertNotIncludes("std.mem.get provenance resolver", stdMemGetProvenanceBody, "strcmp(");
|
||||
|
||||
const summaryBody = sliceBetween(checker, "static bool function_provenance_summary", "static bool function_return_value_provenance");
|
||||
assertIncludes("function provenance summary", summaryBody, "collect_return_value_provenance_from_stmt_vec");
|
||||
assertIncludes("function provenance summary", summaryBody, "body_complete");
|
||||
assertIncludes("function provenance summary", summaryBody, "seed_param_storage_value_provenance");
|
||||
assertIncludes("function provenance summary", summaryBody, "provenance_storage_effect_vec_add");
|
||||
assertIncludes("function provenance summary", summaryBody, "return summary->return_complete && summary->effect_complete");
|
||||
assertIncludes("choice type provenance", checker, "const Choice *choice = find_choice(program, type)");
|
||||
assertIncludes("choice constructor provenance", checker, "choice_constructor_value_provenance(ctx, program, expr, scope, origins)");
|
||||
const choiceConstructorBody = sliceBetween(checker, "static bool choice_constructor_value_provenance", "static void register_match_payload_binding_provenance");
|
||||
assertIncludes("choice constructor provenance", choiceConstructorBody, "resolve_choice_constructor_call");
|
||||
assertIncludes("choice constructor provenance", choiceConstructorBody, "z_call_resolution_add_arg");
|
||||
assertIncludes("choice match payload provenance", checker, "register_match_payload_binding_provenance(ctx, program, stmt->expr, scope, &arm_scope, arm->payload_name, arm->case_name)");
|
||||
|
||||
const storageSummaryBody = sliceBetween(checker, "static bool function_storage_effect_summary", "static bool apply_provenance_storage_effect");
|
||||
assertIncludes("storage-effect summary", storageSummaryBody, "function_provenance_summary");
|
||||
|
||||
const returnSummaryBody = sliceBetween(checker, "static bool function_return_value_provenance", "static const Expr *call_actual_for_param");
|
||||
assertIncludes("return summary", returnSummaryBody, "function_provenance_summary");
|
||||
|
||||
const expressionEffectBody = sliceBetween(checker, "static bool apply_expr_call_storage_effects", "static bool check_assignment_not_borrowed");
|
||||
assertIncludes("expression storage effects", expressionEffectBody, "if (expr->kind == EXPR_CALL) return apply_resolved_call_storage_effects");
|
||||
assertIncludes("expression storage effects", expressionEffectBody, "provenance_scope_snapshot_restore_optional_branch");
|
||||
assertIncludes("array literal provenance", checker, "value_provenance_add_all_with_prefix(origins, &item_origins, element_path)");
|
||||
assertIncludes("assignment path clearing", checker, "origin_path_is_definitely_within(entry->value_path, path)");
|
||||
|
||||
const checkExprExpectedBody = sliceBetween(
|
||||
checker,
|
||||
"static bool check_expr_expected(CheckContext *ctx, const Program *program, const Expr *expr, Scope *scope, ZDiag *diag, const char *expected) {",
|
||||
"static bool check_expr(CheckContext *ctx, const Program *program, const Expr *expr, Scope *scope, ZDiag *diag) {"
|
||||
);
|
||||
const callCase = sliceBetween(checkExprExpectedBody, "case EXPR_CALL:", "case EXPR_CAST:");
|
||||
assertIncludes("call checking dispatcher", callCase, "check_call_expr_expected");
|
||||
const callCaseStorageApplications = (callCase.match(/apply_checked_call_storage_effects\(ctx, program, expr, scope, diag\)/g) ?? []).length;
|
||||
const namedCallStorageApplications = (namedCallBody.match(/apply_checked_call_storage_effects\(ctx, program, expr, scope, diag\)/g) ?? []).length;
|
||||
const shapeNamespaceCallStorageApplications = (shapeNamespaceCallBody.match(/apply_checked_call_storage_effects\(ctx, program, expr, scope, diag\)/g) ?? []).length;
|
||||
const receiverCallStorageApplications = (receiverCallBody.match(/apply_checked_call_storage_effects\(ctx, program, expr, scope, diag\)/g) ?? []).length;
|
||||
const constrainedInterfaceCallStorageApplications = (constrainedInterfaceCallBody.match(/apply_checked_call_storage_effects\(ctx, program, expr, scope, diag\)/g) ?? []).length;
|
||||
const totalCallStorageApplications =
|
||||
callCaseStorageApplications +
|
||||
namedCallStorageApplications +
|
||||
shapeNamespaceCallStorageApplications +
|
||||
receiverCallStorageApplications +
|
||||
constrainedInterfaceCallStorageApplications;
|
||||
if (totalCallStorageApplications < 4) {
|
||||
fail(`EXPR_CALL provenance: expected storage-effect application for all user call forms, found ${totalCallStorageApplications}`);
|
||||
}
|
||||
|
||||
const lines = checker.split("\n");
|
||||
const currentFunction = currentFunctionByLine(checker);
|
||||
const mutationAllowlist = new Map([
|
||||
["scope_set_value_provenance(", new Set(["scope_set_value_provenance", "register_borrow_binding", "seed_param_storage_value_provenance", "register_match_payload_binding_provenance"])],
|
||||
["scope_set_value_provenance_path_in_scope(", new Set(["scope_set_value_provenance_path_in_scope", "update_borrow_assignment", "assignment_provenance_snapshot_clear", "assignment_provenance_snapshot_restore", "apply_provenance_storage_effect", "stdlib_install_item_write_provenance"])],
|
||||
["scope_borrow_counts_for_place(", new Set(["scope_borrow_counts_for_place", "check_borrow_conflict_at", "check_read_not_mutably_borrowed", "check_assignment_not_borrowed"])],
|
||||
]);
|
||||
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
const line = lines[index];
|
||||
if (line.trimStart().startsWith("static ")) continue;
|
||||
for (const [needle, allowed] of mutationAllowlist) {
|
||||
if (!line.includes(needle)) continue;
|
||||
const owner = currentFunction[index];
|
||||
if (!allowed.has(owner)) {
|
||||
fail(`checker provenance bypass: '${needle}' at ${checkerPath}:${index + 1} is in '${owner}', not ${[...allowed].join(", ")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertMatches(
|
||||
"callee-local storage rejection",
|
||||
checker,
|
||||
/cannot store a reference to a callee-local binding through a mutable parameter/
|
||||
);
|
||||
assertMatches(
|
||||
"incomplete mutref summary rejection",
|
||||
checker,
|
||||
/cannot verify provenance effects for mutable parameter call/
|
||||
);
|
||||
assertMatches(
|
||||
"shorter-lived storage rejection",
|
||||
checker,
|
||||
/cannot assign a reference to a shorter-lived binding/
|
||||
);
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error("provenance guardrails failed:");
|
||||
for (const failure of failures) console.error(`- ${failure}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`provenance guardrails ok (${rows.length} surfaces)`);
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const outDir = join("/tmp", `zero-reliability-smoke-${process.pid}`);
|
||||
const zero = "bin/zero";
|
||||
const rows = [];
|
||||
|
||||
await mkdir(outDir, { recursive: true });
|
||||
|
||||
async function run(args, { allowFailure = false } = {}) {
|
||||
try {
|
||||
const result = await execFileAsync(zero, args);
|
||||
return { code: 0, stdout: result.stdout, stderr: result.stderr };
|
||||
} catch (error) {
|
||||
if (!allowFailure) throw error;
|
||||
return {
|
||||
code: error.code ?? error.status ?? 1,
|
||||
stdout: error.stdout?.toString() ?? "",
|
||||
stderr: error.stderr?.toString() ?? "",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function json(args, options = {}) {
|
||||
const result = await run(args, options);
|
||||
return { ...result, body: JSON.parse(result.stdout) };
|
||||
}
|
||||
|
||||
function record(id, kind, ok, facts = {}) {
|
||||
rows.push({ id, kind, ok, ...facts });
|
||||
assert.equal(ok, true, `${id} failed`);
|
||||
}
|
||||
|
||||
const packageTests = await json(["test", "--json", "conformance/packages/test-app"]);
|
||||
record("package-test-golden", "golden", packageTests.body.ok === true && packageTests.body.stdout === "3 test(s) ok\n", {
|
||||
selectedTests: packageTests.body.selectedTests,
|
||||
expectedFailures: packageTests.body.expectedFailures,
|
||||
snapshotKey: packageTests.body.fixtures?.snapshotKey,
|
||||
});
|
||||
|
||||
const filteredTests = await json(["test", "--json", "--filter", "helper", "conformance/packages/test-app"]);
|
||||
record("package-test-filter", "golden", filteredTests.body.selectedTests === 2 && filteredTests.body.discoveredTests === 3, {
|
||||
stdout: filteredTests.body.stdout,
|
||||
});
|
||||
|
||||
const expectedFail = await json(["test", "--json", "conformance/native/pass/test-expected-fail.graph"]);
|
||||
record("expected-fail-contract", "golden", expectedFail.body.expectedFailures === 1 && expectedFail.body.failedTests === 0, {
|
||||
status: expectedFail.body.results?.[0]?.status,
|
||||
});
|
||||
|
||||
const validFuzz = join(outDir, "valid-fuzz.0");
|
||||
await writeFile(validFuzz, `fn add(a: i32, b: i32) -> i32 {
|
||||
return a + b
|
||||
}
|
||||
|
||||
test "fuzz valid arithmetic" {
|
||||
expect (add(21, 21) == 42)
|
||||
}
|
||||
`);
|
||||
const validTokens = await json(["tokens", "--json", validFuzz]);
|
||||
const validParse = await json(["parse", "--json", validFuzz]);
|
||||
const validGraph = join(outDir, "valid-fuzz.graph");
|
||||
await json(["import", "--json", "--format", "binary", "--out", validGraph, validFuzz]);
|
||||
const validCheck = await json(["check", "--json", validGraph]);
|
||||
record("parser-checker-fuzz-valid", "fuzz", validTokens.body.tokens.length > 0 && validParse.body.root.kind === "module" && validCheck.body.ok === true, {
|
||||
tokenCount: validTokens.body.tokens.length,
|
||||
});
|
||||
|
||||
const invalidFuzz = join(outDir, "invalid-fuzz.0");
|
||||
await writeFile(invalidFuzz, "pub fn main(world: World -> Void {\n");
|
||||
const invalidGraph = join(outDir, "invalid-fuzz.graph");
|
||||
const invalidImport = await json(["import", "--json", "--format", "binary", "--out", invalidGraph, invalidFuzz], { allowFailure: true });
|
||||
record("parser-checker-fuzz-invalid", "fuzz", invalidImport.code !== 0 && invalidImport.body.diagnostics?.length > 0, {
|
||||
diagnostic: invalidImport.body.diagnostics?.[0]?.code,
|
||||
});
|
||||
|
||||
const fmtFixture = await run(["fmt", "conformance/native/pass/test-blocks.0"]);
|
||||
const fmtExpected = await readFile("conformance/native/pass/test-blocks.0", "utf8");
|
||||
record("formatter-snapshot", "snapshot", fmtFixture.stdout === fmtExpected, {
|
||||
bytes: fmtFixture.stdout.length,
|
||||
});
|
||||
|
||||
const stdlibJson = await json(["size", "--json", "conformance/native/pass/std-codec-json-url.graph"]);
|
||||
record("stdlib-parser-golden", "golden", stdlibJson.body.usedStdlibHelpers?.some((helper) => helper.name === "std.json.field") && stdlibJson.body.compilerCaches?.some((cache) => cache.sourceKind === "program-graph"), {
|
||||
helperCount: stdlibJson.body.usedStdlibHelpers?.length ?? 0,
|
||||
});
|
||||
|
||||
const stdlibEdgesGraph = "conformance/native/pass/std-codec-json-url.graph";
|
||||
const stdlibEdgesRun = await run(["run", stdlibEdgesGraph]);
|
||||
record("stdlib-codec-json-http-edges", "golden", stdlibEdgesRun.code === 0 && stdlibEdgesRun.stdout === "std codec json url ok\n", {
|
||||
fixture: stdlibEdgesGraph,
|
||||
});
|
||||
|
||||
const crasher = join(outDir, "crasher-repro-unclosed-string.0");
|
||||
await writeFile(crasher, "pub fn main() -> Void {\n let value: String = \"unterminated\n}\n");
|
||||
const crasherGraph = join(outDir, "crasher-repro-unclosed-string.graph");
|
||||
const crasherImport = await json(["import", "--json", "--format", "binary", "--out", crasherGraph, crasher], { allowFailure: true });
|
||||
record("minimized-crasher-repro", "crasher", crasherImport.code !== 0 && crasherImport.body.diagnostics?.length > 0, {
|
||||
diagnostic: crasherImport.body.diagnostics?.[0]?.code,
|
||||
});
|
||||
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
ok: rows.every((row) => row.ok),
|
||||
rows,
|
||||
conventions: {
|
||||
fuzzCorpus: [validGraph, invalidFuzz, "conformance/format/functions-blocks.0", "examples/std-data-formats.graph"],
|
||||
goldenOutputs: ["zero test --json conformance/packages/test-app", "zero fmt conformance/native/pass/test-blocks.0"],
|
||||
crasherRepros: [crasher],
|
||||
sanitizerGate: "pnpm run native:sanitize",
|
||||
},
|
||||
};
|
||||
|
||||
await writeFile(join(outDir, "report.json"), `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log("reliability smoke ok");
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { performance } from "node:perf_hooks";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const root = "conformance/program-graph";
|
||||
const storePath = `${root}/zero.graph`;
|
||||
const target = "linux-musl-x64";
|
||||
const zeroBin = resolve("bin/zero");
|
||||
const execMaxBuffer = 16 * 1024 * 1024;
|
||||
|
||||
const budgets = {
|
||||
statusMs: 2000,
|
||||
verifyProjectionMs: 3000,
|
||||
importMs: 5000,
|
||||
exportMs: 5000,
|
||||
};
|
||||
|
||||
function run(args: string[]) {
|
||||
const start = performance.now();
|
||||
const stdout = execFileSync(zeroBin, args, {
|
||||
encoding: "utf8",
|
||||
maxBuffer: execMaxBuffer,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
return { stdout, elapsedMs: performance.now() - start };
|
||||
}
|
||||
|
||||
function runJson(args: string[]) {
|
||||
const result = run(args);
|
||||
return { ...result, body: JSON.parse(result.stdout) };
|
||||
}
|
||||
|
||||
function assertBudget(name: string, elapsedMs: number, maxMs: number) {
|
||||
assert(
|
||||
elapsedMs <= maxMs,
|
||||
`${name} exceeded repository graph fixture budget: ${elapsedMs.toFixed(1)}ms > ${maxMs}ms`,
|
||||
);
|
||||
}
|
||||
|
||||
const sourceBefore = readFileSync(`${root}/hello.0`, "utf8");
|
||||
const storeBefore = readFileSync(storePath);
|
||||
|
||||
const status = runJson(["status", "--json", "--target", target, root]);
|
||||
assert.equal(status.body.ok, true);
|
||||
assert.equal(status.body.repositoryGraph.storePresent, true);
|
||||
assert.equal(status.body.repositoryGraph.storeValid, true);
|
||||
assert.equal(status.body.repositoryGraph.projectionState, "clean");
|
||||
assert.equal(status.body.repositoryGraph.compilerInput, "repository-graph");
|
||||
assertBudget("status", status.elapsedMs, budgets.statusMs);
|
||||
|
||||
const check = runJson(["check", "--json", "--target", target, root]);
|
||||
assert.equal(check.body.ok, true);
|
||||
assert.equal(check.body.sourceFile, storePath);
|
||||
assert.equal(check.body.graph.artifact, storePath);
|
||||
assert.equal(check.body.graph.canonicalSource, false);
|
||||
assert.equal(check.body.graph.moduleIdentity, "package:program-graph-fixture@0.1.0");
|
||||
|
||||
const verify = runJson(["verify-projection", "--json", "--target", target, root]);
|
||||
assert.equal(verify.body.ok, true);
|
||||
assert.equal(verify.body.writes, false);
|
||||
assert.equal(verify.body.repositoryGraph.projectionState, "clean");
|
||||
assertBudget("verify-projection", verify.elapsedMs, budgets.verifyProjectionMs);
|
||||
|
||||
const fromSource = runJson(["import", "--json", "--target", target, root]);
|
||||
assert.equal(fromSource.body.ok, true);
|
||||
assert.equal(fromSource.body.repositoryGraph.projectionState, "clean");
|
||||
assert.deepEqual(fromSource.body.changedPaths, [storePath]);
|
||||
assert.equal(readFileSync(`${root}/hello.0`, "utf8"), sourceBefore);
|
||||
assert.equal(Buffer.compare(readFileSync(storePath), storeBefore), 0);
|
||||
assertBudget("import", fromSource.elapsedMs, budgets.importMs);
|
||||
|
||||
const fromGraph = runJson(["export", "--json", "--target", target, root]);
|
||||
assert.equal(fromGraph.body.ok, true);
|
||||
assert.equal(fromGraph.body.repositoryGraph.projectionState, "clean");
|
||||
assert.deepEqual(fromGraph.body.changedPaths, []);
|
||||
assert.equal(readFileSync(`${root}/hello.0`, "utf8"), sourceBefore);
|
||||
assert.equal(Buffer.compare(readFileSync(storePath), storeBefore), 0);
|
||||
assertBudget("export", fromGraph.elapsedMs, budgets.exportMs);
|
||||
|
||||
console.log(
|
||||
`repository graph fixture ok (status ${status.elapsedMs.toFixed(1)}ms, verify ${verify.elapsedMs.toFixed(1)}ms, import ${fromSource.elapsedMs.toFixed(1)}ms, export ${fromGraph.elapsedMs.toFixed(1)}ms)`,
|
||||
);
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import assert from "node:assert/strict";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
const root = join("/tmp", `zero-repository-graph-scale-${process.pid}`);
|
||||
const source = join(root, "main.0");
|
||||
const store = join(root, "zero.graph");
|
||||
const zero = resolve("bin/zero");
|
||||
const maxMs = Number(process.env.ZERO_REPOSITORY_GRAPH_SCALE_MAX_MS ?? "15000");
|
||||
|
||||
function json(args: string[], allowFailure = false) {
|
||||
try {
|
||||
const stdout = execFileSync(zero, args, { encoding: "utf8", maxBuffer: 16 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
||||
return { code: 0, body: JSON.parse(stdout) };
|
||||
} catch (error) {
|
||||
if (!allowFailure) throw error;
|
||||
return { code: error.status ?? 1, body: JSON.parse(error.stdout?.toString() ?? "{}") };
|
||||
}
|
||||
}
|
||||
|
||||
function elapsed<T>(fn: () => T) {
|
||||
const started = Date.now();
|
||||
const value = fn();
|
||||
return { value, ms: Date.now() - started };
|
||||
}
|
||||
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
mkdirSync(root, { recursive: true });
|
||||
|
||||
const functions: string[] = [];
|
||||
for (let i = 0; i < 96; i++) {
|
||||
functions.push(`fn value_${i}() -> i32 {\n return ${i}\n}\n`);
|
||||
}
|
||||
writeFileSync(
|
||||
source,
|
||||
`${functions.join("\n")}\npub fn main(world: World) -> Void raises {\n check world.out.write("repository graph scale ok\\n")\n}\n`,
|
||||
);
|
||||
|
||||
const imported = elapsed(() => json(["import", "--format", "text", "--json", source]));
|
||||
assert.equal(imported.value.code, 0);
|
||||
assert.equal(imported.value.body.repositoryGraph.projectionState, "clean");
|
||||
assert(imported.ms < maxMs, `repository graph import took ${imported.ms}ms`);
|
||||
|
||||
const status = elapsed(() => json(["status", "--json", source]));
|
||||
assert.equal(status.value.body.repositoryGraph.storeValid, true);
|
||||
assert.equal(status.value.body.storage.encoding, "single-file-text");
|
||||
assert.equal(status.value.body.storage.interface, "ProgramGraphStore");
|
||||
assert.equal(status.value.body.repositoryGraph.semanticValidity, "shape-valid");
|
||||
assert.equal(status.value.body.repositoryGraph.projectionValidity, "clean");
|
||||
assert.equal(status.value.body.compilerStore.shape, "compiler-oriented-tables");
|
||||
assert.equal(status.value.body.compilerStore.sourceFreeInspection, true);
|
||||
assert.equal(status.value.body.compilerStore.sourceProjectionRequiredForSemanticFacts, false);
|
||||
assert.equal(status.value.body.compilerStore.tables.schema, 1);
|
||||
assert.equal(status.value.body.compilerStore.tables.node, status.value.body.scale.nodes);
|
||||
assert.equal(status.value.body.compilerStore.tables.edge, status.value.body.scale.edges);
|
||||
assert.deepEqual(status.value.body.compilerStore.hashInputs.graphHashExcludes, ["sourcePath", "line", "column", "projectionText"]);
|
||||
assert(status.value.body.scale.nodes >= 300, `expected a large graph, got ${status.value.body.scale.nodes} nodes`);
|
||||
assert(status.value.body.scale.edges >= 200, `expected many graph edges, got ${status.value.body.scale.edges}`);
|
||||
assert(status.ms < maxMs, `repository graph status took ${status.ms}ms`);
|
||||
|
||||
const verify = elapsed(() => json(["verify-projection", "--json", source]));
|
||||
assert.equal(verify.value.code, 0);
|
||||
assert.equal(verify.value.body.ok, true);
|
||||
assert(verify.ms < maxMs, `repository graph verify-projection took ${verify.ms}ms`);
|
||||
|
||||
const originalStore = readFileSync(store, "utf8");
|
||||
assert.match(originalStore, /^compilerStore schemaVersion:1 shape:"compiler-oriented-tables"/m);
|
||||
assert.match(originalStore, /^compilerTables schema:1 package:/m);
|
||||
assert.match(originalStore, /^compilerHashInputs graphHashExcludes:"source-path,line,column,projection-text"/m);
|
||||
|
||||
rmSync(source, { force: true });
|
||||
const sourceFreeStatus = elapsed(() => json(["status", "--json", root]));
|
||||
assert.equal(sourceFreeStatus.value.body.repositoryGraph.storeValid, true);
|
||||
assert.equal(sourceFreeStatus.value.body.repositoryGraph.semanticValidity, "shape-valid");
|
||||
assert.equal(sourceFreeStatus.value.body.repositoryGraph.projectionValidity, "missing");
|
||||
assert.equal(sourceFreeStatus.value.body.compilerStore.sourceFreeInspection, true);
|
||||
assert.equal(sourceFreeStatus.value.body.compilerStore.semanticValidity.ok, true);
|
||||
assert.equal(sourceFreeStatus.value.body.compilerStore.projectionValidity.state, "missing");
|
||||
assert.equal(sourceFreeStatus.value.body.compilerStore.tables.node, status.value.body.scale.nodes);
|
||||
assert(sourceFreeStatus.ms < maxMs, `repository graph source-free status took ${sourceFreeStatus.ms}ms`);
|
||||
|
||||
writeFileSync(store, originalStore.replace("zero-repository-graph v1", "zero-repository-graph v2"));
|
||||
const futureSchema = json(["status", "--json", root], true);
|
||||
assert.notEqual(futureSchema.code, 0);
|
||||
assert.equal(futureSchema.body.diagnostics[0].code, "RGP003");
|
||||
assert.match(futureSchema.body.diagnostics[0].actual, /invalid repository graph store|valid zero.graph/);
|
||||
writeFileSync(store, originalStore);
|
||||
|
||||
console.log(`repository graph scale ok (${status.value.body.scale.nodes} nodes, ${status.value.body.scale.edges} edges)`);
|
||||
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
||||
|
||||
const skippedDirs = new Set([
|
||||
".git",
|
||||
".next",
|
||||
".turbo",
|
||||
".zero",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"target",
|
||||
"zig-cache",
|
||||
"zig-out",
|
||||
]);
|
||||
|
||||
function usage() {
|
||||
console.error("usage: repository-graph-verify-projection [--root <path>] [--target <target>]");
|
||||
}
|
||||
|
||||
let root = process.cwd();
|
||||
let target = "";
|
||||
for (let i = 2; i < process.argv.length; i++) {
|
||||
const arg = process.argv[i];
|
||||
if (arg === "--root") {
|
||||
const value = process.argv[++i];
|
||||
if (!value) {
|
||||
usage();
|
||||
process.exit(2);
|
||||
}
|
||||
root = value;
|
||||
} else if (arg === "--target") {
|
||||
const value = process.argv[++i];
|
||||
if (!value) {
|
||||
usage();
|
||||
process.exit(2);
|
||||
}
|
||||
target = value;
|
||||
} else if (arg === "--help" || arg === "-h") {
|
||||
usage();
|
||||
process.exit(0);
|
||||
} else {
|
||||
usage();
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const nativeZeroBin = resolve(repoRoot, ".zero/bin/zero");
|
||||
const zeroBin = process.env.ZERO_BIN
|
||||
? resolve(repoRoot, process.env.ZERO_BIN)
|
||||
: existsSync(nativeZeroBin) ? nativeZeroBin : resolve(repoRoot, "bin/zero");
|
||||
const absoluteRoot = resolve(root);
|
||||
|
||||
if (!existsSync(zeroBin)) {
|
||||
console.error("repository graph verify-projection requires a built Zero compiler from the repository root");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(absoluteRoot)) {
|
||||
console.error(`repository graph verify-projection root does not exist: ${absoluteRoot}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function findStores(path: string, stores: string[]) {
|
||||
const stat = lstatSync(path);
|
||||
if (stat.isSymbolicLink()) return;
|
||||
if (stat.isFile()) {
|
||||
if (path.endsWith("/zero.graph") || path === "zero.graph") stores.push(path);
|
||||
return;
|
||||
}
|
||||
if (!stat.isDirectory()) return;
|
||||
|
||||
for (const entry of readdirSync(path, { withFileTypes: true })) {
|
||||
if (entry.isDirectory() && skippedDirs.has(entry.name)) continue;
|
||||
const child = join(path, entry.name);
|
||||
if (entry.isSymbolicLink()) continue;
|
||||
if (entry.isFile() && entry.name === "zero.graph") stores.push(child);
|
||||
else if (entry.isDirectory()) findStores(child, stores);
|
||||
}
|
||||
}
|
||||
|
||||
const stores: string[] = [];
|
||||
findStores(absoluteRoot, stores);
|
||||
stores.sort();
|
||||
|
||||
if (stores.length === 0) {
|
||||
console.log("repository graph verify-projection ok (0 stores)");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let failures = 0;
|
||||
for (const store of stores) {
|
||||
const input = sourceInputForStore(store);
|
||||
const display = relative(repoRoot, input) || ".";
|
||||
console.log(`repository graph verify-projection: ${display}`);
|
||||
const args = ["verify-projection"];
|
||||
if (target) args.push("--target", target);
|
||||
args.push(input);
|
||||
try {
|
||||
execFileSync(zeroBin, args, { stdio: "inherit" });
|
||||
} catch {
|
||||
failures++;
|
||||
const targetArgs = target ? ` --target ${target}` : "";
|
||||
console.error(`repository graph verify-projection failed: bin/zero verify-projection${targetArgs} ${display}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
console.error(`repository graph verify-projection failed (${failures}/${stores.length} stores)`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`repository graph verify-projection ok (${stores.length} ${stores.length === 1 ? "store" : "stores"})`);
|
||||
|
||||
function sourceInputForStore(store: string) {
|
||||
const root = dirname(store);
|
||||
if (existsSync(join(root, "zero.toml")) || existsSync(join(root, "zero.json"))) return root;
|
||||
|
||||
const text = readFileSync(store, "utf8");
|
||||
const projections = text.matchAll(/^projection path:"((?:\\.|[^"])*)" text:/gm);
|
||||
for (const projection of projections) {
|
||||
const source = sourceInputForProjection(root, projection[1]);
|
||||
if (source) return source;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function sourceInputForProjection(root: string, projection: string) {
|
||||
let path = "";
|
||||
try {
|
||||
path = JSON.parse(`"${projection}"`);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
if (!path) return "";
|
||||
const source = resolve(root, path);
|
||||
const local = relative(root, source);
|
||||
if (!local || local === ".." || local.startsWith(`..${sep}`) || isAbsolute(local)) return "";
|
||||
if (dirname(source) !== root) return "";
|
||||
return existsSync(source) ? source : "";
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
type RosettaCase = {
|
||||
id: string;
|
||||
slug: string;
|
||||
source: string;
|
||||
expectedStdout: string;
|
||||
expectedStderr: string;
|
||||
};
|
||||
|
||||
type RosettaManifest = {
|
||||
schemaVersion: number;
|
||||
corpus: string;
|
||||
taskCount: number;
|
||||
target: string;
|
||||
cases: RosettaCase[];
|
||||
};
|
||||
|
||||
const manifestPath = "benchmarks/rosetta/manifest.json";
|
||||
const hostTarget = process.platform === "darwin" && process.arch === "arm64" ? "darwin-arm64" : "linux-musl-x64";
|
||||
const target = readFlag("--target") ?? process.env.ZERO_ROSETTA_TARGET ?? hostTarget;
|
||||
const outDir = join(".zero/rosetta", target);
|
||||
const runOutputs = canRunTarget(target);
|
||||
|
||||
function readFlag(name: string) {
|
||||
const index = process.argv.indexOf(name);
|
||||
if (index === -1) return null;
|
||||
const value = process.argv[index + 1];
|
||||
if (!value || value.startsWith("--")) fail(`${name} requires a value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function canRunTarget(targetName: string) {
|
||||
return (targetName === "linux-musl-x64" && process.platform === "linux" && process.arch === "x64") ||
|
||||
(targetName === "darwin-arm64" && process.platform === "darwin" && process.arch === "arm64") ||
|
||||
(targetName === "darwin-x64" && process.platform === "darwin" && (process.arch === "x64" || spawnSync("arch", ["-x86_64", "/usr/bin/true"]).status === 0));
|
||||
}
|
||||
|
||||
function expectedObjectEmissionPaths(targetName: string) {
|
||||
if (targetName === "linux-musl-x64") return new Set(["direct-elf64-exe", "direct-elf64-object"]);
|
||||
if (targetName === "darwin-arm64") return new Set(["direct-macho64-exe", "direct-macho64-object"]);
|
||||
if (targetName === "darwin-x64") return new Set(["direct-macho-x64-exe", "direct-macho-x64-object"]);
|
||||
fail(`unsupported Rosetta target: ${targetName}`);
|
||||
}
|
||||
|
||||
function fail(message: string): never {
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function commandFailure(command: string, args: string[], result: ReturnType<typeof spawnSync>) {
|
||||
let diagnostic = "";
|
||||
if (typeof result.stdout === "string" && result.stdout.length > 0) {
|
||||
try {
|
||||
diagnostic = JSON.parse(result.stdout).diagnostics?.[0]?.message ?? result.stdout;
|
||||
} catch {
|
||||
diagnostic = result.stdout;
|
||||
}
|
||||
}
|
||||
if (!diagnostic && typeof result.stderr === "string") diagnostic = result.stderr;
|
||||
return {
|
||||
command: [command, ...args].join(" "),
|
||||
status: result.status,
|
||||
signal: result.signal,
|
||||
diagnostic: diagnostic.trim(),
|
||||
error: result.error?.message,
|
||||
};
|
||||
}
|
||||
|
||||
function readManifest(): RosettaManifest {
|
||||
if (!existsSync(manifestPath)) fail(`missing Rosetta manifest: ${manifestPath}`);
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as RosettaManifest;
|
||||
if (manifest.schemaVersion !== 1) fail(`unsupported Rosetta manifest schema: ${manifest.schemaVersion}`);
|
||||
if (manifest.corpus !== "zero-rosetta") fail(`unexpected Rosetta corpus: ${manifest.corpus}`);
|
||||
if (typeof manifest.target !== "string" || manifest.target.length === 0) fail("Rosetta manifest target must be a string");
|
||||
if (!Array.isArray(manifest.cases)) fail("Rosetta manifest cases must be an array");
|
||||
if (manifest.taskCount !== manifest.cases.length) fail(`Rosetta manifest taskCount ${manifest.taskCount} does not match ${manifest.cases.length} cases`);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
function validateCases(cases: RosettaCase[]) {
|
||||
const slugs = new Set<string>();
|
||||
const ids = new Set<string>();
|
||||
const bad = [];
|
||||
for (const entry of cases) {
|
||||
if (ids.has(entry.id)) bad.push(`${entry.id}: duplicate id`);
|
||||
ids.add(entry.id);
|
||||
if (slugs.has(entry.slug)) bad.push(`${entry.id}: duplicate slug ${entry.slug}`);
|
||||
slugs.add(entry.slug);
|
||||
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(entry.slug)) bad.push(`${entry.id}: slug is not param-case`);
|
||||
if (entry.source !== `benchmarks/rosetta/${entry.slug}.0`) bad.push(`${entry.id}: source must be benchmarks/rosetta/${entry.slug}.0`);
|
||||
if (dirname(entry.source) !== "benchmarks/rosetta") bad.push(`${entry.id}: source must be a flat .0 file`);
|
||||
if (!existsSync(entry.source)) bad.push(`${entry.id}: missing source ${entry.source}`);
|
||||
if (!existsSync(graphInputForSource(entry.source))) bad.push(`${entry.id}: missing graph sidecar ${graphInputForSource(entry.source)}`);
|
||||
if (typeof entry.expectedStdout !== "string") bad.push(`${entry.id}: expectedStdout must be a string`);
|
||||
if (typeof entry.expectedStderr !== "string") bad.push(`${entry.id}: expectedStderr must be a string`);
|
||||
}
|
||||
if (bad.length > 0) fail(`Rosetta manifest validation failed:\n${bad.slice(0, 40).join("\n")}`);
|
||||
}
|
||||
|
||||
function graphInputForSource(source: string) {
|
||||
return source.endsWith(".0") ? `${source.slice(0, -2)}.graph` : source;
|
||||
}
|
||||
|
||||
function buildCase(entry: RosettaCase) {
|
||||
const out = join(outDir, entry.slug);
|
||||
const input = graphInputForSource(entry.source);
|
||||
const args = ["build", "--json", "--emit", "exe", "--target", target, input, "--out", out];
|
||||
const result = spawnSync("bin/zero", args, { encoding: "utf8", maxBuffer: 20_000_000 });
|
||||
if (result.status !== 0) return { ok: false, out, failure: commandFailure("bin/zero", args, result) };
|
||||
const body = JSON.parse(result.stdout);
|
||||
if (body.generatedCBytes !== 0) {
|
||||
return { ok: false, out, failure: { diagnostic: `expected generatedCBytes=0, got ${body.generatedCBytes}` } };
|
||||
}
|
||||
const emissionPath = body.objectBackend?.objectEmission?.path ?? "<missing>";
|
||||
const expectedPaths = expectedObjectEmissionPaths(target);
|
||||
if (!expectedPaths.has(emissionPath)) {
|
||||
return { ok: false, out, failure: { diagnostic: `expected one of ${[...expectedPaths].join(", ")}, got ${emissionPath}` } };
|
||||
}
|
||||
return { ok: true, out };
|
||||
}
|
||||
|
||||
function runCase(entry: RosettaCase, out: string) {
|
||||
const result = spawnSync(out, [], { encoding: "utf8", timeout: 5000, maxBuffer: 10_000_000 });
|
||||
const ok = result.status === 0 &&
|
||||
result.signal === null &&
|
||||
result.stdout === entry.expectedStdout &&
|
||||
result.stderr === entry.expectedStderr;
|
||||
if (ok) return null;
|
||||
return {
|
||||
id: entry.id,
|
||||
slug: entry.slug,
|
||||
status: result.status,
|
||||
signal: result.signal,
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
expectedStdout: entry.expectedStdout,
|
||||
expectedStderr: entry.expectedStderr,
|
||||
error: result.error?.message,
|
||||
};
|
||||
}
|
||||
|
||||
const manifest = readManifest();
|
||||
validateCases(manifest.cases);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
const buildFailures = [];
|
||||
const runFailures = [];
|
||||
let built = 0;
|
||||
let passed = 0;
|
||||
|
||||
for (const entry of manifest.cases) {
|
||||
const build = buildCase(entry);
|
||||
if (!build.ok) {
|
||||
buildFailures.push({ id: entry.id, slug: entry.slug, source: entry.source, ...build.failure });
|
||||
continue;
|
||||
}
|
||||
built++;
|
||||
if (!runOutputs) continue;
|
||||
const runFailure = runCase(entry, build.out);
|
||||
if (runFailure) runFailures.push(runFailure);
|
||||
else passed++;
|
||||
}
|
||||
|
||||
const summary = {
|
||||
schemaVersion: 1,
|
||||
corpus: manifest.corpus,
|
||||
target,
|
||||
total: manifest.cases.length,
|
||||
built,
|
||||
buildFailures: buildFailures.length,
|
||||
runChecked: runOutputs,
|
||||
passed,
|
||||
runFailures: runFailures.length,
|
||||
failures: [...buildFailures, ...runFailures].slice(0, 20),
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
if (buildFailures.length > 0 || runFailures.length > 0) process.exit(1);
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const node = process.execPath;
|
||||
|
||||
run(node, [
|
||||
resolve(repoRoot, "evals", "node_modules", "typescript", "bin", "tsc"),
|
||||
"-p",
|
||||
resolve(repoRoot, "evals", "tsconfig.json"),
|
||||
]);
|
||||
run(node, [
|
||||
resolve(repoRoot, "evals", "dist", "run.js"),
|
||||
...process.argv.slice(2),
|
||||
]);
|
||||
|
||||
function run(command, args) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: repoRoot,
|
||||
env: process.env,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
console.error(result.error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
if (result.signal) {
|
||||
console.error(`${command} terminated by ${result.signal}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$root"
|
||||
|
||||
out=".zero/sanitizer/zero"
|
||||
mkdir -p "$(dirname "$out")"
|
||||
|
||||
make -C native/zero-c \
|
||||
OUT="$root/$out" \
|
||||
CFLAGS="-std=c11 -Wall -Wextra -Wpedantic -g -O1 -fsanitize=address,undefined"
|
||||
|
||||
"$out" --version >/dev/null
|
||||
"$out" check examples/hello.graph >/dev/null
|
||||
"$out" build --json --target linux-musl-x64 examples/hello.graph --out .zero/sanitizer/hello-linux >/dev/null
|
||||
"$out" check conformance/native/pass/stdlib-target-neutral.graph >/dev/null
|
||||
"$out" run conformance/native/pass/stdlib-target-neutral.graph >/dev/null
|
||||
"$out" inspect --json examples/memory-package >/dev/null
|
||||
"$out" size --json examples/memory-package >/dev/null
|
||||
"$out" size --json examples/std-str.graph >/dev/null
|
||||
"$out" mem --json examples/hello.graph >/dev/null
|
||||
|
||||
# Regression: dependency manifest read errors must keep an owned diagnostic
|
||||
# path after the transient manifest path buffer is freed.
|
||||
if [ "$(id -u)" != "0" ]; then
|
||||
dep_root="$(mktemp -d)"
|
||||
cp -R conformance/packages/dep-app "$dep_root/dep-app"
|
||||
cp -R conformance/packages/dep-lib "$dep_root/dep-lib"
|
||||
rm -rf "$dep_root/dep-app/.zero" "$dep_root/dep-lib/.zero"
|
||||
chmod 000 "$dep_root/dep-lib/zero.toml"
|
||||
dep_out="$("$out" check --json "$dep_root/dep-app" 2>&1 || true)"
|
||||
chmod 644 "$dep_root/dep-lib/zero.toml"
|
||||
rm -rf "$dep_root"
|
||||
case "$dep_out" in
|
||||
*AddressSanitizer*|*runtime\ error:*)
|
||||
echo "sanitizer error in dependency manifest read-error diagnostic path" >&2
|
||||
printf '%s\n' "$dep_out" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
printf '%s' "$dep_out" | grep -q "\"path\": \"$dep_root/dep-lib/zero.toml\"" || {
|
||||
echo "dependency manifest read-error diagnostic did not report the dependency manifest path" >&2
|
||||
printf '%s\n' "$dep_out" >&2
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
Executable
+8690
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,700 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import { existsSync } from "node:fs";
|
||||
import { readdir, readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
type StdHelper = {
|
||||
name: string;
|
||||
module: string;
|
||||
returnType: string;
|
||||
argCount: number;
|
||||
argTypes: Array<string | null>;
|
||||
errorNames: string[];
|
||||
capability: string;
|
||||
targetSupport: string;
|
||||
allocationBehavior: string;
|
||||
emitsRuntimeHelper: boolean;
|
||||
kind: string;
|
||||
};
|
||||
|
||||
type StdSourceModule = {
|
||||
module: string;
|
||||
path: string;
|
||||
graphPath: string;
|
||||
};
|
||||
|
||||
type StdSourceCall = {
|
||||
publicName: string;
|
||||
targetName: string;
|
||||
module: string;
|
||||
};
|
||||
|
||||
type StdSkillSignature = {
|
||||
returnType: string;
|
||||
argTypes: string[];
|
||||
errorNames: string[];
|
||||
};
|
||||
|
||||
type AllocationCategory =
|
||||
| "borrowed-view"
|
||||
| "caller-storage"
|
||||
| "explicit-allocator"
|
||||
| "host-resource"
|
||||
| "no-allocation"
|
||||
| "owned-resource";
|
||||
|
||||
type ReturnViewCategory =
|
||||
| "borrowed-view"
|
||||
| "caller-storage-view"
|
||||
| "explicit-allocator-view"
|
||||
| "static-view";
|
||||
|
||||
type ResourceLifetimeCategory =
|
||||
| "borrowed-resource"
|
||||
| "owned-resource"
|
||||
| "resource-capability"
|
||||
| "resource-value";
|
||||
|
||||
type StreamingBehaviorCategory =
|
||||
| "bounded-input"
|
||||
| "buffer-copy"
|
||||
| "chunked-read"
|
||||
| "line-scan"
|
||||
| "stream-scan";
|
||||
|
||||
const knownCapabilities = new Set([
|
||||
"alloc",
|
||||
"args",
|
||||
"codec",
|
||||
"env",
|
||||
"fs",
|
||||
"memory",
|
||||
"net",
|
||||
"none",
|
||||
"parse",
|
||||
"path",
|
||||
"proc",
|
||||
"rand",
|
||||
"time",
|
||||
]);
|
||||
const hostOnlyCapabilities = new Set(["args", "env", "fs", "proc"]);
|
||||
const runtimeCapabilities = new Set(["memory", "net"]);
|
||||
const targetSupportValues = new Set(["target-neutral", "host", "host-runtime"]);
|
||||
const errorNamePattern = /^[A-Z][A-Za-z0-9]*$/;
|
||||
const helperKindPattern = /^Z_STD_HELPER_KIND_[A-Z_]+$/;
|
||||
const helperNamePattern = /^std\.[a-z][A-Za-z0-9]*\.[A-Za-z][A-Za-z0-9]*$/;
|
||||
const moduleNamePattern = /^std\.[a-z][A-Za-z0-9]*$/;
|
||||
const vagueAllocationBehaviors = new Set(["directory traversal"]);
|
||||
const knownResourceTypes = new Set([
|
||||
"BufferedReader",
|
||||
"BufferedWriter",
|
||||
"ByteBuf",
|
||||
"File",
|
||||
"FixedBufAlloc",
|
||||
"FixedReader",
|
||||
"FixedWriter",
|
||||
"RandSource",
|
||||
"Vec",
|
||||
]);
|
||||
const countedResourceTypes = new Set([
|
||||
"Address",
|
||||
"Alloc",
|
||||
"BufferedReader",
|
||||
"BufferedWriter",
|
||||
"ByteBuf",
|
||||
"File",
|
||||
"FixedBufAlloc",
|
||||
"FixedReader",
|
||||
"FixedWriter",
|
||||
"Fs",
|
||||
"GeneralAlloc",
|
||||
"HttpClient",
|
||||
"HttpError",
|
||||
"HttpHeaderValue",
|
||||
"HttpMethod",
|
||||
"HttpResult",
|
||||
"HttpServer",
|
||||
"JsonDoc",
|
||||
"Net",
|
||||
"NullAlloc",
|
||||
"PageAlloc",
|
||||
"ProcChild",
|
||||
"ProcStatus",
|
||||
"RandSource",
|
||||
"Vec",
|
||||
]);
|
||||
const publicModuleDocsDir = "docs/articles/modules";
|
||||
const skillPath = "skill-data/stdlib.md";
|
||||
const stdSigPath = "native/zero-c/src/std_sig.c";
|
||||
const stdSourcePath = "native/zero-c/src/std_source.c";
|
||||
const embeddedStdlibGraphPath = "native/zero-c/src/embedded_stdlib_graph.inc";
|
||||
const embeddedStdlibGraphDir = "native/zero-c/src/embedded_stdlib_graph";
|
||||
const fixtureRoots = ["examples", "conformance", "benchmarks/rosetta"];
|
||||
const generatedFixtureFiles = ["conformance/run.mjs"];
|
||||
|
||||
function cBlock(text: string, marker: string): string {
|
||||
const start = text.indexOf(marker);
|
||||
if (start < 0) return "";
|
||||
const open = text.indexOf("{", start);
|
||||
if (open < 0) return "";
|
||||
let depth = 0;
|
||||
for (let index = open; index < text.length; index++) {
|
||||
const ch = text[index];
|
||||
if (ch === "{") depth++;
|
||||
else if (ch === "}") {
|
||||
depth--;
|
||||
if (depth === 0) return text.slice(open + 1, index);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function parseCStringArray(raw: string): Array<string | null> {
|
||||
const values: Array<string | null> = [];
|
||||
const tokenPattern = /"((?:\\.|[^"\\])*)"|NULL/g;
|
||||
for (const match of raw.matchAll(tokenPattern)) {
|
||||
values.push(match[0] === "NULL" ? null : match[1].replace(/\\"/g, "\"").replace(/\\\\/g, "\\"));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function duplicateValues(values: string[]): string[] {
|
||||
const seen = new Set<string>();
|
||||
const duplicates = new Set<string>();
|
||||
for (const value of values) {
|
||||
if (seen.has(value)) duplicates.add(value);
|
||||
seen.add(value);
|
||||
}
|
||||
return [...duplicates].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function cIdent(text: string): string {
|
||||
return text.replace(/[^A-Za-z0-9_]/g, "_");
|
||||
}
|
||||
|
||||
function parseEmbeddedStdlibGraphBytes(text: string, graphPath: string): Buffer | null {
|
||||
const ident = `zero_embedded_stdlib_graph_${cIdent(graphPath)}_bytes`;
|
||||
const block = cBlock(text, `static const unsigned char ${ident}[] =`);
|
||||
if (block.length === 0) return null;
|
||||
const bytes = [...block.matchAll(/0x([0-9a-fA-F]{2})/g)].map((match) => Number.parseInt(match[1], 16));
|
||||
return Buffer.from(bytes);
|
||||
}
|
||||
|
||||
function parseEmbeddedStdlibGraphPart(text: string, graphPath: string): string | null {
|
||||
const partName = `${cIdent(graphPath)}.inc`;
|
||||
const pattern = new RegExp(`#include\\s+"embedded_stdlib_graph/${partName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}"`);
|
||||
return pattern.test(text) ? join(embeddedStdlibGraphDir, partName) : null;
|
||||
}
|
||||
|
||||
async function readEmbeddedStdlibGraphBytes(text: string | null, graphPath: string): Promise<Buffer | null> {
|
||||
if (text !== null) {
|
||||
const embeddedGraph = parseEmbeddedStdlibGraphBytes(text, graphPath);
|
||||
if (embeddedGraph !== null) return embeddedGraph;
|
||||
const partPath = parseEmbeddedStdlibGraphPart(text, graphPath);
|
||||
if (partPath !== null && existsSync(partPath)) {
|
||||
const partText = await readFile(partPath, "utf8");
|
||||
return parseEmbeddedStdlibGraphBytes(partText, graphPath);
|
||||
}
|
||||
}
|
||||
return existsSync(graphPath) ? readFile(graphPath) : null;
|
||||
}
|
||||
|
||||
function parseStdHelpers(text: string): StdHelper[] {
|
||||
const block = cBlock(text, "const ZStdHelperInfo z_std_helpers[] =");
|
||||
const helpers: StdHelper[] = [];
|
||||
const rowPattern = /\{\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*(-?\d+)\s*,\s*\{([^}]*)\}\s*,\s*\{([^}]*)\}\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*(true|false)\s*,\s*(Z_STD_HELPER_KIND_[A-Z_]+)\s*\}/g;
|
||||
for (const match of block.matchAll(rowPattern)) {
|
||||
const name = match[1];
|
||||
const parts = name.split(".");
|
||||
helpers.push({
|
||||
name,
|
||||
module: `${parts[0]}.${parts[1]}`,
|
||||
returnType: match[2],
|
||||
argCount: Number(match[3]),
|
||||
argTypes: parseCStringArray(match[4]),
|
||||
errorNames: parseCStringArray(match[5]).filter((value): value is string => value !== null),
|
||||
capability: match[6],
|
||||
targetSupport: match[7],
|
||||
allocationBehavior: match[8],
|
||||
emitsRuntimeHelper: match[9] === "true",
|
||||
kind: match[10],
|
||||
});
|
||||
}
|
||||
return helpers.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
function parseSkillSignatures(text: string): Map<string, StdSkillSignature> {
|
||||
const start = text.indexOf("## Function Signatures");
|
||||
if (start < 0) return new Map();
|
||||
const end = text.indexOf("## Maybe Pattern", start);
|
||||
const catalog = text.slice(start, end < 0 ? undefined : end);
|
||||
const signatures = new Map<string, StdSkillSignature>();
|
||||
const modulePattern = /### (std\.[A-Za-z0-9]+)\s+```text\n([\s\S]*?)\n```/g;
|
||||
for (const moduleMatch of catalog.matchAll(modulePattern)) {
|
||||
const module = moduleMatch[1];
|
||||
for (const rawLine of moduleMatch[2].split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (line.length === 0) continue;
|
||||
const signatureMatch = line.match(/^([A-Za-z][A-Za-z0-9]*)\((.*)\) -> (.+)$/);
|
||||
if (!signatureMatch) continue;
|
||||
const argText = signatureMatch[2].trim();
|
||||
const argTypes = argText.length === 0
|
||||
? []
|
||||
: splitTopLevelCommaList(argText).map((arg) => {
|
||||
const colon = arg.indexOf(":");
|
||||
return (colon >= 0 ? arg.slice(colon + 1) : arg).trim();
|
||||
});
|
||||
const returnAndErrors = signatureMatch[3].trim();
|
||||
const errorMatch = returnAndErrors.match(/\s+raises\s+\[([^\]]*)\]\s*$/);
|
||||
const errorNames = errorMatch
|
||||
? errorMatch[1].split(/\s*,\s*/).map((name) => name.trim()).filter((name) => name.length > 0)
|
||||
: [];
|
||||
const returnType = returnAndErrors.replace(/\s+raises\s+\[[^\]]*\]\s*$/, "").trim();
|
||||
signatures.set(`${module}.${signatureMatch[1]}`, { returnType, argTypes, errorNames });
|
||||
}
|
||||
}
|
||||
return signatures;
|
||||
}
|
||||
|
||||
function splitTopLevelCommaList(text: string): string[] {
|
||||
const parts: string[] = [];
|
||||
let depth = 0;
|
||||
let start = 0;
|
||||
for (let index = 0; index < text.length; index++) {
|
||||
const ch = text[index];
|
||||
if (ch === "<" || ch === "(" || ch === "[") {
|
||||
depth++;
|
||||
} else if (ch === ">" || ch === ")" || ch === "]") {
|
||||
if (depth > 0) depth--;
|
||||
} else if (ch === "," && depth === 0) {
|
||||
const part = text.slice(start, index).trim();
|
||||
if (part.length > 0) parts.push(part);
|
||||
start = index + 1;
|
||||
}
|
||||
}
|
||||
const tail = text.slice(start).trim();
|
||||
if (tail.length > 0) parts.push(tail);
|
||||
return parts;
|
||||
}
|
||||
|
||||
function parseStdSourceModules(text: string): StdSourceModule[] {
|
||||
const block = cBlock(text, "static const ZStdSourceModule std_source_modules[] =");
|
||||
return [...block.matchAll(/\{\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*zero_embedded_stdlib_graph_[A-Za-z0-9_]+_bytes\s*,\s*sizeof\s*\(\s*zero_embedded_stdlib_graph_[A-Za-z0-9_]+_bytes\s*\)\s*\}/g)]
|
||||
.map((match) => ({ module: match[1], path: match[2], graphPath: match[2].replace(/\.0$/, ".graph") }))
|
||||
.sort((a, b) => a.module.localeCompare(b.module));
|
||||
}
|
||||
|
||||
function parseStdSourceCalls(text: string): StdSourceCall[] {
|
||||
const block = cBlock(text, "static const ZStdSourceCall std_source_calls[] =");
|
||||
return [...block.matchAll(/\{\s*"([^"]+)"\s*,\s*"([^"]+)"\s*,\s*"([^"]+)"\s*\}/g)]
|
||||
.map((match) => ({ publicName: match[1], targetName: match[2], module: match[3] }))
|
||||
.sort((a, b) => a.publicName.localeCompare(b.publicName));
|
||||
}
|
||||
|
||||
async function readOptional(path: string): Promise<string | null> {
|
||||
if (!existsSync(path)) return null;
|
||||
return readFile(path, "utf8");
|
||||
}
|
||||
|
||||
async function sourceFilesUnder(root: string): Promise<string[]> {
|
||||
if (!existsSync(root)) return [];
|
||||
const entries = await readdir(root, { withFileTypes: true });
|
||||
const groups = await Promise.all(entries.map(async (entry) => {
|
||||
const path = join(root, entry.name);
|
||||
if (entry.isDirectory()) return sourceFilesUnder(path);
|
||||
if (entry.isFile() && entry.name.endsWith(".0")) return [path];
|
||||
return [];
|
||||
}));
|
||||
return groups.flat();
|
||||
}
|
||||
|
||||
function pushIf(condition: boolean, failures: string[], message: string) {
|
||||
if (condition) failures.push(message);
|
||||
}
|
||||
|
||||
function incrementCount(map: Map<string, number>, key: string) {
|
||||
map.set(key, (map.get(key) ?? 0) + 1);
|
||||
}
|
||||
|
||||
function sortedObjectFromCounts(counts: Map<string, number>): Record<string, number> {
|
||||
return Object.fromEntries([...counts.entries()].sort((a, b) => a[0].localeCompare(b[0])));
|
||||
}
|
||||
|
||||
function allocationCategory(helper: StdHelper): AllocationCategory | null {
|
||||
const behavior = helper.allocationBehavior.toLowerCase();
|
||||
if (
|
||||
behavior.includes("no allocation") ||
|
||||
behavior.includes("never allocates") ||
|
||||
behavior.includes("bounds-checked") ||
|
||||
behavior.includes("checks ") ||
|
||||
behavior.includes("counts ") ||
|
||||
behavior.includes("decodes a bounded") ||
|
||||
behavior.includes("deterministic test source") ||
|
||||
behavior.includes("inspects ") ||
|
||||
behavior.includes("little-endian byte read") ||
|
||||
behavior.includes("little-endian byte write primitive") ||
|
||||
behavior.includes("matches ") ||
|
||||
behavior.includes("reads body start offset") ||
|
||||
behavior.includes("reads header-value") ||
|
||||
behavior.includes("reads http status metadata") ||
|
||||
behavior.includes("reads response") ||
|
||||
behavior.includes("reads transport error metadata") ||
|
||||
behavior.includes("returns structured validation status") ||
|
||||
behavior.includes("structured validation status code") ||
|
||||
behavior.includes("streaming token count") ||
|
||||
behavior.includes("typed decode boundary metadata")
|
||||
) return "no-allocation";
|
||||
if (
|
||||
behavior.includes("caller") ||
|
||||
behavior.includes("buffer") ||
|
||||
behavior.includes("storage") ||
|
||||
behavior.includes("writes ") ||
|
||||
behavior.includes("fills ") ||
|
||||
behavior.includes("sorts ")
|
||||
) return "caller-storage";
|
||||
if (
|
||||
behavior.includes("borrows") ||
|
||||
behavior.includes("borrowed") ||
|
||||
behavior.includes("raw query parameter") ||
|
||||
behavior.includes("raw top-level") ||
|
||||
behavior.includes("field value slice")
|
||||
) return "borrowed-view";
|
||||
if (behavior.includes("owned") || behavior.includes("handle") || behavior.includes("closes ")) return "owned-resource";
|
||||
if (behavior.includes("alloc") || behavior.includes("allocator") || behavior.includes("arena")) return "explicit-allocator";
|
||||
if (
|
||||
behavior.includes("directory") ||
|
||||
behavior.includes("entropy") ||
|
||||
behavior.includes("file") ||
|
||||
behavior.includes("http listener") ||
|
||||
behavior.includes("network") ||
|
||||
behavior.includes("process") ||
|
||||
behavior.includes("tls boundary")
|
||||
) return "host-resource";
|
||||
return null;
|
||||
}
|
||||
|
||||
function resourceWrapperUses(type: string): Array<{ wrapper: string; typeName: string }> {
|
||||
return [...type.matchAll(/\b(owned|mutref|ref)<([A-Za-z][A-Za-z0-9]*)>/g)]
|
||||
.map((match) => ({ wrapper: match[1], typeName: match[2] }));
|
||||
}
|
||||
|
||||
function resourceTypeUses(type: string): string[] {
|
||||
const wrappers = resourceWrapperUses(type).map((use) => use.typeName);
|
||||
const exact = type.match(/^[A-Za-z][A-Za-z0-9]*$/) && countedResourceTypes.has(type) ? [type] : [];
|
||||
return [...wrappers, ...exact];
|
||||
}
|
||||
|
||||
function unwrapMaybe(type: string): string {
|
||||
const match = type.match(/^Maybe<(.+)>$/);
|
||||
return match ? match[1] : type;
|
||||
}
|
||||
|
||||
function returnsBorrowedViewType(type: string): boolean {
|
||||
const unwrapped = unwrapMaybe(type);
|
||||
return unwrapped === "String" || unwrapped.includes("Span<");
|
||||
}
|
||||
|
||||
function hasMutableSpanArg(helper: StdHelper): boolean {
|
||||
return helper.argTypes.some((type) => type?.startsWith("MutSpan<"));
|
||||
}
|
||||
|
||||
function describesStaticView(behavior: string): boolean {
|
||||
return behavior.includes("boundary metadata") ||
|
||||
behavior.includes("status text") ||
|
||||
behavior.includes("tls boundary") ||
|
||||
behavior.includes("borrows static") ||
|
||||
(behavior.includes("static") && !behavior.includes("borrows/static"));
|
||||
}
|
||||
|
||||
function returnViewCategory(helper: StdHelper, category: AllocationCategory | null): ReturnViewCategory | null {
|
||||
if (!returnsBorrowedViewType(helper.returnType)) return null;
|
||||
const behavior = helper.allocationBehavior.toLowerCase();
|
||||
if (describesStaticView(behavior)) return "static-view";
|
||||
if (behavior.includes("borrows")) return "borrowed-view";
|
||||
if (helper.returnType.includes("MutSpan<") && category === "explicit-allocator") return "explicit-allocator-view";
|
||||
if (category === "caller-storage" && hasMutableSpanArg(helper)) return "caller-storage-view";
|
||||
if (category === "explicit-allocator") return "explicit-allocator-view";
|
||||
if (category === "borrowed-view") return "borrowed-view";
|
||||
return null;
|
||||
}
|
||||
|
||||
function resourceLifetimeCategories(helper: StdHelper): ResourceLifetimeCategory[] {
|
||||
const categories = new Set<ResourceLifetimeCategory>();
|
||||
if (helper.returnType.includes("owned<")) categories.add("owned-resource");
|
||||
if (helperTypes(helper).some((type) => resourceWrapperUses(type).some((use) => use.wrapper === "ref" || use.wrapper === "mutref"))) {
|
||||
categories.add("borrowed-resource");
|
||||
}
|
||||
const unwrappedReturn = unwrapMaybe(helper.returnType);
|
||||
if (countedResourceTypes.has(unwrappedReturn) && !helper.returnType.includes("<")) categories.add("resource-value");
|
||||
if (helper.argTypes.some((type) => type !== null && countedResourceTypes.has(type))) categories.add("resource-capability");
|
||||
return [...categories].sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function streamingBehaviorCategory(helper: StdHelper): StreamingBehaviorCategory | null {
|
||||
const behavior = helper.allocationBehavior.toLowerCase();
|
||||
if (helper.name.endsWith("streamTokens") || helper.name.endsWith("streamTokensBytes") || helper.name.endsWith("readUntilDelimiter") || helper.name.endsWith("readUntilDelimiterStart") || behavior.includes("streaming")) return "stream-scan";
|
||||
if (helper.name.endsWith("readBytesAt") || behavior.includes("chunked")) return "chunked-read";
|
||||
if (helper.name.includes("Within") || behavior.includes("within size limit")) return "bounded-input";
|
||||
if (helper.name.endsWith("nextLine") || helper.name.endsWith("nextLineStart") || helper.name.endsWith("readLine") || helper.name.endsWith("readLineStart") || helper.name.endsWith("countLines")) return "line-scan";
|
||||
if (helper.name.endsWith(".copy") || helper.name.endsWith("copyN") || helper.name.endsWith("copyBuffer") || behavior.includes("copies through caller buffer")) return "buffer-copy";
|
||||
return null;
|
||||
}
|
||||
|
||||
function capabilityTargetKey(helper: StdHelper): string {
|
||||
return `${helper.targetSupport}:${helper.capability}`;
|
||||
}
|
||||
|
||||
function helperTypes(helper: StdHelper): string[] {
|
||||
return [
|
||||
helper.returnType,
|
||||
...helper.argTypes.filter((type): type is string => type !== null),
|
||||
];
|
||||
}
|
||||
|
||||
const [stdSig, stdSource, embeddedStdlibGraph, skill] = await Promise.all([
|
||||
readFile(stdSigPath, "utf8"),
|
||||
readFile(stdSourcePath, "utf8"),
|
||||
readOptional(embeddedStdlibGraphPath),
|
||||
readFile(skillPath, "utf8"),
|
||||
]);
|
||||
|
||||
const stdEntries = await readdir("std", { withFileTypes: true });
|
||||
const stdProjectionFiles = stdEntries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".0"))
|
||||
.map((entry) => `std/${entry.name}`)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const stdGraphFiles = stdEntries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".graph"))
|
||||
.map((entry) => `std/${entry.name}`)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const helpers = parseStdHelpers(stdSig);
|
||||
const skillSignatures = parseSkillSignatures(skill);
|
||||
const modules = [...new Set(helpers.map((helper) => helper.module))].sort((a, b) => a.localeCompare(b));
|
||||
const sourceModules = parseStdSourceModules(stdSource);
|
||||
const sourceCalls = parseStdSourceCalls(stdSource);
|
||||
const fixtureFiles = [
|
||||
...(await Promise.all(fixtureRoots.map((root) => sourceFilesUnder(root)))).flat(),
|
||||
...generatedFixtureFiles.filter((path) => existsSync(path)),
|
||||
]
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const fixtureTexts = await Promise.all(fixtureFiles.map(async (path) => ({
|
||||
path,
|
||||
text: await readFile(path, "utf8"),
|
||||
})));
|
||||
const helpersByName = new Map(helpers.map((helper) => [helper.name, helper]));
|
||||
const sourceModulesByName = new Map(sourceModules.map((module) => [module.module, module]));
|
||||
const sourceModulePaths = new Set(sourceModules.map((module) => module.path));
|
||||
const sourceModuleGraphPaths = new Set(sourceModules.map((module) => module.graphPath));
|
||||
const sourceCallsByPublicName = new Map(sourceCalls.map((call) => [call.publicName, call]));
|
||||
const graphImplementedHelperModules = new Set(sourceCalls.map((call) => call.module));
|
||||
const partiallyGraphBackedModules = new Set(["std.args", "std.cli", "std.codec", "std.crypto", "std.env", "std.fmt", "std.fs", "std.http", "std.io", "std.json", "std.math", "std.mem", "std.net", "std.parse", "std.path", "std.proc", "std.rand", "std.search", "std.str", "std.term", "std.time", "std.toml"]);
|
||||
const docsEntries = await readdir(publicModuleDocsDir, { withFileTypes: true });
|
||||
const publicModuleDocs = new Set(
|
||||
docsEntries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
||||
.map((entry) => `std.${entry.name.replace(/\.md$/, "")}`),
|
||||
);
|
||||
const docsByModule = new Map<string, string>();
|
||||
await Promise.all(modules.map(async (module) => {
|
||||
const path = `${publicModuleDocsDir}/${module.slice("std.".length)}.md`;
|
||||
const content = await readOptional(path);
|
||||
if (content !== null) docsByModule.set(module, content);
|
||||
}));
|
||||
|
||||
const failures: string[] = [];
|
||||
const allocationCategoryCounts = new Map<string, number>();
|
||||
const capabilityCounts = new Map<string, number>();
|
||||
const capabilityTargetCounts = new Map<string, number>();
|
||||
const namedErrorCounts = new Map<string, number>();
|
||||
const resourceTypeCounts = new Map<string, number>();
|
||||
const resourceLifetimeCounts = new Map<string, number>();
|
||||
const returnViewCounts = new Map<string, number>();
|
||||
const targetSupportCounts = new Map<string, number>();
|
||||
const streamingBehaviorCounts = new Map<string, number>();
|
||||
|
||||
pushIf(stdSource.includes("embedded_stdlib.inc"), failures, "std_source.c must not include embedded stdlib source chunks");
|
||||
pushIf(/zero_embedded_stdlib_[A-Za-z0-9_]+_chunks/.test(stdSource), failures, "std_source.c must not reference embedded stdlib source chunks");
|
||||
pushIf(/z_std_source_module_copy_source/.test(stdSource), failures, "std_source.c must not expose source-copy stdlib bridges");
|
||||
|
||||
pushIf(helpers.length === 0, failures, "no stdlib helpers were parsed from std_sig.c");
|
||||
for (const duplicate of duplicateValues(helpers.map((helper) => helper.name))) {
|
||||
failures.push(`duplicate stdlib helper contract: ${duplicate}`);
|
||||
}
|
||||
for (const helper of helpers) {
|
||||
pushIf(!helperNamePattern.test(helper.name), failures, `${helper.name}: helper name must be std.<module>.<name>`);
|
||||
pushIf(!moduleNamePattern.test(helper.module), failures, `${helper.name}: helper module must be std.<module>`);
|
||||
pushIf(helper.returnType.length === 0, failures, `${helper.name}: return type is empty`);
|
||||
pushIf(helper.argCount < 0 || helper.argCount > 5, failures, `${helper.name}: arg count ${helper.argCount} exceeds contract bounds`);
|
||||
pushIf(helper.argTypes.length > 5, failures, `${helper.name}: arg type list exceeds contract bounds`);
|
||||
for (let index = 0; index < helper.argCount; index++) {
|
||||
pushIf(helper.argTypes[index] === null || helper.argTypes[index] === undefined, failures, `${helper.name}: arg ${index + 1} is missing from std_sig.c`);
|
||||
}
|
||||
pushIf(helper.errorNames.length > 4, failures, `${helper.name}: error list exceeds contract bounds`);
|
||||
for (const duplicate of duplicateValues(helper.errorNames)) {
|
||||
failures.push(`${helper.name}: duplicate named error '${duplicate}'`);
|
||||
}
|
||||
for (const errorName of helper.errorNames) {
|
||||
pushIf(!errorNamePattern.test(errorName), failures, `${helper.name}: invalid named error '${errorName}'`);
|
||||
incrementCount(namedErrorCounts, errorName);
|
||||
}
|
||||
pushIf(helper.name.endsWith("OrRaise") && helper.errorNames.length === 0, failures, `${helper.name}: OrRaise helper must declare named errors`);
|
||||
pushIf(helper.errorNames.length > 0 && helper.returnType.startsWith("Maybe<"), failures, `${helper.name}: named-error helpers must not also use Maybe return failure`);
|
||||
pushIf(!targetSupportValues.has(helper.targetSupport), failures, `${helper.name}: unknown target support '${helper.targetSupport}'`);
|
||||
if (targetSupportValues.has(helper.targetSupport)) incrementCount(targetSupportCounts, helper.targetSupport);
|
||||
pushIf(helper.capability.length === 0, failures, `${helper.name}: capability is empty`);
|
||||
pushIf(helper.capability.length > 0 && !knownCapabilities.has(helper.capability), failures, `${helper.name}: unknown capability '${helper.capability}'`);
|
||||
if (helper.capability.length > 0 && knownCapabilities.has(helper.capability)) incrementCount(capabilityCounts, helper.capability);
|
||||
if (targetSupportValues.has(helper.targetSupport) && helper.capability.length > 0 && knownCapabilities.has(helper.capability)) {
|
||||
incrementCount(capabilityTargetCounts, capabilityTargetKey(helper));
|
||||
}
|
||||
pushIf(helper.allocationBehavior.length === 0, failures, `${helper.name}: allocation behavior is empty`);
|
||||
pushIf(vagueAllocationBehaviors.has(helper.allocationBehavior), failures, `${helper.name}: allocation behavior '${helper.allocationBehavior}' is too vague`);
|
||||
const category = allocationCategory(helper);
|
||||
pushIf(category === null, failures, `${helper.name}: allocation behavior '${helper.allocationBehavior}' does not fit a known ownership category`);
|
||||
if (category !== null) incrementCount(allocationCategoryCounts, category);
|
||||
pushIf(hostOnlyCapabilities.has(helper.capability) && helper.targetSupport !== "host", failures, `${helper.name}: capability '${helper.capability}' must use host target support`);
|
||||
pushIf(helper.targetSupport === "host" && helper.capability === "none", failures, `${helper.name}: host helper must declare its host capability`);
|
||||
pushIf(helper.targetSupport === "host-runtime" && !runtimeCapabilities.has(helper.capability), failures, `${helper.name}: host-runtime helper must use a runtime-facing capability`);
|
||||
pushIf(helper.targetSupport === "target-neutral" && category === "host-resource", failures, `${helper.name}: target-neutral helper must not depend on host resources`);
|
||||
const viewCategory = returnViewCategory(helper, category);
|
||||
pushIf(returnsBorrowedViewType(helper.returnType) && viewCategory === null, failures, `${helper.name}: return type ${helper.returnType} needs explicit return-view lifetime metadata`);
|
||||
if (viewCategory !== null) incrementCount(returnViewCounts, viewCategory);
|
||||
for (const lifetimeCategory of resourceLifetimeCategories(helper)) {
|
||||
incrementCount(resourceLifetimeCounts, lifetimeCategory);
|
||||
}
|
||||
const streamingCategory = streamingBehaviorCategory(helper);
|
||||
if (streamingCategory !== null) incrementCount(streamingBehaviorCounts, streamingCategory);
|
||||
pushIf(helper.name.includes("Within") && streamingCategory !== "bounded-input", failures, `${helper.name}: bounded helper must declare bounded-input streaming behavior`);
|
||||
pushIf(helper.name.endsWith("readBytesAt") && streamingCategory !== "chunked-read", failures, `${helper.name}: offset read helper must declare chunked-read behavior`);
|
||||
pushIf(helper.name.endsWith("streamTokens") && streamingCategory !== "stream-scan", failures, `${helper.name}: stream helper must declare stream-scan behavior`);
|
||||
for (const type of helperTypes(helper)) {
|
||||
for (const use of resourceWrapperUses(type)) {
|
||||
pushIf(!knownResourceTypes.has(use.typeName), failures, `${helper.name}: ${use.wrapper}<${use.typeName}> uses an unknown resource type`);
|
||||
}
|
||||
for (const typeName of resourceTypeUses(type)) incrementCount(resourceTypeCounts, typeName);
|
||||
}
|
||||
if (helper.returnType.includes("owned<")) {
|
||||
pushIf(category !== "owned-resource" && category !== "explicit-allocator", failures, `${helper.name}: owned return must declare owned-resource or explicit-allocator allocation behavior`);
|
||||
}
|
||||
pushIf(!helperKindPattern.test(helper.kind), failures, `${helper.name}: helper kind '${helper.kind}' is not a std helper kind`);
|
||||
}
|
||||
|
||||
for (const module of modules) {
|
||||
const docsPath = `${publicModuleDocsDir}/${module.slice("std.".length)}.md`;
|
||||
pushIf(!publicModuleDocs.has(module), failures, `${module}: missing public module docs at ${docsPath}`);
|
||||
pushIf(!skill.includes(module), failures, `${module}: skill-data/stdlib.md does not mention module`);
|
||||
}
|
||||
|
||||
for (const helper of helpers) {
|
||||
const signature = skillSignatures.get(helper.name);
|
||||
pushIf(!signature, failures, `${helper.name}: skill-data/stdlib.md is missing a Function Signatures row`);
|
||||
if (signature) {
|
||||
pushIf(signature.returnType !== helper.returnType, failures, `${helper.name}: skill return type ${signature.returnType} does not match std_sig.c ${helper.returnType}`);
|
||||
pushIf(signature.argTypes.length !== helper.argCount, failures, `${helper.name}: skill arg count ${signature.argTypes.length} does not match std_sig.c ${helper.argCount}`);
|
||||
pushIf(signature.errorNames.join(",") !== helper.errorNames.join(","), failures, `${helper.name}: skill named errors [${signature.errorNames.join(", ")}] do not match std_sig.c [${helper.errorNames.join(", ")}]`);
|
||||
for (let index = 0; index < helper.argCount && index < signature.argTypes.length; index++) {
|
||||
const expected = helper.argTypes[index];
|
||||
if (expected !== null && expected !== undefined) {
|
||||
pushIf(signature.argTypes[index] !== expected, failures, `${helper.name}: skill arg ${index + 1} type ${signature.argTypes[index]} does not match std_sig.c ${expected}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const docs = docsByModule.get(helper.module);
|
||||
if (docs) {
|
||||
pushIf(!docs.includes(helper.name), failures, `${helper.name}: public module docs do not mention helper`);
|
||||
}
|
||||
pushIf(!fixtureTexts.some((fixture) => fixture.text.includes(helper.name)), failures, `${helper.name}: no example, conformance fixture, or Rosetta task exercises helper`);
|
||||
}
|
||||
for (const name of [...skillSignatures.keys()].sort((a, b) => a.localeCompare(b))) {
|
||||
pushIf(!helpersByName.has(name), failures, `${name}: skill-data/stdlib.md Function Signatures row has no std_sig.c helper`);
|
||||
}
|
||||
|
||||
for (const projectionPath of stdProjectionFiles) {
|
||||
const graphPath = projectionPath.replace(/\.0$/, ".graph");
|
||||
pushIf(!stdGraphFiles.includes(graphPath), failures, `${projectionPath}: missing sibling graph store ${graphPath}`);
|
||||
pushIf(!sourceModulePaths.has(projectionPath), failures, `${projectionPath}: std projection is not registered as an embedded graph-backed module`);
|
||||
}
|
||||
for (const graphPath of stdGraphFiles) {
|
||||
const projectionPath = graphPath.replace(/\.graph$/, ".0");
|
||||
pushIf(!stdProjectionFiles.includes(projectionPath), failures, `${graphPath}: graph store has no sibling projection ${projectionPath}`);
|
||||
pushIf(!sourceModuleGraphPaths.has(graphPath), failures, `${graphPath}: std graph store is not registered for embedding`);
|
||||
}
|
||||
|
||||
for (const sourceModule of sourceModules) {
|
||||
pushIf(!moduleNamePattern.test(sourceModule.module), failures, `${sourceModule.module}: invalid graph-backed std module name`);
|
||||
pushIf(!existsSync(sourceModule.path), failures, `${sourceModule.module}: missing std projection ${sourceModule.path}`);
|
||||
pushIf(!modules.includes(sourceModule.module), failures, `${sourceModule.module}: graph-backed module has no public helpers`);
|
||||
const embeddedGraph = await readEmbeddedStdlibGraphBytes(embeddedStdlibGraph, sourceModule.graphPath);
|
||||
const graph = existsSync(sourceModule.graphPath) ? await readFile(sourceModule.graphPath) : null;
|
||||
pushIf(!existsSync(sourceModule.graphPath), failures, `${sourceModule.module}: missing graph-backed std module ${sourceModule.graphPath}`);
|
||||
pushIf(embeddedGraph === null, failures, `${sourceModule.module}: embedded stdlib graph is missing for ${sourceModule.graphPath}`);
|
||||
pushIf(graph !== null && graph.subarray(0, 8).toString("latin1") !== "ZRGBIN1\0", failures, `${sourceModule.module}: std graph store must be binary ${sourceModule.graphPath}`);
|
||||
pushIf(graph !== null && embeddedGraph !== null && Buffer.compare(embeddedGraph, graph) !== 0, failures, `${sourceModule.module}: embedded stdlib graph is stale for ${sourceModule.graphPath}`);
|
||||
}
|
||||
for (const duplicate of duplicateValues(sourceCalls.map((call) => call.publicName))) {
|
||||
failures.push(`duplicate graph-backed public helper mapping: ${duplicate}`);
|
||||
}
|
||||
for (const duplicate of duplicateValues(sourceCalls.map((call) => call.targetName))) {
|
||||
failures.push(`duplicate graph-backed target helper mapping: ${duplicate}`);
|
||||
}
|
||||
for (const call of sourceCalls) {
|
||||
const helper = helpersByName.get(call.publicName);
|
||||
const sourceModule = sourceModulesByName.get(call.module);
|
||||
pushIf(!helper, failures, `${call.publicName}: graph-backed mapping has no std_sig contract`);
|
||||
pushIf(!sourceModule, failures, `${call.publicName}: graph-backed mapping references unknown module ${call.module}`);
|
||||
pushIf(helper !== undefined && helper.module !== call.module, failures, `${call.publicName}: graph-backed mapping module ${call.module} does not match helper module ${helper?.module}`);
|
||||
pushIf(helper !== undefined && !helper.emitsRuntimeHelper, failures, `${call.publicName}: graph-backed helper must emit runtime helper code`);
|
||||
if (sourceModule && existsSync(sourceModule.path)) {
|
||||
const source = await readFile(sourceModule.path, "utf8");
|
||||
const targetPattern = new RegExp(`\\bfn\\s+${call.targetName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:<[^>]+>)?\\s*\\(`);
|
||||
pushIf(!targetPattern.test(source), failures, `${call.publicName}: target function ${call.targetName} is missing from ${sourceModule.path}`);
|
||||
}
|
||||
}
|
||||
for (const sourceModule of sourceModules) {
|
||||
if (!graphImplementedHelperModules.has(sourceModule.module)) continue;
|
||||
if (partiallyGraphBackedModules.has(sourceModule.module)) continue;
|
||||
for (const helper of helpers.filter((candidate) => candidate.module === sourceModule.module)) {
|
||||
pushIf(!sourceCallsByPublicName.has(helper.name), failures, `${helper.name}: graph-backed module helper is missing embedded std graph mapping`);
|
||||
}
|
||||
}
|
||||
const graphStorageComplete = stdProjectionFiles.length === sourceModules.length &&
|
||||
stdGraphFiles.length === sourceModules.length &&
|
||||
stdProjectionFiles.every((projectionPath) => sourceModulePaths.has(projectionPath)) &&
|
||||
stdGraphFiles.every((graphPath) => sourceModuleGraphPaths.has(graphPath));
|
||||
const summary = {
|
||||
schema: 1,
|
||||
helperCount: helpers.length,
|
||||
moduleCount: modules.length,
|
||||
modules,
|
||||
stdProjectionModuleCount: stdProjectionFiles.length,
|
||||
embeddedStdModuleCount: sourceModules.length,
|
||||
graphStoredModuleCount: sourceModules.length,
|
||||
graphStorageComplete,
|
||||
graphImplementedHelperModuleCount: graphImplementedHelperModules.size,
|
||||
graphBackedHelperCount: sourceCalls.length,
|
||||
nativeOrTableHelperCount: helpers.length - sourceCalls.length,
|
||||
allocationCategoryCounts: sortedObjectFromCounts(allocationCategoryCounts),
|
||||
capabilityCounts: sortedObjectFromCounts(capabilityCounts),
|
||||
capabilityTargetCounts: sortedObjectFromCounts(capabilityTargetCounts),
|
||||
namedErrorCounts: sortedObjectFromCounts(namedErrorCounts),
|
||||
returnViewCounts: sortedObjectFromCounts(returnViewCounts),
|
||||
resourceLifetimeCounts: sortedObjectFromCounts(resourceLifetimeCounts),
|
||||
resourceTypeCounts: sortedObjectFromCounts(resourceTypeCounts),
|
||||
streamingBehaviorCounts: sortedObjectFromCounts(streamingBehaviorCounts),
|
||||
targetSupportCounts: sortedObjectFromCounts(targetSupportCounts),
|
||||
fixtureFileCount: fixtureFiles.length,
|
||||
ok: failures.length === 0,
|
||||
failures,
|
||||
};
|
||||
|
||||
if (process.argv.includes("--json")) {
|
||||
console.log(JSON.stringify(summary, null, 2));
|
||||
} else if (failures.length === 0) {
|
||||
console.log(`stdlib contracts ok (${summary.helperCount} helpers, ${summary.graphStoredModuleCount}/${summary.stdProjectionModuleCount} std modules graph-stored, ${summary.graphBackedHelperCount} graph-backed helper mappings)`);
|
||||
} else {
|
||||
console.error(`stdlib contracts failed (${failures.length} issue${failures.length === 1 ? "" : "s"})`);
|
||||
for (const failure of failures.slice(0, 40)) console.error(`- ${failure}`);
|
||||
if (failures.length > 40) console.error(`- ... ${failures.length - 40} more`);
|
||||
}
|
||||
|
||||
if (failures.length > 0) process.exitCode = 1;
|
||||
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const zero = "bin/zero";
|
||||
const outDir = ".zero/stdlib-target-matrix";
|
||||
const fixtures = [
|
||||
"conformance/native/pass/stdlib-target-neutral.graph",
|
||||
"conformance/native/pass/direct-checksum-helpers.graph",
|
||||
];
|
||||
const artifactBudgetBytes = 120_000;
|
||||
const targets = [
|
||||
"darwin-arm64",
|
||||
"darwin-x64",
|
||||
"linux-arm64",
|
||||
"linux-musl-arm64",
|
||||
"linux-musl-x64",
|
||||
"linux-x64",
|
||||
"win32-arm64.exe",
|
||||
"win32-x64.exe",
|
||||
];
|
||||
const representativeStdlibTargets = [
|
||||
"darwin-arm64",
|
||||
"linux-musl-x64",
|
||||
"win32-x64.exe",
|
||||
];
|
||||
const matrixScope = process.env.ZERO_STDLIB_TARGET_MATRIX_SCOPE === "fast" ? "fast" : "deep";
|
||||
|
||||
type MatrixRow = {
|
||||
fixture: string;
|
||||
target: string;
|
||||
ok: boolean;
|
||||
artifactPath: string;
|
||||
artifactBytes: number;
|
||||
generatedCBytes: number;
|
||||
directObjectEmitter?: string;
|
||||
diagnostic?: string;
|
||||
};
|
||||
|
||||
function parsePositiveInt(value: string | undefined, fallback: number) {
|
||||
const parsed = Number.parseInt(value ?? "", 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
async function mapLimit<T, R>(items: T[], limit: number, callback: (item: T, index: number) => Promise<R>) {
|
||||
const results = new Array<R>(items.length);
|
||||
let next = 0;
|
||||
const workerCount = Math.min(Math.max(1, limit), Math.max(1, items.length));
|
||||
|
||||
async function worker() {
|
||||
for (;;) {
|
||||
const index = next++;
|
||||
if (index >= items.length) return;
|
||||
results[index] = await callback(items[index], index);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
||||
return results;
|
||||
}
|
||||
|
||||
await mkdir(outDir, { recursive: true });
|
||||
|
||||
async function json(args: string[]) {
|
||||
try {
|
||||
const result = await execFileAsync(zero, args, { maxBuffer: 1024 * 1024 * 4 });
|
||||
return { code: 0, stdout: result.stdout, stderr: result.stderr, body: JSON.parse(result.stdout) };
|
||||
} catch (error) {
|
||||
const stdout = error.stdout?.toString() ?? "";
|
||||
let body = null;
|
||||
try {
|
||||
body = JSON.parse(stdout);
|
||||
} catch {
|
||||
body = null;
|
||||
}
|
||||
return {
|
||||
code: error.code ?? error.status ?? 1,
|
||||
stdout,
|
||||
stderr: error.stderr?.toString() ?? "",
|
||||
body,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const rows: MatrixRow[] = [];
|
||||
const deepMatrix = fixtures.flatMap((fixture) => {
|
||||
const fixtureTargets = fixture.includes("stdlib-target-neutral")
|
||||
? representativeStdlibTargets
|
||||
: targets;
|
||||
return fixtureTargets.map((target) => ({ fixture, target }));
|
||||
});
|
||||
const fastMatrix = [
|
||||
{ fixture: "conformance/native/pass/stdlib-target-neutral.graph", target: "linux-musl-x64" },
|
||||
{ fixture: "conformance/native/pass/stdlib-target-neutral.graph", target: "win32-x64.exe" },
|
||||
{ fixture: "conformance/native/pass/direct-checksum-helpers.graph", target: "linux-musl-x64" },
|
||||
{ fixture: "conformance/native/pass/direct-checksum-helpers.graph", target: "darwin-arm64" },
|
||||
{ fixture: "conformance/native/pass/direct-checksum-helpers.graph", target: "win32-x64.exe" },
|
||||
];
|
||||
const matrix = matrixScope === "fast" ? fastMatrix : deepMatrix;
|
||||
const jobs = parsePositiveInt(process.env.ZERO_STDLIB_TARGET_MATRIX_JOBS, 4);
|
||||
|
||||
rows.push(...await mapLimit(matrix, jobs, async ({ fixture, target }) => {
|
||||
const startedAt = Date.now();
|
||||
const ext = target.includes("win32") ? ".obj" : ".o";
|
||||
const stem = fixture.split("/").pop()?.replace(/\.(?:0|graph)$/, "") ?? "fixture";
|
||||
const artifactPath = join(outDir, `${stem}-${target}${ext}`);
|
||||
const result = await json(["build", "--json", "--emit", "obj", "--target", target, fixture, "--out", artifactPath]);
|
||||
const durationMs = Date.now() - startedAt;
|
||||
const diagnostic = result.body?.diagnostics?.[0];
|
||||
const row: MatrixRow = {
|
||||
fixture,
|
||||
target,
|
||||
ok: result.code === 0 && result.body?.ok !== false,
|
||||
artifactPath,
|
||||
artifactBytes: result.body?.artifactBytes ?? 0,
|
||||
generatedCBytes: result.body?.generatedCBytes ?? -1,
|
||||
directObjectEmitter: result.body?.releaseTargetContract?.directObjectEmitter,
|
||||
diagnostic: diagnostic ? `${diagnostic.code}: ${diagnostic.message}` : undefined,
|
||||
};
|
||||
console.error(`stdlib target matrix ${row.ok ? "ok" : "failed"}: ${fixture} ${target} (${durationMs}ms)`);
|
||||
assert.equal(row.ok, true, `${fixture} ${target} failed: ${row.diagnostic ?? result.stderr}`);
|
||||
assert.equal(row.generatedCBytes, 0, `${fixture} ${target} used generated C fallback`);
|
||||
assert.ok(row.artifactBytes > 0, `${fixture} ${target} did not produce an object artifact`);
|
||||
assert.ok(row.artifactBytes <= artifactBudgetBytes, `${fixture} ${target} object artifact exceeded ${artifactBudgetBytes} bytes`);
|
||||
return row;
|
||||
}));
|
||||
|
||||
for (const fixture of [...new Set(matrix.map((row) => row.fixture))]) {
|
||||
const hostRun = await execFileAsync(zero, ["run", fixture]);
|
||||
assert.equal(hostRun.stdout, "", `${fixture} should run silently`);
|
||||
}
|
||||
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
ok: rows.every((row) => row.ok),
|
||||
scope: matrixScope,
|
||||
fixtures,
|
||||
artifactBudgetBytes,
|
||||
representativeStdlibTargets,
|
||||
targets: rows,
|
||||
};
|
||||
|
||||
await writeFile(join(outDir, "report.json"), `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log("stdlib target matrix ok");
|
||||
Executable
+1287
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"strictNullChecks": false,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"types": ["node"],
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"include": ["*.mts"]
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { rmSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, "..");
|
||||
const tmpBase = path.join("/tmp", `zero-type-core-smoke-${process.pid}`);
|
||||
const sourcePath = `${tmpBase}.c`;
|
||||
const exePath = process.platform === "win32" ? `${tmpBase}.exe` : tmpBase;
|
||||
|
||||
const source = String.raw`
|
||||
#include "type_core.h"
|
||||
#include "unify.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static void expect(int ok, const char *message) {
|
||||
if (ok) return;
|
||||
fprintf(stderr, "%s\n", message);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
static ZTypeId parse_or_die(ZTypeArena *arena, const char *text) {
|
||||
ZTypeId type = Z_TYPE_ID_INVALID;
|
||||
ZTypeParseError error = {0};
|
||||
if (!z_type_parse(arena, text, &type, &error)) {
|
||||
fprintf(stderr, "failed to parse '%s': %s at %zu\n", text, error.message, error.offset);
|
||||
exit(1);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
static ZTypeId parse_with_binders_or_die(ZTypeArena *arena, const char *text, const ZTypeBinderScope *scope) {
|
||||
ZTypeId type = Z_TYPE_ID_INVALID;
|
||||
ZTypeParseError error = {0};
|
||||
if (!z_type_parse_with_binders(arena, text, scope, &type, &error)) {
|
||||
fprintf(stderr, "failed to parse '%s' with binders: %s at %zu\n", text, error.message, error.offset);
|
||||
exit(1);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
static void expect_roundtrip(const char *source, const char *expected) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeId type = parse_or_die(&arena, source);
|
||||
char *formatted = z_type_format(&arena, type);
|
||||
expect(formatted && strcmp(formatted, expected) == 0, "type format mismatch");
|
||||
free(formatted);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_static_roundtrip(const char *source, const char *expected, ZStaticValueKind kind) {
|
||||
ZStaticValue value = {0};
|
||||
ZTypeParseError error = {0};
|
||||
if (!z_static_value_parse(source, &value, &error)) {
|
||||
fprintf(stderr, "failed to parse static value '%s': %s at %zu\n", source, error.message, error.offset);
|
||||
exit(1);
|
||||
}
|
||||
expect(value.kind == kind, "static value kind mismatch");
|
||||
char *formatted = z_static_value_format(&value);
|
||||
expect(formatted && strcmp(formatted, expected) == 0, "static value format mismatch");
|
||||
free(formatted);
|
||||
z_static_value_free(&value);
|
||||
}
|
||||
|
||||
static void expect_invalid_type(const char *source) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeId type = Z_TYPE_ID_INVALID;
|
||||
ZTypeParseError error = {0};
|
||||
int ok = z_type_parse(&arena, source, &type, &error);
|
||||
expect(!ok && type == Z_TYPE_ID_INVALID && error.message[0] != 0, "invalid type parsed successfully");
|
||||
expect(arena.len == 0, "failed type parse mutated arena");
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_invalid_type_offset(const char *source, size_t offset) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeId type = Z_TYPE_ID_INVALID;
|
||||
ZTypeParseError error = {0};
|
||||
int ok = z_type_parse(&arena, source, &type, &error);
|
||||
expect(!ok && type == Z_TYPE_ID_INVALID && error.message[0] != 0, "invalid type parsed successfully");
|
||||
expect(error.offset == offset, "invalid type reported wrong offset");
|
||||
expect(arena.len == 0, "failed type parse mutated arena");
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_invalid_static(const char *source) {
|
||||
ZStaticValue value = {0};
|
||||
ZTypeParseError error = {0};
|
||||
int ok = z_static_value_parse(source, &value, &error);
|
||||
expect(!ok && error.message[0] != 0, "invalid static value parsed successfully");
|
||||
z_static_value_free(&value);
|
||||
}
|
||||
|
||||
static void expect_equal_and_hash(const char *left_text, const char *right_text) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeId left = parse_or_die(&arena, left_text);
|
||||
ZTypeId right = parse_or_die(&arena, right_text);
|
||||
expect(z_type_equal(&arena, left, right), "equal types did not compare equal");
|
||||
expect(z_type_hash(&arena, left) == z_type_hash(&arena, right), "equal types produced different hashes");
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_not_equal(const char *left_text, const char *right_text) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeId left = parse_or_die(&arena, left_text);
|
||||
ZTypeId right = parse_or_die(&arena, right_text);
|
||||
expect(!z_type_equal(&arena, left, right), "different types compared equal");
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_failed_parse_rewinds(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeId base = parse_or_die(&arena, "i32");
|
||||
size_t before = arena.len;
|
||||
ZTypeId bad = Z_TYPE_ID_INVALID;
|
||||
ZTypeParseError error = {0};
|
||||
expect(!z_type_parse(&arena, "Span<", &bad, &error), "bad type parsed successfully");
|
||||
expect(arena.len == before, "failed parse did not rewind arena");
|
||||
char *formatted = z_type_format(&arena, base);
|
||||
expect(formatted && strcmp(formatted, "i32") == 0, "rewind corrupted prior type");
|
||||
free(formatted);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_speculative_arg_rewinds(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeId gate = parse_or_die(&arena, "Gate<Mode.tiny>");
|
||||
expect(arena.len == 1, "static enum arg left speculative type nodes");
|
||||
char *formatted = z_type_format(&arena, gate);
|
||||
expect(formatted && strcmp(formatted, "Gate<Mode.tiny>") == 0, "static enum arg formatted incorrectly");
|
||||
free(formatted);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_binder_identity(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl left_decl[] = {
|
||||
{.name = "T", .kind = Z_TYPE_BINDER_TYPE, .id = 1},
|
||||
{.name = "N", .kind = Z_TYPE_BINDER_STATIC, .id = 2, .static_type = "usize"},
|
||||
};
|
||||
ZTypeBinderDecl right_decl[] = {
|
||||
{.name = "T", .kind = Z_TYPE_BINDER_TYPE, .id = 3},
|
||||
{.name = "N", .kind = Z_TYPE_BINDER_STATIC, .id = 4, .static_type = "usize"},
|
||||
};
|
||||
ZTypeBinderScope left_scope = {.items = left_decl, .len = 2};
|
||||
ZTypeBinderScope right_scope = {.items = right_decl, .len = 2};
|
||||
ZTypeId left = parse_with_binders_or_die(&arena, "FixedVec<T,N>", &left_scope);
|
||||
ZTypeId right = parse_with_binders_or_die(&arena, "FixedVec<T,N>", &right_scope);
|
||||
expect(!z_type_equal(&arena, left, right), "binder identity collapsed to names");
|
||||
char *formatted = z_type_format(&arena, left);
|
||||
expect(formatted && strcmp(formatted, "FixedVec<T,N>") == 0, "binder formatting changed public type text");
|
||||
free(formatted);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_unify_and_substitute(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "T", .kind = Z_TYPE_BINDER_TYPE, .id = 10},
|
||||
{.name = "N", .kind = Z_TYPE_BINDER_STATIC, .id = 11, .static_type = "usize"},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 2};
|
||||
ZTypeId pattern = parse_with_binders_or_die(&arena, "FixedVec<T,N>", &scope);
|
||||
ZTypeId actual = parse_or_die(&arena, "FixedVec<u8,0x4>");
|
||||
|
||||
ZUnifyTrace trace;
|
||||
z_unify_trace_init(&trace);
|
||||
expect(z_type_unify(&arena, pattern, actual, &trace), "binder pattern did not unify with concrete type");
|
||||
const ZUnifyBinding *type_binding = z_unify_trace_lookup(&trace, 10, Z_UNIFY_BINDING_TYPE);
|
||||
const ZUnifyBinding *static_binding = z_unify_trace_lookup(&trace, 11, Z_UNIFY_BINDING_STATIC);
|
||||
expect(type_binding && static_binding && trace.len == 2, "unification trace missed binder facts");
|
||||
char *bound_type = z_type_format(&arena, type_binding->type);
|
||||
expect(bound_type && strcmp(bound_type, "u8") == 0, "type binder bound to wrong type");
|
||||
free(bound_type);
|
||||
expect(static_binding->static_value.kind == Z_STATIC_VALUE_NUMBER && static_binding->static_value.number == 4, "static binder bound to wrong value");
|
||||
|
||||
ZTypeId substituted = Z_TYPE_ID_INVALID;
|
||||
expect(z_type_substitute(&arena, pattern, &trace, &substituted), "substitution failed");
|
||||
expect(z_type_equal(&arena, substituted, actual), "substitution result differs from concrete type");
|
||||
expect(z_type_occurs(&arena, pattern, 10), "occurs check did not find type binder");
|
||||
expect(z_type_occurs(&arena, pattern, 11), "occurs check did not find static binder");
|
||||
expect(!z_type_occurs(&arena, actual, 10), "occurs check found binder in concrete type");
|
||||
z_unify_trace_free(&trace);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_occurs_check_rejects_recursive_binding(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "T", .kind = Z_TYPE_BINDER_TYPE, .id = 20},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 1};
|
||||
ZTypeId pattern = parse_with_binders_or_die(&arena, "T", &scope);
|
||||
ZTypeId recursive = parse_with_binders_or_die(&arena, "Maybe<T>", &scope);
|
||||
ZUnifyTrace trace;
|
||||
z_unify_trace_init(&trace);
|
||||
expect(!z_type_unify(&arena, pattern, recursive, &trace), "recursive type binding passed occurs check");
|
||||
expect(strstr(trace.message, "occurs") != NULL, "occurs check failure did not leave a trace message");
|
||||
z_unify_trace_free(&trace);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_occurs_check_follows_trace_bindings(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "T", .kind = Z_TYPE_BINDER_TYPE, .id = 30},
|
||||
{.name = "U", .kind = Z_TYPE_BINDER_TYPE, .id = 31},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 2};
|
||||
ZTypeId pattern = parse_with_binders_or_die(&arena, "Pair<T,U>", &scope);
|
||||
ZTypeId recursive = parse_with_binders_or_die(&arena, "Pair<U,Maybe<T>>", &scope);
|
||||
ZUnifyTrace trace;
|
||||
z_unify_trace_init(&trace);
|
||||
expect(!z_type_unify(&arena, pattern, recursive, &trace), "transitive recursive type binding passed occurs check");
|
||||
expect(strstr(trace.message, "occurs") != NULL, "transitive occurs check failure did not leave a trace message");
|
||||
z_unify_trace_free(&trace);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_binder_alias_swap_unifies(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "T", .kind = Z_TYPE_BINDER_TYPE, .id = 40},
|
||||
{.name = "U", .kind = Z_TYPE_BINDER_TYPE, .id = 41},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 2};
|
||||
ZTypeId pattern = parse_with_binders_or_die(&arena, "Pair<T,U>", &scope);
|
||||
ZTypeId actual = parse_with_binders_or_die(&arena, "Pair<U,T>", &scope);
|
||||
ZUnifyTrace trace;
|
||||
z_unify_trace_init(&trace);
|
||||
expect(z_type_unify(&arena, pattern, actual, &trace), "plain binder alias swap did not unify");
|
||||
ZTypeId substituted = Z_TYPE_ID_INVALID;
|
||||
expect(z_type_substitute(&arena, pattern, &trace, &substituted), "alias substitution failed");
|
||||
char *formatted = z_type_format(&arena, substituted);
|
||||
expect(formatted && strcmp(formatted, "Pair<U,U>") == 0, "alias substitution did not use canonical binder");
|
||||
free(formatted);
|
||||
z_unify_trace_free(&trace);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_chained_type_substitution(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "T", .kind = Z_TYPE_BINDER_TYPE, .id = 50},
|
||||
{.name = "U", .kind = Z_TYPE_BINDER_TYPE, .id = 51},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 2};
|
||||
ZTypeId pattern = parse_with_binders_or_die(&arena, "Pair<T,T>", &scope);
|
||||
ZTypeId actual = parse_with_binders_or_die(&arena, "Pair<U,i32>", &scope);
|
||||
ZUnifyTrace trace;
|
||||
z_unify_trace_init(&trace);
|
||||
expect(z_type_unify(&arena, pattern, actual, &trace), "chained type binder pattern did not unify");
|
||||
ZTypeId substituted = Z_TYPE_ID_INVALID;
|
||||
expect(z_type_substitute(&arena, pattern, &trace, &substituted), "chained type substitution failed");
|
||||
char *formatted = z_type_format(&arena, substituted);
|
||||
expect(formatted && strcmp(formatted, "Pair<i32,i32>") == 0, "chained type substitution left an unresolved binder");
|
||||
free(formatted);
|
||||
z_unify_trace_free(&trace);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_concrete_first_type_substitution(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "T", .kind = Z_TYPE_BINDER_TYPE, .id = 52},
|
||||
{.name = "U", .kind = Z_TYPE_BINDER_TYPE, .id = 53},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 2};
|
||||
ZTypeId pattern = parse_with_binders_or_die(&arena, "Pair<Box<T>,Box<T>>", &scope);
|
||||
ZTypeId actual = parse_with_binders_or_die(&arena, "Pair<Box<i32>,Box<U>>", &scope);
|
||||
ZUnifyTrace trace;
|
||||
z_unify_trace_init(&trace);
|
||||
expect(z_type_unify(&arena, pattern, actual, &trace), "concrete-first type binder pattern did not unify");
|
||||
ZTypeId substituted = Z_TYPE_ID_INVALID;
|
||||
expect(z_type_substitute(&arena, actual, &trace, &substituted), "concrete-first actual substitution failed");
|
||||
char *formatted = z_type_format(&arena, substituted);
|
||||
expect(formatted && strcmp(formatted, "Pair<Box<i32>,Box<i32>>") == 0, "concrete-first type substitution left an unresolved binder");
|
||||
free(formatted);
|
||||
z_unify_trace_free(&trace);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_chained_static_substitution(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "N", .kind = Z_TYPE_BINDER_STATIC, .id = 60, .static_type = "usize"},
|
||||
{.name = "M", .kind = Z_TYPE_BINDER_STATIC, .id = 61, .static_type = "usize"},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 2};
|
||||
ZTypeId pattern = parse_with_binders_or_die(&arena, "Pair<FixedVec<u8,N>,FixedVec<u8,N>>", &scope);
|
||||
ZTypeId actual = parse_with_binders_or_die(&arena, "Pair<FixedVec<u8,M>,FixedVec<u8,4>>", &scope);
|
||||
ZUnifyTrace trace;
|
||||
z_unify_trace_init(&trace);
|
||||
expect(z_type_unify(&arena, pattern, actual, &trace), "chained static binder pattern did not unify");
|
||||
ZTypeId substituted = Z_TYPE_ID_INVALID;
|
||||
expect(z_type_substitute(&arena, pattern, &trace, &substituted), "chained static substitution failed");
|
||||
char *formatted = z_type_format(&arena, substituted);
|
||||
expect(formatted && strcmp(formatted, "Pair<FixedVec<u8,4>,FixedVec<u8,4>>") == 0, "chained static substitution left an unresolved binder");
|
||||
free(formatted);
|
||||
z_unify_trace_free(&trace);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_concrete_first_static_substitution(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "N", .kind = Z_TYPE_BINDER_STATIC, .id = 62, .static_type = "usize"},
|
||||
{.name = "M", .kind = Z_TYPE_BINDER_STATIC, .id = 63, .static_type = "usize"},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 2};
|
||||
ZTypeId pattern = parse_with_binders_or_die(&arena, "Pair<FixedVec<u8,N>,FixedVec<u8,N>>", &scope);
|
||||
ZTypeId actual = parse_with_binders_or_die(&arena, "Pair<FixedVec<u8,4>,FixedVec<u8,M>>", &scope);
|
||||
ZUnifyTrace trace;
|
||||
z_unify_trace_init(&trace);
|
||||
expect(z_type_unify(&arena, pattern, actual, &trace), "concrete-first static binder pattern did not unify");
|
||||
ZTypeId substituted = Z_TYPE_ID_INVALID;
|
||||
expect(z_type_substitute(&arena, actual, &trace, &substituted), "concrete-first static substitution failed");
|
||||
char *formatted = z_type_format(&arena, substituted);
|
||||
expect(formatted && strcmp(formatted, "Pair<FixedVec<u8,4>,FixedVec<u8,4>>") == 0, "concrete-first static substitution left an unresolved binder");
|
||||
free(formatted);
|
||||
z_unify_trace_free(&trace);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static bool mixed_arg_kind(const void *context, const char *type_name, size_t arg_index, ZTypeArgKind *out_kind) {
|
||||
(void)context;
|
||||
if (!type_name || strcmp(type_name, "Mixed") != 0 || arg_index > 1 || !out_kind) return false;
|
||||
*out_kind = arg_index == 0 ? Z_TYPE_ARG_TYPE : Z_TYPE_ARG_STATIC;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void expect_slot_aware_ambiguous_arg_parse(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "Foo", .kind = Z_TYPE_BINDER_STATIC, .id = 63, .static_type = "usize"},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 1, .arg_kind = mixed_arg_kind};
|
||||
ZTypeId mixed = parse_with_binders_or_die(&arena, "Mixed<Foo,Foo>", &scope);
|
||||
expect(z_type_kind(&arena, mixed) == Z_TYPE_NODE_APPLY, "slot-aware parse did not create an applied type");
|
||||
const ZTypeArg *type_arg = z_type_apply_arg(&arena, mixed, 0);
|
||||
const ZTypeArg *static_arg = z_type_apply_arg(&arena, mixed, 1);
|
||||
expect(type_arg && type_arg->kind == Z_TYPE_ARG_TYPE, "ambiguous type slot parsed as static value");
|
||||
expect(z_type_kind(&arena, type_arg->as.type) == Z_TYPE_NODE_NAME, "ambiguous type slot did not parse as a concrete type");
|
||||
expect(static_arg && static_arg->kind == Z_TYPE_ARG_STATIC, "ambiguous static slot parsed as a type");
|
||||
expect(static_arg->as.static_value.kind == Z_STATIC_VALUE_BINDER && static_arg->as.static_value.binder == 63, "ambiguous static slot did not use the static binder");
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_static_binder_types_are_checked(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "N", .kind = Z_TYPE_BINDER_STATIC, .id = 64, .static_type = "usize"},
|
||||
{.name = "Flag", .kind = Z_TYPE_BINDER_STATIC, .id = 65, .static_type = "Bool"},
|
||||
{.name = "ModeValue", .kind = Z_TYPE_BINDER_STATIC, .id = 66, .static_type = "Mode"},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 3};
|
||||
ZTypeId integer_pattern = parse_with_binders_or_die(&arena, "FixedVec<u8,N>", &scope);
|
||||
ZTypeId bool_actual = parse_or_die(&arena, "FixedVec<u8,true>");
|
||||
ZUnifyTrace trace;
|
||||
z_unify_trace_init(&trace);
|
||||
expect(!z_type_unify(&arena, integer_pattern, bool_actual, &trace), "static integer binder accepted a Bool value");
|
||||
expect(strstr(trace.message, "static value") != NULL, "static type failure did not leave a trace message");
|
||||
|
||||
ZTypeId alias_pattern = parse_with_binders_or_die(&arena, "Pair<N,N>", &scope);
|
||||
ZTypeId alias_actual = parse_with_binders_or_die(&arena, "Pair<Flag,N>", &scope);
|
||||
expect(!z_type_unify(&arena, alias_pattern, alias_actual, &trace), "static binders with different declared types unified");
|
||||
|
||||
ZTypeId enum_pattern = parse_with_binders_or_die(&arena, "Gate<ModeValue>", &scope);
|
||||
ZTypeId enum_actual = parse_or_die(&arena, "Gate<Mode.tiny>");
|
||||
expect(z_type_unify(&arena, enum_pattern, enum_actual, &trace), "enum-like static binder rejected matching symbol value");
|
||||
z_unify_trace_free(&trace);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_failed_unify_rolls_back_trace(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "T", .kind = Z_TYPE_BINDER_TYPE, .id = 70},
|
||||
{.name = "U", .kind = Z_TYPE_BINDER_TYPE, .id = 71},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 2};
|
||||
ZTypeId pattern = parse_with_binders_or_die(&arena, "Pair<T,T>", &scope);
|
||||
ZTypeId bad = parse_or_die(&arena, "Pair<i32,String>");
|
||||
ZTypeId prior_pattern = parse_with_binders_or_die(&arena, "U", &scope);
|
||||
ZTypeId prior_actual = parse_or_die(&arena, "u8");
|
||||
ZUnifyTrace trace;
|
||||
z_unify_trace_init(&trace);
|
||||
expect(z_type_unify(&arena, prior_pattern, prior_actual, &trace), "prior trace binding did not unify");
|
||||
expect(trace.len == 1, "prior trace binding was not committed");
|
||||
expect(!z_type_unify(&arena, pattern, bad, &trace), "conflicting type binder pattern unified successfully");
|
||||
expect(trace.len == 1, "failed unification did not roll back to the prior trace length");
|
||||
const ZUnifyBinding *prior_binding = z_unify_trace_lookup(&trace, 71, Z_UNIFY_BINDING_TYPE);
|
||||
expect(prior_binding != NULL, "failed unification dropped a prior trace binding");
|
||||
expect(trace.message[0] != 0, "failed unification did not preserve a diagnostic message");
|
||||
|
||||
ZTypeId good = parse_or_die(&arena, "Pair<u8,u8>");
|
||||
expect(z_type_unify(&arena, pattern, good, &trace), "trace was poisoned after failed unification rollback");
|
||||
expect(trace.message[0] == 0, "successful retry left a stale unification failure message");
|
||||
expect(trace.len == 2, "successful retry did not commit expected binding count");
|
||||
const ZUnifyBinding *type_binding = z_unify_trace_lookup(&trace, 70, Z_UNIFY_BINDING_TYPE);
|
||||
expect(type_binding != NULL, "successful retry did not bind T");
|
||||
char *bound_type = z_type_format(&arena, type_binding->type);
|
||||
expect(bound_type && strcmp(bound_type, "u8") == 0, "successful retry bound T to the wrong type");
|
||||
free(bound_type);
|
||||
z_unify_trace_free(&trace);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
static void expect_failed_substitute_rolls_back_arena(void) {
|
||||
ZTypeArena arena;
|
||||
z_type_arena_init(&arena);
|
||||
ZTypeBinderDecl decls[] = {
|
||||
{.name = "T", .kind = Z_TYPE_BINDER_TYPE, .id = 80},
|
||||
};
|
||||
ZTypeBinderScope scope = {.items = decls, .len = 1};
|
||||
ZTypeId source = parse_with_binders_or_die(&arena, "Tuple<i32,T,u8>", &scope);
|
||||
ZTypeId recursive = parse_with_binders_or_die(&arena, "T", &scope);
|
||||
size_t before = arena.len;
|
||||
|
||||
ZUnifyTrace trace;
|
||||
z_unify_trace_init(&trace);
|
||||
trace.items = calloc(1, sizeof(ZUnifyBinding));
|
||||
expect(trace.items != NULL, "failed to allocate regression trace");
|
||||
trace.len = 1;
|
||||
trace.cap = 1;
|
||||
trace.items[0] = (ZUnifyBinding){.binder = 80, .kind = Z_UNIFY_BINDING_TYPE, .type = recursive};
|
||||
|
||||
ZTypeId substituted = Z_TYPE_ID_INVALID;
|
||||
expect(!z_type_substitute(&arena, source, &trace, &substituted), "recursive substitution unexpectedly succeeded");
|
||||
expect(substituted == Z_TYPE_ID_INVALID, "failed substitution returned a type id");
|
||||
expect(arena.len == before, "failed substitution did not roll back arena");
|
||||
|
||||
z_unify_trace_free(&trace);
|
||||
z_type_arena_free(&arena);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
expect_roundtrip("i32", "i32");
|
||||
expect_roundtrip("const i32", "const i32");
|
||||
expect_roundtrip("[4]u8", "[4]u8");
|
||||
expect_roundtrip("[0x10]u8", "[16]u8");
|
||||
expect_roundtrip("[cap]u8", "[cap]u8");
|
||||
expect_roundtrip("Span<u8>", "Span<u8>");
|
||||
expect_roundtrip("MutSpan<Span<u8>>", "MutSpan<Span<u8>>");
|
||||
expect_roundtrip("Maybe<owned<ByteBuf>>", "Maybe<owned<ByteBuf>>");
|
||||
expect_roundtrip("Box<trueThing<u8>>", "Box<trueThing<u8>>");
|
||||
expect_roundtrip("ref<FixedVec<u8, 4>>", "ref<FixedVec<u8,4>>");
|
||||
expect_roundtrip("mutref<MutSpan<u8>>", "mutref<MutSpan<u8>>");
|
||||
expect_roundtrip("FixedVec<T,N>", "FixedVec<T,N>");
|
||||
expect_roundtrip("FixedVec<u8, 4_usize>", "FixedVec<u8,4>");
|
||||
expect_roundtrip("Gate<true, Mode.tiny>", "Gate<true,Mode.tiny>");
|
||||
expect_roundtrip("const [4]Maybe<ref<Point>>", "const [4]Maybe<ref<Point>>");
|
||||
|
||||
expect_static_roundtrip("42", "42", Z_STATIC_VALUE_NUMBER);
|
||||
expect_static_roundtrip("4_096", "4096", Z_STATIC_VALUE_NUMBER);
|
||||
expect_static_roundtrip("0b1010", "10", Z_STATIC_VALUE_NUMBER);
|
||||
expect_static_roundtrip("true", "true", Z_STATIC_VALUE_BOOL);
|
||||
expect_static_roundtrip("Mode.tiny", "Mode.tiny", Z_STATIC_VALUE_SYMBOL);
|
||||
|
||||
expect_equal_and_hash("FixedVec<u8,4>", "FixedVec<u8,0x4>");
|
||||
expect_equal_and_hash("Maybe<Span<u8>>", "Maybe<Span<u8>>");
|
||||
expect_not_equal("FixedVec<u8,4>", "FixedVec<u8,5>");
|
||||
expect_not_equal("Span<u8>", "MutSpan<u8>");
|
||||
|
||||
expect_failed_parse_rewinds();
|
||||
expect_speculative_arg_rewinds();
|
||||
expect_binder_identity();
|
||||
expect_unify_and_substitute();
|
||||
expect_occurs_check_rejects_recursive_binding();
|
||||
expect_occurs_check_follows_trace_bindings();
|
||||
expect_binder_alias_swap_unifies();
|
||||
expect_chained_type_substitution();
|
||||
expect_chained_static_substitution();
|
||||
expect_concrete_first_type_substitution();
|
||||
expect_concrete_first_static_substitution();
|
||||
expect_slot_aware_ambiguous_arg_parse();
|
||||
expect_static_binder_types_are_checked();
|
||||
expect_failed_unify_rolls_back_trace();
|
||||
expect_failed_substitute_rolls_back_arena();
|
||||
|
||||
expect_invalid_type("");
|
||||
expect_invalid_type("Span<");
|
||||
expect_invalid_type("[4");
|
||||
expect_invalid_type("Maybe<>");
|
||||
expect_invalid_type("Span<u8,,i32>");
|
||||
expect_invalid_type("[]u8");
|
||||
expect_invalid_type("const");
|
||||
expect_invalid_type("FixedVec<u8,>");
|
||||
expect_invalid_type("[4_]u8");
|
||||
expect_invalid_type("[4__5]u8");
|
||||
expect_invalid_type("[0x_1]u8");
|
||||
expect_invalid_type("FixedVec<u8,4_nope>");
|
||||
expect_invalid_type_offset("FixedVec<u8,4_>", 12);
|
||||
expect_invalid_type_offset("[4_]u8", 1);
|
||||
|
||||
expect_invalid_static("");
|
||||
expect_invalid_static("Mode.");
|
||||
expect_invalid_static("0x");
|
||||
expect_invalid_static("4_");
|
||||
expect_invalid_static("4__5");
|
||||
expect_invalid_static("0x_1");
|
||||
expect_invalid_static("4_nope");
|
||||
|
||||
printf("type core smoke ok\n");
|
||||
return 0;
|
||||
}
|
||||
`;
|
||||
|
||||
try {
|
||||
writeFileSync(sourcePath, source);
|
||||
execFileSync(
|
||||
"cc",
|
||||
[
|
||||
"-std=c11",
|
||||
"-Wall",
|
||||
"-Wextra",
|
||||
"-Wpedantic",
|
||||
"-I",
|
||||
path.join(repoRoot, "native/zero-c/src"),
|
||||
sourcePath,
|
||||
path.join(repoRoot, "native/zero-c/src/type_core.c"),
|
||||
path.join(repoRoot, "native/zero-c/src/unify.c"),
|
||||
"-o",
|
||||
exePath,
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
execFileSync(exePath, [], { stdio: "inherit" });
|
||||
} finally {
|
||||
rmSync(sourcePath, { force: true });
|
||||
rmSync(exePath, { force: true });
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const nodeArgs = ["--experimental-strip-types", "--disable-warning=ExperimentalWarning"];
|
||||
const reportDir = ".zero/validation";
|
||||
|
||||
type Phase = {
|
||||
name: string;
|
||||
command: string;
|
||||
args: string[];
|
||||
setup?: boolean;
|
||||
};
|
||||
|
||||
type Suite = {
|
||||
setup: Phase[];
|
||||
phases: Phase[];
|
||||
defaultJobs?: number;
|
||||
};
|
||||
|
||||
type PhaseResult = {
|
||||
name: string;
|
||||
command: string;
|
||||
ok: boolean;
|
||||
code: number | null;
|
||||
signal: NodeJS.Signals | null;
|
||||
durationMs: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
const suites: Record<string, Suite> = {
|
||||
conformance: {
|
||||
setup: [
|
||||
{ name: "native-build", command: "make", args: ["-C", "native/zero-c"], setup: true },
|
||||
],
|
||||
phases: [
|
||||
{ name: "graph-input-policy", command: process.execPath, args: [...nodeArgs, "scripts/graph-input-policy.mts"] },
|
||||
{ name: "native-contracts", command: process.execPath, args: [...nodeArgs, "scripts/native-contracts.mts"] },
|
||||
{ name: "provenance-guardrails", command: process.execPath, args: [...nodeArgs, "scripts/provenance-guardrails.mts"] },
|
||||
{ name: "type-core-smoke", command: process.execPath, args: [...nodeArgs, "scripts/type-core-smoke.mts"] },
|
||||
{ name: "mir-verifier-smoke", command: process.execPath, args: [...nodeArgs, "scripts/mir-verifier-smoke.mts"] },
|
||||
{ name: "program-graph-smoke", command: process.execPath, args: [...nodeArgs, "scripts/program-graph-smoke.mts"] },
|
||||
{ name: "program-graph-parity", command: process.execPath, args: [...nodeArgs, "scripts/program-graph-parity.mts"] },
|
||||
{ name: "canonical-text-smoke", command: process.execPath, args: [...nodeArgs, "scripts/canonical-text-smoke.mts"] },
|
||||
{ name: "examples-gate", command: process.execPath, args: [...nodeArgs, "scripts/examples-gate.mts"] },
|
||||
{ name: "conformance-run", command: process.execPath, args: ["conformance/run.mjs"] },
|
||||
],
|
||||
},
|
||||
"command-contracts": {
|
||||
setup: [
|
||||
{ name: "native-build", command: "make", args: ["-C", "native/zero-c"], setup: true },
|
||||
],
|
||||
phases: [
|
||||
{ name: "graph-input-policy", command: process.execPath, args: [...nodeArgs, "scripts/graph-input-policy.mts"] },
|
||||
{ name: "snapshot-command-contracts", command: process.execPath, args: [...nodeArgs, "scripts/snapshot-command-contracts.mts"] },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function usage() {
|
||||
const suiteNames = Object.keys(suites).join("|");
|
||||
console.log(`Run validation phases with aggregate failure reporting.
|
||||
|
||||
Usage:
|
||||
node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/validation-suite.mts <${suiteNames}> [options]
|
||||
|
||||
Options:
|
||||
--list List phases and exit.
|
||||
--phases <a,b> Run only the named comma-separated phases after setup.
|
||||
--shard <index/count> Run only this 1-based phase shard after setup.
|
||||
--jobs <count> Run selected phases concurrently. Defaults to 1.
|
||||
--fail-fast Stop after the first failing phase.
|
||||
|
||||
Environment:
|
||||
ZERO_VALIDATION_PHASES Default phase allowlist, for example a,b,c.
|
||||
ZERO_VALIDATION_SHARD Default shard, for example 1/4.
|
||||
ZERO_VALIDATION_JOBS Default jobs count.
|
||||
ZERO_VALIDATION_FAIL_FAST=1 Stop after the first failing phase.`);
|
||||
}
|
||||
|
||||
const rawArgs = process.argv.slice(2);
|
||||
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
||||
usage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const suiteName = rawArgs.find((arg) => !arg.startsWith("--"));
|
||||
if (!suiteName || !suites[suiteName]) {
|
||||
usage();
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
function optionValue(name: string) {
|
||||
const index = rawArgs.indexOf(name);
|
||||
return index === -1 ? undefined : rawArgs[index + 1];
|
||||
}
|
||||
|
||||
function parsePositiveInt(value: string | undefined, fallback: number) {
|
||||
const parsed = Number.parseInt(value ?? "", 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
|
||||
function parseShard(value: string | undefined) {
|
||||
const text = value || process.env.ZERO_VALIDATION_SHARD || "";
|
||||
if (!text) return null;
|
||||
const match = /^([1-9][0-9]*)\/([1-9][0-9]*)$/.exec(text);
|
||||
if (!match) throw new Error(`invalid shard ${text}; expected index/count, for example 1/4`);
|
||||
const index = Number.parseInt(match[1], 10);
|
||||
const count = Number.parseInt(match[2], 10);
|
||||
if (index > count) throw new Error(`invalid shard ${text}; index must be <= count`);
|
||||
return { index, count, text };
|
||||
}
|
||||
|
||||
function parsePhaseFilter(value: string | undefined, suite: Suite) {
|
||||
const text = value || process.env.ZERO_VALIDATION_PHASES || "";
|
||||
if (!text.trim()) return null;
|
||||
const wanted = new Set(text.split(",").map((part) => part.trim()).filter(Boolean));
|
||||
const known = new Set(suite.phases.map((phase) => phase.name));
|
||||
const unknown = [...wanted].filter((name) => !known.has(name));
|
||||
if (unknown.length > 0) {
|
||||
throw new Error(`unknown validation phase(s): ${unknown.join(", ")}`);
|
||||
}
|
||||
return wanted;
|
||||
}
|
||||
|
||||
const suite = suites[suiteName];
|
||||
const listOnly = rawArgs.includes("--list");
|
||||
const phaseFilter = parsePhaseFilter(optionValue("--phases"), suite);
|
||||
const shard = parseShard(optionValue("--shard"));
|
||||
const jobs = parsePositiveInt(optionValue("--jobs") ?? process.env.ZERO_VALIDATION_JOBS, suite.defaultJobs ?? 1);
|
||||
const failFast = rawArgs.includes("--fail-fast") || process.env.ZERO_VALIDATION_FAIL_FAST === "1";
|
||||
|
||||
const filteredPhases = phaseFilter
|
||||
? suite.phases.filter((phase) => phaseFilter.has(phase.name))
|
||||
: suite.phases;
|
||||
const selectedPhases = shard
|
||||
? filteredPhases.filter((_, index) => index % shard.count === shard.index - 1)
|
||||
: filteredPhases;
|
||||
|
||||
if (listOnly) {
|
||||
for (const phase of suite.setup) console.log(`${phase.name} setup`);
|
||||
for (const [index, phase] of suite.phases.entries()) {
|
||||
const selected = selectedPhases.includes(phase)
|
||||
? "selected"
|
||||
: phaseFilter && !phaseFilter.has(phase.name) ? "filtered" : "skipped";
|
||||
console.log(`${index + 1}. ${phase.name} ${selected}`);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function runPhase(phase: Phase): Promise<PhaseResult> {
|
||||
const startedAt = Date.now();
|
||||
process.stderr.write(`validation phase start: ${phase.name}\n`);
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(phase.command, phase.args, {
|
||||
cwd: process.cwd(),
|
||||
env: process.env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.on("data", (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stdout += text;
|
||||
process.stdout.write(text);
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
const text = chunk.toString();
|
||||
stderr += text;
|
||||
process.stderr.write(text);
|
||||
});
|
||||
child.on("error", (error) => {
|
||||
stderr += `${error.message}\n`;
|
||||
});
|
||||
child.on("close", (code, signal) => {
|
||||
const durationMs = Date.now() - startedAt;
|
||||
const ok = code === 0 && signal === null;
|
||||
process.stderr.write(`validation phase ${ok ? "ok" : "failed"}: ${phase.name} (${durationMs}ms)\n`);
|
||||
resolve({
|
||||
name: phase.name,
|
||||
command: [phase.command, ...phase.args].join(" "),
|
||||
ok,
|
||||
code,
|
||||
signal,
|
||||
durationMs,
|
||||
stdout,
|
||||
stderr,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function runSequential(phases: Phase[]) {
|
||||
const results: PhaseResult[] = [];
|
||||
for (const phase of phases) {
|
||||
const result = await runPhase(phase);
|
||||
results.push(result);
|
||||
if (!result.ok && failFast) break;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async function runConcurrent(phases: Phase[]) {
|
||||
const results: PhaseResult[] = [];
|
||||
let next = 0;
|
||||
let stop = false;
|
||||
const workerCount = Math.min(jobs, Math.max(1, phases.length));
|
||||
|
||||
async function worker() {
|
||||
for (;;) {
|
||||
if (stop) return;
|
||||
const index = next++;
|
||||
if (index >= phases.length) return;
|
||||
const result = await runPhase(phases[index]);
|
||||
results[index] = result;
|
||||
if (!result.ok && failFast) stop = true;
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
||||
return results.filter(Boolean);
|
||||
}
|
||||
|
||||
function printFailures(results: any[]) {
|
||||
const failures = results.filter((result) => !result.ok);
|
||||
if (failures.length === 0) return;
|
||||
process.stderr.write(`\n${suiteName} collected ${failures.length} failing phase(s):\n`);
|
||||
for (const [index, failure] of failures.entries()) {
|
||||
process.stderr.write(`\n${index + 1}. ${failure.name}\n`);
|
||||
process.stderr.write(` command: ${failure.command}\n`);
|
||||
process.stderr.write(` exit: ${failure.code ?? "signal"}${failure.signal ? ` signal ${failure.signal}` : ""}\n`);
|
||||
const detail = (failure.stderr || failure.stdout || "").trim().split("\n").slice(-40).join("\n");
|
||||
if (detail) process.stderr.write(`${detail}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
mkdirSync(reportDir, { recursive: true });
|
||||
const startedAt = Date.now();
|
||||
const setupResults = await runSequential(suite.setup);
|
||||
const setupFailed = setupResults.some((result) => !result.ok);
|
||||
const phaseResults = setupFailed && failFast
|
||||
? []
|
||||
: jobs > 1 ? await runConcurrent(selectedPhases) : await runSequential(selectedPhases);
|
||||
const results = [...setupResults, ...phaseResults];
|
||||
const ok = results.every((result) => result.ok);
|
||||
const report = {
|
||||
suite: suiteName,
|
||||
ok,
|
||||
phases: phaseFilter ? [...phaseFilter] : null,
|
||||
shard: shard?.text ?? null,
|
||||
jobs,
|
||||
failFast,
|
||||
durationMs: Date.now() - startedAt,
|
||||
results,
|
||||
};
|
||||
const reportPath = join(reportDir, `${suiteName}${shard ? `-shard-${shard.index}-of-${shard.count}` : ""}.json`);
|
||||
writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
|
||||
printFailures(results);
|
||||
if (!ok) {
|
||||
process.stderr.write(`\nvalidation suite failed: ${suiteName}\nreport: ${reportPath}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
process.stderr.write(`validation suite ok: ${suiteName}\nreport: ${reportPath}\n`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error instanceof Error ? error.stack || error.message : String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$root"
|
||||
|
||||
make -C native/zero-c
|
||||
|
||||
embedded_skills_before="/tmp/zero-embedded-skills-$$.inc"
|
||||
cp native/zero-c/src/embedded_skills.inc "$embedded_skills_before"
|
||||
node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/embed-skill-data.mts
|
||||
if ! cmp -s "$embedded_skills_before" native/zero-c/src/embedded_skills.inc; then
|
||||
diff -u "$embedded_skills_before" native/zero-c/src/embedded_skills.inc
|
||||
rm -f "$embedded_skills_before"
|
||||
exit 1
|
||||
fi
|
||||
rm -f "$embedded_skills_before"
|
||||
|
||||
pnpm run check
|
||||
pnpm run repository-graph:check
|
||||
pnpm run rosetta:local
|
||||
pnpm run stdlib:contracts
|
||||
pnpm run test:zero
|
||||
pnpm run extension:test
|
||||
|
||||
if [[ -n "${VERCEL_OIDC_TOKEN:-}" ]]; then
|
||||
ZERO_BENCH_RUNS=1 pnpm run bench:budgets
|
||||
else
|
||||
ZERO_BENCH_RUNS=1 ZERO_BENCH_BUDGET=warn pnpm run bench:local
|
||||
fi
|
||||
|
||||
bin/zero check --target linux-musl-x64 examples/memory-package
|
||||
bin/zero build --emit obj --target linux-musl-x64 examples/direct-obj-add.graph --out .zero/out/direct-obj-add-linux-musl.o
|
||||
bin/zero build --target linux-musl-x64 examples/hello.graph --out .zero/out/hello-linux-musl
|
||||
bin/zero build --target win32-x64.exe examples/hello.graph --out .zero/out/hello-win32
|
||||
|
||||
mkdir -p .zero/ci-release
|
||||
node --experimental-strip-types --disable-warning=ExperimentalWarning scripts/embed-skill-data.mts
|
||||
build_hash="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
||||
ZIG_GLOBAL_CACHE_DIR=.zero/zig-global-cache ZIG_LOCAL_CACHE_DIR=.zero/zig-local-cache zig cc -target x86_64-linux-musl -std=c11 -D_POSIX_C_SOURCE=200809L -DZERO_BUILD_HASH="\"$build_hash\"" -Os -Inative/zero-c/include native/zero-c/src/*.c -o .zero/ci-release/zero-linux-musl-x64
|
||||
test -s .zero/ci-release/zero-linux-musl-x64
|
||||
if [[ "$(uname -s):$(uname -m)" == "Linux:x86_64" ]]; then
|
||||
./.zero/ci-release/zero-linux-musl-x64 --version
|
||||
./.zero/ci-release/zero-linux-musl-x64 skills list --json
|
||||
fi
|
||||
Executable
+563
@@ -0,0 +1,563 @@
|
||||
#!/usr/bin/env -S node --experimental-strip-types --disable-warning=ExperimentalWarning
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const zero = "bin/zero";
|
||||
const documents = new Map();
|
||||
const symbols = new Map();
|
||||
const documentFacts = new Map();
|
||||
|
||||
function uriToPath(uri) {
|
||||
return uri && uri.startsWith("file://") ? fileURLToPath(uri) : uri;
|
||||
}
|
||||
|
||||
function pathToUri(path) {
|
||||
return pathToFileURL(path).href;
|
||||
}
|
||||
|
||||
function graphSidecarPath(path) {
|
||||
return path.endsWith(".0") ? `${path.slice(0, -2)}.graph` : null;
|
||||
}
|
||||
|
||||
function diagnosticsFromBody(body) {
|
||||
return (body?.diagnostics ?? []).map((diag) => ({
|
||||
range: {
|
||||
start: { line: Math.max(0, diag.line - 1), character: Math.max(0, diag.column - 1) },
|
||||
end: { line: Math.max(0, diag.line - 1), character: Math.max(0, diag.column) },
|
||||
},
|
||||
severity: 1,
|
||||
code: diag.code,
|
||||
source: "zero",
|
||||
message: diag.message,
|
||||
data: { repair: diag.repair, fixSafety: diag.fixSafety },
|
||||
}));
|
||||
}
|
||||
|
||||
async function importProjectionSidecar(path) {
|
||||
const sidecar = graphSidecarPath(path);
|
||||
if (!sidecar) return null;
|
||||
const result = await execFileAsync(zero, ["import", "--json", "--format", "binary", "--out", sidecar, path]).catch((error) => error);
|
||||
if (!result.stdout) return null;
|
||||
try {
|
||||
return JSON.parse(result.stdout);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function positionAt(text, offset) {
|
||||
const prefix = text.slice(0, offset);
|
||||
const lines = prefix.split("\n");
|
||||
return { line: lines.length - 1, character: lines[lines.length - 1].length };
|
||||
}
|
||||
|
||||
function offsetAt(text, position) {
|
||||
const lines = text.split("\n");
|
||||
let offset = 0;
|
||||
for (let i = 0; i < Math.min(position.line, lines.length); i++) offset += lines[i].length + 1;
|
||||
return offset + Math.min(position.character, lines[position.line]?.length ?? 0);
|
||||
}
|
||||
|
||||
function wordAt(text, position) {
|
||||
const offset = offsetAt(text, position);
|
||||
let start = offset;
|
||||
let end = offset;
|
||||
while (start > 0 && /[A-Za-z0-9_]/.test(text[start - 1])) start--;
|
||||
while (end < text.length && /[A-Za-z0-9_]/.test(text[end])) end++;
|
||||
return text.slice(start, end);
|
||||
}
|
||||
|
||||
function rangeForLine(line, start, end) {
|
||||
return {
|
||||
start: { line, character: start },
|
||||
end: { line, character: end },
|
||||
};
|
||||
}
|
||||
|
||||
function symbolKind(kind) {
|
||||
return {
|
||||
function: 12,
|
||||
shape: 23,
|
||||
enum: 10,
|
||||
choice: 23,
|
||||
interface: 11,
|
||||
const: 14,
|
||||
alias: 5,
|
||||
"type-alias": 5,
|
||||
type: 5,
|
||||
}[kind] ?? 13;
|
||||
}
|
||||
|
||||
function analyzeText(uri, text) {
|
||||
const found = [];
|
||||
const lines = text.split("\n");
|
||||
const declaration = /^\s*(?:pub\s+)?(?:(export\s+c)\s+)?(?:(extern|packed)\s+)?(fn|type|enum|choice|interface|const|alias)\s+([A-Za-z_][A-Za-z0-9_]*)/;
|
||||
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
||||
const match = lines[lineIndex].match(declaration);
|
||||
if (!match) continue;
|
||||
const kind = match[3] === "fn" ? "function" : match[3];
|
||||
const name = match[4];
|
||||
const start = lines[lineIndex].indexOf(name);
|
||||
found.push({
|
||||
name,
|
||||
kind,
|
||||
detail: lines[lineIndex].trim(),
|
||||
uri,
|
||||
range: rangeForLine(lineIndex, start, start + name.length),
|
||||
selectionRange: rangeForLine(lineIndex, start, start + name.length),
|
||||
});
|
||||
}
|
||||
symbols.set(uri, found);
|
||||
return found;
|
||||
}
|
||||
|
||||
function simpleDiagnostics(uri, text) {
|
||||
const diagnostics = [];
|
||||
let balance = 0;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text[i] === "{") balance++;
|
||||
if (text[i] === "}") balance--;
|
||||
if (balance < 0) {
|
||||
diagnostics.push({
|
||||
range: { start: positionAt(text, i), end: positionAt(text, i + 1) },
|
||||
severity: 1,
|
||||
code: "PAR100",
|
||||
source: "zero",
|
||||
message: "unmatched closing brace",
|
||||
});
|
||||
balance = 0;
|
||||
}
|
||||
}
|
||||
if (balance > 0) {
|
||||
diagnostics.push({
|
||||
range: { start: positionAt(text, text.length), end: positionAt(text, text.length) },
|
||||
severity: 1,
|
||||
code: "PAR100",
|
||||
source: "zero",
|
||||
message: "missing closing brace",
|
||||
});
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
async function compilerDiagnostics(uri, text) {
|
||||
const path = uriToPath(uri);
|
||||
try {
|
||||
await writeFile(path, text);
|
||||
const sidecar = graphSidecarPath(path);
|
||||
const imported = await importProjectionSidecar(path);
|
||||
if (imported && imported.ok === false) return diagnosticsFromBody(imported);
|
||||
const input = sidecar && imported ? sidecar : path;
|
||||
const result = await execFileAsync(zero, ["check", "--json", input]).catch((error) => error);
|
||||
if (!result.stdout) return simpleDiagnostics(uri, text);
|
||||
const body = JSON.parse(result.stdout);
|
||||
return diagnosticsFromBody(body);
|
||||
} catch {
|
||||
return simpleDiagnostics(uri, text);
|
||||
}
|
||||
}
|
||||
|
||||
async function compilerFacts(path) {
|
||||
const sidecar = graphSidecarPath(path);
|
||||
const imported = await importProjectionSidecar(path);
|
||||
const input = sidecar && imported ? sidecar : path;
|
||||
const [graphResult, sizeResult, memResult, docResult] = await Promise.all([
|
||||
execFileAsync(zero, ["inspect", "--json", input]).catch((error) => error),
|
||||
execFileAsync(zero, ["size", "--json", input]).catch((error) => error),
|
||||
execFileAsync(zero, ["mem", "--json", input]).catch((error) => error),
|
||||
execFileAsync(zero, ["doc", "--json", input]).catch((error) => error),
|
||||
]);
|
||||
const parseBody = (result) => {
|
||||
if (!result.stdout) return null;
|
||||
try {
|
||||
return JSON.parse(result.stdout);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const graph = parseBody(graphResult);
|
||||
const size = parseBody(sizeResult);
|
||||
const mem = parseBody(memResult);
|
||||
const doc = parseBody(docResult);
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
sourceFile: graph?.sourceFile ?? size?.sourceFile ?? mem?.sourceFile ?? path,
|
||||
generatedCBytes: Math.max(graph?.generatedCBytes ?? 0, size?.generatedCBytes ?? 0, mem?.generatedCBytes ?? 0, doc?.generatedCBytes ?? 0),
|
||||
cBridgeFallback: Boolean(
|
||||
graph?.selfHostRouting?.cBridge?.required ??
|
||||
size?.selfHostRouting?.cBridge?.required ??
|
||||
mem?.cBridgeFallback ??
|
||||
doc?.cBridgeFallback ??
|
||||
false
|
||||
),
|
||||
targetCapabilityFacts: graph?.targetSupport ?? size?.targetSupport ?? null,
|
||||
directBackendStatus: {
|
||||
graph: graph?.selfHostRouting ?? null,
|
||||
objectBackend: size?.objectBackend ?? mem?.objectBackend ?? null,
|
||||
},
|
||||
generatedBindingPreviews: (graph?.cImports ?? []).map((item) => ({
|
||||
header: item.header,
|
||||
alias: item.alias,
|
||||
functions: item.typedModel?.functions?.map((fn) => fn.name) ?? [],
|
||||
constants: item.typedModel?.constants?.map((constant) => constant.name) ?? [],
|
||||
structs: item.typedModel?.structs?.map((shape) => shape.name) ?? [],
|
||||
})),
|
||||
runtimeHelperCosts: {
|
||||
usedStdlibHelpers: size?.usedStdlibHelpers ?? [],
|
||||
runtimeShims: size?.runtimeShims ?? [],
|
||||
compilerRuntimeHelpers: size?.compilerRuntimeHelpers ?? [],
|
||||
},
|
||||
memoryBudget: {
|
||||
sections: mem?.sections ?? [],
|
||||
stackBytes: mem?.stackBytes ?? null,
|
||||
heapBytes: mem?.heapBytes ?? null,
|
||||
staticBytes: mem?.staticBytes ?? null,
|
||||
},
|
||||
docs: {
|
||||
publicSymbols: doc?.symbols ?? [],
|
||||
requiresCapabilities: doc?.requiresCapabilities ?? [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function completionItems() {
|
||||
const keywordItems = [
|
||||
"pub",
|
||||
"fn",
|
||||
"type",
|
||||
"enum",
|
||||
"choice",
|
||||
"interface",
|
||||
"alias",
|
||||
"const",
|
||||
"let",
|
||||
"var",
|
||||
"return",
|
||||
"if",
|
||||
"else",
|
||||
"while",
|
||||
"for",
|
||||
"match",
|
||||
"check",
|
||||
"rescue",
|
||||
"raise",
|
||||
"raises",
|
||||
"use",
|
||||
"extern",
|
||||
"packed",
|
||||
"static",
|
||||
"meta",
|
||||
"test",
|
||||
"break",
|
||||
"continue",
|
||||
"defer",
|
||||
].map((label) => ({ label, kind: 14 }));
|
||||
const symbolItems = [...symbols.values()].flat().map((symbol) => ({ label: symbol.name, kind: symbolKind(symbol.kind), detail: symbol.detail }));
|
||||
return [...keywordItems, ...symbolItems];
|
||||
}
|
||||
|
||||
function findSymbol(name) {
|
||||
for (const list of symbols.values()) {
|
||||
const found = list.find((symbol) => symbol.name === name);
|
||||
if (found) return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function hover(params) {
|
||||
const doc = documents.get(params.textDocument.uri);
|
||||
const name = doc ? wordAt(doc.text, params.position) : "";
|
||||
const symbol = findSymbol(name);
|
||||
if (!symbol) return null;
|
||||
const facts = documentFacts.get(params.textDocument.uri);
|
||||
const directStatus = facts?.directBackendStatus?.objectBackend?.targetFacts?.directStatus ?? facts?.targetCapabilityFacts?.requiredCapabilitySupport?.status ?? "unknown";
|
||||
const target = facts?.targetCapabilityFacts?.target ?? "host";
|
||||
const capabilities = (facts?.docs?.requiresCapabilities ?? []).map((item) => item.name ?? item).filter(Boolean).join(", ") || "none";
|
||||
const bindingPreview = (facts?.generatedBindingPreviews ?? [])
|
||||
.map((item) => `${item.alias ?? item.header}: ${(item.functions ?? []).slice(0, 3).join(", ") || "typed header"}`)
|
||||
.join("; ") || "none";
|
||||
return {
|
||||
contents: {
|
||||
kind: "markdown",
|
||||
value: `\`${symbol.detail}\`\n\nkind: ${symbol.kind}\n\ntarget: ${target}\n\ncapabilities: ${capabilities}\n\ndirect backend: ${directStatus}\n\ngenerated binding previews: ${bindingPreview}\n\ngeneratedCBytes: ${facts?.generatedCBytes ?? 0}`,
|
||||
},
|
||||
range: symbol.selectionRange,
|
||||
};
|
||||
}
|
||||
|
||||
function documentSymbols(params) {
|
||||
const uri = params.textDocument.uri;
|
||||
return (symbols.get(uri) ?? []).map((symbol) => ({
|
||||
name: symbol.name,
|
||||
kind: symbolKind(symbol.kind),
|
||||
detail: symbol.detail,
|
||||
range: symbol.range,
|
||||
selectionRange: symbol.selectionRange,
|
||||
}));
|
||||
}
|
||||
|
||||
function signatureHelp(params) {
|
||||
const doc = documents.get(params.textDocument.uri);
|
||||
if (!doc) return { signatures: [], activeSignature: 0, activeParameter: 0 };
|
||||
const offset = offsetAt(doc.text, params.position);
|
||||
const before = doc.text.slice(0, offset);
|
||||
const call = before.match(/([A-Za-z_][A-Za-z0-9_]*)\([^()]*$/);
|
||||
const symbol = call ? findSymbol(call[1]) : null;
|
||||
return {
|
||||
signatures: symbol ? [{ label: symbol.detail, parameters: [] }] : [],
|
||||
activeSignature: 0,
|
||||
activeParameter: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function definition(params) {
|
||||
const doc = documents.get(params.textDocument.uri);
|
||||
const name = doc ? wordAt(doc.text, params.position) : "";
|
||||
const symbol = findSymbol(name);
|
||||
return symbol ? { uri: symbol.uri, range: symbol.selectionRange } : null;
|
||||
}
|
||||
|
||||
function references(params) {
|
||||
const doc = documents.get(params.textDocument.uri);
|
||||
if (!doc) return [];
|
||||
const name = wordAt(doc.text, params.position);
|
||||
const locations = [];
|
||||
const pattern = new RegExp(`\\b${name}\\b`, "g");
|
||||
let match = null;
|
||||
while ((match = pattern.exec(doc.text)) !== null) {
|
||||
const start = positionAt(doc.text, match.index);
|
||||
locations.push({ uri: params.textDocument.uri, range: { start, end: { line: start.line, character: start.character + name.length } } });
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
|
||||
function rename(params) {
|
||||
const refs = references(params);
|
||||
return {
|
||||
changes: {
|
||||
[params.textDocument.uri]: refs.map((location) => ({ range: location.range, newText: params.newName })),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function codeActions(params) {
|
||||
const path = uriToPath(params.textDocument.uri);
|
||||
const diagnosticActions = (params.context?.diagnostics ?? [])
|
||||
.filter((diagnostic) => diagnostic.data?.repair?.id)
|
||||
.map((diagnostic) => ({
|
||||
title: diagnostic.data.repair.summary ?? `Repair ${diagnostic.code}`,
|
||||
kind: "quickfix",
|
||||
diagnostics: [diagnostic],
|
||||
data: {
|
||||
id: diagnostic.data.repair.id,
|
||||
diagnosticCode: diagnostic.code,
|
||||
safety: diagnostic.data.fixSafety ?? "requires-human-review",
|
||||
patches: [],
|
||||
},
|
||||
}));
|
||||
const result = await execFileAsync(zero, ["fix", "--patch", "--json", path]).catch((error) => error);
|
||||
if (!result.stdout) return diagnosticActions;
|
||||
const body = JSON.parse(result.stdout);
|
||||
const patchActions = (body.fixes ?? []).map((fix) => ({
|
||||
title: fix.summary,
|
||||
kind: "quickfix",
|
||||
diagnostics: params.context?.diagnostics ?? [],
|
||||
data: {
|
||||
id: fix.id,
|
||||
diagnosticCode: fix.diagnosticCode,
|
||||
safety: fix.safety,
|
||||
patches: body.patches ?? [],
|
||||
},
|
||||
}));
|
||||
return [...diagnosticActions, ...patchActions];
|
||||
}
|
||||
|
||||
async function didOpen(params, send) {
|
||||
const { uri, text } = params.textDocument;
|
||||
documents.set(uri, { text, version: params.textDocument.version ?? 0 });
|
||||
analyzeText(uri, text);
|
||||
const diagnostics = await compilerDiagnostics(uri, text);
|
||||
send("textDocument/publishDiagnostics", { uri, diagnostics });
|
||||
const facts = await compilerFacts(uriToPath(uri));
|
||||
documentFacts.set(uri, facts);
|
||||
send("zero/editorMetadata", { uri, facts });
|
||||
}
|
||||
|
||||
async function didChange(params, send) {
|
||||
const uri = params.textDocument.uri;
|
||||
const text = params.contentChanges.at(-1)?.text ?? documents.get(uri)?.text ?? "";
|
||||
documents.set(uri, { text, version: params.textDocument.version ?? 0 });
|
||||
analyzeText(uri, text);
|
||||
const diagnostics = await compilerDiagnostics(uri, text);
|
||||
send("textDocument/publishDiagnostics", { uri, diagnostics });
|
||||
const facts = await compilerFacts(uriToPath(uri));
|
||||
documentFacts.set(uri, facts);
|
||||
send("zero/editorMetadata", { uri, facts });
|
||||
}
|
||||
|
||||
function workspaceSymbols(query) {
|
||||
return [...symbols.values()].flat()
|
||||
.filter((symbol) => !query || symbol.name.toLowerCase().includes(query.toLowerCase()))
|
||||
.map((symbol) => ({ name: symbol.name, kind: symbolKind(symbol.kind), location: { uri: symbol.uri, range: symbol.selectionRange } }));
|
||||
}
|
||||
|
||||
async function handle(method, params, send) {
|
||||
if (method === "initialize") {
|
||||
return {
|
||||
capabilities: {
|
||||
textDocumentSync: 2,
|
||||
workspaceSymbolProvider: true,
|
||||
completionProvider: { triggerCharacters: ["."] },
|
||||
hoverProvider: true,
|
||||
signatureHelpProvider: { triggerCharacters: ["("] },
|
||||
definitionProvider: true,
|
||||
documentSymbolProvider: true,
|
||||
referencesProvider: true,
|
||||
renameProvider: true,
|
||||
codeActionProvider: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method === "textDocument/didOpen") return didOpen(params, send);
|
||||
if (method === "textDocument/didChange") return didChange(params, send);
|
||||
if (method === "workspace/symbol") return workspaceSymbols(params.query ?? "");
|
||||
if (method === "textDocument/completion") return completionItems();
|
||||
if (method === "textDocument/hover") return hover(params);
|
||||
if (method === "textDocument/signatureHelp") return signatureHelp(params);
|
||||
if (method === "textDocument/definition") return definition(params);
|
||||
if (method === "textDocument/documentSymbol") return documentSymbols(params);
|
||||
if (method === "textDocument/references") return references(params);
|
||||
if (method === "textDocument/rename") return rename(params);
|
||||
if (method === "textDocument/codeAction") return codeActions(params);
|
||||
return null;
|
||||
}
|
||||
|
||||
function sendMessage(message) {
|
||||
const json = JSON.stringify(message);
|
||||
process.stdout.write(`Content-Length: ${Buffer.byteLength(json, "utf8")}\r\n\r\n${json}`);
|
||||
}
|
||||
|
||||
function startServer() {
|
||||
let buffer = Buffer.alloc(0);
|
||||
const send = (method, params) => sendMessage({ jsonrpc: "2.0", method, params });
|
||||
process.stdin.on("data", async (chunk) => {
|
||||
const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
||||
buffer = Buffer.concat([buffer, data]);
|
||||
while (true) {
|
||||
const headerEnd = buffer.indexOf("\r\n\r\n");
|
||||
if (headerEnd < 0) return;
|
||||
const header = buffer.slice(0, headerEnd).toString("utf8");
|
||||
const match = header.match(/Content-Length: (\d+)/i);
|
||||
if (!match) return;
|
||||
const length = Number(match[1]);
|
||||
const bodyStart = headerEnd + 4;
|
||||
if (buffer.length < bodyStart + length) return;
|
||||
const body = buffer.slice(bodyStart, bodyStart + length).toString("utf8");
|
||||
buffer = buffer.slice(bodyStart + length);
|
||||
const message = JSON.parse(body);
|
||||
if (message.method === "exit") process.exit(0);
|
||||
const result = await handle(message.method, message.params ?? {}, send);
|
||||
if (Object.hasOwn(message, "id")) sendMessage({ jsonrpc: "2.0", id: message.id, result });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function selfTest() {
|
||||
const dir = ".zero/lsp";
|
||||
await mkdir(dir, { recursive: true });
|
||||
const path = join(dir, "sample.0");
|
||||
const uri = pathToUri(path);
|
||||
const text = "pub fn add(a: i32, b: i32) -> i32 {\n return a + b\n}\n\npub fn main(world: World) -> Void raises {\n check world.out.write(\"ok\\n\")\n}\n";
|
||||
await writeFile(path, text);
|
||||
const notifications = [];
|
||||
await didOpen({ textDocument: { uri, text, version: 1 } }, (method, params) => notifications.push({ method, params }));
|
||||
const metadata = notifications.find((item) => item.method === "zero/editorMetadata")?.params?.facts;
|
||||
assert.equal(metadata.generatedCBytes, 0);
|
||||
assert.equal(metadata.cBridgeFallback, false);
|
||||
assert(metadata.targetCapabilityFacts);
|
||||
assert(metadata.runtimeHelperCosts.usedStdlibHelpers.length >= 0);
|
||||
assert(Array.isArray(metadata.memoryBudget.sections));
|
||||
assert(metadata.directBackendStatus.objectBackend);
|
||||
assert(Array.isArray(metadata.generatedBindingPreviews));
|
||||
assert(symbols.get(uri).some((symbol) => symbol.name === "add"));
|
||||
assert(notifications.some((item) => item.method === "textDocument/publishDiagnostics"));
|
||||
assert(workspaceSymbols("add").some((symbol) => symbol.name === "add"));
|
||||
const completions = completionItems();
|
||||
assert(completions.some((item) => item.label === "add"));
|
||||
assert(completions.some((item) => item.label === "fn"));
|
||||
assert(completions.some((item) => item.label === "return"));
|
||||
assert(!completions.some((item) => item.label === "fun"));
|
||||
assert(completions.some((item) => item.label === "raises"));
|
||||
const hoverText = hover({ textDocument: { uri }, position: { line: 0, character: 9 } }).contents.value;
|
||||
assert(hoverText.includes("add"));
|
||||
assert(hoverText.includes("capabilities:"));
|
||||
assert(hoverText.includes("generated binding previews:"));
|
||||
assert(hoverText.includes("generatedCBytes: 0"));
|
||||
assert(documentSymbols({ textDocument: { uri } }).some((symbol) => symbol.name === "add"));
|
||||
assert(signatureHelp({ textDocument: { uri }, position: { line: 4, character: 20 } }).signatures.length >= 0);
|
||||
assert(definition({ textDocument: { uri }, position: { line: 0, character: 9 } }).uri === uri);
|
||||
assert(references({ textDocument: { uri }, position: { line: 0, character: 9 } }).length >= 1);
|
||||
assert(rename({ textDocument: { uri }, position: { line: 0, character: 9 }, newName: "sum" }).changes[uri].length >= 1);
|
||||
const mutableFixPath = join(dir, "mutable-fix.0");
|
||||
const mutableFixUri = pathToUri(mutableFixPath);
|
||||
const mutableFixText = "pub fn main() -> Void {\n let dst: [2]u8 = [0_u8; 2]\n let src: [2]u8 = [1_u8; 2]\n std.mem.copy(dst, src)\n}\n";
|
||||
await writeFile(mutableFixPath, mutableFixText);
|
||||
const mutableNotifications = [];
|
||||
await didOpen({ textDocument: { uri: mutableFixUri, text: mutableFixText, version: 1 } }, (method, params) => mutableNotifications.push({ method, params }));
|
||||
const mutableDiagnostics = mutableNotifications.find((item) => item.method === "textDocument/publishDiagnostics")?.params?.diagnostics ?? [];
|
||||
const actions = await codeActions({ textDocument: { uri: mutableFixUri }, context: { diagnostics: mutableDiagnostics } });
|
||||
assert(actions.some((action) => action.data.id === "make-binding-mutable"));
|
||||
const targetActions = await codeActions({
|
||||
textDocument: { uri },
|
||||
context: {
|
||||
diagnostics: [{
|
||||
code: "TAR002",
|
||||
message: "target does not provide hosted filesystem capability",
|
||||
data: { repair: { id: "choose-target-with-required-capability", summary: "Build for a target with the required capability or move the code behind a target-specific entry point." }, fixSafety: "requires-human-review" },
|
||||
}],
|
||||
},
|
||||
});
|
||||
assert(targetActions.some((action) => action.data.id === "choose-target-with-required-capability"));
|
||||
const typeAnnotationActions = await codeActions({
|
||||
textDocument: { uri },
|
||||
context: {
|
||||
diagnostics: [{
|
||||
code: "PUB001",
|
||||
message: "public declaration requires explicit type metadata",
|
||||
data: { repair: { id: "add-public-api-type", summary: "Add an explicit public type annotation so graph and docs metadata stay stable." }, fixSafety: "behavior-preserving" },
|
||||
}],
|
||||
},
|
||||
});
|
||||
assert(typeAnnotationActions.some((action) => action.data.id === "add-public-api-type"));
|
||||
const errorSetActions = await codeActions({
|
||||
textDocument: { uri },
|
||||
context: {
|
||||
diagnostics: [
|
||||
{
|
||||
code: "ERR002",
|
||||
message: "caller error set is missing a callee error",
|
||||
data: { repair: { id: "add-missing-error-name", summary: "Add the missing error name to the caller `raises [...]` set." }, fixSafety: "api-changing" },
|
||||
},
|
||||
{
|
||||
code: "ERR003",
|
||||
message: "fallible call requires check or rescue",
|
||||
data: { repair: { id: "check-or-rescue-fallible-call", summary: "Wrap the fallible call with check or handle it locally with rescue." }, fixSafety: "api-changing" },
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
assert(errorSetActions.some((action) => action.data.id === "add-missing-error-name"));
|
||||
assert(errorSetActions.some((action) => action.data.id === "check-or-rescue-fallible-call"));
|
||||
console.log("zls self-test ok");
|
||||
}
|
||||
|
||||
if (process.argv.includes("--self-test")) {
|
||||
await selfTest();
|
||||
} else {
|
||||
startServer();
|
||||
}
|
||||
Reference in New Issue
Block a user