Files
scriptc/tests/harness/deadstrip.test.ts
T
Malte UblandClaude Opus 5.5 26b703a821 Keep React Flight's development-only stack parsing out of production builds
React Flight's production server never runs its V8 structured-stack parser
(ReactFlightStackConfigV8): every use is behind __DEV__. Builds compiled from
its sources keep that code behind `throw new Error('... should never be
called in production mode')` stubs, so scriptc lowered it, failed on
Error.prepareStackTrace, and a dead `.stack` read turned on stack-trace mode
(a frame push in every function) for the whole program.

- Statements after return/throw/break/continue in a function body are not
  lowered. Declarations keep their hoisting through the existing forward
  machinery (later function declarations, `var` reads undefined, `let`/
  `const` TDZ); class, enum, module, import/export and generic function
  declarations still lower. Functions referenced only from such code are
  unreached and dead-stripped.
- Stack-trace mode requires a stack read reachable through live code
  (observesStacks).
- Synchronous functions in executables push and pop their source frame
  inline instead of calling scr_stack_enter/leave.
- Functions that only perform operations which never create or throw an
  Error, and only call such functions, keep no frame (captureFreeFunctions):
  no capture can include them.

Error.prepareStackTrace and CallSite stay refused: only development Flight
runs them. Good code is byte-identical (zero-cost gate: bench and
self-hosted compiler IR, LLVM and executables).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 12:44:31 -07:00

112 lines
4.8 KiB
TypeScript

/* Reachability's build contract — scriptc-only, deliberately NOT
* differential (tests/corpus/6170-flight-dev-stacks-dead pins statement-level
* reachability differentially; here its emitted LLVM is grepped):
*
* - The dead-strip side (a program importing a module whose UNUSED exports
* hold rejected constructs must build and run) is covered differentially
* by tests/corpus/420-dead-strip-modules. Here the same program's emitted
* C is grepped: an unreached body leaves NO trace — no function, no async
* spawn wrapper, no generic instance.
* - The failure side cannot be differential (Node runs the "reached"
* variants fine): the SAME constructs, reached, must fail the build with
* exactly the diagnostics they always produced.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { compile } from "@scriptc/compiler";
const repoRoot = join(import.meta.dirname, "../..");
const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests");
const sanitize = process.env["SCRIPTC_SAN"] === "1";
test("unreached bodies leave no trace in the emitted LLVM", async () => {
const outDir = join(cacheDir, `deadstrip-c${sanitize ? "-san" : ""}`);
const result = await compile(join(repoRoot, "tests/corpus/420-dead-strip-modules/main.ts"), {
outPath: join(outDir, "program"),
outDir,
sanitize,
backend: "llvm",
});
if (!result.ok) {
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
}
const c = readFileSync(result.llvmPath, "utf8");
// Reached bodies are there (mangled names embed the source name)...
for (const reached of ["used", "double", "evenSteps", "oddSteps", "usedMethod", "tag"]) {
expect(c, `expected reached '${reached}' in the emitted LLVM`).toContain(reached);
}
// ...unreached ones are not: no function body, no async spawn wrapper or
// trampoline (unusedAsync), no monomorphized instance (unusedGeneric),
// no vtable slot or method function (unusedMethod).
for (const gone of [
"unusedForIn",
"unusedAsync",
"unusedComptime",
"unusedGeneric",
"unusedMethod",
]) {
expect(c, `unreached '${gone}' must leave no trace in the emitted LLVM`).not.toContain(gone);
}
});
test("statements after an abrupt statement leave no trace and no stack frames", async () => {
const outDir = join(cacheDir, `deadstrip-abrupt${sanitize ? "-san" : ""}`);
const result = await compile(join(repoRoot, "tests/corpus/6170-flight-dev-stacks-dead.ts"), {
outPath: join(outDir, "program"),
outDir,
sanitize,
backend: "llvm",
});
if (!result.ok) {
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
}
const c = readFileSync(result.llvmPath, "utf8");
for (const reached of ["outlineComponentInfo", "emitOutlinedDebugModelChunk", "renderModel"]) {
expect(c, `expected reached '${reached}' in the emitted LLVM`).toContain(reached);
}
// The development-only stack parser is referenced only after the
// production stubs' throw: none of it is emitted, and the program keeps
// no source frames (its only stack read never runs).
for (const gone of [
"parseStackTrace",
"collectStackTrace",
"getMethodCallName",
"renderDebugModel",
]) {
expect(c, `unreached '${gone}' must leave no trace in the emitted LLVM`).not.toContain(gone);
}
expect(c).not.toContain("source_frame");
expect(c).not.toContain("scr_stack_enter");
});
/** Compiles an in-repo fixture and returns its diagnostics codes+messages.
* Deliberately NO backend pin: every program here fails during lowering,
* before any backend runs — the default is irrelevant by construction, and
* pinning would claim otherwise. */
async function diagnosticsOf(file: string): Promise<string[]> {
const outDir = join(cacheDir, "deadstrip-reached");
const result = await compile(join(repoRoot, file), {
outPath: join(outDir, "never"),
outDir,
});
if (result.ok) throw new Error(`${file} compiled but must fail`);
return result.diagnostics.map((d) => `${d.code}: ${d.message}`);
}
describe("the same constructs, reached, fail exactly as before", () => {
test("a reached class-instance for-in fails the build", async () => {
const diags = await diagnosticsOf("tests/deadstrip/reached-for-in.ts");
expect(diags).toEqual([
"SC1052: for-in over class instances (which keys exist on an instance depends on runtime property creation the class model does not track — for-in over records and arrays compiles) are not supported yet",
]);
});
test("a reached comptime throw fails at compile time", async () => {
const diags = await diagnosticsOf("tests/deadstrip/reached-comptime-throw.ts");
expect(diags).toHaveLength(1);
expect(diags[0]).toContain("SC1110");
expect(diags[0]).toContain("the callback threw");
});
});