@scriptc/compiler 0.0.32 → 0.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/backend/cc-cache.test.js +308 -1
  2. package/dist/backend/cc-cache.test.js.map +1 -1
  3. package/dist/backend/cc.d.ts +6 -0
  4. package/dist/backend/cc.js +707 -68
  5. package/dist/backend/cc.js.map +1 -1
  6. package/dist/backend/emission/emit-exprs.js +53 -2
  7. package/dist/backend/emission/emit-exprs.js.map +1 -1
  8. package/dist/backend/emission/emitter.d.ts +12 -5
  9. package/dist/backend/emission/emitter.js +160 -22
  10. package/dist/backend/emission/emitter.js.map +1 -1
  11. package/dist/backend/emission/may-throw.js +7 -4
  12. package/dist/backend/emission/may-throw.js.map +1 -1
  13. package/dist/backend/ffi-callbacks.d.ts +38 -1
  14. package/dist/backend/ffi-callbacks.js +76 -3
  15. package/dist/backend/ffi-callbacks.js.map +1 -1
  16. package/dist/backend/llvm/emitter.js +243 -12
  17. package/dist/backend/llvm/emitter.js.map +1 -1
  18. package/dist/diagnostics/diagnostic.js +2 -0
  19. package/dist/diagnostics/diagnostic.js.map +1 -1
  20. package/dist/ffi/profile.d.ts +17 -5
  21. package/dist/ffi/profile.js +169 -22
  22. package/dist/ffi/profile.js.map +1 -1
  23. package/dist/frontend/lowering/lower-calls.d.ts +1 -0
  24. package/dist/frontend/lowering/lower-calls.js +89 -23
  25. package/dist/frontend/lowering/lower-calls.js.map +1 -1
  26. package/dist/frontend/lowering/lower-containers.js +65 -19
  27. package/dist/frontend/lowering/lower-containers.js.map +1 -1
  28. package/dist/frontend/lowering/lowerer.d.ts +12 -0
  29. package/dist/frontend/lowering/lowerer.js +20 -1
  30. package/dist/frontend/lowering/lowerer.js.map +1 -1
  31. package/dist/frontend/ts7/checker.d.ts +2 -0
  32. package/dist/frontend/ts7/checker.js +20 -3
  33. package/dist/frontend/ts7/checker.js.map +1 -1
  34. package/dist/frontend/types.d.ts +8 -0
  35. package/dist/frontend/types.js +33 -5
  36. package/dist/frontend/types.js.map +1 -1
  37. package/dist/index.js +35 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/ir/nodes.d.ts +22 -9
  40. package/dist/ir/nodes.js +7 -1
  41. package/dist/ir/nodes.js.map +1 -1
  42. package/dist/ir/serialize.d.ts +1 -1
  43. package/dist/ir/serialize.js +1 -1
  44. package/dist/ir/validate.js +74 -1
  45. package/dist/ir/validate.js.map +1 -1
  46. package/dist/library/int-infer.test.js +4 -4
  47. package/dist/library/profile.d.ts +3 -0
  48. package/dist/library/profile.js +9 -1
  49. package/dist/library/profile.js.map +1 -1
  50. package/package.json +2 -2
  51. package/surface-manifest.json +1 -1
