mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 08:35:07 +08:00
feat: support multi-instance library mode via runtime symbol localization (#136)
* feat: support multi-instance library mode via runtime symbol localization - Add the abi.localize_runtime profile field: the archive build combines the program object with exactly the runtime/vendor members it reaches and demotes every external definition except the profile-declared symbols to a local symbol (darwin: one ld -r pass with -exported_symbols_list; linux: ld -r then objcopy --keep-global-symbols) - N archives built under pairwise-distinct prefixes now link into one process with no symbol collisions and no shared mutable runtime state: each instance owns a private copy of the allocator, collector, result arena, and panic sink, so sinks register per instance and a trap poisons only the instance it fired in - Document the embedder contract beside the profile spec: one thread per instance (an instance is never entered from two threads), and values cross instances only through the embedder's own byte/record marshalling - Refuse cross-target localized builds with SC3002 (the step runs the host toolchain's ld/objcopy over host-format objects); absent or false keeps the classic single-archive artifact byte-for-byte - Add the two-instance acceptance probe (two archives, two embedder threads, independent collects, a trap delivered to its own sink exactly once while the other instance keeps answering) plus symbol-exactness, profile-shape, and target-posture suites, and schedule the artifact contracts on the gate host * fix: tighten runtime localization validation * fix: harden localized library publication
This commit is contained in:
@@ -1121,6 +1121,17 @@ export interface LibArchiveOptions {
|
||||
* matching compileC's arbitrary-input safety boundary. */
|
||||
cacheIdentity?: string;
|
||||
sanitize?: boolean;
|
||||
/** Multi-instance library mode (the profile's abi.localize_runtime): the
|
||||
* external symbols to KEEP global — every other scriptc external
|
||||
* definition in the archive (the runtime's internals, the program TU's
|
||||
* mangled functions and globals, vendor objects) is demoted to a local
|
||||
* symbol. Toolchain sanitizer ABI remains external as required,
|
||||
* so N archives built under pairwise-distinct prefixes link into one
|
||||
* process with no symbol collisions and no shared mutable runtime state.
|
||||
* Undefined references (libc/libm, plus sanitizer ABI in instrumented
|
||||
* builds) keep their global binding. Omitted = the classic archive,
|
||||
* byte-for-byte. Host-native builds only. */
|
||||
localizeSymbols?: readonly string[];
|
||||
/** IR-detected link gates (the compileC precedent, refusal-narrowed). */
|
||||
regex?: boolean;
|
||||
assert?: boolean;
|
||||
@@ -1241,7 +1252,12 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
driver,
|
||||
`${toolchainEnv}\0${implicitToolchain ?? "<uncached>"}`,
|
||||
);
|
||||
// Runtime-localized archives skip the completed-archive tier: their bytes
|
||||
// additionally depend on the host's ld/objcopy identities, which the
|
||||
// archive key does not fingerprint. The runtime-object tier still serves
|
||||
// them (localization consumes the same per-flavor objects).
|
||||
const cacheCompleteArchive =
|
||||
opts.localizeSymbols === undefined &&
|
||||
root !== null && await archiverSupportsPersistentCache(arArgv, driver);
|
||||
let cachedArchive: string | null = null;
|
||||
let compilerVersion = "";
|
||||
@@ -1428,18 +1444,39 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
}
|
||||
}
|
||||
const objects = [programObject, ...runtimeObjects, ...lreObjects, ...zlibObjects];
|
||||
// A cacheable build owns a private archive from `ar` through publication.
|
||||
// The caller-visible output can be shared by another invocation without
|
||||
// letting that invocation's bytes poison this key.
|
||||
// Multi-instance library mode: the archive's one member becomes the
|
||||
// combined, symbol-localized object (cached vendor/runtime objects
|
||||
// are read-only inputs here — the combine step never mutates them).
|
||||
const archiveMembers =
|
||||
opts.localizeSymbols === undefined
|
||||
? objects
|
||||
: [
|
||||
await localizeLibraryObjects(
|
||||
driver,
|
||||
arArgv,
|
||||
buildDir,
|
||||
programObject,
|
||||
[...runtimeObjects, ...lreObjects, ...zlibObjects],
|
||||
opts.localizeSymbols,
|
||||
stem,
|
||||
),
|
||||
];
|
||||
// A cacheable or runtime-localized build owns a private archive from
|
||||
// `ar` through publication. Localized archives deliberately bypass the
|
||||
// completed-artifact cache, but still need atomic installation so two
|
||||
// invocations sharing a caller-visible output cannot race `rm`/`ar` on
|
||||
// that path.
|
||||
const archiveOutput =
|
||||
cachedArchive === null ? opts.outPath : join(buildDir, "artifact.lib.a");
|
||||
cachedArchive === null && opts.localizeSymbols === undefined
|
||||
? opts.outPath
|
||||
: join(buildDir, "artifact.lib.a");
|
||||
await rm(archiveOutput, { force: true }); // `ar r` would append into a stale archive
|
||||
await mkdir(dirname(archiveOutput), { recursive: true });
|
||||
await execFileAsync(arArgv[0] ?? "ar", [
|
||||
...arArgv.slice(1),
|
||||
"rcs",
|
||||
archiveOutput,
|
||||
...objects,
|
||||
...archiveMembers,
|
||||
]);
|
||||
if (archiveOutput !== opts.outPath) await installArtifact(archiveOutput, opts.outPath);
|
||||
let runtimeStillMatchesKey = false;
|
||||
@@ -1502,6 +1539,78 @@ export async function compileLibArchive(opts: LibArchiveOptions): Promise<void>
|
||||
if (root !== null) await pruneCache(root).catch(() => undefined);
|
||||
}
|
||||
|
||||
/* Multi-instance library mode's localization step: combine the program
|
||||
* object with exactly the runtime/vendor members it reaches into ONE
|
||||
* relocatable object, then demote every scriptc external definition except
|
||||
* the profile-declared symbols to a local symbol. The internals are not
|
||||
* renamed apart — they stop being visible to the embedder's linker at all,
|
||||
* so a second archive built under a different prefix brings its own private
|
||||
* copy of the whole runtime (allocator, collector, arena, panic sink) into
|
||||
* the same process. Undefined references (libc/libm, plus sanitizer ABI in
|
||||
* instrumented builds) keep their global binding: the C library and
|
||||
* sanitizer are the embedder's, shared by design.
|
||||
*
|
||||
* Member selection matters: a classic archive's unused members (and their
|
||||
* undefined references to units library mode excludes, like the
|
||||
* fs-promises unit's fiber symbols) never reach an embedder's link. A
|
||||
* blind merge of every object would carry those references into the one
|
||||
* combined member. Staging the support objects into an intermediate
|
||||
* archive keeps the linker's own member semantics: `ld -r` pulls only the
|
||||
* members the program object transitively needs.
|
||||
*
|
||||
* darwin — one ld64 invocation: -r merges program + needed members,
|
||||
* -exported_symbols_list demotes every unlisted global to
|
||||
* private extern, and -r without -keep_private_externs writes
|
||||
* private externs out as non-external symbols. Apple ASan's
|
||||
* image-registration COMMON remains shared so the final Mach-O
|
||||
* image registers its globals once.
|
||||
* linux — ld -r merges, then binutils objcopy --keep-global-symbols
|
||||
* localizes every other DEFINED global (objcopy leaves
|
||||
* undefined symbols global by its own rule).
|
||||
*
|
||||
* Host-native only: compileLibrary refuses localization for cross targets
|
||||
* before emission (this step runs the host's ld/objcopy over host-format
|
||||
* objects). */
|
||||
async function localizeLibraryObjects(
|
||||
driver: CcDriver,
|
||||
arArgv: readonly string[],
|
||||
buildDir: string,
|
||||
programObject: string,
|
||||
supportObjects: readonly string[],
|
||||
keepSymbols: readonly string[],
|
||||
stem: string,
|
||||
): Promise<string> {
|
||||
const platform = targetPlatform(driver);
|
||||
const combined = join(buildDir, `${stem}.localized.o`);
|
||||
const staging = join(buildDir, `${stem}.localize-staging.a`);
|
||||
const keepFile = join(buildDir, "localize-keep.syms");
|
||||
const run = async (argv: readonly string[]): Promise<void> => {
|
||||
try {
|
||||
await execFileAsync(argv[0]!, [...argv.slice(1)]);
|
||||
} catch (err) {
|
||||
const stderr = (err as { stderr?: string }).stderr ?? String(err);
|
||||
throw new Error(
|
||||
`${argv[0]} failed while localizing the library archive's runtime symbols (abi.localize_runtime).\n` +
|
||||
`Runtime symbol localization needs the host toolchain's ${platform === "darwin" ? "ld" : "ld and objcopy"} beside the C compiler.\n\n${stderr}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
await run([arArgv[0] ?? "ar", ...arArgv.slice(1), "rcs", staging, ...supportObjects]);
|
||||
if (platform === "darwin") {
|
||||
await writeFile(keepFile, keepSymbols.map((s) => `_${s}\n`).join(""));
|
||||
await run(["ld", "-r", programObject, staging, "-o", combined, "-exported_symbols_list", keepFile]);
|
||||
} else if (platform === "linux") {
|
||||
await writeFile(keepFile, keepSymbols.map((s) => `${s}\n`).join(""));
|
||||
await run(["ld", "-r", programObject, staging, "-o", combined]);
|
||||
await run(["objcopy", `--keep-global-symbols=${keepFile}`, combined]);
|
||||
} else {
|
||||
throw new Error(
|
||||
`runtime symbol localization (abi.localize_runtime) supports darwin and linux host builds; this build targets ${platform}`,
|
||||
);
|
||||
}
|
||||
return combined;
|
||||
}
|
||||
|
||||
/* -------------------------- persistent build cache ---------------------------
|
||||
* Content-addressed caches that let repeat builds of unchanged programs skip
|
||||
* payload code generation/linking — the test lanes' dominant cost. Executable
|
||||
|
||||
@@ -1454,6 +1454,29 @@ export async function compileLibrary(opts: CompileLibraryOptions): Promise<Compi
|
||||
const entryPath = profile.entry;
|
||||
const buildPlatform = buildTargetPlatform();
|
||||
|
||||
// Multi-instance library mode (abi.localize_runtime) is native on the
|
||||
// two hosts whose localization toolchains are implemented. Cross-target
|
||||
// builds and unsupported native hosts refuse before frontend/backend work
|
||||
// rather than reaching the ld/objcopy step and throwing an unstructured
|
||||
// toolchain error. WASI retains the general library-mode refusal below.
|
||||
if (profile.localizeRuntime && buildPlatform !== "wasi") {
|
||||
const driver = resolveCc();
|
||||
const platform = targetPlatform(driver);
|
||||
if (driver.target !== null || (platform !== "darwin" && platform !== "linux")) {
|
||||
return {
|
||||
ok: false,
|
||||
diagnostics: decorateLibraryRefusals([
|
||||
targetRefusalDiag(
|
||||
driver.target ?? platform,
|
||||
"runtime-localized (multi-instance) library archives",
|
||||
{ file: entryPath, start: 0, end: 0 },
|
||||
),
|
||||
], profile),
|
||||
sourceTexts: new Map(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Bare npm specifiers in a library graph take the STATIC-OR-REFUSE
|
||||
// posture: "lib" runs the same auto-detection and eligibility bar as
|
||||
// the executable lane's --npm-static (own .d.ts, unminified shipped JS,
|
||||
@@ -1682,11 +1705,27 @@ export async function compileLibrary(opts: CompileLibraryOptions): Promise<Compi
|
||||
}
|
||||
|
||||
const archivePath = opts.outPath ?? join(opts.outDir, `${stem}.lib.a`);
|
||||
// Multi-instance library mode: the profile-declared external surface —
|
||||
// the mode-provided entries, the identity getters, and the export map —
|
||||
// is exactly the set the localization step keeps global.
|
||||
const localizeSymbols = profile.localizeRuntime
|
||||
? [
|
||||
profile.initSymbol,
|
||||
profile.sinkRegisterSymbol,
|
||||
...(profile.collectSymbol !== null ? [profile.collectSymbol] : []),
|
||||
...(profile.resultResetSymbol !== null ? [profile.resultResetSymbol] : []),
|
||||
...(profile.sidecar !== null
|
||||
? [profile.sidecar.buildIdSymbol, profile.sidecar.abiVersionSymbol]
|
||||
: []),
|
||||
...profile.exports.map((e) => e.symbol),
|
||||
]
|
||||
: undefined;
|
||||
await compileLibArchive({
|
||||
cPath,
|
||||
outPath: archivePath,
|
||||
cacheIdentity: "scriptc-generated-library-v1",
|
||||
sanitize: opts.sanitize ?? false,
|
||||
...(localizeSymbols !== undefined ? { localizeSymbols } : {}),
|
||||
regex: moduleUsesRegex(mod),
|
||||
assert: moduleUsesAssert(mod),
|
||||
inspect: moduleUsesInspect(mod),
|
||||
|
||||
@@ -19,7 +19,11 @@
|
||||
* "init_symbol": "<prefix>_init",
|
||||
* "sink_register_symbol": "<prefix>_set_panic_sink",
|
||||
* "collect_symbol": "<prefix>_collect" | null, // session ruling 2
|
||||
* "result_reset_symbol": "<prefix>_reset" | null // §4.3 two postures
|
||||
* "result_reset_symbol": "<prefix>_reset" | null, // §4.3 two postures
|
||||
* "localize_runtime": false // multi-instance library
|
||||
* // mode (see below); absent
|
||||
* // = false, the classic
|
||||
* // single-archive artifact
|
||||
* },
|
||||
* "exports": [ { "export": "update", "symbol": "<prefix>_update",
|
||||
* "params": ["f64", "string"], "returns": "bytes" } ],
|
||||
@@ -78,6 +82,32 @@
|
||||
* getters are pure data returns exempt from the poisoned guard and every
|
||||
* runtime touch (ratified): callable before init and after a trap.
|
||||
*
|
||||
* Multi-instance library mode (`abi.localize_runtime: true`): the archive's
|
||||
* runtime internals — allocator, cycle collector, result arena, panic-sink
|
||||
* registration, every other piece of runtime state — are demoted to LOCAL
|
||||
* symbols behind the profile's prefix, so an ordinary archive's only
|
||||
* external definitions are the profile-declared symbols and its only
|
||||
* external references are libc/libm. Sanitized artifacts additionally carry
|
||||
* their sanitizer ABI; Darwin ASan's image-registration COMMON deliberately
|
||||
* remains shared so the final Mach-O image registers its globals exactly
|
||||
* once. N such archives, built under pairwise-distinct prefixes, then link
|
||||
* into ONE process with no scriptc-runtime symbol collisions or shared
|
||||
* mutable state: each instance owns a private copy of the whole runtime,
|
||||
* panic sinks register per instance, and a trap poisons only the instance it
|
||||
* fired in. The embedder contract that makes this sound:
|
||||
*
|
||||
* - Each instance keeps the single-threaded execution model. The embedder
|
||||
* confines each instance to one thread and never enters one instance
|
||||
* from two threads; different instances may run on different threads
|
||||
* concurrently.
|
||||
* - No value crosses instances directly: results are instance-owned
|
||||
* memory with instance-tied lifetimes, so data moves between instances
|
||||
* only through the embedder's own byte/record marshalling.
|
||||
*
|
||||
* Off by default: an absent or false `localize_runtime` produces the
|
||||
* classic artifact, byte-for-byte. Localization is host-native (darwin and
|
||||
* linux); cross-target archive builds refuse it with SC3002.
|
||||
*
|
||||
* Marshalling classes (design §4.2 + session ruling 3 + ask 4): f64, bool,
|
||||
* string, bytes for params and returns; u8/u32/i32 are PARAM-ONLY plumbing
|
||||
* classes; i64/u64 are ask-4's declared integer boundary classes (params:
|
||||
@@ -196,6 +226,11 @@ export interface LibraryProfile {
|
||||
/** §4.3: declared → results accumulate until the host calls it; null →
|
||||
* every entry prologue resets the result arena. */
|
||||
resultResetSymbol: string | null;
|
||||
/** Multi-instance library mode: true localizes every runtime-internal
|
||||
* symbol so N archives with distinct prefixes link into one process (see
|
||||
* the header contract). False (the default) produces the classic
|
||||
* single-archive artifact unchanged. */
|
||||
localizeRuntime: boolean;
|
||||
exports: LibraryExportEntry[];
|
||||
/** The ask-2 contract-sidecar section; null = the profile declares no
|
||||
* sidecar and the invocation emits none. */
|
||||
@@ -312,7 +347,7 @@ export function loadLibraryProfile(
|
||||
if (abi === null || typeof abi !== "object" || Array.isArray(abi)) {
|
||||
throw new ProfileError("'abi' must be an object");
|
||||
}
|
||||
rejectUnknownKeys(abi, "abi", ["prefix", "init_symbol", "sink_register_symbol", "collect_symbol", "result_reset_symbol"]);
|
||||
rejectUnknownKeys(abi, "abi", ["prefix", "init_symbol", "sink_register_symbol", "collect_symbol", "result_reset_symbol", "localize_runtime"]);
|
||||
const a = abi as Record<string, unknown>;
|
||||
const prefix = req<string>(a["prefix"], "abi.prefix", "string");
|
||||
if (!C_IDENT.test(prefix)) {
|
||||
@@ -322,6 +357,13 @@ export function loadLibraryProfile(
|
||||
const sinkRegisterSymbol = symbolField(a["sink_register_symbol"], "abi.sink_register_symbol", prefix, false)!;
|
||||
const collectSymbol = symbolField(a["collect_symbol"], "abi.collect_symbol", prefix, true);
|
||||
const resultResetSymbol = symbolField(a["result_reset_symbol"], "abi.result_reset_symbol", prefix, true);
|
||||
// Multi-instance library mode: strictly boolean when present (the field
|
||||
// gates the artifact's whole link surface, so a truthy non-boolean is a
|
||||
// refusal, never a coercion).
|
||||
const localizeRuntime =
|
||||
a["localize_runtime"] === undefined
|
||||
? false
|
||||
: req<boolean>(a["localize_runtime"], "abi.localize_runtime", "boolean");
|
||||
|
||||
const exportsRaw = p["exports"];
|
||||
if (!Array.isArray(exportsRaw)) throw new ProfileError("'exports' must be an array");
|
||||
@@ -578,6 +620,7 @@ export function loadLibraryProfile(
|
||||
sinkRegisterSymbol,
|
||||
collectSymbol,
|
||||
resultResetSymbol,
|
||||
localizeRuntime,
|
||||
exports: entries,
|
||||
sidecar,
|
||||
profileBytes: bytes,
|
||||
|
||||
@@ -10,6 +10,15 @@
|
||||
* profile-agnostic and the program TU carries every profile-named symbol —
|
||||
* one place for the conformance symbol audit to look.
|
||||
*
|
||||
* Multi-instance processes (the profile's abi.localize_runtime): the
|
||||
* archive build demotes this file's externals — and every other runtime
|
||||
* internal — to LOCAL symbols, so each linked archive carries its own
|
||||
* private copy of all the state here: its own sink registration, poison
|
||||
* flag, arena, and entry-symbol slot. Sinks therefore register per
|
||||
* instance, and a trap poisons only the instance it fired in. Nothing in
|
||||
* this file is thread-aware; the embedder contract is one thread per
|
||||
* instance (an instance is never entered from two threads).
|
||||
*
|
||||
* State after a sink call: none is legal. The trap fired mid-operation with
|
||||
* no unwinding — heap, arena, and collector state are unspecified; the library
|
||||
* is POISONED. Every runtime-touching entry prologue aborts on the flag
|
||||
@@ -62,9 +71,11 @@ void scr_library_set_sink(ScrLibSinkFn fn, void *ctx) {
|
||||
|
||||
/* The current-entry slot: every generated entry's prologue records its
|
||||
* external symbol before dispatching into core code. A single static slot
|
||||
* is sound — exactly one core is live per process (the one-live-core rule),
|
||||
* entries never nest, and a trap can only fire while an entry is on the
|
||||
* stack. NULL (never entered) renders as the empty symbol field. */
|
||||
* is sound — exactly one core is live per copy of this state (the sole
|
||||
* core in a classic process; each archive's own instance under
|
||||
* abi.localize_runtime, where this slot is a per-instance local), entries
|
||||
* never nest, and a trap can only fire while an entry is on the stack.
|
||||
* NULL (never entered) renders as the empty symbol field. */
|
||||
static const char *scr_library_entry_symbol = NULL;
|
||||
|
||||
/* Detected-trap classification: the runtime's trap sites self-classify
|
||||
|
||||
@@ -114,6 +114,7 @@ const remoteWorkspaceReset = workspaceResetCommand("/workspace");
|
||||
const hostLaneContractFiles = [
|
||||
"tests/harness/ffi.test.ts",
|
||||
"tests/harness/island.test.ts",
|
||||
"tests/harness/library-multi.test.ts",
|
||||
"tests/harness/differential.test.ts",
|
||||
"tests/harness/server.test.ts",
|
||||
"tests/harness/dgram.test.ts",
|
||||
@@ -123,6 +124,8 @@ const hostLaneContractPattern = [
|
||||
"calls the manifest-bound archive across every v1 ABI class",
|
||||
"a missing FFI symbol is an SC5004 diagnostic",
|
||||
"deep island recursion on a fiber is a catchable RangeError",
|
||||
"M1: external definitions equal the declared set exactly",
|
||||
"M2: independent state and collects",
|
||||
"net-echo",
|
||||
"udp-loopback-pair",
|
||||
"1564-fs-watch.ts",
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
/* Multi-instance library mode (the profile's abi.localize_runtime): N
|
||||
* library archives built under pairwise-distinct symbol prefixes link into
|
||||
* ONE process. The archive build combines the program, runtime, and vendor
|
||||
* objects into one relocatable member and demotes every external
|
||||
* definition except the profile-declared symbols to a local symbol, so the
|
||||
* embedder's linker sees no runtime internals at all — no symbol
|
||||
* collisions, and each instance owns a private copy of the whole runtime
|
||||
* (allocator, collector, result arena, panic sink, poison flag).
|
||||
*
|
||||
* M1 symbols-exact nm over a localized archive: the external defined
|
||||
* set equals the profile-declared set EXACTLY (plus
|
||||
* Darwin ASan's one image-registration common in a
|
||||
* sanitized build), and undefineds stay libc/libm-
|
||||
* shaped apart from sanitizer ABI references
|
||||
* M2 two-instance run the acceptance probe: two archives (ma_/mb_), two
|
||||
* embedder threads (one per instance — the
|
||||
* documented contract), independent init and
|
||||
* collect, a deliberate trap in A delivered to A's
|
||||
* sink exactly once (structured: SC4014, ma_boom,
|
||||
* A's ctx) while B keeps answering through and after
|
||||
* the trap window; B's sink never fires. Runs per
|
||||
* emission and once with mixed emissions (one
|
||||
* archive per backend).
|
||||
* M3 profile shape abi.localize_runtime is strictly boolean (SC4001)
|
||||
* M4 target posture localization is host-native: a cross-target build
|
||||
* refuses SC3002 before emission
|
||||
*/
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { delimiter, join } from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { compileLibrary, loadLibraryProfile } from "@scriptc/compiler";
|
||||
|
||||
const repoRoot = join(import.meta.dirname, "../..");
|
||||
const fixtureDir = join(repoRoot, "tests/library-mode/multi");
|
||||
const localizationTest = process.platform === "darwin" || process.platform === "linux" ? test : test.skip;
|
||||
/* Suite-flavor segment (the library suites' convention): the plain and
|
||||
* SCRIPTC_SAN=1 suites may run concurrently and must never share build
|
||||
* dirs. */
|
||||
const sanitize = process.env["SCRIPTC_SAN"] === "1";
|
||||
const flavor = sanitize ? "san" : "plain";
|
||||
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests/library-multi", flavor);
|
||||
|
||||
type Emission = "llvm" | "c";
|
||||
const EMISSIONS: Emission[] = ["llvm", "c"];
|
||||
|
||||
/** Build one instance's localized archive for one emission: the fixture
|
||||
* profile is patched (emission flipped, entry made absolute) into the
|
||||
* build dir, then compiled through the real compileLibrary pipeline.
|
||||
* Memoized per (instance, emission) — the probe pairings reuse builds. */
|
||||
const built = new Map<string, Promise<string>>();
|
||||
function buildInstance(instance: "a" | "b", emission: Emission): Promise<string> {
|
||||
const key = `${instance}-${emission}`;
|
||||
let archive = built.get(key);
|
||||
if (archive === undefined) {
|
||||
archive = (async () => {
|
||||
const outDir = join(cacheDir, key);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const profile = JSON.parse(readFileSync(join(fixtureDir, `profile_${instance}.json`), "utf8")) as {
|
||||
entry: string;
|
||||
emission: string;
|
||||
};
|
||||
profile.emission = emission;
|
||||
profile.entry = join(fixtureDir, profile.entry);
|
||||
const profilePath = join(outDir, "profile.json");
|
||||
writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
||||
const result = await compileLibrary({ profilePath, outDir, sanitize });
|
||||
if (!result.ok) {
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
}
|
||||
expect(result.backend).toBe(emission);
|
||||
return result.archivePath;
|
||||
})();
|
||||
built.set(key, archive);
|
||||
}
|
||||
return archive;
|
||||
}
|
||||
|
||||
/** nm over an archive: [definedExternal, undefined] symbol sets, macOS/
|
||||
* Linux leading-underscore normalized away. */
|
||||
function nmSymbols(archive: string): { defined: Set<string>; undef: Set<string> } {
|
||||
const parse = (out: string): Set<string> => {
|
||||
const set = new Set<string>();
|
||||
for (const line of out.split("\n")) {
|
||||
const sym = line.trim().split(/\s+/).pop();
|
||||
if (sym === undefined || sym === "" || sym.endsWith(":")) continue;
|
||||
set.add(sym.replace(/^_/, ""));
|
||||
}
|
||||
return set;
|
||||
};
|
||||
const defined = parse(execFileSync("nm", ["-gU", archive], { encoding: "utf8" }));
|
||||
const undef = parse(execFileSync("nm", ["-u", archive], { encoding: "utf8" }));
|
||||
return { defined, undef };
|
||||
}
|
||||
|
||||
const A_SYMBOLS = ["ma_boom", "ma_bump", "ma_calls_seen", "ma_collect", "ma_init", "ma_set_panic_sink"];
|
||||
const B_SYMBOLS = ["mb_add", "mb_collect", "mb_init", "mb_set_panic_sink", "mb_sum_to"];
|
||||
|
||||
const PROBE_EXPECTED = `multi-a ready
|
||||
multi-b ready
|
||||
a: bump(1) x200 -> 201, calls_seen 200, trap fell through 0
|
||||
a sink: calls=1 ctx_ok=1 fields=3 code=[SC4014] symbol=[ma_boom] text_printable=1 addr_nonzero=1
|
||||
b: concurrent sums_ok=1 adds_ok=1 reached_200=1
|
||||
b: post-trap answers ok=1
|
||||
b sink: calls=0
|
||||
`;
|
||||
|
||||
/* ── M1: the localized archive's exact link surface ─────────────────────── */
|
||||
|
||||
describe.each(EMISSIONS)("localized archive symbols, %s emission", (emission) => {
|
||||
localizationTest("M1: external definitions equal the declared set exactly", async () => {
|
||||
const [archiveA, archiveB] = await Promise.all([
|
||||
buildInstance("a", emission),
|
||||
buildInstance("b", emission),
|
||||
]);
|
||||
for (const [archive, declared] of [
|
||||
[archiveA, A_SYMBOLS],
|
||||
[archiveB, B_SYMBOLS],
|
||||
] as const) {
|
||||
const { defined, undef } = nmSymbols(archive);
|
||||
// The WHOLE defined set — a classic archive additionally defines
|
||||
// every runtime internal; a localized one defines nothing else.
|
||||
// Darwin ASan's image-wide registration guard is the sole sanitized
|
||||
// exception: keeping its COMMON shared makes the final Mach-O image
|
||||
// register its ASan globals exactly once when N archives contribute
|
||||
// module constructors.
|
||||
const toolchainDefinitions =
|
||||
sanitize && process.platform === "darwin"
|
||||
? ["___asan_globals_registered"]
|
||||
: [];
|
||||
expect([...defined].sort()).toEqual([...declared, ...toolchainDefinitions].sort());
|
||||
// Undefineds: no runtime-internal or prefix-carrying reference
|
||||
// escapes; libc/libm (and sanitizer ABI) references keep their global
|
||||
// binding.
|
||||
expect([...undef].filter((s) => s.startsWith("scr_") || s.startsWith("ma_") || s.startsWith("mb_"))).toEqual([]);
|
||||
// The ambient audit holds through the combine step: no
|
||||
// process-disposition or threading surface, no atexit teardown.
|
||||
for (const banned of ["sigaction", "signal", "pthread_create", "atexit", "setvbuf"]) {
|
||||
expect(undef.has(banned), `undefined reference to ${banned}`).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/* ── M2: the two-instance, two-thread acceptance probe ──────────────────── */
|
||||
|
||||
function buildProbe(archiveA: string, archiveB: string, outDir: string, tag: string): string {
|
||||
const bin = join(outDir, `probe-${tag}`);
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
execFileSync("clang", [
|
||||
"-std=c11",
|
||||
"-pthread",
|
||||
...(sanitize ? ["-fsanitize=address"] : []),
|
||||
join(fixtureDir, "probe.c"),
|
||||
archiveA,
|
||||
archiveB,
|
||||
"-lm",
|
||||
"-o", bin,
|
||||
]);
|
||||
return bin;
|
||||
}
|
||||
|
||||
const PAIRINGS: { tag: string; a: Emission; b: Emission }[] = [
|
||||
{ tag: "llvm-llvm", a: "llvm", b: "llvm" },
|
||||
{ tag: "c-c", a: "c", b: "c" },
|
||||
// Two embedder builds need not share a backend: one archive per emission
|
||||
// links and runs the same.
|
||||
{ tag: "llvm-c", a: "llvm", b: "c" },
|
||||
];
|
||||
|
||||
describe.each(PAIRINGS)("two instances, one process ($tag)", ({ tag, a, b }) => {
|
||||
localizationTest("M2: independent state and collects; a trap reaches only its own sink, once", async () => {
|
||||
const [archiveA, archiveB] = await Promise.all([buildInstance("a", a), buildInstance("b", b)]);
|
||||
const probe = buildProbe(archiveA, archiveB, join(cacheDir, "probes"), tag);
|
||||
const run = spawnSync(probe, { encoding: "utf8", timeout: 60_000 });
|
||||
expect(run.signal).toBeNull();
|
||||
expect(run.status).toBe(0);
|
||||
expect(run.stdout).toBe(PROBE_EXPECTED);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── M3: profile shape ───────────────────────────────────────────────────── */
|
||||
|
||||
test("M3: abi.localize_runtime is strictly boolean", () => {
|
||||
const dir = join(cacheDir, "profile-shape");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
const path = join(dir, "profile.json");
|
||||
const base = {
|
||||
profile_format: 1,
|
||||
name: "shape",
|
||||
entry: "lib.ts",
|
||||
emission: "llvm",
|
||||
abi: {
|
||||
prefix: "sp_",
|
||||
init_symbol: "sp_init",
|
||||
sink_register_symbol: "sp_set_panic_sink",
|
||||
collect_symbol: null,
|
||||
result_reset_symbol: null,
|
||||
localize_runtime: "yes",
|
||||
},
|
||||
exports: [],
|
||||
};
|
||||
for (const invalid of ["yes", null] as const) {
|
||||
writeFileSync(path, JSON.stringify({
|
||||
...base,
|
||||
abi: { ...base.abi, localize_runtime: invalid },
|
||||
}));
|
||||
const refused = loadLibraryProfile(path);
|
||||
expect(refused.ok).toBe(false);
|
||||
if (!refused.ok) {
|
||||
expect(refused.diagnostics[0]!.code).toBe("SC4001");
|
||||
expect(refused.diagnostics[0]!.message).toContain("abi.localize_runtime");
|
||||
}
|
||||
}
|
||||
// The boolean forms load, and absence means false.
|
||||
for (const [value, expected] of [[true, true], [false, false], [undefined, false]] as const) {
|
||||
const abi: Record<string, unknown> = { ...base.abi };
|
||||
if (value === undefined) delete abi["localize_runtime"];
|
||||
else abi["localize_runtime"] = value;
|
||||
writeFileSync(path, JSON.stringify({ ...base, abi }));
|
||||
const loaded = loadLibraryProfile(path);
|
||||
expect(loaded.ok).toBe(true);
|
||||
if (loaded.ok) expect(loaded.profile.localizeRuntime).toBe(expected);
|
||||
}
|
||||
});
|
||||
|
||||
/* ── M4: host-native posture ─────────────────────────────────────────────── */
|
||||
|
||||
test("M4: a cross-target build refuses runtime localization with SC3002", async () => {
|
||||
const outDir = join(cacheDir, "cross-refusal");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const profile = JSON.parse(readFileSync(join(fixtureDir, "profile_a.json"), "utf8")) as {
|
||||
entry: string;
|
||||
};
|
||||
profile.entry = join(fixtureDir, profile.entry);
|
||||
const profilePath = join(outDir, "profile.json");
|
||||
writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
||||
// compileLibrary reads SCRIPTC_CC/SCRIPTC_TARGET at call time; the
|
||||
// refusal fires before any toolchain runs, so zig need not exist here.
|
||||
const prevCc = process.env["SCRIPTC_CC"];
|
||||
const prevTarget = process.env["SCRIPTC_TARGET"];
|
||||
process.env["SCRIPTC_CC"] = "zigcc";
|
||||
process.env["SCRIPTC_TARGET"] = "x86_64-linux-musl";
|
||||
try {
|
||||
const result = await compileLibrary({ profilePath, outDir });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.diagnostics[0]!.code).toBe("SC3002");
|
||||
expect(result.diagnostics[0]!.message).toContain("x86_64-linux-musl");
|
||||
expect(result.diagnostics[0]!.message).toContain("runtime-localized");
|
||||
}
|
||||
} finally {
|
||||
if (prevCc === undefined) delete process.env["SCRIPTC_CC"];
|
||||
else process.env["SCRIPTC_CC"] = prevCc;
|
||||
if (prevTarget === undefined) delete process.env["SCRIPTC_TARGET"];
|
||||
else process.env["SCRIPTC_TARGET"] = prevTarget;
|
||||
}
|
||||
});
|
||||
|
||||
test("M4: an unsupported native host refuses runtime localization with SC3002", async () => {
|
||||
const outDir = join(cacheDir, "native-refusal");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const profile = JSON.parse(readFileSync(join(fixtureDir, "profile_a.json"), "utf8")) as {
|
||||
entry: string;
|
||||
};
|
||||
profile.entry = join(fixtureDir, profile.entry);
|
||||
const profilePath = join(outDir, "profile.json");
|
||||
writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
||||
const platformDescriptor = Object.getOwnPropertyDescriptor(process, "platform")!;
|
||||
const prevCc = process.env["SCRIPTC_CC"];
|
||||
const prevTarget = process.env["SCRIPTC_TARGET"];
|
||||
delete process.env["SCRIPTC_CC"];
|
||||
delete process.env["SCRIPTC_TARGET"];
|
||||
Object.defineProperty(process, "platform", { ...platformDescriptor, value: "win32" });
|
||||
try {
|
||||
const result = await compileLibrary({ profilePath, outDir });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.diagnostics[0]!.code).toBe("SC3002");
|
||||
expect(result.diagnostics[0]!.message).toContain("win32");
|
||||
expect(result.diagnostics[0]!.message).toContain("runtime-localized");
|
||||
}
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", platformDescriptor);
|
||||
if (prevCc === undefined) delete process.env["SCRIPTC_CC"];
|
||||
else process.env["SCRIPTC_CC"] = prevCc;
|
||||
if (prevTarget === undefined) delete process.env["SCRIPTC_TARGET"];
|
||||
else process.env["SCRIPTC_TARGET"] = prevTarget;
|
||||
}
|
||||
});
|
||||
|
||||
/* ── M5: caller-visible archive publication ─────────────────────────────── */
|
||||
|
||||
test.skipIf(process.platform !== "darwin" && process.platform !== "linux")(
|
||||
"M5: localization archives privately before atomically installing the caller-visible output",
|
||||
async () => {
|
||||
const outDir = join(cacheDir, "atomic-publication");
|
||||
const binDir = join(outDir, "bin");
|
||||
const outPath = join(outDir, "localized.lib.a");
|
||||
mkdirSync(binDir, { recursive: true });
|
||||
const profile = JSON.parse(readFileSync(join(fixtureDir, "profile_a.json"), "utf8")) as {
|
||||
entry: string;
|
||||
};
|
||||
profile.entry = join(fixtureDir, profile.entry);
|
||||
const profilePath = join(outDir, "profile.json");
|
||||
writeFileSync(profilePath, JSON.stringify(profile, null, 2));
|
||||
|
||||
const oldPath = process.env["PATH"];
|
||||
const oldCc = process.env["SCRIPTC_CC"];
|
||||
const oldTarget = process.env["SCRIPTC_TARGET"];
|
||||
const oldRealAr = process.env["SCRIPTC_TEST_REAL_AR"];
|
||||
const oldForbiddenOutput = process.env["SCRIPTC_TEST_FORBIDDEN_ARCHIVE_OUTPUT"];
|
||||
const originalAr = (oldPath ?? "")
|
||||
.split(delimiter)
|
||||
.map((entry) => join(entry === "" ? process.cwd() : entry, "ar"))
|
||||
.find((candidate) => existsSync(candidate));
|
||||
expect(originalAr).toBeDefined();
|
||||
|
||||
const wrapper = join(binDir, "ar");
|
||||
writeFileSync(
|
||||
wrapper,
|
||||
`#!/bin/sh
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "$SCRIPTC_TEST_FORBIDDEN_ARCHIVE_OUTPUT" ]; then
|
||||
echo "archiver received caller-visible output" >&2
|
||||
exit 97
|
||||
fi
|
||||
done
|
||||
exec "$SCRIPTC_TEST_REAL_AR" "$@"
|
||||
`,
|
||||
);
|
||||
chmodSync(wrapper, 0o755);
|
||||
|
||||
process.env["PATH"] = `${binDir}${delimiter}${oldPath ?? ""}`;
|
||||
process.env["SCRIPTC_TEST_REAL_AR"] = originalAr!;
|
||||
process.env["SCRIPTC_TEST_FORBIDDEN_ARCHIVE_OUTPUT"] = outPath;
|
||||
delete process.env["SCRIPTC_CC"];
|
||||
delete process.env["SCRIPTC_TARGET"];
|
||||
try {
|
||||
const result = await compileLibrary({ profilePath, outDir, outPath, sanitize });
|
||||
expect(result.ok, result.ok ? undefined : result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")).toBe(true);
|
||||
expect(existsSync(outPath)).toBe(true);
|
||||
} finally {
|
||||
if (oldPath === undefined) delete process.env["PATH"];
|
||||
else process.env["PATH"] = oldPath;
|
||||
if (oldCc === undefined) delete process.env["SCRIPTC_CC"];
|
||||
else process.env["SCRIPTC_CC"] = oldCc;
|
||||
if (oldTarget === undefined) delete process.env["SCRIPTC_TARGET"];
|
||||
else process.env["SCRIPTC_TARGET"] = oldTarget;
|
||||
if (oldRealAr === undefined) delete process.env["SCRIPTC_TEST_REAL_AR"];
|
||||
else process.env["SCRIPTC_TEST_REAL_AR"] = oldRealAr;
|
||||
if (oldForbiddenOutput === undefined) delete process.env["SCRIPTC_TEST_FORBIDDEN_ARCHIVE_OUTPUT"];
|
||||
else process.env["SCRIPTC_TEST_FORBIDDEN_ARCHIVE_OUTPUT"] = oldForbiddenOutput;
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
// Multi-instance fixture, instance A (prefix ma_): mutable module state the
|
||||
// probe advances from A's dedicated thread, plus a deliberately trapping
|
||||
// export (array index OOB — the runtime's own range trap) that must reach
|
||||
// ONLY this instance's sink.
|
||||
let calls = 0;
|
||||
|
||||
export function bump(x: number): number {
|
||||
calls++;
|
||||
return x + calls;
|
||||
}
|
||||
|
||||
export function callsSeen(): number {
|
||||
return calls;
|
||||
}
|
||||
|
||||
export function boom(i: number): number {
|
||||
const xs = [1, 2, 3];
|
||||
return xs[i]!;
|
||||
}
|
||||
|
||||
console.log("multi-a ready");
|
||||
@@ -0,0 +1,21 @@
|
||||
// Multi-instance fixture, instance B (prefix mb_): allocation-heavy work the
|
||||
// probe drives concurrently with instance A from B's dedicated thread. Every
|
||||
// call builds and folds a fresh array, so B's allocator and collector churn
|
||||
// while A runs (and traps) — independence shows up as B's answers staying
|
||||
// exact throughout.
|
||||
let total = 0;
|
||||
|
||||
export function sumTo(n: number): number {
|
||||
const xs: number[] = [];
|
||||
for (let i = 1; i <= n; i++) xs.push(i);
|
||||
let s = 0;
|
||||
for (const x of xs) s += x;
|
||||
return s;
|
||||
}
|
||||
|
||||
export function add(x: number): number {
|
||||
total += x;
|
||||
return total;
|
||||
}
|
||||
|
||||
console.log("multi-b ready");
|
||||
@@ -0,0 +1,198 @@
|
||||
/* Multi-instance acceptance probe: TWO runtime-localized library archives
|
||||
* (prefixes ma_ / mb_) linked into ONE process, each instance driven from
|
||||
* its own dedicated embedder thread — the documented contract: one thread
|
||||
* per instance, an instance never entered from two threads. Checks:
|
||||
* - both instances init, run their export families, and run their
|
||||
* collect entries independently;
|
||||
* - a deliberate range trap in instance A reaches ONLY A's registered
|
||||
* sink, exactly once, as the structured message (SC4014, symbol
|
||||
* ma_boom) with A's registration ctx and a nonzero fault address;
|
||||
* - instance B keeps answering: its worker loops through allocation-
|
||||
* heavy calls across A's trap window and still answers afterwards,
|
||||
* with exact values throughout;
|
||||
* - B's sink never fires.
|
||||
* Workers record results into per-thread state; main prints everything
|
||||
* after both joins, so stdout is deterministic under any interleaving. */
|
||||
#include <pthread.h>
|
||||
#include <setjmp.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
extern void ma_init(void);
|
||||
extern void ma_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx);
|
||||
extern void ma_collect(void);
|
||||
extern double ma_bump(double x);
|
||||
extern double ma_calls_seen(void);
|
||||
extern double ma_boom(double i);
|
||||
|
||||
extern void mb_init(void);
|
||||
extern void mb_set_panic_sink(void (*fn)(void *, const uint8_t *, size_t, uint64_t), void *ctx);
|
||||
extern void mb_collect(void);
|
||||
extern double mb_sum_to(double n);
|
||||
extern double mb_add(double x);
|
||||
|
||||
/* Probe stages: 0 start; 1 A initialized; 2 B initialized (both workers'
|
||||
* concurrent loops run); 3 both fixed loops finished (A traps, B keeps
|
||||
* looping); 4 A's trap delivered (B answers once more and exits). */
|
||||
static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
|
||||
static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
|
||||
static int stage = 0;
|
||||
static int fixed_loops_done = 0;
|
||||
|
||||
static void stage_set(int s) {
|
||||
pthread_mutex_lock(&mu);
|
||||
if (stage < s) stage = s;
|
||||
pthread_cond_broadcast(&cv);
|
||||
pthread_mutex_unlock(&mu);
|
||||
}
|
||||
|
||||
static int stage_get(void) {
|
||||
pthread_mutex_lock(&mu);
|
||||
int s = stage;
|
||||
pthread_mutex_unlock(&mu);
|
||||
return s;
|
||||
}
|
||||
|
||||
static void stage_wait(int s) {
|
||||
pthread_mutex_lock(&mu);
|
||||
while (stage < s) pthread_cond_wait(&cv, &mu);
|
||||
pthread_mutex_unlock(&mu);
|
||||
}
|
||||
|
||||
static void fixed_loop_done(void) {
|
||||
pthread_mutex_lock(&mu);
|
||||
fixed_loops_done++;
|
||||
if (fixed_loops_done == 2 && stage < 3) {
|
||||
stage = 3;
|
||||
pthread_cond_broadcast(&cv);
|
||||
}
|
||||
pthread_mutex_unlock(&mu);
|
||||
}
|
||||
|
||||
/* Per-instance sink records. Both instances register the SAME sink
|
||||
* function with instance-specific ctx: delivery routing is observable as
|
||||
* which record advances. */
|
||||
typedef struct {
|
||||
int calls;
|
||||
int ctx_ok;
|
||||
int fields;
|
||||
int text_printable;
|
||||
int addr_nonzero;
|
||||
char code[32];
|
||||
char symbol[64];
|
||||
} SinkRec;
|
||||
|
||||
static SinkRec rec_a, rec_b;
|
||||
static jmp_buf trap_jmp_a;
|
||||
|
||||
static void copy_field(char *dst, size_t cap, const uint8_t *p, size_t len) {
|
||||
if (len >= cap) len = cap - 1;
|
||||
memcpy(dst, p, len);
|
||||
dst[len] = 0;
|
||||
}
|
||||
|
||||
static void record_structured(SinkRec *r, const uint8_t *msg, size_t len) {
|
||||
if (len == 0 || msg[0] != 0x01) return; /* fields stays 0: not structured */
|
||||
r->text_printable = len > 1 && msg[1] >= 0x20;
|
||||
const uint8_t *p = msg + 1, *end = msg + len;
|
||||
int i = 0;
|
||||
for (;;) {
|
||||
const uint8_t *sep = memchr(p, 0x1f, (size_t)(end - p));
|
||||
const uint8_t *stop = sep != NULL ? sep : end;
|
||||
if (i == 1) copy_field(r->code, sizeof r->code, p, (size_t)(stop - p));
|
||||
if (i == 2) copy_field(r->symbol, sizeof r->symbol, p, (size_t)(stop - p));
|
||||
i++;
|
||||
if (sep == NULL) break;
|
||||
p = sep + 1;
|
||||
}
|
||||
r->fields = i;
|
||||
}
|
||||
|
||||
static void sink(void *ctx, const uint8_t *msg, size_t len, uint64_t addr) {
|
||||
SinkRec *r = ctx;
|
||||
r->calls++;
|
||||
r->addr_nonzero = addr != 0;
|
||||
record_structured(r, msg, len);
|
||||
if (r == &rec_a) {
|
||||
r->ctx_ok = 1;
|
||||
longjmp(trap_jmp_a, 1); /* the conforming survival pattern */
|
||||
}
|
||||
/* B's sink must never fire; returning would abort, and main would never
|
||||
* print (the probe's failure shows as missing output). */
|
||||
}
|
||||
|
||||
/* Instance A's dedicated thread: state-advancing calls, collects, then the
|
||||
* deliberate trap. */
|
||||
static double a_last_bump, a_calls;
|
||||
static int a_trap_fell_through = 0;
|
||||
|
||||
static void *worker_a(void *arg) {
|
||||
(void)arg;
|
||||
ma_set_panic_sink(sink, &rec_a);
|
||||
ma_init(); /* prints "multi-a ready" */
|
||||
stage_set(1);
|
||||
stage_wait(2);
|
||||
double last = 0;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
last = ma_bump(1);
|
||||
if ((i + 1) % 50 == 0) ma_collect();
|
||||
}
|
||||
a_last_bump = last;
|
||||
a_calls = ma_calls_seen();
|
||||
fixed_loop_done();
|
||||
stage_wait(3);
|
||||
if (setjmp(trap_jmp_a) == 0) {
|
||||
ma_boom(9); /* xs[9] of a length-3 array: the runtime's range trap */
|
||||
a_trap_fell_through = 1;
|
||||
}
|
||||
stage_set(4);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Instance B's dedicated thread: allocation-heavy calls that keep running
|
||||
* across A's trap window, then post-trap answers. */
|
||||
static int b_sums_ok = 1, b_adds_ok = 1, b_reached_200 = 0, b_post_ok = 0;
|
||||
|
||||
static void *worker_b(void *arg) {
|
||||
(void)arg;
|
||||
mb_set_panic_sink(sink, &rec_b);
|
||||
stage_wait(1);
|
||||
mb_init(); /* prints "multi-b ready" */
|
||||
stage_set(2);
|
||||
double total = 0;
|
||||
long iters = 0;
|
||||
for (;;) {
|
||||
if (mb_sum_to(100) != 5050.0) b_sums_ok = 0;
|
||||
total = mb_add(1);
|
||||
iters++;
|
||||
if (total != (double)iters) b_adds_ok = 0;
|
||||
if (iters % 25 == 0) mb_collect();
|
||||
if (iters == 200) fixed_loop_done();
|
||||
if (iters >= 200 && stage_get() >= 4) break;
|
||||
}
|
||||
b_reached_200 = iters >= 200;
|
||||
/* A's trap has been delivered; B answers again. */
|
||||
double post_sum = mb_sum_to(10);
|
||||
double post_add = mb_add(5);
|
||||
mb_collect();
|
||||
b_post_ok = post_sum == 55.0 && post_add == total + 5.0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
pthread_t ta, tb;
|
||||
pthread_create(&ta, NULL, worker_a, NULL);
|
||||
pthread_create(&tb, NULL, worker_b, NULL);
|
||||
pthread_join(ta, NULL);
|
||||
pthread_join(tb, NULL);
|
||||
printf("a: bump(1) x200 -> %.0f, calls_seen %.0f, trap fell through %d\n",
|
||||
a_last_bump, a_calls, a_trap_fell_through);
|
||||
printf("a sink: calls=%d ctx_ok=%d fields=%d code=[%s] symbol=[%s] text_printable=%d addr_nonzero=%d\n",
|
||||
rec_a.calls, rec_a.ctx_ok, rec_a.fields, rec_a.code, rec_a.symbol,
|
||||
rec_a.text_printable, rec_a.addr_nonzero);
|
||||
printf("b: concurrent sums_ok=%d adds_ok=%d reached_200=%d\n", b_sums_ok, b_adds_ok, b_reached_200);
|
||||
printf("b: post-trap answers ok=%d\n", b_post_ok);
|
||||
printf("b sink: calls=%d\n", rec_b.calls);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"profile_format": 1,
|
||||
"name": "conformance-multi-a",
|
||||
"entry": "lib_a.ts",
|
||||
"emission": "llvm",
|
||||
"abi": {
|
||||
"prefix": "ma_",
|
||||
"init_symbol": "ma_init",
|
||||
"sink_register_symbol": "ma_set_panic_sink",
|
||||
"collect_symbol": "ma_collect",
|
||||
"result_reset_symbol": null,
|
||||
"localize_runtime": true
|
||||
},
|
||||
"exports": [
|
||||
{ "export": "bump", "symbol": "ma_bump", "params": ["f64"], "returns": "f64" },
|
||||
{ "export": "callsSeen", "symbol": "ma_calls_seen", "params": [], "returns": "f64" },
|
||||
{ "export": "boom", "symbol": "ma_boom", "params": ["f64"], "returns": "f64" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"profile_format": 1,
|
||||
"name": "conformance-multi-b",
|
||||
"entry": "lib_b.ts",
|
||||
"emission": "llvm",
|
||||
"abi": {
|
||||
"prefix": "mb_",
|
||||
"init_symbol": "mb_init",
|
||||
"sink_register_symbol": "mb_set_panic_sink",
|
||||
"collect_symbol": "mb_collect",
|
||||
"result_reset_symbol": null,
|
||||
"localize_runtime": true
|
||||
},
|
||||
"exports": [
|
||||
{ "export": "sumTo", "symbol": "mb_sum_to", "params": ["f64"], "returns": "f64" },
|
||||
{ "export": "add", "symbol": "mb_add", "params": ["f64"], "returns": "f64" }
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user