@@ -2,9 +2,9 @@ import { execFile, spawnSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
3
  import { constants as fsConstants, existsSync, readdirSync } from "node:fs";
4
4
  import { createRequire } from "node:module";
5
- import { access, chmod, copyFile, link, mkdir, mkdtemp, readdir, readFile, realpath, rename, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
5
+ import { access, chmod, copyFile, link, lstat, mkdir, mkdtemp, readdir, readFile, realpath, rename, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
6
6
  import { availableParallelism, homedir, tmpdir } from "node:os";
7
- import { basename, delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path";
7
+ import { basename, delimiter, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
8
8
  import { promisify } from "node:util";
9
9
  import { localizeElfObject, mergeAndLocalizeCoffObjects } from "./object-localize.js";
10
10
  const execFileAsync = promisify(execFile);
@@ -32,7 +32,7 @@ function stableTestMemo(cache, key, probe) {
32
32
  });
33
33
  return pending;
34
34
  }
35
- const RUNTIME_SOURCES = ["scr_number.c", "scr_string.c", "scr_array.c", "scr_bytes.c", "scr_bytes_io.c", "scr_map.c", "scr_closure.c", "scr_object.c", "scr_union.c", "scr_exception.c", "scr_error.c", "scr_console.c", "scr_lib.c", "scr_path.c", "scr_url.c", "scr_json.c", "scr_async.c", "scr_child.c", "scr_cycle.c"];
35
+ const RUNTIME_SOURCES = ["scr_number.c", "scr_string.c", "scr_array.c", "scr_bytes.c", "scr_bytes_io.c", "scr_map.c", "scr_closure.c", "scr_ffi.c", "scr_object.c", "scr_union.c", "scr_exception.c", "scr_error.c", "scr_console.c", "scr_lib.c", "scr_path.c", "scr_url.c", "scr_json.c", "scr_async.c", "scr_child.c", "scr_cycle.c"];
36
36
  /** The pinned quickjs-ng snapshot under packages/runtime/vendor/quickjs-ng
37
37
  * (see vendor/README.md — update both together). Keys the archive cache so
38
38
  * a vendor bump can never reuse a stale engine build. */
@@ -974,13 +974,14 @@ async function ensureTlsArchive(sanitize, driver, buildIdentity, cacheRoot = ven
974
974
  /** The library base: the executable lane's unconditional sources minus the
975
975
  * fiber/loop and child-process units, plus the library-mode TU. */
976
976
  const LIB_RUNTIME_SOURCES = [
977
- ...RUNTIME_SOURCES.filter((f) => f !== "scr_async.c" && f !== "scr_child.c"),
977
+ ...RUNTIME_SOURCES.filter((f) => f !== "scr_async.c" && f !== "scr_child.c" && f !== "scr_ffi.c"),
978
978
  "scr_library.c",
979
979
  ];
980
980
  export async function compileLibArchive(opts) {
981
981
  const rtDir = runtimeSrcDir();
982
982
  const driver = resolveCc();
983
983
  const sanitize = opts.sanitize ?? false;
984
+ const optimization = opts.optimization ?? "release";
984
985
  const regex = opts.regex ?? false;
985
986
  const sources = [
986
987
  ...LIB_RUNTIME_SOURCES,
@@ -1008,7 +1009,9 @@ export async function compileLibArchive(opts) {
1008
1009
  const cflags = [
1009
1010
  "-std=c11",
1010
1011
  ...driver.targetArgs,
1011
- ...(sanitize ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"] : ["-O2"]),
1012
+ ...(sanitize
1013
+ ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
1014
+ : [optimization === "dev" ? "-O0" : "-O2"]),
1012
1015
  "-fno-math-errno",
1013
1016
  "-fno-strict-aliasing", // the emitted object model type-puns — see compileC's buildArgs
1014
1017
  "-Wno-deprecated-declarations",
@@ -1602,6 +1605,15 @@ async function resolvedToolIdentity(command, env = process.env) {
1602
1605
  return (await resolvedTool(command, env))?.cacheIdentity ?? null;
1603
1606
  }
1604
1607
  const directCompilerDriverMemos = new Map();
1608
+ const directCompilerSelections = new Map();
1609
+ function compilerDriverProbeKey(driver, environmentFingerprint) {
1610
+ return [
1611
+ environmentFingerprint,
1612
+ driver.argv.join("\x1f"),
1613
+ driver.target ?? "<native>",
1614
+ driver.targetArgs.join("\x1f"),
1615
+ ].join("\0");
1616
+ }
1605
1617
  /** `/usr/bin/clang` on Darwin is Apple's immutable driver shim: its `-###`
1606
1618
  * trace names the selected Xcode/CommandLineTools clang rather than the shim
1607
1619
  * itself. The ordinary implicit-toolchain fingerprint below captures that
@@ -1623,12 +1635,7 @@ async function compilerDriverSupportsPersistentCache(driver, environmentFingerpr
1623
1635
  // wrapper. This is deliberately undocumented and test-scoped.
1624
1636
  if (process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1")
1625
1637
  return true;
1626
- const driverKey = [
1627
- environmentFingerprint,
1628
- driver.argv.join("\x1f"),
1629
- driver.target ?? "<native>",
1630
- driver.targetArgs.join("\x1f"),
1631
- ].join("\0");
1638
+ const driverKey = compilerDriverProbeKey(driver, environmentFingerprint);
1632
1639
  const compiler = driver.argv[0] ?? "clang";
1633
1640
  const resolvedDriver = await resolvedTool(compiler);
1634
1641
  // A prior dependency list cannot prove that name resolution is unchanged:
@@ -1669,6 +1676,8 @@ async function compilerDriverSupportsPersistentCache(driver, environmentFingerpr
1669
1676
  direct = effectiveCompiler !== null &&
1670
1677
  (effectiveCompiler.fileIdentity === resolvedDriver.fileIdentity ||
1671
1678
  isAppleSystemClangHandoff(resolvedDriver, effectiveCompiler));
1679
+ if (direct)
1680
+ directCompilerSelections.set(driverKey, effectiveCompiler);
1672
1681
  }
1673
1682
  }
1674
1683
  catch {
@@ -1678,6 +1687,8 @@ async function compilerDriverSupportsPersistentCache(driver, environmentFingerpr
1678
1687
  await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1679
1688
  }
1680
1689
  directCompilerDriverMemos.set(probeKey, direct);
1690
+ if (!direct)
1691
+ directCompilerSelections.delete(driverKey);
1681
1692
  return direct;
1682
1693
  }
1683
1694
  /** Complete library archives may skip `ar`, so the selected archiver must be
@@ -1758,6 +1769,29 @@ async function toolVersionOnce(argv, environmentFingerprint = toolchainEnvironme
1758
1769
  toolVersionFallbacks.set(spellingKey, memo);
1759
1770
  return memo;
1760
1771
  }
1772
+ /** Dependency paths discovered while computing one strict fingerprint. The
1773
+ * output-local cache stamp snapshots these exact files/directories after a
1774
+ * validated build, then can prove a later same-output no-op without spawning
1775
+ * clang again. The content identity closes the gap between hashing and that
1776
+ * metadata snapshot: a file changed in the gap cannot make new bytes ride an
1777
+ * old key. Keep the map bounded for long-lived corpus/test processes. */
1778
+ const fingerprintDependencies = new Map();
1779
+ function rememberFingerprintDependencies(fingerprint, paths, contentPaths = [], contentFingerprint = null) {
1780
+ fingerprintDependencies.set(fingerprint, {
1781
+ paths: [...new Set(paths)].sort(),
1782
+ contentPaths: [...new Set(contentPaths)].sort(),
1783
+ contentFingerprint,
1784
+ });
1785
+ if (fingerprintDependencies.size > 256) {
1786
+ const oldest = fingerprintDependencies.keys().next().value;
1787
+ if (oldest !== undefined)
1788
+ fingerprintDependencies.delete(oldest);
1789
+ }
1790
+ return fingerprint;
1791
+ }
1792
+ function fingerprintDependencyPaths(fingerprint) {
1793
+ return fingerprintDependencies.get(fingerprint)?.paths ?? [];
1794
+ }
1761
1795
  function parseMakeDependencies(output, cwd = process.cwd()) {
1762
1796
  const flattened = output.replace(/\\\r?\n/g, " ");
1763
1797
  const separator = flattened.indexOf(": ");
@@ -1839,13 +1873,14 @@ async function effectiveCompilerInvocationFingerprintFresh(driver, environmentFi
1839
1873
  invocation: normalizedProbeInvocation(invocation, probeDir),
1840
1874
  dependencies,
1841
1875
  dependencyFingerprint: await fingerprintDependencyFiles(dependencies),
1876
+ invocationPaths: await existingDriverTracePaths(`${invocation.stdout}\n${invocation.stderr}`, probeDir, probeDir),
1842
1877
  };
1843
1878
  }
1844
1879
  finally {
1845
1880
  await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1846
1881
  }
1847
1882
  }
1848
- return createHash("sha256")
1883
+ const fingerprint = createHash("sha256")
1849
1884
  .update("effective-compiler-invocation-v2\0")
1850
1885
  .update(environmentFingerprint)
1851
1886
  .update("\0")
@@ -1863,6 +1898,7 @@ async function effectiveCompilerInvocationFingerprintFresh(driver, environmentFi
1863
1898
  .update("\0")
1864
1899
  .update(probe.dependencyFingerprint)
1865
1900
  .digest("hex");
1901
+ return rememberFingerprintDependencies(fingerprint, [...probe.dependencies, ...probe.invocationPaths], probe.dependencies, probe.dependencyFingerprint);
1866
1902
  }
1867
1903
  const stableEffectiveCompilerInvocationMemos = new Map();
1868
1904
  function effectiveCompilerInvocationFingerprint(driver, environmentFingerprint, compileArgs, sourceExtension = ".c") {
@@ -1876,17 +1912,30 @@ function effectiveCompilerInvocationFingerprint(driver, environmentFingerprint,
1876
1912
  }
1877
1913
  async function nativeSourceFiles(directory, recursive, include = (name) => name.endsWith(".c") || name.endsWith(".h")) {
1878
1914
  const files = [];
1879
- const walk = async (current) => {
1915
+ const walk = async (current, ancestors) => {
1916
+ const canonical = await realpath(current).catch(() => resolve(current));
1917
+ if (ancestors.has(canonical))
1918
+ return;
1919
+ const nestedAncestors = new Set(ancestors).add(canonical);
1880
1920
  const entries = await readdir(current, { withFileTypes: true }).catch(() => []);
1881
1921
  for (const entry of entries) {
1882
1922
  const path = join(current, entry.name);
1883
- if (entry.isDirectory() && recursive)
1884
- await walk(path);
1885
- else if (entry.isFile() && include(entry.name))
1923
+ if (entry.isDirectory() && recursive) {
1924
+ await walk(path, nestedAncestors);
1925
+ }
1926
+ else if (entry.isSymbolicLink()) {
1927
+ const target = await stat(path).catch(() => null);
1928
+ if (target?.isDirectory() && recursive)
1929
+ await walk(path, nestedAncestors);
1930
+ else if (target?.isFile() && include(entry.name))
1931
+ files.push(path);
1932
+ }
1933
+ else if (entry.isFile() && include(entry.name)) {
1886
1934
  files.push(path);
1935
+ }
1887
1936
  }
1888
1937
  };
1889
- await walk(directory);
1938
+ await walk(directory, new Set());
1890
1939
  return files.sort();
1891
1940
  }
1892
1941
  /** Every system-header spelling consumed by a source tree scriptc owns. The
@@ -1938,6 +1987,18 @@ async function fingerprintDependencyFiles(paths) {
1938
1987
  }
1939
1988
  return hash.digest("hex");
1940
1989
  }
1990
+ async function fingerprintDependenciesStillMatch(fingerprints) {
1991
+ const distinct = [...new Set(fingerprints)];
1992
+ const identities = distinct
1993
+ .map((fingerprint) => fingerprintDependencies.get(fingerprint))
1994
+ // A restored native-metadata stamp carries and revalidates the dependency
1995
+ // snapshot from the process that minted this fingerprint. Only identities
1996
+ // computed in this process have an additional content hash to recheck here.
1997
+ .filter((identity) => identity !== undefined);
1998
+ return (await Promise.all(identities.map(async (identity) => identity.contentFingerprint === null ||
1999
+ await fingerprintDependencyFiles(identity.contentPaths).catch(() => null) ===
2000
+ identity.contentFingerprint))).every(Boolean);
2001
+ }
1941
2002
  /** Exact headers selected while preprocessing the caller's translation unit.
1942
2003
  * The shared toolchain probe covers the runtime/vendor trees, but compileC is
1943
2004
  * also a public API: an opted-in caller can include a system or header-only SDK
@@ -1946,7 +2007,7 @@ async function fingerprintDependencyFiles(paths) {
1946
2007
  * quote-include directory and compiler-visible source spelling. */
1947
2008
  async function translationUnitDependencyFingerprintFresh(driver, cflags, sourcePath, sourceBytes, environmentFingerprint) {
1948
2009
  if (sourcePath.endsWith(".ll")) {
1949
- return createHash("sha256").update("translation-unit-dependencies-v1\0llvm-ir").digest("hex");
2010
+ return rememberFingerprintDependencies(createHash("sha256").update("translation-unit-dependencies-v1\0llvm-ir").digest("hex"), []);
1950
2011
  }
1951
2012
  const compiler = driver.argv[0] ?? "clang";
1952
2013
  const compilerIdentity = await resolvedToolIdentity(compiler);
@@ -1983,7 +2044,7 @@ async function translationUnitDependencyFingerprintFresh(driver, cflags, sourceP
1983
2044
  await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
1984
2045
  }
1985
2046
  }
1986
- return createHash("sha256")
2047
+ const fingerprint = createHash("sha256")
1987
2048
  .update("translation-unit-dependencies-v1\0")
1988
2049
  .update(environmentFingerprint)
1989
2050
  .update("\0")
@@ -1993,6 +2054,7 @@ async function translationUnitDependencyFingerprintFresh(driver, cflags, sourceP
1993
2054
  .update("\0")
1994
2055
  .update(probe.dependencyFingerprint)
1995
2056
  .digest("hex");
2057
+ return rememberFingerprintDependencies(fingerprint, probe.dependencies, probe.dependencies, probe.dependencyFingerprint);
1996
2058
  }
1997
2059
  const stableTranslationUnitDependencyMemos = new Map();
1998
2060
  function translationUnitDependencyFingerprint(driver, cflags, sourcePath, sourceBytes, environmentFingerprint) {
@@ -2086,10 +2148,15 @@ async function implicitToolchainFingerprintFresh(driver, environmentFingerprint)
2086
2148
  compilerInvocation: normalizedProbeInvocation(compilerInvocation, probeDir),
2087
2149
  dependencies: dependencyPaths,
2088
2150
  dependencyFingerprint: await fingerprintDependencyFiles(dependencyPaths),
2089
- tools: await Promise.all(toolSpellings.map(async (spelling) => ({
2090
- spelling,
2091
- identity: await resolvedToolIdentity(spelling),
2092
- }))),
2151
+ invocationPaths: await existingDriverTracePaths(`${compilerInvocation.stdout}\n${compilerInvocation.stderr}`, probeDir, probeDir),
2152
+ tools: await Promise.all(toolSpellings.map(async (spelling) => {
2153
+ const resolved = await resolvedTool(spelling);
2154
+ return {
2155
+ spelling,
2156
+ identity: resolved?.cacheIdentity ?? null,
2157
+ path: resolved?.canonicalPath ?? null,
2158
+ };
2159
+ })),
2093
2160
  };
2094
2161
  }
2095
2162
  finally {
@@ -2120,7 +2187,11 @@ async function implicitToolchainFingerprintFresh(driver, environmentFingerprint)
2120
2187
  .update(currentIdentity ?? tool.identity ?? "<unresolved>")
2121
2188
  .update("\0");
2122
2189
  }
2123
- return hash.digest("hex");
2190
+ return rememberFingerprintDependencies(hash.digest("hex"), [
2191
+ ...probe.dependencies,
2192
+ ...probe.invocationPaths,
2193
+ ...probe.tools.flatMap((tool) => tool.path === null ? [] : [tool.path]),
2194
+ ], probe.dependencies, probe.dependencyFingerprint);
2124
2195
  }
2125
2196
  const stableImplicitToolchainMemos = new Map();
2126
2197
  function implicitToolchainFingerprint(driver, environmentFingerprint) {
@@ -2156,6 +2227,51 @@ function driverTraceCandidates(line) {
2156
2227
  }
2157
2228
  return candidates;
2158
2229
  }
2230
+ async function existingDriverTracePaths(output, cwd, excludedRoot) {
2231
+ const candidates = new Set();
2232
+ for (const line of output.split(/\r?\n/)) {
2233
+ const tokens = driverTraceCandidates(line);
2234
+ for (let index = 0; index < tokens.length; index++) {
2235
+ const token = tokens[index];
2236
+ const joinedPathOption = ["-I", "-L", "-F"].find((option) => token.startsWith(option) && token.length > option.length);
2237
+ const equalsPathOption = ["--sysroot=", "-resource-dir="].find((option) => token.startsWith(option));
2238
+ const separatePathOptions = [
2239
+ "-I",
2240
+ "-L",
2241
+ "-F",
2242
+ "--sysroot",
2243
+ "-isysroot",
2244
+ "-resource-dir",
2245
+ "-isystem",
2246
+ "-iquote",
2247
+ "-internal-isystem",
2248
+ "-internal-externc-isystem",
2249
+ "-internal-iframework",
2250
+ ];
2251
+ const optionPath = joinedPathOption !== undefined
2252
+ ? token.slice(joinedPathOption.length)
2253
+ : equalsPathOption !== undefined
2254
+ ? token.slice(equalsPathOption.length)
2255
+ : separatePathOptions.includes(token)
2256
+ ? tokens[index + 1] ?? ""
2257
+ : token;
2258
+ if (!isAbsolute(optionPath))
2259
+ continue;
2260
+ const path = resolve(cwd, optionPath);
2261
+ if (path === excludedRoot ||
2262
+ path.startsWith(`${excludedRoot}/`) ||
2263
+ path.startsWith(`${excludedRoot}\\`)) {
2264
+ continue;
2265
+ }
2266
+ candidates.add(path);
2267
+ }
2268
+ }
2269
+ const existing = await Promise.all([...candidates].map(async (path) => [path, await lstat(path).catch(() => null)]));
2270
+ return existing
2271
+ .filter((entry) => entry[1] !== null)
2272
+ .map(([path]) => path)
2273
+ .sort();
2274
+ }
2159
2275
  export async function parseLinkTraceFiles(output, cwd, excludedRoot, driverDryRun = false) {
2160
2276
  const files = new Set();
2161
2277
  for (const line of output.split(/\r?\n/)) {
@@ -2241,9 +2357,15 @@ async function implicitLinkerFingerprintFresh(driver, environmentFingerprint, li
2241
2357
  linkerInvocation: normalizedProbeInvocation(driverInvocation, probeDir),
2242
2358
  dependencies,
2243
2359
  dependencyFingerprint: await fingerprintDependencyFiles(dependencies),
2360
+ invocationPaths: await existingDriverTracePaths(`${driverInvocation.stdout}\n${driverInvocation.stderr}`, probeDir, probeDir),
2244
2361
  linker: {
2245
2362
  spelling: linkerSpelling,
2246
- identity: linkerSpelling === "" ? null : await resolvedToolIdentity(linkerSpelling),
2363
+ identity: linkerSpelling === ""
2364
+ ? null
2365
+ : (await resolvedTool(linkerSpelling))?.cacheIdentity ?? null,
2366
+ path: linkerSpelling === ""
2367
+ ? null
2368
+ : (await resolvedTool(linkerSpelling))?.canonicalPath ?? null,
2247
2369
  },
2248
2370
  };
2249
2371
  }
@@ -2254,7 +2376,7 @@ async function implicitLinkerFingerprintFresh(driver, environmentFingerprint, li
2254
2376
  const linkerIdentity = probe.linker.spelling === ""
2255
2377
  ? null
2256
2378
  : await resolvedToolIdentity(probe.linker.spelling);
2257
- return createHash("sha256")
2379
+ const fingerprint = createHash("sha256")
2258
2380
  .update("implicit-linker-v3\0")
2259
2381
  .update(environmentFingerprint)
2260
2382
  .update("\0")
@@ -2280,6 +2402,11 @@ async function implicitLinkerFingerprintFresh(driver, environmentFingerprint, li
2280
2402
  .update("\0")
2281
2403
  .update(linkerIdentity ?? probe.linker.identity ?? "<unresolved>")
2282
2404
  .digest("hex");
2405
+ return rememberFingerprintDependencies(fingerprint, [
2406
+ ...probe.dependencies,
2407
+ ...probe.invocationPaths,
2408
+ ...(probe.linker.path === null ? [] : [probe.linker.path]),
2409
+ ], probe.dependencies, probe.dependencyFingerprint);
2283
2410
  }
2284
2411
  const stableImplicitLinkerMemos = new Map();
2285
2412
  function implicitLinkerFingerprint(driver, environmentFingerprint, linkArgs, effectiveInvocationArgs, traceInvocationArgs) {
@@ -2301,21 +2428,18 @@ function ccacheAvailable() {
2301
2428
  ccacheMemo ??= execFileAsync("ccache", ["--version"]).then(() => true, () => false);
2302
2429
  return ccacheMemo;
2303
2430
  }
2304
- /** Content hash of every .c/.h in the runtime src dir, the Ryū sources
2431
+ /** Content hash of every .c/.h in the runtime src tree, the Ryū sources
2305
2432
  * textually included by scr_number.c, and the separately-built vendor pins —
2306
2433
  * everything a binary links that the emitted C bytes don't already cover
2307
2434
  * (npm-embedded C rides inside the emitted C; the engine and standalone
2308
2435
  * vendor archives are pinned by their version constants). The small source
2309
- * tree is hashed on every identity calculation: a stat-only memo can miss a
2310
- * same-size edit whose timestamp was preserved by a copy/sync tool. */
2436
+ * tree is hashed on every identity calculation: recursive enumeration is
2437
+ * required because a newly added nested header can begin shadowing a system
2438
+ * include without changing any previously selected dependency, while content
2439
+ * hashing catches same-size edits whose timestamp was preserved by a copy/sync
2440
+ * tool. */
2311
2441
  async function runtimeFingerprintFresh(rtDir) {
2312
- const groups = await Promise.all([
2313
- { label: "runtime", dir: rtDir },
2314
- { label: "ryu", dir: join(rtDir, "..", "vendor", "ryu") },
2315
- ].map(async (group) => {
2316
- const names = (await readdir(group.dir)).filter((n) => n.endsWith(".c") || n.endsWith(".h")).sort();
2317
- return { ...group, names };
2318
- }));
2442
+ const groups = await runtimeFingerprintInputGroups(rtDir);
2319
2443
  const h = createHash("sha256").update(QJS_COMMIT).update(MBEDTLS_VERSION).update(ZLIB_VERSION);
2320
2444
  for (const group of groups) {
2321
2445
  for (const n of group.names) {
@@ -2324,6 +2448,20 @@ async function runtimeFingerprintFresh(rtDir) {
2324
2448
  }
2325
2449
  return h.digest("hex");
2326
2450
  }
2451
+ async function runtimeFingerprintInputGroups(rtDir) {
2452
+ return Promise.all([
2453
+ { label: "runtime", dir: rtDir },
2454
+ { label: "ryu", dir: join(rtDir, "..", "vendor", "ryu") },
2455
+ ].map(async (group) => {
2456
+ const names = (await nativeSourceFiles(group.dir, true))
2457
+ .map((path) => relative(group.dir, path))
2458
+ .sort();
2459
+ return { ...group, names };
2460
+ }));
2461
+ }
2462
+ async function runtimeFingerprintInputPaths(rtDir) {
2463
+ return (await runtimeFingerprintInputGroups(rtDir)).flatMap((group) => group.names.map((name) => join(group.dir, name)));
2464
+ }
2327
2465
  const stableRuntimeFingerprintMemos = new Map();
2328
2466
  export function runtimeFingerprint(rtDir) {
2329
2467
  const key = resolve(rtDir);
@@ -2412,6 +2550,279 @@ async function publishCachedFile(source, destination) {
2412
2550
  ]);
2413
2551
  }
2414
2552
  }
2553
+ async function directoryTreeDigest(root, excludedPaths = []) {
2554
+ const hash = createHash("sha256").update("native-dependency-tree-v1\0");
2555
+ const visited = new Set();
2556
+ const excluded = excludedPaths.map((path) => resolve(path));
2557
+ const walk = async (directory, relative) => {
2558
+ const canonical = await realpath(directory);
2559
+ if (visited.has(canonical)) {
2560
+ hash.update(relative).update("\0cycle\0").update(canonical).update("\0");
2561
+ return;
2562
+ }
2563
+ visited.add(canonical);
2564
+ const entries = await readdir(directory, { withFileTypes: true });
2565
+ entries.sort((a, b) => a.name.localeCompare(b.name));
2566
+ for (const entry of entries) {
2567
+ const path = join(directory, entry.name);
2568
+ const absolute = resolve(path);
2569
+ if (excluded.some((candidate) => absolute === candidate ||
2570
+ absolute.startsWith(`${candidate}/`) ||
2571
+ absolute.startsWith(`${candidate}\\`)))
2572
+ continue;
2573
+ const child = relative === "" ? entry.name : `${relative}/${entry.name}`;
2574
+ const info = await lstat(path);
2575
+ const kind = info.isDirectory()
2576
+ ? "directory"
2577
+ : info.isFile()
2578
+ ? "file"
2579
+ : info.isSymbolicLink()
2580
+ ? "symlink"
2581
+ : "other";
2582
+ hash.update(child).update("\0").update(kind).update("\0");
2583
+ if (kind === "symlink") {
2584
+ const target = await realpath(path).catch(() => "<missing>");
2585
+ hash.update(target).update("\0");
2586
+ const targetInfo = await stat(path).catch(() => null);
2587
+ if (targetInfo?.isDirectory())
2588
+ await walk(path, child);
2589
+ }
2590
+ if (kind === "directory")
2591
+ await walk(path, child);
2592
+ }
2593
+ visited.delete(canonical);
2594
+ };
2595
+ await walk(root, "");
2596
+ return hash.digest("hex");
2597
+ }
2598
+ function localDependencyKind(info) {
2599
+ return info.isFile()
2600
+ ? "file"
2601
+ : info.isDirectory()
2602
+ ? "directory"
2603
+ : info.isSymbolicLink()
2604
+ ? "symlink"
2605
+ : null;
2606
+ }
2607
+ async function snapshotLocalArtifactDependency(path, treeExclusions = null) {
2608
+ const info = await lstat(path);
2609
+ const kind = localDependencyKind(info);
2610
+ if (kind === null)
2611
+ throw new Error(`unsupported local artifact dependency: ${path}`);
2612
+ const dependency = {
2613
+ path,
2614
+ kind,
2615
+ dev: info.dev,
2616
+ ino: info.ino,
2617
+ size: info.size,
2618
+ mtimeMs: info.mtimeMs,
2619
+ ctimeMs: info.ctimeMs,
2620
+ };
2621
+ if (kind === "directory" && treeExclusions !== null) {
2622
+ dependency.treeExclusions = [...new Set(treeExclusions.map((entry) => resolve(entry)))].sort();
2623
+ dependency.treeDigest = await directoryTreeDigest(path, dependency.treeExclusions);
2624
+ }
2625
+ if (kind === "symlink") {
2626
+ const targetPath = await realpath(path);
2627
+ const target = await stat(path);
2628
+ const targetKind = target.isFile() ? "file" : target.isDirectory() ? "directory" : null;
2629
+ if (targetKind === null)
2630
+ throw new Error(`unsupported symlink target dependency: ${path}`);
2631
+ dependency.targetPath = targetPath;
2632
+ dependency.targetKind = targetKind;
2633
+ dependency.targetDev = target.dev;
2634
+ dependency.targetIno = target.ino;
2635
+ dependency.targetSize = target.size;
2636
+ dependency.targetMtimeMs = target.mtimeMs;
2637
+ dependency.targetCtimeMs = target.ctimeMs;
2638
+ if (targetKind === "directory" && treeExclusions !== null) {
2639
+ dependency.treeExclusions = [...new Set(treeExclusions.map((entry) => resolve(entry)))].sort();
2640
+ dependency.treeDigest = await directoryTreeDigest(path, dependency.treeExclusions);
2641
+ }
2642
+ }
2643
+ return dependency;
2644
+ }
2645
+ async function snapshotLocalArtifactDependencies(dependencyPaths, recursiveDirectories = [], recursiveExclusions = []) {
2646
+ const recursive = new Set(recursiveDirectories.map((path) => resolve(path)));
2647
+ return Promise.all([...new Set(dependencyPaths)].sort().map((path) => snapshotLocalArtifactDependency(path, recursive.has(resolve(path)) ? recursiveExclusions : null)));
2648
+ }
2649
+ async function localArtifactDependenciesStillMatch(dependencies) {
2650
+ return (await Promise.all(dependencies.map(async (dependency) => {
2651
+ const current = await snapshotLocalArtifactDependency(dependency.path, dependency.treeDigest === undefined ? null : dependency.treeExclusions ?? []).catch(() => null);
2652
+ return current !== null && JSON.stringify(current) === JSON.stringify(dependency);
2653
+ }))).every(Boolean);
2654
+ }
2655
+ function nativeMetadataStampPath(root, key) {
2656
+ return join(root, "meta", createHash("sha256").update(key).digest("hex"));
2657
+ }
2658
+ function nativeMetadataStampIntegrity(stamp) {
2659
+ return createHash("sha256")
2660
+ .update("native-metadata-stamp-v2\0")
2661
+ .update(JSON.stringify(stamp))
2662
+ .digest("hex");
2663
+ }
2664
+ async function readNativeMetadataStamp(root, key) {
2665
+ try {
2666
+ const stamp = JSON.parse(await readFile(nativeMetadataStampPath(root, key), "utf8"));
2667
+ if (stamp.version !== 2 ||
2668
+ stamp.key !== key ||
2669
+ stamp.values === null ||
2670
+ typeof stamp.values !== "object" ||
2671
+ !Array.isArray(stamp.dependencies) ||
2672
+ !/^[0-9a-f]{64}$/.test(stamp.integrity) ||
2673
+ nativeMetadataStampIntegrity({
2674
+ version: stamp.version,
2675
+ key: stamp.key,
2676
+ values: stamp.values,
2677
+ dependencies: stamp.dependencies,
2678
+ }) !== stamp.integrity ||
2679
+ !(await localArtifactDependenciesStillMatch(stamp.dependencies))) {
2680
+ return null;
2681
+ }
2682
+ return stamp;
2683
+ }
2684
+ catch {
2685
+ return null;
2686
+ }
2687
+ }
2688
+ async function publishNativeMetadataStamp(root, key, values, dependencyPaths, fingerprints = []) {
2689
+ const destination = nativeMetadataStampPath(root, key);
2690
+ await mkdir(dirname(destination), { recursive: true });
2691
+ const dependencies = await snapshotLocalArtifactDependencies(dependencyPaths);
2692
+ if (!(await fingerprintDependenciesStillMatch(fingerprints))) {
2693
+ throw new CacheInputsChangedError();
2694
+ }
2695
+ const unsigned = { version: 2, key, values, dependencies };
2696
+ const stamp = {
2697
+ ...unsigned,
2698
+ integrity: nativeMetadataStampIntegrity(unsigned),
2699
+ };
2700
+ const tmp = `${destination}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2701
+ try {
2702
+ await writeFile(tmp, `${JSON.stringify(stamp)}\n`, { mode: 0o600 });
2703
+ await rename(tmp, destination);
2704
+ }
2705
+ finally {
2706
+ await rm(tmp, { force: true }).catch(() => undefined);
2707
+ }
2708
+ return stamp;
2709
+ }
2710
+ function nativeMetadataKey(kind, parts) {
2711
+ const hash = createHash("sha256").update(`native-metadata-${kind}-v2\0`);
2712
+ for (const part of parts) {
2713
+ hash.update(typeof part === "string" ? part : part.join("\x1f")).update("\0");
2714
+ }
2715
+ return `${kind}-${hash.digest("hex")}`;
2716
+ }
2717
+ /** The caller-visible output is itself the cheapest safe cache tier. Once a
2718
+ * generated TU has produced this exact binary, an unchanged rebuild need not
2719
+ * rediscover every SDK header and linker input merely to copy equivalent bytes
2720
+ * back onto the same path. This tier is deliberately narrower than the CAS:
2721
+ * only frontend-generated programs with no caller-owned native inputs opt in.
2722
+ * The generated TU bytes, every scriptc runtime source, the selected direct
2723
+ * compiler inode, target/options/environment, and the output path all join the
2724
+ * key. A digest rejects a modified/truncated output before the no-op hit. */
2725
+ function localArtifactIdentity(opts, driver, environmentFingerprint, compilerIdentity, runtimeHash, programBytes) {
2726
+ const normalizedOptions = Object.fromEntries(Object.entries(opts)
2727
+ .filter(([, value]) => value !== undefined)
2728
+ .sort(([a], [b]) => a.localeCompare(b)));
2729
+ return createHash("sha256")
2730
+ .update("local-artifact-v1\0")
2731
+ .update(cacheTargetIdentity(driver)).update("\0")
2732
+ .update(environmentFingerprint).update("\0")
2733
+ .update(compilerIdentity).update("\0")
2734
+ .update(runtimeHash).update("\0")
2735
+ .update(driver.argv.join("\x1f")).update("\0")
2736
+ .update(driver.targetArgs.join("\x1f")).update("\0")
2737
+ .update(driver.linkArgs.join("\x1f")).update("\0")
2738
+ .update(process.env["SCRIPTC_FETCH_CURL"] === "1" ? "fetch-curl" : "fetch-native").update("\0")
2739
+ .update(JSON.stringify(normalizedOptions)).update("\0")
2740
+ .update(resolve(opts.cPath)).update("\0")
2741
+ .update(resolve(opts.outPath)).update("\0")
2742
+ .update(programBytes)
2743
+ .digest("hex");
2744
+ }
2745
+ function localArtifactStampPath(root, outPath) {
2746
+ const outputKey = createHash("sha256").update(resolve(outPath)).digest("hex");
2747
+ return join(root, "local", outputKey);
2748
+ }
2749
+ function localArtifactStampIntegrity(stamp) {
2750
+ return createHash("sha256")
2751
+ .update("local-artifact-stamp-v2\0")
2752
+ .update(JSON.stringify(stamp))
2753
+ .digest("hex");
2754
+ }
2755
+ async function localArtifactHit(stampPath, outPath, key) {
2756
+ try {
2757
+ const stamp = JSON.parse(await readFile(stampPath, "utf8"));
2758
+ const output = await lstat(outPath);
2759
+ const expectedMode = 0o777 & ~process.umask();
2760
+ if (stamp.version !== 2 ||
2761
+ stamp.key !== key ||
2762
+ !/^[0-9a-f]{64}$/.test(stamp.digest ?? "") ||
2763
+ !Array.isArray(stamp.dependencies) ||
2764
+ !/^[0-9a-f]{64}$/.test(stamp.integrity ?? "") ||
2765
+ localArtifactStampIntegrity({
2766
+ version: stamp.version,
2767
+ key: stamp.key,
2768
+ digest: stamp.digest,
2769
+ dependencies: stamp.dependencies,
2770
+ }) !== stamp.integrity ||
2771
+ !output.isFile() ||
2772
+ (output.mode & 0o777) !== expectedMode ||
2773
+ stamp.dependencies.some((dependency) => dependency === null ||
2774
+ typeof dependency !== "object" ||
2775
+ typeof dependency.path !== "string" ||
2776
+ dependency.kind !== "file" &&
2777
+ dependency.kind !== "directory" &&
2778
+ dependency.kind !== "symlink" ||
2779
+ typeof dependency.dev !== "number" ||
2780
+ typeof dependency.ino !== "number" ||
2781
+ typeof dependency.size !== "number" ||
2782
+ typeof dependency.mtimeMs !== "number" ||
2783
+ typeof dependency.ctimeMs !== "number" ||
2784
+ dependency.treeDigest !== undefined && typeof dependency.treeDigest !== "string" ||
2785
+ dependency.kind === "symlink" && (typeof dependency.targetPath !== "string" ||
2786
+ dependency.targetKind !== "file" && dependency.targetKind !== "directory" ||
2787
+ typeof dependency.targetDev !== "number" ||
2788
+ typeof dependency.targetIno !== "number" ||
2789
+ typeof dependency.targetSize !== "number" ||
2790
+ typeof dependency.targetMtimeMs !== "number" ||
2791
+ typeof dependency.targetCtimeMs !== "number")) ||
2792
+ !(await localArtifactDependenciesStillMatch(stamp.dependencies)) ||
2793
+ await fileDigest(outPath) !== stamp.digest) {
2794
+ return false;
2795
+ }
2796
+ const now = new Date();
2797
+ await utimes(stampPath, now, now).catch(() => undefined);
2798
+ return true;
2799
+ }
2800
+ catch {
2801
+ return false;
2802
+ }
2803
+ }
2804
+ async function publishLocalArtifactStamp(stampPath, outPath, key, dependencyPaths, recursiveDirectories = [], recursiveExclusions = []) {
2805
+ await mkdir(dirname(stampPath), { recursive: true });
2806
+ const tmp = `${stampPath}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
2807
+ try {
2808
+ const dependencies = await snapshotLocalArtifactDependencies(dependencyPaths, recursiveDirectories, recursiveExclusions);
2809
+ const unsigned = {
2810
+ version: 2,
2811
+ key,
2812
+ digest: await fileDigest(outPath),
2813
+ dependencies,
2814
+ };
2815
+ const stamp = {
2816
+ ...unsigned,
2817
+ integrity: localArtifactStampIntegrity(unsigned),
2818
+ };
2819
+ await writeFile(tmp, `${JSON.stringify(stamp)}\n`, { mode: 0o600 });
2820
+ await rename(tmp, stampPath);
2821
+ }
2822
+ finally {
2823
+ await rm(tmp, { force: true }).catch(() => undefined);
2824
+ }
2825
+ }
2415
2826
  /** The cached .o set for one flag flavor, compiled on first need. Concurrent
2416
2827
  * first builds (parallel test workers on a cold cache) may duplicate work;
2417
2828
  * per-file atomic renames make every winner equivalent. Publication is held
@@ -2445,7 +2856,22 @@ async function ensureRuntimeObjects(root, ccArgv, cflags, sources, keyPrefix, ve
2445
2856
  await Promise.all(missing.slice(i, i + width).map(async (src) => {
2446
2857
  const tmpObj = join(tmpDir, `${basename(src, ".c")}.o`);
2447
2858
  const argv = [...(useCcache ? ["ccache"] : []), ...ccArgv, ...cflags, "-c", src, "-o", tmpObj];
2448
- await execFileAsync(argv[0] ?? "clang", argv.slice(1));
2859
+ await execFileAsync(argv[0] ?? "clang", argv.slice(1), useCcache
2860
+ ? {
2861
+ // ccache direct mode remembers only the headers selected by
2862
+ // its previous manifest and can miss a newly created,
2863
+ // higher-priority header. The scriptc object-set key already
2864
+ // includes the recursive runtime namespace fingerprint, so
2865
+ // carry it into ccache's own keyspace as well.
2866
+ env: {
2867
+ ...process.env,
2868
+ CCACHE_NAMESPACE: [
2869
+ process.env["CCACHE_NAMESPACE"],
2870
+ `scriptc-${setKey}`,
2871
+ ].filter((value) => value !== undefined && value !== "").join(":"),
2872
+ },
2873
+ }
2874
+ : undefined);
2449
2875
  compiled.set(src, { object: tmpObj, digest: await fileDigest(tmpObj) });
2450
2876
  }));
2451
2877
  }
@@ -2657,10 +3083,88 @@ export async function compileC(opts) {
2657
3083
  root = null;
2658
3084
  }
2659
3085
  }
3086
+ let localArtifact = null;
3087
+ // Generated executable TUs are closed over scriptc's own runtime tree. A
3088
+ // same-output rebuild can therefore check those bytes directly before the
3089
+ // broader cross-output CAS performs its compiler/SDK/linker rediscovery.
3090
+ // FFI/native-input builds and the public arbitrary-C cache API stay on the
3091
+ // strict path because their dependency graphs are caller-owned.
3092
+ if (root !== null &&
3093
+ cachePolicy.completeArtifacts &&
3094
+ opts.cacheIdentity === "scriptc-generated-v1" &&
3095
+ (opts.linkInputs?.length ?? 0) === 0 &&
3096
+ (opts.systemLibraries?.length ?? 0) === 0 &&
3097
+ process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] !== "1") {
3098
+ try {
3099
+ const [compiler, runtimeHash, programBytes] = await Promise.all([
3100
+ resolvedTool(driver.argv[0] ?? "clang"),
3101
+ runtimeFingerprint(rtDir),
3102
+ readFile(opts.cPath),
3103
+ ]);
3104
+ if (compiler !== null) {
3105
+ const effectiveCompiler = directCompilerSelections.get(compilerDriverProbeKey(driver, toolchainEnv)) ?? compiler;
3106
+ const key = localArtifactIdentity(opts, driver, toolchainEnv, `${compiler.cacheIdentity}\0${effectiveCompiler.cacheIdentity}`, runtimeHash, programBytes);
3107
+ const stampPath = localArtifactStampPath(root, opts.outPath);
3108
+ localArtifact = {
3109
+ stampPath,
3110
+ key,
3111
+ runtimeHash,
3112
+ programBytes,
3113
+ compilerPath: effectiveCompiler.canonicalPath,
3114
+ };
3115
+ if (await localArtifactHit(stampPath, opts.outPath, key))
3116
+ return;
3117
+ }
3118
+ }
3119
+ catch {
3120
+ // The output-local tier is only an optimization; the fully validated
3121
+ // CAS below remains the source of truth on any metadata trouble.
3122
+ localArtifact = null;
3123
+ }
3124
+ }
2660
3125
  let implicitToolchain = null;
3126
+ let toolchainMetadataStamp = null;
3127
+ const metadataCompiler = root === null
3128
+ ? null
3129
+ : await resolvedTool(driver.argv[0] ?? "clang");
3130
+ const metadataEffectiveCompiler = directCompilerSelections.get(compilerDriverProbeKey(driver, toolchainEnv)) ?? metadataCompiler;
3131
+ const toolchainMetadataKey = root === null ||
3132
+ metadataCompiler === null ||
3133
+ metadataEffectiveCompiler === null ||
3134
+ process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
3135
+ ? null
3136
+ : nativeMetadataKey("toolchain", [
3137
+ cacheTargetIdentity(driver),
3138
+ toolchainEnv,
3139
+ driver.argv,
3140
+ driver.targetArgs,
3141
+ metadataCompiler.cacheIdentity,
3142
+ metadataEffectiveCompiler.cacheIdentity,
3143
+ rtDir,
3144
+ ]);
2661
3145
  if (persistentDriverCache) {
2662
3146
  try {
2663
- implicitToolchain = await implicitToolchainFingerprint(driver, toolchainEnv);
3147
+ toolchainMetadataStamp =
3148
+ root === null || toolchainMetadataKey === null
3149
+ ? null
3150
+ : await readNativeMetadataStamp(root, toolchainMetadataKey);
3151
+ implicitToolchain = toolchainMetadataStamp?.values["implicitToolchain"] ??
3152
+ await implicitToolchainFingerprint(driver, toolchainEnv);
3153
+ let compilerVersion = toolchainMetadataStamp?.values["compilerVersion"];
3154
+ if (compilerVersion === undefined) {
3155
+ compilerVersion = await ccVersionOnce(driver.argv, toolchainEnv, true);
3156
+ }
3157
+ if (toolchainMetadataStamp === null &&
3158
+ root !== null &&
3159
+ toolchainMetadataKey !== null) {
3160
+ if (metadataCompiler !== null && metadataEffectiveCompiler !== null) {
3161
+ toolchainMetadataStamp = await publishNativeMetadataStamp(root, toolchainMetadataKey, { implicitToolchain, compilerVersion }, [
3162
+ metadataCompiler.canonicalPath,
3163
+ metadataEffectiveCompiler.canonicalPath,
3164
+ ...fingerprintDependencyPaths(implicitToolchain),
3165
+ ], [implicitToolchain]);
3166
+ }
3167
+ }
2664
3168
  }
2665
3169
  catch {
2666
3170
  // Cache discovery is best-effort. In particular, a compiler wrapper can
@@ -2817,6 +3321,7 @@ export async function compileC(opts) {
2817
3321
  ...(opts.http2 ?? false ? [rt(join(rtDir, "scr_http2.c"))] : []),
2818
3322
  ...(opts.dgram ? [rt(join(rtDir, "scr_dgram.c"))] : []),
2819
3323
  ...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []),
3324
+ ...(opts.foreignFfi ? [rt(join(rtDir, "scr_ffi_queue.c"))] : []),
2820
3325
  ...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []),
2821
3326
  // The CA-store unit rides its own gate OR the tls one: scr_tls.c
2822
3327
  // references its default-set override unconditionally.
@@ -2959,12 +3464,63 @@ export async function compileC(opts) {
2959
3464
  };
2960
3465
  let runtimeCompilerInvocation = null;
2961
3466
  let programCompilerInvocation = null;
3467
+ let payloadMetadata = null;
3468
+ let compileMetadataStamp = null;
3469
+ const compileMetadataKey = root === null || process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
3470
+ ? null
3471
+ : nativeMetadataKey("compile", [
3472
+ cacheTargetIdentity(driver),
3473
+ toolchainEnv,
3474
+ implicitToolchain ?? "<uncached>",
3475
+ driver.argv,
3476
+ cflags,
3477
+ programCompilerArgs,
3478
+ programSourceExtension,
3479
+ ]);
2962
3480
  if (root !== null) {
2963
3481
  try {
2964
- runtimeCompilerInvocation = await effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags);
2965
- programCompilerInvocation = programSourceExtension === ".ll"
2966
- ? await effectiveCompilerInvocationFingerprint(driver, toolchainEnv, programCompilerArgs, programSourceExtension)
2967
- : runtimeCompilerInvocation;
3482
+ // These probes inspect disjoint inputs. Start the payload reads here as
3483
+ // well so runtime hashing and clang's dry-run traces overlap instead of
3484
+ // forming a serial prelude before every cache lookup.
3485
+ payloadMetadata = Promise.all([
3486
+ Promise.resolve(toolchainMetadataStamp?.values["compilerVersion"] ??
3487
+ ccVersionOnce(driver.argv, toolchainEnv, true)),
3488
+ localArtifact === null
3489
+ ? runtimeFingerprint(rtDir)
3490
+ : Promise.resolve(localArtifact.runtimeHash),
3491
+ localArtifact === null
3492
+ ? readFile(opts.cPath)
3493
+ : Promise.resolve(localArtifact.programBytes),
3494
+ ]);
3495
+ compileMetadataStamp = compileMetadataKey === null
3496
+ ? null
3497
+ : await readNativeMetadataStamp(root, compileMetadataKey);
3498
+ if (compileMetadataStamp !== null) {
3499
+ runtimeCompilerInvocation = compileMetadataStamp.values["runtimeInvocation"] ?? null;
3500
+ programCompilerInvocation = compileMetadataStamp.values["programInvocation"] ?? null;
3501
+ if (runtimeCompilerInvocation === null || programCompilerInvocation === null) {
3502
+ compileMetadataStamp = null;
3503
+ }
3504
+ }
3505
+ if (compileMetadataStamp === null) {
3506
+ const [runtimeInvocation, programInvocation] = await Promise.all([
3507
+ effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags),
3508
+ programSourceExtension === ".ll"
3509
+ ? effectiveCompilerInvocationFingerprint(driver, toolchainEnv, programCompilerArgs, programSourceExtension)
3510
+ : Promise.resolve(null),
3511
+ ]);
3512
+ runtimeCompilerInvocation = runtimeInvocation;
3513
+ programCompilerInvocation = programInvocation ?? runtimeInvocation;
3514
+ if (compileMetadataKey !== null) {
3515
+ compileMetadataStamp = await publishNativeMetadataStamp(root, compileMetadataKey, {
3516
+ runtimeInvocation: runtimeCompilerInvocation,
3517
+ programInvocation: programCompilerInvocation,
3518
+ }, [
3519
+ ...fingerprintDependencyPaths(runtimeCompilerInvocation),
3520
+ ...fingerprintDependencyPaths(programCompilerInvocation),
3521
+ ], [runtimeCompilerInvocation, programCompilerInvocation]);
3522
+ }
3523
+ }
2968
3524
  }
2969
3525
  catch {
2970
3526
  // Preserve the uncached build for wrappers that compile successfully but
@@ -2989,11 +3545,11 @@ export async function compileC(opts) {
2989
3545
  let fingerprint;
2990
3546
  let cBytes;
2991
3547
  try {
2992
- [cv, fingerprint, cBytes] = await Promise.all([
3548
+ [cv, fingerprint, cBytes] = await (payloadMetadata ?? Promise.all([
2993
3549
  ccVersionOnce(driver.argv, toolchainEnv, true),
2994
3550
  runtimeFingerprint(rtDir),
2995
3551
  readFile(opts.cPath),
2996
- ]);
3552
+ ]));
2997
3553
  }
2998
3554
  catch {
2999
3555
  // A version/fingerprint probe is an optimization boundary. If the compiler
@@ -3012,16 +3568,6 @@ export async function compileC(opts) {
3012
3568
  (opts.linkInputs?.length ?? 0) === 0 &&
3013
3569
  (opts.systemLibraries?.length ?? 0) === 0;
3014
3570
  let programDependencies = null;
3015
- if (cacheCompleteArtifact) {
3016
- try {
3017
- programDependencies = await translationUnitDependencyFingerprint(driver, cflags, opts.cPath, cBytes, toolchainEnv);
3018
- }
3019
- catch {
3020
- // Program-header discovery is needed only for a complete hit. Runtime
3021
- // objects remain safe because they never contain the caller's TU.
3022
- cacheCompleteArtifact = false;
3023
- }
3024
- }
3025
3571
  const linkProbeArgs = [
3026
3572
  ...(sanitize ? ["-fsanitize=address"] : []),
3027
3573
  ...threadArgs,
@@ -3062,15 +3608,89 @@ export async function compileC(opts) {
3062
3608
  ? effectiveLinkInvocationArgs
3063
3609
  : effectiveLinkInvocationArgs.filter((arg) => arg !== `-L${curlStubDir}`);
3064
3610
  let implicitLinker = null;
3611
+ let preBuildDependencies = null;
3612
+ let localArtifactDependencyPaths = null;
3613
+ let linkMetadataStamp = null;
3614
+ const linkMetadataKey = root === null || process.env["SCRIPTC_TEST_TRUST_COMPILER_WRAPPER"] === "1"
3615
+ ? null
3616
+ : nativeMetadataKey("link", [
3617
+ cacheTargetIdentity(driver),
3618
+ toolchainEnv,
3619
+ implicitToolchain ?? "<uncached>",
3620
+ runtimeCompilerInvocation ?? "<uncached>",
3621
+ programCompilerInvocation ?? "<uncached>",
3622
+ driver.argv,
3623
+ linkProbeArgs,
3624
+ effectiveLinkInvocationArgs,
3625
+ linkTraceInvocationArgs,
3626
+ ]);
3065
3627
  if (cacheCompleteArtifact) {
3066
3628
  try {
3067
- implicitLinker = await implicitLinkerFingerprint(driver, toolchainEnv, linkProbeArgs, effectiveLinkInvocationArgs, linkTraceInvocationArgs);
3629
+ // Header discovery and linker tracing are independent subprocess trees.
3630
+ // Running them together removes one complete probe round-trip from both
3631
+ // cache hits and ordinary edit/build misses without changing either key.
3632
+ linkMetadataStamp = linkMetadataKey === null
3633
+ ? null
3634
+ : await readNativeMetadataStamp(root, linkMetadataKey);
3635
+ if (linkMetadataStamp !== null) {
3636
+ implicitLinker = linkMetadataStamp.values["implicitLinker"] ?? null;
3637
+ if (implicitLinker === null)
3638
+ linkMetadataStamp = null;
3639
+ }
3640
+ [programDependencies, implicitLinker] = await Promise.all([
3641
+ translationUnitDependencyFingerprint(driver, cflags, opts.cPath, cBytes, toolchainEnv),
3642
+ linkMetadataStamp === null
3643
+ ? implicitLinkerFingerprint(driver, toolchainEnv, linkProbeArgs, effectiveLinkInvocationArgs, linkTraceInvocationArgs)
3644
+ : Promise.resolve(implicitLinker),
3645
+ ]);
3646
+ if (linkMetadataStamp === null && linkMetadataKey !== null) {
3647
+ linkMetadataStamp = await publishNativeMetadataStamp(root, linkMetadataKey, { implicitLinker }, fingerprintDependencyPaths(implicitLinker), [implicitLinker]);
3648
+ }
3649
+ preBuildDependencies = await snapshotLocalArtifactDependencies([
3650
+ ...(await runtimeFingerprintInputPaths(rtDir)),
3651
+ ...(toolchainMetadataStamp?.dependencies.map((dependency) => dependency.path) ??
3652
+ fingerprintDependencyPaths(implicitToolchain)),
3653
+ ...(compileMetadataStamp?.dependencies.map((dependency) => dependency.path) ?? []),
3654
+ ...(linkMetadataStamp?.dependencies.map((dependency) => dependency.path) ??
3655
+ fingerprintDependencyPaths(implicitLinker)),
3656
+ ...fingerprintDependencyPaths(programDependencies),
3657
+ ]);
3658
+ // Every content-bearing fingerprint was computed before this metadata
3659
+ // snapshot. Re-read those exact files now so the snapshot cannot certify
3660
+ // bytes that changed after hashing but before the final compile starts.
3661
+ if (!(await fingerprintDependenciesStillMatch([
3662
+ implicitToolchain,
3663
+ runtimeCompilerInvocation,
3664
+ programCompilerInvocation,
3665
+ implicitLinker,
3666
+ programDependencies,
3667
+ ])) ||
3668
+ await runtimeFingerprint(rtDir).catch(() => null) !== fingerprint ||
3669
+ !(await Promise.all([toolchainMetadataStamp, compileMetadataStamp, linkMetadataStamp]
3670
+ .filter((stamp) => stamp !== null)
3671
+ .map((stamp) => localArtifactDependenciesStillMatch(stamp.dependencies)))).every(Boolean)) {
3672
+ throw new CacheInputsChangedError();
3673
+ }
3674
+ if (localArtifact !== null) {
3675
+ // Native metadata stamps persist their dependency paths across CLI
3676
+ // processes; fingerprintDependencyPaths deliberately does not. Build
3677
+ // the output-local stamp from this complete validated snapshot so a
3678
+ // source edit in a fresh process cannot replace it with only the two
3679
+ // process-local fallback paths.
3680
+ localArtifactDependencyPaths = [
3681
+ localArtifact.compilerPath,
3682
+ dirname(resolve(opts.cPath)),
3683
+ ...preBuildDependencies.map((dependency) => dependency.path),
3684
+ ];
3685
+ }
3068
3686
  }
3069
3687
  catch {
3070
- // Some compiler wrappers/linkers do not implement trace mode. Runtime
3071
- // objects remain safely cacheable, but a complete executable cannot be
3072
- // keyed without knowing every implicit link input.
3688
+ // Program-header discovery and linker tracing are both required for a
3689
+ // complete hit. Runtime objects remain safely cacheable if either probe
3690
+ // is unavailable.
3073
3691
  cacheCompleteArtifact = false;
3692
+ preBuildDependencies = null;
3693
+ localArtifactDependencyPaths = null;
3074
3694
  }
3075
3695
  }
3076
3696
  const binDir = join(root, "bin");
@@ -3123,6 +3743,9 @@ export async function compileC(opts) {
3123
3743
  // cache entry populated by a less restrictive shell must not widen access.
3124
3744
  await chmod(tmpOut, 0o777 & ~process.umask());
3125
3745
  await rename(tmpOut, opts.outPath);
3746
+ if (localArtifact !== null && localArtifactDependencyPaths !== null) {
3747
+ await publishLocalArtifactStamp(localArtifact.stampPath, opts.outPath, localArtifact.key, localArtifactDependencyPaths, [dirname(resolve(opts.cPath))], [root]).catch(() => undefined);
3748
+ }
3126
3749
  return; // hit: the program/runtime payload compile and link were skipped
3127
3750
  }
3128
3751
  catch {
@@ -3158,18 +3781,25 @@ export async function compileC(opts) {
3158
3781
  await writeFile(programPath, cBytes);
3159
3782
  let objects = null;
3160
3783
  let cacheInputsStable = true;
3161
- let objectImplicitVerification = null;
3784
+ let strictObjectVerification = null;
3162
3785
  const objectImplicitToolchainStillMatches = () => {
3163
- objectImplicitVerification ??= Promise.all([
3786
+ if (preBuildDependencies !== null) {
3787
+ return localArtifactDependenciesStillMatch(preBuildDependencies);
3788
+ }
3789
+ // Complete-artifact caching can be disabled by caller-owned native
3790
+ // inputs while the safe runtime-object tier remains active. Preserve its
3791
+ // strict discovery fallback in that posture.
3792
+ strictObjectVerification ??= Promise.all([
3793
+ runtimeFingerprint(rtDir),
3164
3794
  implicitToolchainFingerprint(driver, toolchainEnv),
3165
3795
  effectiveCompilerInvocationFingerprint(driver, toolchainEnv, cflags),
3166
- ]).then(([currentImplicit, currentInvocation]) => currentImplicit === implicitToolchain &&
3796
+ ]).then(([currentRuntime, currentImplicit, currentInvocation]) => currentRuntime === fingerprint &&
3797
+ currentImplicit === implicitToolchain &&
3167
3798
  currentInvocation === runtimeCompilerInvocation, () => false);
3168
- return objectImplicitVerification;
3799
+ return strictObjectVerification;
3169
3800
  };
3170
3801
  try {
3171
- const cached = await ensureRuntimeObjects(root, driver.argv, cflags, rtInputs, `obj-v5\0${cacheTargetIdentity(driver)}\0${toolchainEnv}\0${implicitToolchain}\0${runtimeCompilerInvocation}\0${ccName}\0${cv}\0${fingerprint}\0`, async () => (await runtimeFingerprint(rtDir)) === fingerprint &&
3172
- (await objectImplicitToolchainStillMatches()));
3802
+ const cached = await ensureRuntimeObjects(root, driver.argv, cflags, rtInputs, `obj-v5\0${cacheTargetIdentity(driver)}\0${toolchainEnv}\0${implicitToolchain}\0${runtimeCompilerInvocation}\0${ccName}\0${cv}\0${fingerprint}\0`, async () => await objectImplicitToolchainStillMatches());
3173
3803
  objects = await stageRuntimeObjects(cached, join(buildDir, "runtime-objects"));
3174
3804
  }
3175
3805
  catch (err) {
@@ -3180,10 +3810,11 @@ export async function compileC(opts) {
3180
3810
  await runClang(buildArgs((p) => objects?.get(p) ?? p, { programPath, outPath: privateOut, compilerVisibleSource: opts.cPath }));
3181
3811
  await installArtifact(privateOut, opts.outPath);
3182
3812
  if (cachedBin !== null && keyHex !== null) {
3183
- // This check is deliberately fresh rather than the object-publication
3184
- // verification above. The final program compile/link can itself race an
3185
- // SDK header, compiler, linker, CRT, or system-library replacement; its
3186
- // bytes must not be published under the pre-build identity in that case.
3813
+ // Metadata comparison catches ordinary changes cheaply, but cannot by
3814
+ // itself prove the snapshot was taken from the same bytes hashed into
3815
+ // the key. Recompute every content-bearing identity after the final link
3816
+ // so a header/SDK/compiler change in either pre-build gap cannot publish
3817
+ // new output under an old key.
3187
3818
  const [currentRuntime, currentImplicit, currentRuntimeInvocation, currentProgramInvocation, currentProgramDependencies, currentLinker, currentCompiler] = await Promise.all([
3188
3819
  runtimeFingerprint(rtDir).catch(() => null),
3189
3820
  implicitToolchainFingerprint(driver, toolchainEnv).catch(() => null),
@@ -3195,6 +3826,8 @@ export async function compileC(opts) {
3195
3826
  ]);
3196
3827
  cacheInputsStable =
3197
3828
  cacheInputsStable &&
3829
+ preBuildDependencies !== null &&
3830
+ await localArtifactDependenciesStillMatch(preBuildDependencies) &&
3198
3831
  currentRuntime === fingerprint &&
3199
3832
  currentImplicit === implicitToolchain &&
3200
3833
  currentRuntimeInvocation === runtimeCompilerInvocation &&
@@ -3214,6 +3847,12 @@ export async function compileC(opts) {
3214
3847
  /* publishing is best-effort */
3215
3848
  }
3216
3849
  }
3850
+ if (localArtifact !== null &&
3851
+ localArtifactDependencyPaths !== null &&
3852
+ cacheCompleteArtifact &&
3853
+ cacheInputsStable) {
3854
+ await publishLocalArtifactStamp(localArtifact.stampPath, opts.outPath, localArtifact.key, localArtifactDependencyPaths, [dirname(resolve(opts.cPath))], [root]).catch(() => undefined);
3855
+ }
3217
3856
  }
3218
3857
  finally {
3219
3858
  await rm(buildDir, { recursive: true, force: true }).catch(() => undefined);