@scriptc/compiler 0.0.0 → 0.0.2

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 (206) hide show
  1. package/LICENSE +202 -0
  2. package/ambient/package.json +3 -0
  3. package/ambient/scriptc-node-fallback.d.ts +3001 -0
  4. package/ambient/scriptc-overrides.d.ts +95 -0
  5. package/ambient/scriptc.d.ts +74 -0
  6. package/dist/backend/cc.d.ts +169 -0
  7. package/dist/backend/cc.js +897 -0
  8. package/dist/backend/cc.js.map +1 -0
  9. package/dist/backend/emission/emit-async.d.ts +145 -0
  10. package/dist/backend/emission/emit-async.js +996 -0
  11. package/dist/backend/emission/emit-async.js.map +1 -0
  12. package/dist/backend/emission/emit-exprs.d.ts +3 -0
  13. package/dist/backend/emission/emit-exprs.js +5949 -0
  14. package/dist/backend/emission/emit-exprs.js.map +1 -0
  15. package/dist/backend/emission/emit-island.d.ts +45 -0
  16. package/dist/backend/emission/emit-island.js +271 -0
  17. package/dist/backend/emission/emit-island.js.map +1 -0
  18. package/dist/backend/emission/emit-shapes.d.ts +142 -0
  19. package/dist/backend/emission/emit-shapes.js +575 -0
  20. package/dist/backend/emission/emit-shapes.js.map +1 -0
  21. package/dist/backend/emission/emit-stmts.d.ts +78 -0
  22. package/dist/backend/emission/emit-stmts.js +960 -0
  23. package/dist/backend/emission/emit-stmts.js.map +1 -0
  24. package/dist/backend/emission/emit-types.d.ts +65 -0
  25. package/dist/backend/emission/emit-types.js +652 -0
  26. package/dist/backend/emission/emit-types.js.map +1 -0
  27. package/dist/backend/emission/emit-walkers.d.ts +154 -0
  28. package/dist/backend/emission/emit-walkers.js +1587 -0
  29. package/dist/backend/emission/emit-walkers.js.map +1 -0
  30. package/dist/backend/emission/emitter.d.ts +452 -0
  31. package/dist/backend/emission/emitter.js +1249 -0
  32. package/dist/backend/emission/emitter.js.map +1 -0
  33. package/dist/backend/emission/may-throw.d.ts +15 -0
  34. package/dist/backend/emission/may-throw.js +250 -0
  35. package/dist/backend/emission/may-throw.js.map +1 -0
  36. package/dist/backend/llvm/blocks.d.ts +21 -0
  37. package/dist/backend/llvm/blocks.js +68 -0
  38. package/dist/backend/llvm/blocks.js.map +1 -0
  39. package/dist/backend/llvm/classes.d.ts +84 -0
  40. package/dist/backend/llvm/classes.js +438 -0
  41. package/dist/backend/llvm/classes.js.map +1 -0
  42. package/dist/backend/llvm/dyn.d.ts +123 -0
  43. package/dist/backend/llvm/dyn.js +2624 -0
  44. package/dist/backend/llvm/dyn.js.map +1 -0
  45. package/dist/backend/llvm/emitter.d.ts +3 -0
  46. package/dist/backend/llvm/emitter.js +10102 -0
  47. package/dist/backend/llvm/emitter.js.map +1 -0
  48. package/dist/backend/llvm/shapes.d.ts +78 -0
  49. package/dist/backend/llvm/shapes.js +754 -0
  50. package/dist/backend/llvm/shapes.js.map +1 -0
  51. package/dist/backend/llvm/unsupported.d.ts +6 -0
  52. package/dist/backend/llvm/unsupported.js +12 -0
  53. package/dist/backend/llvm/unsupported.js.map +1 -0
  54. package/dist/backend/llvm/walkers.d.ts +58 -0
  55. package/dist/backend/llvm/walkers.js +793 -0
  56. package/dist/backend/llvm/walkers.js.map +1 -0
  57. package/dist/backend/mangle.d.ts +124 -0
  58. package/dist/backend/mangle.js +232 -0
  59. package/dist/backend/mangle.js.map +1 -0
  60. package/dist/coverage/report.d.ts +65 -0
  61. package/dist/coverage/report.js +238 -0
  62. package/dist/coverage/report.js.map +1 -0
  63. package/dist/diagnostics/diagnostic.d.ts +140 -0
  64. package/dist/diagnostics/diagnostic.js +458 -0
  65. package/dist/diagnostics/diagnostic.js.map +1 -0
  66. package/dist/diagnostics/render.d.ts +11 -0
  67. package/dist/diagnostics/render.js +58 -0
  68. package/dist/diagnostics/render.js.map +1 -0
  69. package/dist/frontend/cjs-lexer.d.ts +20 -0
  70. package/dist/frontend/cjs-lexer.js +813 -0
  71. package/dist/frontend/cjs-lexer.js.map +1 -0
  72. package/dist/frontend/lowering/http2-constants.d.ts +1 -0
  73. package/dist/frontend/lowering/http2-constants.js +251 -0
  74. package/dist/frontend/lowering/http2-constants.js.map +1 -0
  75. package/dist/frontend/lowering/lib-boundary.d.ts +6 -0
  76. package/dist/frontend/lowering/lib-boundary.js +143 -0
  77. package/dist/frontend/lowering/lib-boundary.js.map +1 -0
  78. package/dist/frontend/lowering/lower-assert.d.ts +18 -0
  79. package/dist/frontend/lowering/lower-assert.js +1269 -0
  80. package/dist/frontend/lowering/lower-assert.js.map +1 -0
  81. package/dist/frontend/lowering/lower-builtins.d.ts +511 -0
  82. package/dist/frontend/lowering/lower-builtins.js +5249 -0
  83. package/dist/frontend/lowering/lower-builtins.js.map +1 -0
  84. package/dist/frontend/lowering/lower-calls.d.ts +581 -0
  85. package/dist/frontend/lowering/lower-calls.js +6286 -0
  86. package/dist/frontend/lowering/lower-calls.js.map +1 -0
  87. package/dist/frontend/lowering/lower-classes.d.ts +727 -0
  88. package/dist/frontend/lowering/lower-classes.js +4701 -0
  89. package/dist/frontend/lowering/lower-classes.js.map +1 -0
  90. package/dist/frontend/lowering/lower-comptime.d.ts +53 -0
  91. package/dist/frontend/lowering/lower-comptime.js +244 -0
  92. package/dist/frontend/lowering/lower-comptime.js.map +1 -0
  93. package/dist/frontend/lowering/lower-containers.d.ts +486 -0
  94. package/dist/frontend/lowering/lower-containers.js +6345 -0
  95. package/dist/frontend/lowering/lower-containers.js.map +1 -0
  96. package/dist/frontend/lowering/lower-dgram.d.ts +20 -0
  97. package/dist/frontend/lowering/lower-dgram.js +319 -0
  98. package/dist/frontend/lowering/lower-dgram.js.map +1 -0
  99. package/dist/frontend/lowering/lower-emitter.d.ts +10 -0
  100. package/dist/frontend/lowering/lower-emitter.js +688 -0
  101. package/dist/frontend/lowering/lower-emitter.js.map +1 -0
  102. package/dist/frontend/lowering/lower-enums.d.ts +22 -0
  103. package/dist/frontend/lowering/lower-enums.js +235 -0
  104. package/dist/frontend/lowering/lower-enums.js.map +1 -0
  105. package/dist/frontend/lowering/lower-expando.d.ts +35 -0
  106. package/dist/frontend/lowering/lower-expando.js +276 -0
  107. package/dist/frontend/lowering/lower-expando.js.map +1 -0
  108. package/dist/frontend/lowering/lower-exprs.d.ts +524 -0
  109. package/dist/frontend/lowering/lower-exprs.js +8111 -0
  110. package/dist/frontend/lowering/lower-exprs.js.map +1 -0
  111. package/dist/frontend/lowering/lower-generators.d.ts +50 -0
  112. package/dist/frontend/lowering/lower-generators.js +421 -0
  113. package/dist/frontend/lowering/lower-generators.js.map +1 -0
  114. package/dist/frontend/lowering/lower-inspect.d.ts +20 -0
  115. package/dist/frontend/lowering/lower-inspect.js +1074 -0
  116. package/dist/frontend/lowering/lower-inspect.js.map +1 -0
  117. package/dist/frontend/lowering/lower-island.d.ts +108 -0
  118. package/dist/frontend/lowering/lower-island.js +650 -0
  119. package/dist/frontend/lowering/lower-island.js.map +1 -0
  120. package/dist/frontend/lowering/lower-mixins.d.ts +96 -0
  121. package/dist/frontend/lowering/lower-mixins.js +534 -0
  122. package/dist/frontend/lowering/lower-mixins.js.map +1 -0
  123. package/dist/frontend/lowering/lower-modules.d.ts +135 -0
  124. package/dist/frontend/lowering/lower-modules.js +1613 -0
  125. package/dist/frontend/lowering/lower-modules.js.map +1 -0
  126. package/dist/frontend/lowering/lower-namespaces.d.ts +168 -0
  127. package/dist/frontend/lowering/lower-namespaces.js +706 -0
  128. package/dist/frontend/lowering/lower-namespaces.js.map +1 -0
  129. package/dist/frontend/lowering/lower-server.d.ts +90 -0
  130. package/dist/frontend/lowering/lower-server.js +3471 -0
  131. package/dist/frontend/lowering/lower-server.js.map +1 -0
  132. package/dist/frontend/lowering/lower-stmts.d.ts +361 -0
  133. package/dist/frontend/lowering/lower-stmts.js +5341 -0
  134. package/dist/frontend/lowering/lower-stmts.js.map +1 -0
  135. package/dist/frontend/lowering/lower-stream.d.ts +88 -0
  136. package/dist/frontend/lowering/lower-stream.js +1533 -0
  137. package/dist/frontend/lowering/lower-stream.js.map +1 -0
  138. package/dist/frontend/lowering/lower-test.d.ts +27 -0
  139. package/dist/frontend/lowering/lower-test.js +353 -0
  140. package/dist/frontend/lowering/lower-test.js.map +1 -0
  141. package/dist/frontend/lowering/lowerer.d.ts +1748 -0
  142. package/dist/frontend/lowering/lowerer.js +6257 -0
  143. package/dist/frontend/lowering/lowerer.js.map +1 -0
  144. package/dist/frontend/lowering/surfaces.d.ts +272 -0
  145. package/dist/frontend/lowering/surfaces.js +1096 -0
  146. package/dist/frontend/lowering/surfaces.js.map +1 -0
  147. package/dist/frontend/npm-static.d.ts +55 -0
  148. package/dist/frontend/npm-static.js +300 -0
  149. package/dist/frontend/npm-static.js.map +1 -0
  150. package/dist/frontend/npm.d.ts +330 -0
  151. package/dist/frontend/npm.js +1259 -0
  152. package/dist/frontend/npm.js.map +1 -0
  153. package/dist/frontend/program.d.ts +186 -0
  154. package/dist/frontend/program.js +2255 -0
  155. package/dist/frontend/program.js.map +1 -0
  156. package/dist/frontend/provenance-registry.d.ts +48 -0
  157. package/dist/frontend/provenance-registry.js +87 -0
  158. package/dist/frontend/provenance-registry.js.map +1 -0
  159. package/dist/frontend/provenance.d.ts +6 -0
  160. package/dist/frontend/provenance.js +459 -0
  161. package/dist/frontend/provenance.js.map +1 -0
  162. package/dist/frontend/resolve.d.ts +56 -0
  163. package/dist/frontend/resolve.js +682 -0
  164. package/dist/frontend/resolve.js.map +1 -0
  165. package/dist/frontend/shared.d.ts +67 -0
  166. package/dist/frontend/shared.js +241 -0
  167. package/dist/frontend/shared.js.map +1 -0
  168. package/dist/frontend/ts7/adapter.d.ts +7 -0
  169. package/dist/frontend/ts7/adapter.js +54 -0
  170. package/dist/frontend/ts7/adapter.js.map +1 -0
  171. package/dist/frontend/ts7/ast.d.ts +50 -0
  172. package/dist/frontend/ts7/ast.js +211 -0
  173. package/dist/frontend/ts7/ast.js.map +1 -0
  174. package/dist/frontend/ts7/census-check.d.ts +1 -0
  175. package/dist/frontend/ts7/census-check.js +12 -0
  176. package/dist/frontend/ts7/census-check.js.map +1 -0
  177. package/dist/frontend/ts7/checker.d.ts +140 -0
  178. package/dist/frontend/ts7/checker.js +544 -0
  179. package/dist/frontend/ts7/checker.js.map +1 -0
  180. package/dist/frontend/ts7/enums.d.ts +18 -0
  181. package/dist/frontend/ts7/enums.js +43 -0
  182. package/dist/frontend/ts7/enums.js.map +1 -0
  183. package/dist/frontend/ts7/program.d.ts +99 -0
  184. package/dist/frontend/ts7/program.js +278 -0
  185. package/dist/frontend/ts7/program.js.map +1 -0
  186. package/dist/frontend/ts7/world-check.d.ts +3 -0
  187. package/dist/frontend/ts7/world-check.js +34 -0
  188. package/dist/frontend/ts7/world-check.js.map +1 -0
  189. package/dist/frontend/types.d.ts +205 -0
  190. package/dist/frontend/types.js +2485 -0
  191. package/dist/frontend/types.js.map +1 -0
  192. package/dist/index.d.ts +85 -0
  193. package/dist/index.js +429 -0
  194. package/dist/index.js.map +1 -0
  195. package/dist/ir/nodes.d.ts +4334 -0
  196. package/dist/ir/nodes.js +1906 -0
  197. package/dist/ir/nodes.js.map +1 -0
  198. package/dist/ir/serialize.d.ts +4 -0
  199. package/dist/ir/serialize.js +34 -0
  200. package/dist/ir/serialize.js.map +1 -0
  201. package/dist/ir/validate.d.ts +36 -0
  202. package/dist/ir/validate.js +4885 -0
  203. package/dist/ir/validate.js.map +1 -0
  204. package/package.json +30 -6
  205. package/README.md +0 -3
  206. package/index.js +0 -2
@@ -0,0 +1,897 @@
1
+ import { execFile } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { createRequire } from "node:module";
4
+ import { chmod, copyFile, mkdir, mkdtemp, readdir, readFile, rename, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
5
+ import { availableParallelism, tmpdir } from "node:os";
6
+ import { basename, dirname, join } from "node:path";
7
+ import { promisify } from "node:util";
8
+ const execFileAsync = promisify(execFile);
9
+ 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"];
10
+ /** The pinned quickjs-ng snapshot under packages/runtime/vendor/quickjs-ng
11
+ * (see vendor/README.md — update both together). Keys the archive cache so
12
+ * a vendor bump can never reuse a stale engine build. */
13
+ const QJS_COMMIT = "3c8f3d68953955950074c41c6e4d999562ae82a7";
14
+ /** The pinned mbedTLS snapshot under packages/runtime/vendor/mbedtls (see
15
+ * vendor/README.md — update both together). Keys the archive cache so a
16
+ * vendor bump can never reuse a stale TLS build, and joins the runtime
17
+ * fingerprint so cached binaries re-key too. */
18
+ const MBEDTLS_VERSION = "3.6.7";
19
+ /** The pinned zlib snapshot under packages/runtime/vendor/zlib (see
20
+ * vendor/README.md — update both together). Keys the per-target object
21
+ * cache and joins the runtime fingerprint. Host builds never compile it —
22
+ * they keep the historical system `-lz` link; the vendored copy exists so
23
+ * zlib-using programs CROSS-compile (zig has no target sysroot libz). */
24
+ const ZLIB_VERSION = "1.3.1";
25
+ export function runtimeSrcDir() {
26
+ const require = createRequire(import.meta.url);
27
+ return join(dirname(require.resolve("@scriptc/runtime/package.json")), "src");
28
+ }
29
+ export function resolveCc(env = process.env) {
30
+ const cc = env["SCRIPTC_CC"] ?? "";
31
+ const target = env["SCRIPTC_TARGET"] ?? "";
32
+ if (cc === "" || cc === "clang") {
33
+ if (target !== "") {
34
+ throw new Error(`SCRIPTC_TARGET=${target} requires SCRIPTC_CC=zigcc — the default clang path has no cross-target sysroots.`);
35
+ }
36
+ return { argv: ["clang"], target: null, targetArgs: [] };
37
+ }
38
+ if (cc !== "zigcc") {
39
+ throw new Error(`unknown SCRIPTC_CC '${cc}' (supported: clang, zigcc)`);
40
+ }
41
+ if (target === "")
42
+ return { argv: ["zig", "cc"], target: null, targetArgs: [] };
43
+ return {
44
+ argv: ["zig", "cc"],
45
+ target,
46
+ targetArgs: ["-target", target, ...(target.includes("linux") ? ["-D_GNU_SOURCE"] : [])],
47
+ };
48
+ }
49
+ /** The OS the produced binary runs on: the triple's OS under SCRIPTC_TARGET,
50
+ * the host's otherwise — so platform-conditional link flags follow the
51
+ * TARGET, not the machine running the compiler. Exported for compile()/
52
+ * analyze(): the FRONTEND consults it too (path.sep / os.EOL literals and
53
+ * the path-module binding follow the target — a win32 triple compiles
54
+ * Node-on-Windows semantics, path.win32 backing the bare module). */
55
+ export function targetPlatform(driver) {
56
+ if (driver.target === null)
57
+ return process.platform;
58
+ if (driver.target.includes("linux"))
59
+ return "linux";
60
+ if (driver.target.includes("windows"))
61
+ return "win32";
62
+ return driver.target.includes("macos") || driver.target.includes("darwin") ? "darwin" : "other";
63
+ }
64
+ function vendorEngineDir() {
65
+ return join(runtimeSrcDir(), "..", "vendor", "quickjs-ng");
66
+ }
67
+ async function fileExists(path) {
68
+ return stat(path).then((s) => s.isFile(), () => false);
69
+ }
70
+ /** The engine archive for one flavor, built lazily on the first --dynamic
71
+ * compile (~10s) and cached under vendor/.cache/<commit>-<flavor>/ — unlike
72
+ * the runtime's own sources (recompiled every build, ~100ms), the engine is
73
+ * far too big to rebuild per compile. The plain flavor is ALWAYS MinSizeRel
74
+ * regardless of the program's optimization level: measured binary delta is
75
+ * ~620KB vs ~900KB at -O2, and eval-in-an-island workloads don't earn the
76
+ * difference. The asan flavor (Debug + QJS_ENABLE_ASAN) exists so the
77
+ * sanitized test lane checks engine interop under instrumentation.
78
+ *
79
+ * Concurrency: parallel first builds (e.g. test workers) each build in a
80
+ * private temp dir and publish with an atomic rename — first one wins,
81
+ * losers discard their work and use the winner's archive.
82
+ *
83
+ * SCRIPTC_TARGET adds a per-target cache flavor built WITHOUT CMake (the
84
+ * mbedTLS recipe): the qjs library target is just four TUs
85
+ * (QJS_ENGINE_SOURCES) compiled with `zig cc -target <triple>` under the
86
+ * same flags CMake's MinSizeRel/Debug+ASan configurations apply, packed
87
+ * with `zig ar`. Host builds keep the exact historical CMake recipe. */
88
+ async function ensureEngineArchive(sanitize, driver) {
89
+ const flavor = `${sanitize ? "asan" : "plain"}${driver.target !== null ? `-${driver.target}` : ""}`;
90
+ const vendor = vendorEngineDir();
91
+ const cacheRoot = join(vendor, "..", ".cache");
92
+ const cacheDir = join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-${flavor}`);
93
+ const archive = join(cacheDir, "libqjs.a");
94
+ if (await fileExists(archive))
95
+ return archive;
96
+ if (driver.target !== null)
97
+ return buildEngineArchiveCross(sanitize, driver, cacheRoot, cacheDir);
98
+ await mkdir(cacheRoot, { recursive: true });
99
+ const buildDir = await mkdtemp(join(cacheRoot, `build-${flavor}-`));
100
+ const stageDir = `${buildDir}.stage`;
101
+ try {
102
+ const configureArgs = [
103
+ "-S", vendor,
104
+ "-B", buildDir,
105
+ ...(sanitize
106
+ ? ["-DCMAKE_BUILD_TYPE=Debug", "-DQJS_ENABLE_ASAN=ON"]
107
+ : ["-DCMAKE_BUILD_TYPE=MinSizeRel"]),
108
+ "-DQJS_BUILD_EXAMPLES=OFF",
109
+ "-DQJS_ENABLE_INSTALL=OFF",
110
+ ];
111
+ try {
112
+ await execFileAsync("cmake", configureArgs);
113
+ }
114
+ catch (err) {
115
+ if (err.code === "ENOENT") {
116
+ throw new Error("--dynamic builds the embedded engine once with CMake, which was not found on PATH.\n" +
117
+ "Install cmake (e.g. `brew install cmake`) and retry. Static builds do not need it.");
118
+ }
119
+ throw new Error(`cmake failed configuring the embedded engine (${vendor}).\n\n` +
120
+ `${err.stderr ?? String(err)}`);
121
+ }
122
+ await execFileAsync("cmake", [
123
+ "--build", buildDir,
124
+ "--target", "qjs",
125
+ `-j${availableParallelism()}`,
126
+ ]);
127
+ // Publish only the archive, atomically: rename fails if a concurrent
128
+ // build won the race, and the winner's archive is just as good.
129
+ await mkdir(stageDir);
130
+ await copyFile(join(buildDir, "libqjs.a"), join(stageDir, "libqjs.a"));
131
+ await rename(stageDir, cacheDir).catch(() => undefined);
132
+ }
133
+ finally {
134
+ await rm(buildDir, { recursive: true, force: true });
135
+ await rm(stageDir, { recursive: true, force: true });
136
+ }
137
+ return archive;
138
+ }
139
+ /** The qjs library target's source list (CMakeLists.txt `qjs_sources` with
140
+ * QJS_BUILD_LIBC off — cutils is header-only): the cross-build recipe
141
+ * compiles exactly these. */
142
+ const QJS_ENGINE_SOURCES = ["dtoa.c", "libregexp.c", "libunicode.c", "quickjs.c"];
143
+ /** The engine archive for a CROSS target, per-TU `zig cc` compiles + `zig
144
+ * ar` (no CMake — the mbedTLS recipe). The flags mirror what the CMake
145
+ * configurations apply to the qjs library target: gnu11 (CMAKE_C_EXTENSIONS
146
+ * ON), hidden visibility, -funsigned-char, -DQUICKJS_NG_BUILD and
147
+ * -D_GNU_SOURCE (qjs_defines; linux targetArgs already carry the latter),
148
+ * then MinSizeRel (-Os -DNDEBUG) for plain or Debug+QJS_ENABLE_ASAN
149
+ * (-O0 -ggdb -fno-omit-frame-pointer -fsanitize=address
150
+ * -fno-sanitize-recover=all) for asan. Same atomic-rename publish as every
151
+ * vendor cache. */
152
+ async function buildEngineArchiveCross(sanitize, driver, cacheRoot, cacheDir) {
153
+ const vendor = vendorEngineDir();
154
+ const archive = join(cacheDir, "libqjs.a");
155
+ const arArgv = [...driver.argv.slice(0, 1), "ar"];
156
+ const cflags = [
157
+ "-std=gnu11",
158
+ ...driver.targetArgs,
159
+ "-fvisibility=hidden",
160
+ "-funsigned-char",
161
+ "-DQUICKJS_NG_BUILD",
162
+ "-D_GNU_SOURCE",
163
+ // CMake's qjs_defines on WIN32 (quickjs.c's own _WIN32 arms cover the
164
+ // rest — timezoneapi, intrin, cutils.h's _msize usable-size probe).
165
+ ...(targetPlatform(driver) === "win32" ? ["-DWIN32_LEAN_AND_MEAN", "-D_WIN32_WINNT=0x0601"] : []),
166
+ ...(sanitize
167
+ ? ["-O0", "-ggdb", "-fno-omit-frame-pointer", "-fsanitize=address", "-fno-sanitize-recover=all", "-DQJS_ENABLE_ASAN"]
168
+ : ["-Os", "-DNDEBUG"]),
169
+ "-I", vendor,
170
+ ];
171
+ await mkdir(cacheRoot, { recursive: true });
172
+ const buildDir = await mkdtemp(join(cacheRoot, `build-qjs-${driver.target ?? "host"}-`));
173
+ try {
174
+ const width = Math.min(QJS_ENGINE_SOURCES.length, availableParallelism());
175
+ for (let i = 0; i < QJS_ENGINE_SOURCES.length; i += width) {
176
+ await Promise.all(QJS_ENGINE_SOURCES.slice(i, i + width).map((src) => execFileAsync(driver.argv[0] ?? "clang", [...driver.argv.slice(1), ...cflags, "-c", join(vendor, src), "-o", join(buildDir, `${basename(src, ".c")}.o`)], { cwd: buildDir })));
177
+ }
178
+ await execFileAsync(arArgv[0] ?? "ar", [...arArgv.slice(1), "rcs", join(buildDir, "libqjs.a"), ...QJS_ENGINE_SOURCES.map((s) => join(buildDir, `${basename(s, ".c")}.o`))]);
179
+ // Atomic publish: rename fails if a concurrent build won the race, and
180
+ // the winner's archive is just as good.
181
+ const stageDir = `${buildDir}.stage`;
182
+ await mkdir(stageDir);
183
+ await copyFile(join(buildDir, "libqjs.a"), join(stageDir, "libqjs.a"));
184
+ await rename(stageDir, cacheDir).catch(async () => {
185
+ await rm(stageDir, { recursive: true, force: true });
186
+ });
187
+ }
188
+ finally {
189
+ await rm(buildDir, { recursive: true, force: true });
190
+ }
191
+ return archive;
192
+ }
193
+ /** The vendor sources behind static-build regex support: quickjs-ng's
194
+ * libregexp and its unicode tables (cutils is header-only). Deliberately
195
+ * NOT the engine — a regex-using static binary links ~110KB of matcher,
196
+ * never the ~620KB island. */
197
+ const LRE_SOURCES = ["libregexp.c", "libunicode.c"];
198
+ /** The libregexp objects for one flavor, compiled lazily on the first
199
+ * regex-using static build (~1s) and cached like the engine archive —
200
+ * vendor/.cache/<commit>-lre-<flavor>/*.o — with the same atomic-rename
201
+ * publish (parallel first builds race safely; losers discard their work).
202
+ * Plain is -Os (size class matters: the regex fence pins regex-using
203
+ * binaries well under the engine class); asan matches the final link so
204
+ * the sanitized lane instruments the matcher too. Cross targets get their
205
+ * own flavor directory, compiled with the SCRIPTC_CC driver and its target
206
+ * args — libregexp/libunicode are plain C11, so the cross story is exactly
207
+ * the runtime sources' (no host-built inputs); vendored SOURCES are
208
+ * untouched, only the build wiring knows about targets. */
209
+ async function ensureLreObjects(sanitize, driver) {
210
+ // The flavor keys the DRIVER as well as the target: a zig-cc-built object
211
+ // set must never be handed to a clang link (or vice versa) off a shared
212
+ // cache directory — the historical `lre-plain`/`lre-asan` names stay
213
+ // clang's alone.
214
+ const flavor = (sanitize ? "asan" : "plain") +
215
+ (driver.argv.length === 1 && driver.argv[0] === "clang" ? "" : "-zigcc") +
216
+ (driver.target !== null ? `-${driver.target}` : "");
217
+ const vendor = vendorEngineDir();
218
+ const cacheRoot = join(vendor, "..", ".cache");
219
+ const cacheDir = join(cacheRoot, `${QJS_COMMIT.slice(0, 12)}-lre-${flavor}`);
220
+ const objects = LRE_SOURCES.map((f) => join(cacheDir, f.replace(/\.c$/, ".o")));
221
+ if ((await Promise.all(objects.map(fileExists))).every(Boolean))
222
+ return objects;
223
+ await mkdir(cacheRoot, { recursive: true });
224
+ const buildDir = await mkdtemp(join(cacheRoot, `build-lre-${flavor}-`));
225
+ try {
226
+ // One -c per invocation: zig's COFF driver rejects multiple -c inputs
227
+ // in a single command ("coff does not support linking multiple
228
+ // objects"); per-file compiles produce the identical objects on every
229
+ // target.
230
+ for (const f of LRE_SOURCES) {
231
+ await execFileAsync(driver.argv[0] ?? "clang", [
232
+ ...driver.argv.slice(1),
233
+ "-std=c11",
234
+ ...driver.targetArgs,
235
+ ...(sanitize ? ["-O1", "-fsanitize=address"] : ["-Os"]),
236
+ "-I", vendor,
237
+ "-c", join(vendor, f),
238
+ "-o", join(buildDir, f.replace(/\.c$/, ".o")),
239
+ ], { cwd: buildDir });
240
+ }
241
+ // Atomic publish: rename fails if a concurrent build won the race, and
242
+ // the winner's objects are just as good.
243
+ await rename(buildDir, cacheDir).catch(() => undefined);
244
+ }
245
+ finally {
246
+ await rm(buildDir, { recursive: true, force: true });
247
+ }
248
+ return objects;
249
+ }
250
+ function vendorZlibDir() {
251
+ return join(runtimeSrcDir(), "..", "vendor", "zlib");
252
+ }
253
+ /** The vendored zlib TUs behind CROSS-target zlib support: every root *.c
254
+ * except the gzFile file-I/O units (gz*.c — nothing in scr_zlib.c
255
+ * references the gzFile API, and those TUs alone want unistd/io headers).
256
+ * Host builds never touch this list — they link the system libz. */
257
+ const ZLIB_SOURCES = ["adler32.c", "compress.c", "crc32.c", "deflate.c", "infback.c", "inffast.c", "inflate.c", "inftrees.c", "trees.c", "uncompr.c", "zutil.c"];
258
+ /** The zlib objects for one flavor, compiled lazily on the first zlib-using
259
+ * CROSS build (~1s) and cached like the lre objects —
260
+ * vendor/.cache/zlib-<version>-<flavor>/*.o — with the same atomic-rename
261
+ * publish (parallel first builds race safely; losers discard their work).
262
+ * Plain is -Os, asan matches the final link so the sanitized lane
263
+ * instruments the codec too. The flavor keys the driver and target exactly
264
+ * like the lre flavor: only cross builds call this today, but the keying
265
+ * must never hand a zig-built object set to a clang link off a shared
266
+ * directory. */
267
+ async function ensureZlibObjects(sanitize, driver) {
268
+ const flavor = (sanitize ? "asan" : "plain") +
269
+ (driver.argv.length === 1 && driver.argv[0] === "clang" ? "" : "-zigcc") +
270
+ (driver.target !== null ? `-${driver.target}` : "");
271
+ const vendor = vendorZlibDir();
272
+ const cacheRoot = join(vendor, "..", ".cache");
273
+ const cacheDir = join(cacheRoot, `zlib-${ZLIB_VERSION}-${flavor}`);
274
+ const objects = ZLIB_SOURCES.map((f) => join(cacheDir, f.replace(/\.c$/, ".o")));
275
+ if ((await Promise.all(objects.map(fileExists))).every(Boolean))
276
+ return objects;
277
+ await mkdir(cacheRoot, { recursive: true });
278
+ const buildDir = await mkdtemp(join(cacheRoot, `build-zlib-${flavor}-`));
279
+ try {
280
+ // One -c per invocation, the lre recipe (zig's COFF driver rejects
281
+ // multiple -c inputs in a single command).
282
+ for (const f of ZLIB_SOURCES) {
283
+ await execFileAsync(driver.argv[0] ?? "clang", [
284
+ ...driver.argv.slice(1),
285
+ "-std=c11",
286
+ ...driver.targetArgs,
287
+ ...(sanitize ? ["-O1", "-fsanitize=address"] : ["-Os"]),
288
+ "-I", vendor,
289
+ "-c", join(vendor, f),
290
+ "-o", join(buildDir, f.replace(/\.c$/, ".o")),
291
+ ], { cwd: buildDir });
292
+ }
293
+ // Atomic publish: rename fails if a concurrent build won the race, and
294
+ // the winner's objects are just as good.
295
+ await rename(buildDir, cacheDir).catch(() => undefined);
296
+ }
297
+ finally {
298
+ await rm(buildDir, { recursive: true, force: true });
299
+ }
300
+ return objects;
301
+ }
302
+ function vendorCurlDir() {
303
+ return join(runtimeSrcDir(), "..", "vendor", "curl");
304
+ }
305
+ /** Every libcurl function scr_fetch_curl.c references — the generated import
306
+ * stub defines exactly these. A new curl call in scr_fetch_curl.c that is
307
+ * missing here fails the CROSS link immediately with the symbol's name
308
+ * (host builds resolve it from the real system libcurl and never look). */
309
+ const CURL_STUB_SYMBOLS = [
310
+ "curl_easy_cleanup", "curl_easy_getinfo", "curl_easy_init", "curl_easy_setopt", "curl_easy_strerror",
311
+ "curl_free", "curl_global_cleanup", "curl_global_init",
312
+ "curl_multi_add_handle", "curl_multi_cleanup", "curl_multi_info_read", "curl_multi_init",
313
+ "curl_multi_perform", "curl_multi_poll", "curl_multi_remove_handle",
314
+ "curl_slist_append", "curl_slist_free_all",
315
+ "curl_url", "curl_url_cleanup", "curl_url_get", "curl_url_set",
316
+ ];
317
+ /** The libcurl import stub for one CROSS target, generated lazily on the
318
+ * first fetch-using cross build and cached like the zlib objects —
319
+ * vendor/.cache/curl-stub-<target>/libcurl.so, atomic-rename publish. The
320
+ * stub is empty definitions of CURL_STUB_SYMBOLS compiled `-shared` with
321
+ * `-soname libcurl.so.4`: linking against it satisfies scr_fetch_curl.c's
322
+ * references and records a plain `DT_NEEDED libcurl.so.4`, which the
323
+ * TARGET system's real libcurl satisfies at load time (curl's unversioned
324
+ * exports make the classic import-stub technique sound here — nothing
325
+ * from the stub's bodies ever ships in the binary). One stub per target,
326
+ * no sanitize flavor: the stub's code is never executed or instrumented,
327
+ * and asan links against plain shared libraries freely. */
328
+ async function ensureCurlStub(driver) {
329
+ const cacheRoot = join(vendorCurlDir(), "..", ".cache");
330
+ const cacheDir = join(cacheRoot, `curl-stub-${driver.target}`);
331
+ const lib = join(cacheDir, "libcurl.so");
332
+ if (await fileExists(lib))
333
+ return cacheDir;
334
+ await mkdir(cacheRoot, { recursive: true });
335
+ const buildDir = await mkdtemp(join(cacheRoot, `build-curl-stub-`));
336
+ try {
337
+ const src = join(buildDir, "stub.c");
338
+ await writeFile(src, CURL_STUB_SYMBOLS.map((s) => `void ${s}(void) {}\n`).join(""));
339
+ await execFileAsync(driver.argv[0] ?? "clang", [
340
+ ...driver.argv.slice(1),
341
+ ...driver.targetArgs,
342
+ "-shared", "-fPIC",
343
+ "-Wl,-soname,libcurl.so.4",
344
+ src,
345
+ "-o", join(buildDir, "libcurl.so"),
346
+ ]);
347
+ // Atomic publish: rename fails if a concurrent build won the race, and
348
+ // the winner's stub is just as good.
349
+ const stageDir = `${buildDir}.stage`;
350
+ await mkdir(stageDir);
351
+ await copyFile(join(buildDir, "libcurl.so"), join(stageDir, "libcurl.so"));
352
+ await rename(stageDir, cacheDir).catch(async () => {
353
+ await rm(stageDir, { recursive: true, force: true });
354
+ });
355
+ }
356
+ finally {
357
+ await rm(buildDir, { recursive: true, force: true });
358
+ }
359
+ return cacheDir;
360
+ }
361
+ function vendorTlsDir() {
362
+ return join(runtimeSrcDir(), "..", "vendor", "mbedtls");
363
+ }
364
+ /** The mbedTLS archive for one flavor, compiled lazily on the first
365
+ * TLS-using build (~15s over ~110 TUs, parallelized) and cached like the
366
+ * engine archive — vendor/.cache/mbedtls-<version>-<flavor>/libmbedtls.a —
367
+ * with the same atomic-rename publish (parallel first builds race safely;
368
+ * losers discard their work). Plain is -Os (the TLS stack is link-gated
369
+ * but still wants its size class small); asan matches the final link so
370
+ * the sanitized lane instruments every TLS path too. No CMake: the stock
371
+ * config builds every library/*.c standalone with clang, so the recipe is
372
+ * per-TU `-c` compiles plus one `ar rcs` — vendored-build machinery the
373
+ * lre-objects cache already established.
374
+ *
375
+ * SCRIPTC_TARGET adds a per-target cache flavor (the lre-objects story):
376
+ * TUs compile with `zig cc -target <triple>` and the archive is packed
377
+ * with `zig ar` (llvm-ar — the host BSD ar has no business indexing ELF
378
+ * objects). Host builds keep the exact historical clang + ar recipe. */
379
+ async function ensureTlsArchive(sanitize, driver) {
380
+ const flavor = `${sanitize ? "asan" : "plain"}${driver.target !== null ? `-${driver.target}` : ""}`;
381
+ const compileArgv = driver.target !== null ? driver.argv : ["clang"];
382
+ const arArgv = driver.target !== null ? [...driver.argv.slice(0, 1), "ar"] : ["ar"];
383
+ const vendor = vendorTlsDir();
384
+ const cacheRoot = join(vendor, "..", ".cache");
385
+ const cacheDir = join(cacheRoot, `mbedtls-${MBEDTLS_VERSION}-${flavor}`);
386
+ const archive = join(cacheDir, "libmbedtls.a");
387
+ if (await fileExists(archive))
388
+ return archive;
389
+ await mkdir(cacheRoot, { recursive: true });
390
+ const buildDir = await mkdtemp(join(cacheRoot, `build-mbedtls-${flavor}-`));
391
+ try {
392
+ const sources = (await readdir(join(vendor, "library"))).filter((n) => n.endsWith(".c")).sort();
393
+ const cflags = [
394
+ "-std=c11",
395
+ ...driver.targetArgs, // ALWAYS empty on host builds
396
+ ...(sanitize ? ["-O1", "-fsanitize=address"] : ["-Os"]),
397
+ "-I", join(vendor, "include"),
398
+ "-I", join(vendor, "library"),
399
+ ];
400
+ const width = availableParallelism();
401
+ for (let i = 0; i < sources.length; i += width) {
402
+ await Promise.all(sources.slice(i, i + width).map((src) => execFileAsync(compileArgv[0] ?? "clang", [...compileArgv.slice(1), ...cflags, "-c", join(vendor, "library", src), "-o", join(buildDir, `${basename(src, ".c")}.o`)])));
403
+ }
404
+ await execFileAsync(arArgv[0] ?? "ar", [...arArgv.slice(1), "rcs", join(buildDir, "libmbedtls.a"), ...sources.map((s) => join(buildDir, `${basename(s, ".c")}.o`))]);
405
+ // Atomic publish: rename fails if a concurrent build won the race, and
406
+ // the winner's archive is just as good.
407
+ const stageDir = `${buildDir}.stage`;
408
+ await mkdir(stageDir);
409
+ await copyFile(join(buildDir, "libmbedtls.a"), join(stageDir, "libmbedtls.a"));
410
+ await rename(stageDir, cacheDir).catch(async () => {
411
+ await rm(stageDir, { recursive: true, force: true });
412
+ });
413
+ }
414
+ finally {
415
+ await rm(buildDir, { recursive: true, force: true });
416
+ }
417
+ return archive;
418
+ }
419
+ /* ------------------- build cache (opt-in via SCRIPTC_CACHE_DIR) ----------------
420
+ * Content-addressed caches that let repeat builds of unchanged programs skip
421
+ * clang entirely — the test lanes' dominant cost. Two keyspaces under the
422
+ * cache root:
423
+ *
424
+ * bin/<key> — whole program binaries. key = sha256(clang version,
425
+ * runtime fingerprint (every .c/.h in the runtime src dir
426
+ * plus the vendor pin QJS_COMMIT), the FULL normalized
427
+ * command line, the emitted C bytes). Emitted C is
428
+ * byte-stable by project invariant, so unchanged programs
429
+ * hit; any flag difference — e.g. the sanitized lane's
430
+ * -O1/-fsanitize=address/-DSCR_RC_AUDIT — lands in a
431
+ * naturally distinct key. On a hit the cached binary is
432
+ * copied and atomically renamed onto outPath (never an
433
+ * in-place overwrite — see the hit path) and clang never
434
+ * runs; the binary is still EXECUTED live by whoever
435
+ * asked for it, so no comparison or sanitizer coverage
436
+ * is ever skipped.
437
+ *
438
+ * obj/<set>/<f>.o — per-flavor runtime objects for cache-miss builds. The
439
+ * historical single invocation recompiles every runtime
440
+ * TU per program (~1.3s at -O2); with cached objects a
441
+ * miss compiles ONLY the program's C and links (~0.15s).
442
+ * The clang driver hands every input the same option set,
443
+ * so per-TU `-c` compiles with those options plus a final
444
+ * link reproduce the single invocation exactly. Object
445
+ * compiles go through ccache when it is installed (silent
446
+ * fallback when not).
447
+ *
448
+ * Escape hatches, honored in BOTH directions (no reads, no writes): leave
449
+ * SCRIPTC_CACHE_DIR unset (the production default — the CLI never caches unless
450
+ * asked; vitest.config.ts sets it for the test lanes) or set SCRIPTC_NO_CACHE=1.
451
+ * Either way compileC issues the exact historical command line.
452
+ *
453
+ * Eviction: size-capped LRU over the whole cache root (SCRIPTC_CACHE_MAX_MB,
454
+ * default 4096) swept at most once per process after a write; reads bump
455
+ * mtimes. The harness's oracle cache lives under the same root and is swept
456
+ * by the same pass. Cache trouble is never a build failure — every cache
457
+ * error falls back to a real compile. */
458
+ function cacheRootDir() {
459
+ if (process.env["SCRIPTC_NO_CACHE"] === "1")
460
+ return null;
461
+ const dir = process.env["SCRIPTC_CACHE_DIR"];
462
+ return dir !== undefined && dir !== "" ? dir : null;
463
+ }
464
+ const ccVersionMemos = new Map();
465
+ function ccVersionOnce(argv) {
466
+ const key = argv.join("\x1f");
467
+ let memo = ccVersionMemos.get(key);
468
+ if (memo === undefined) {
469
+ // cwd: the version probe must not touch the caller's directory —
470
+ // `zig cc --version` (zig 0.16) drops an empty a.o wherever it runs.
471
+ memo = execFileAsync(argv[0] ?? "clang", [...argv.slice(1), "--version"], { cwd: tmpdir() }).then((r) => r.stdout.split("\n", 1)[0] ?? "");
472
+ ccVersionMemos.set(key, memo);
473
+ }
474
+ return memo;
475
+ }
476
+ let ccacheMemo = null;
477
+ function ccacheAvailable() {
478
+ ccacheMemo ??= execFileAsync("ccache", ["--version"]).then(() => true, () => false);
479
+ return ccacheMemo;
480
+ }
481
+ /** Content hash of every .c/.h in the runtime src dir plus the vendor pin —
482
+ * everything a binary links that the emitted C bytes don't already cover
483
+ * (npm-embedded C rides inside the emitted C; the engine archive and lre
484
+ * objects are pinned by QJS_COMMIT, the same trust their own caches use).
485
+ * Memoized behind a stat signature so watch-mode edits re-hash. */
486
+ let rtFingerprintMemo = null;
487
+ async function runtimeFingerprint(rtDir) {
488
+ const names = (await readdir(rtDir)).filter((n) => n.endsWith(".c") || n.endsWith(".h")).sort();
489
+ const stats = await Promise.all(names.map((n) => stat(join(rtDir, n))));
490
+ const sig = stats.map((s, i) => `${names[i] ?? ""}:${s.size}:${s.mtimeMs}`).join("|");
491
+ if (rtFingerprintMemo !== null && rtFingerprintMemo.sig === sig)
492
+ return rtFingerprintMemo.hash;
493
+ const h = createHash("sha256").update(QJS_COMMIT).update(MBEDTLS_VERSION).update(ZLIB_VERSION);
494
+ for (const n of names)
495
+ h.update(n).update("\0").update(await readFile(join(rtDir, n))).update("\0");
496
+ const hash = h.digest("hex");
497
+ rtFingerprintMemo = { sig, hash };
498
+ return hash;
499
+ }
500
+ /** The cached .o set for one flag flavor, compiled on first need. Concurrent
501
+ * first builds (parallel test workers on a cold cache) may duplicate work;
502
+ * per-file atomic renames make every winner equivalent. */
503
+ async function ensureRuntimeObjects(root, ccArgv, cflags, sources, keyPrefix) {
504
+ const setKey = createHash("sha256")
505
+ .update(keyPrefix)
506
+ .update(cflags.join("\x1f"))
507
+ .digest("hex")
508
+ .slice(0, 24);
509
+ const objDir = join(root, "obj", setKey);
510
+ const objOf = (src) => join(objDir, `${basename(src, ".c")}.o`);
511
+ const present = await Promise.all(sources.map((s) => fileExists(objOf(s))));
512
+ const missing = sources.filter((_, i) => !present[i]);
513
+ if (missing.length > 0) {
514
+ // ccache wraps only the default clang driver — multi-word drivers
515
+ // (`zig cc`) run bare; their object sets are keyed apart anyway.
516
+ const useCcache = ccArgv.length === 1 && ccArgv[0] === "clang" && (await ccacheAvailable());
517
+ await mkdir(objDir, { recursive: true });
518
+ const tmpDir = await mkdtemp(join(root, "obj", "build-"));
519
+ try {
520
+ // Modest parallelism: a flavor's objects build once, but several cold
521
+ // workers can race here — keep each build's CPU footprint small.
522
+ const width = 4;
523
+ for (let i = 0; i < missing.length; i += width) {
524
+ await Promise.all(missing.slice(i, i + width).map(async (src) => {
525
+ const tmpObj = join(tmpDir, `${basename(src, ".c")}.o`);
526
+ const argv = [...(useCcache ? ["ccache"] : []), ...ccArgv, ...cflags, "-c", src, "-o", tmpObj];
527
+ await execFileAsync(argv[0] ?? "clang", argv.slice(1));
528
+ await rename(tmpObj, objOf(src));
529
+ }));
530
+ }
531
+ }
532
+ finally {
533
+ await rm(tmpDir, { recursive: true, force: true });
534
+ }
535
+ }
536
+ return new Map(sources.map((s) => [s, objOf(s)]));
537
+ }
538
+ /** Size-capped LRU sweep of the whole cache root, at most once per process
539
+ * and only after a write. Oldest-mtime files go first until the tree is back
540
+ * under 75% of the cap; read hits bump mtimes so hot entries survive. */
541
+ let pruneDone = false;
542
+ async function pruneCache(root) {
543
+ if (pruneDone)
544
+ return;
545
+ pruneDone = true;
546
+ const capBytes = Number(process.env["SCRIPTC_CACHE_MAX_MB"] ?? "4096") * 1024 * 1024;
547
+ if (!Number.isFinite(capBytes) || capBytes <= 0)
548
+ return;
549
+ const files = [];
550
+ const walk = async (dir) => {
551
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
552
+ for (const ent of entries) {
553
+ const p = join(dir, ent.name);
554
+ if (ent.isDirectory())
555
+ await walk(p);
556
+ else if (ent.isFile()) {
557
+ const s = await stat(p).catch(() => null);
558
+ if (s !== null)
559
+ files.push({ path: p, size: s.size, mtimeMs: s.mtimeMs });
560
+ }
561
+ }
562
+ };
563
+ await walk(root);
564
+ let total = files.reduce((n, f) => n + f.size, 0);
565
+ if (total <= capBytes)
566
+ return;
567
+ files.sort((a, b) => a.mtimeMs - b.mtimeMs);
568
+ const recent = Date.now() - 60 * 60 * 1000; // never evict anything a live run may be using
569
+ for (const f of files) {
570
+ if (total <= capBytes * 0.75 || f.mtimeMs > recent)
571
+ break;
572
+ await unlink(f.path).catch(() => undefined);
573
+ total -= f.size;
574
+ }
575
+ }
576
+ /** Compiles one C program together with the runtime sources.
577
+ * Without a cache dir (the production default), the runtime (a dozen small
578
+ * files) is recompiled on every build in one historical clang invocation —
579
+ * no cached-archive staleness bugs. --dynamic additionally compiles
580
+ * scr_island.c under SCR_DYNAMIC and links the cached engine archive (built
581
+ * lazily, see above); regex-using programs additionally compile scr_regex.c
582
+ * and link libregexp (the cached objects, or the archive's own copy under
583
+ * --dynamic). Without either, the command line is exactly the historical
584
+ * one — regex-free static builds must stay byte-identical.
585
+ *
586
+ * With SCRIPTC_CACHE_DIR set (the test lanes; see the cache block above),
587
+ * unchanged programs skip clang via the binary cache, and misses link the
588
+ * program's own TU against cached per-flavor runtime objects. */
589
+ export async function compileC(opts) {
590
+ const rtDir = runtimeSrcDir();
591
+ const dynamic = opts.dynamic ?? false;
592
+ const regex = opts.regex ?? false;
593
+ // fetch's implementation switch: the default is the NATIVE bridge
594
+ // (scr_fetch.c over scr_net + scr_tls + scr_http's client parser +
595
+ // zlib — no libcurl anywhere), which implies the socket units into the
596
+ // link. SCRIPTC_FETCH_CURL=1 keeps the retired curl reference
597
+ // (scr_fetch_curl.c + system libcurl / the linux soname stub)
598
+ // compilable for one release as the flip's reference.
599
+ const fetchOn = opts.fetch ?? false;
600
+ const curlFetch = fetchOn && process.env["SCRIPTC_FETCH_CURL"] === "1";
601
+ const nativeFetch = fetchOn && !curlFetch;
602
+ // The island's node:http/https client bridge: embedded graphs that
603
+ // import those builtins get working clients over the same socket units
604
+ // (native-fetch builds always carry it — scr_fetch_install registers it).
605
+ const netIsland = dynamic && ((opts.netIsland ?? false) || nativeFetch);
606
+ const net = (opts.net ?? false) || nativeFetch || netIsland;
607
+ const http = (opts.http ?? false) || nativeFetch || netIsland;
608
+ const tls = (opts.tls ?? false) || nativeFetch || netIsland;
609
+ const driver = resolveCc();
610
+ if (driver.target !== null) {
611
+ // See the resolveCc block: these inputs are built on and for the HOST
612
+ // (vendored archives, system libs). Regex, zlib, and the engine archive
613
+ // are NOT here: their vendored sources are plain C that ensureLreObjects
614
+ // / ensureZlibObjects / buildEngineArchiveCross compile per target with
615
+ // the driver itself — win32 included (the Windows lane runs the
616
+ // @dynamic corpus and the zlib program against the box's Node). The
617
+ // NATIVE fetch rides the socket units and cross-compiles with them —
618
+ // no gate; only the retired curl REFERENCE (SCRIPTC_FETCH_CURL=1)
619
+ // keeps a linux-only arm (the vendored curl headers + the generated
620
+ // soname stub, ensureCurlStub — win32 has no system libcurl contract
621
+ // to bind at load time). The event-loop units, tls, and dgram cross-compile to Linux
622
+ // AND Windows (scr_platform.h poller + per-target mbedTLS; the
623
+ // loop's win32 arm is WSAPoll, scr_loop_wsapoll.c, and the socket
624
+ // units' win32 arms respell winsock behind POSIX-errno wrappers —
625
+ // scr_net.c/scr_dgram.c/scr_tls.c). The events unit cross-compiles
626
+ // everywhere: its win32 arm is CRT signal() +
627
+ // PeekNamedPipe/WaitForSingleObject probes (scr_events.c), served by
628
+ // the loop's capped win32 idle sleep (scr_async.c).
629
+ const unsupported = [
630
+ // The NATIVE fetch cross-compiles wherever the socket units do
631
+ // (linux and win32 both); only the retired curl reference keeps
632
+ // its linux-only soname-stub arm.
633
+ ["fetch (SCRIPTC_FETCH_CURL)", curlFetch && targetPlatform(driver) !== "linux"],
634
+ ]
635
+ .filter(([, on]) => on)
636
+ .map(([name]) => name);
637
+ if (unsupported.length > 0) {
638
+ throw new Error(`SCRIPTC_TARGET=${driver.target}: ${unsupported.join(", ")} not supported under a cross target yet ` +
639
+ `(host-built vendor archives / system libs — see docs/linux-port.md).`);
640
+ }
641
+ }
642
+ const engineArchive = dynamic ? await ensureEngineArchive(opts.sanitize ?? false, driver) : null;
643
+ const tlsArchive = tls ? await ensureTlsArchive(opts.sanitize ?? false, driver) : null;
644
+ // --dynamic + regex shares the archive's libregexp (its host hooks and
645
+ // ours would collide; see scr_regex.c) — the standalone objects are for
646
+ // static builds only.
647
+ const lreObjects = regex && !dynamic ? await ensureLreObjects(opts.sanitize ?? false, driver) : [];
648
+ // Vendored zlib is the CROSS story only — host builds keep the exact
649
+ // historical `-lz` system link (see CcOptions.zlib). The native fetch's
650
+ // gzip decoder rides the same objects/link.
651
+ const zlibObjects = ((opts.zlib ?? false) || nativeFetch) && driver.target !== null
652
+ ? await ensureZlibObjects(opts.sanitize ?? false, driver)
653
+ : [];
654
+ // The libcurl import stub is likewise CROSS-only — host builds keep the
655
+ // exact historical system `-lcurl` link (see CcOptions.fetch). Curl
656
+ // reference builds only.
657
+ const curlStubDir = curlFetch && driver.target !== null ? await ensureCurlStub(driver) : null;
658
+ // rt() maps each runtime source's path on the command line: identity for
659
+ // the historical single invocation, cached-.o substitution on cache misses.
660
+ const buildArgs = (rt) => [
661
+ "-std=c11",
662
+ ...driver.targetArgs, // ALWAYS empty on the default clang path
663
+ ...(opts.sanitize
664
+ ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
665
+ : ["-O2"]),
666
+ "-fno-math-errno",
667
+ "-Wno-deprecated-declarations", // ucontext fibers (scr_async.c)
668
+ "-I", rtDir,
669
+ ...RUNTIME_SOURCES.map((f) => rt(join(rtDir, f))),
670
+ // win32 targets compile the libc-shim TU (stpcpy, arc4random_buf,
671
+ // gmtime_r, strcasestr — the _WIN32 block in scr_runtime.h declares
672
+ // them) and link advapi32 (the CSPRNG RtlGenRandom/SystemFunction036,
673
+ // GetUserNameA), iphlpapi (GetAdaptersAddresses behind
674
+ // os.networkInterfaces), and ws2_32 (inet_ntop there; the socket
675
+ // units ride the same import). Never present on the default path, so
676
+ // the historical line cannot change.
677
+ ...(targetPlatform(driver) === "win32"
678
+ ? [rt(join(rtDir, "scr_win.c")), "-ladvapi32", "-liphlpapi", "-lws2_32"]
679
+ : []),
680
+ ...(regex
681
+ ? ["-I", vendorEngineDir(), rt(join(rtDir, "scr_regex.c")), ...lreObjects]
682
+ : []),
683
+ ...(opts.assert || regex || opts.symbol ? [rt(join(rtDir, "scr_assert.c"))] : []),
684
+ ...(opts.inspect ? [rt(join(rtDir, "scr_inspect.c"))] : []),
685
+ ...(opts.dynInvoke ? [rt(join(rtDir, "scr_dyn_invoke.c"))] : []),
686
+ ...(opts.dc ? [rt(join(rtDir, "scr_dc.c"))] : []),
687
+ ...(opts.dynAsync || opts.dynInvoke || opts.dc ? [rt(join(rtDir, "scr_async_dyn.c"))] : []),
688
+ // The zlib UNIT (scr_zlib.c) gates on zlib.* IR use; the LINK (system
689
+ // libz on hosts, the vendored per-target objects on cross builds)
690
+ // also serves the native fetch's gzip decoder — spread exactly once.
691
+ ...(opts.zlib
692
+ ? driver.target !== null
693
+ ? ["-I", vendorZlibDir(), rt(join(rtDir, "scr_zlib.c")), ...zlibObjects]
694
+ : [rt(join(rtDir, "scr_zlib.c")), "-lz"]
695
+ : nativeFetch
696
+ ? driver.target !== null
697
+ ? ["-I", vendorZlibDir(), ...zlibObjects]
698
+ : ["-lz"]
699
+ : []),
700
+ // The zlib ↔ island bridge: only when BOTH halves are in the build
701
+ // (the scr_inspect_island.c pattern) — the emitted main calls its
702
+ // installer exactly then.
703
+ ...(opts.zlib && opts.dynamic ? [rt(join(rtDir, "scr_zlib_island.c"))] : []),
704
+ ...(opts.events ? [rt(join(rtDir, "scr_events.c")), rt(join(rtDir, "scr_readline.c"))] : []),
705
+ ...(opts.emitter ? [rt(join(rtDir, "scr_events_emitter.c"))] : []),
706
+ // The checked-dynamic HANDLE support unit (listener gate + runtime
707
+ // adapter closures): every referencing unit is one of the emitter or
708
+ // net families (http implies net), so handle-free binaries keep
709
+ // their exact size class.
710
+ ...(opts.emitter || net ? [rt(join(rtDir, "scr_dyn_handle.c"))] : []),
711
+ ...(opts.symbol ? [rt(join(rtDir, "scr_symbol.c"))] : []),
712
+ ...(opts.searchParams ? [rt(join(rtDir, "scr_url_params.c"))] : []),
713
+ ...(opts.stream ? [rt(join(rtDir, "scr_stream.c"))] : []),
714
+ // The readiness-poller backends (scr_platform.h): kqueue on macOS/BSD,
715
+ // epoll on Linux, WSAPoll on Windows — each TU is empty off its
716
+ // platform, so all three link whenever a poller-using unit does and
717
+ // the others cost nothing (ws2_32 rides the unconditional win32 libs
718
+ // above).
719
+ ...(net || opts.dgram
720
+ ? [
721
+ rt(join(rtDir, "scr_loop_kqueue.c")),
722
+ rt(join(rtDir, "scr_loop_epoll.c")),
723
+ rt(join(rtDir, "scr_loop_wsapoll.c")),
724
+ ]
725
+ : []),
726
+ ...(net ? [rt(join(rtDir, "scr_net.c"))] : []),
727
+ ...(http ? [rt(join(rtDir, "scr_http.c"))] : []),
728
+ ...(opts.http2 ?? false ? [rt(join(rtDir, "scr_http2.c"))] : []),
729
+ ...(opts.dgram ? [rt(join(rtDir, "scr_dgram.c"))] : []),
730
+ ...(opts.watch ? [rt(join(rtDir, "scr_watch.c"))] : []),
731
+ ...(opts.nodeTest ? [rt(join(rtDir, "scr_test.c"))] : []),
732
+ ...(tlsArchive
733
+ ? [
734
+ "-I", join(vendorTlsDir(), "include"),
735
+ rt(join(rtDir, "scr_tls.c")),
736
+ tlsArchive,
737
+ // mbedTLS's win32 entropy poll is BCryptGenRandom (bcrypt.h) —
738
+ // an import the unconditional win32 libs above don't carry.
739
+ // Never present on the default path, so the historical TLS
740
+ // link line cannot change.
741
+ ...(targetPlatform(driver) === "win32" ? ["-lbcrypt"] : []),
742
+ ]
743
+ : []),
744
+ ...(engineArchive
745
+ ? [
746
+ "-DSCR_DYNAMIC",
747
+ "-I", vendorEngineDir(),
748
+ rt(join(rtDir, "scr_island.c")),
749
+ rt(join(rtDir, "scr_web.c")),
750
+ // The one unit referencing BOTH the island and the inspect
751
+ // engine (insp.jsval): linked exactly when both halves are.
752
+ ...(opts.inspect ? [rt(join(rtDir, "scr_inspect_island.c"))] : []),
753
+ // fetch: the NATIVE bridge by default (its socket/tls/zlib
754
+ // dependencies joined the link above); the curl REFERENCE
755
+ // (SCRIPTC_FETCH_CURL=1) keeps the historical system -lcurl /
756
+ // linux soname-stub arms for one release.
757
+ ...(nativeFetch ? [rt(join(rtDir, "scr_fetch.c"))] : []),
758
+ // The island's node:http/https CLIENT bridge (scr_net_island.c):
759
+ // the one TU referencing both the socket units and the engine —
760
+ // compiled beside the native fetch (scr_fetch_install registers
761
+ // it) and whenever the embedded graph imports node:http/https
762
+ // (the emitted main calls scr_net_island_install exactly then).
763
+ ...(netIsland ? [rt(join(rtDir, "scr_net_island.c"))] : []),
764
+ ...(curlFetch
765
+ ? curlStubDir !== null
766
+ ? ["-I", join(vendorCurlDir(), "include"), rt(join(rtDir, "scr_fetch_curl.c")), `-L${curlStubDir}`, "-lcurl"]
767
+ : [rt(join(rtDir, "scr_fetch_curl.c")), "-lcurl"]
768
+ : []),
769
+ engineArchive,
770
+ "-lm",
771
+ // ld64 dead-stripping claws back a chunk of the engine archive;
772
+ // harmless elsewhere but only spelled this way on macOS. Keyed on
773
+ // the TARGET platform (= the host on the default path, where this
774
+ // expression is byte-identical to the historical one).
775
+ ...(targetPlatform(driver) === "darwin" ? ["-Wl,-dead_strip"] : []),
776
+ // The PE stack reserve, pinned to the 8MB POSIX main-stack
777
+ // geometry ISL_MAIN_STACK_BUDGET is sized against (4MB engine
778
+ // budget + 4MB excursion margin) — quickjs-ng's own CMake makes
779
+ // the same 8MB choice on Windows. Not left to the driver:
780
+ // classic mingw ld defaults to 2MB (which the budget would blow
781
+ // straight past); zig's lld happens to default to 16MB today,
782
+ // but that is nobody's contract.
783
+ ...(targetPlatform(driver) === "win32" ? ["-Wl,--stack,8388608"] : []),
784
+ ]
785
+ : []),
786
+ // A .ll program TU (the LLVM backend) deliberately carries no target
787
+ // triple (byte-stable output; clang supplies the host/SCRIPTC_TARGET
788
+ // triple exactly as it does for .c) — silence the -Woverride-module
789
+ // note about that. Never present for .c inputs, so the historical C
790
+ // command line cannot change by a byte.
791
+ ...(opts.cPath.endsWith(".ll") ? ["-Wno-override-module"] : []),
792
+ opts.cPath,
793
+ "-o", opts.outPath,
794
+ ];
795
+ const ccName = driver.argv.join(" ");
796
+ const runClang = async (args) => {
797
+ try {
798
+ await execFileAsync(driver.argv[0] ?? "clang", [...driver.argv.slice(1), ...args]);
799
+ }
800
+ catch (err) {
801
+ const stderr = err.stderr ?? String(err);
802
+ throw new Error(`${ccName} failed compiling ${opts.cPath}.\n` +
803
+ `This is a scriptc bug (generated C should always compile) unless ` +
804
+ `${ccName} itself is missing/broken.\n\n${stderr}`);
805
+ }
806
+ };
807
+ const root = cacheRootDir();
808
+ if (root === null) {
809
+ // The exact historical command line, byte for byte.
810
+ await runClang(buildArgs((p) => p));
811
+ return;
812
+ }
813
+ const [cv, fingerprint, cBytes] = await Promise.all([
814
+ ccVersionOnce(driver.argv),
815
+ runtimeFingerprint(rtDir),
816
+ readFile(opts.cPath),
817
+ ]);
818
+ // The key sees the full command line with the two program-specific paths
819
+ // normalized out (their CONTENT is what matters: the C bytes are hashed,
820
+ // the out path is where the result lands). Runtime and vendor paths stay
821
+ // verbatim — their contents are covered by the fingerprint and the pin.
822
+ const identityArgs = buildArgs((p) => p).map((a) => a === opts.cPath ? "<program.c>" : a === opts.outPath ? "<out>" : a);
823
+ const key = createHash("sha256")
824
+ .update("bin-v1\0")
825
+ // The driver spelling joins the version string: `zig cc --version`
826
+ // reports the clang underneath and could otherwise collide with a
827
+ // same-version host clang.
828
+ .update(ccName).update("\0")
829
+ .update(cv).update("\0")
830
+ .update(fingerprint).update("\0")
831
+ .update(identityArgs.join("\x1f")).update("\0")
832
+ .update(cBytes)
833
+ .digest("hex");
834
+ const binDir = join(root, "bin");
835
+ const cachedBin = join(binDir, key);
836
+ try {
837
+ // NEVER copy over outPath in place: overwriting an already-executed
838
+ // signed binary invalidates the kernel's per-vnode code-signature cache
839
+ // on macOS and the next exec dies with SIGKILL. Copy to a fresh inode
840
+ // and rename it into place instead.
841
+ const tmpOut = `${opts.outPath}.hit-${process.pid}-${Math.random().toString(36).slice(2)}`;
842
+ await copyFile(cachedBin, tmpOut);
843
+ await chmod(tmpOut, 0o755);
844
+ await rename(tmpOut, opts.outPath);
845
+ const now = new Date();
846
+ await utimes(cachedBin, now, now).catch(() => undefined); // LRU bump
847
+ return; // hit: clang skipped entirely
848
+ }
849
+ catch {
850
+ /* miss — build below, then publish */
851
+ }
852
+ // Miss: link the program's own TU against cached per-flavor runtime
853
+ // objects. cflags reproduces exactly the option set every TU sees in the
854
+ // single invocation (the clang driver applies all options to all inputs).
855
+ const cflags = [
856
+ "-std=c11",
857
+ ...driver.targetArgs,
858
+ ...(opts.sanitize
859
+ ? ["-O1", "-fsanitize=address", "-DSCR_RC_AUDIT"]
860
+ : ["-O2"]),
861
+ "-fno-math-errno",
862
+ "-Wno-deprecated-declarations",
863
+ "-I", rtDir,
864
+ ...(regex || dynamic ? ["-I", vendorEngineDir()] : []),
865
+ ...(zlibObjects.length > 0 ? ["-I", vendorZlibDir()] : []),
866
+ ...(curlStubDir !== null ? ["-I", join(vendorCurlDir(), "include")] : []),
867
+ ...(tlsArchive !== null ? ["-I", join(vendorTlsDir(), "include")] : []),
868
+ ...(dynamic ? ["-DSCR_DYNAMIC"] : []),
869
+ ];
870
+ // Collect the runtime sources this build actually compiles (the same
871
+ // conditionals as the command line, by construction).
872
+ const rtInputs = [];
873
+ buildArgs((p) => {
874
+ rtInputs.push(p);
875
+ return p;
876
+ });
877
+ let objects = null;
878
+ try {
879
+ objects = await ensureRuntimeObjects(root, driver.argv, cflags, rtInputs, `obj-v1\0${ccName}\0${cv}\0${fingerprint}\0`);
880
+ }
881
+ catch {
882
+ objects = null; // cache trouble is never a build failure
883
+ }
884
+ await runClang(buildArgs((p) => objects?.get(p) ?? p));
885
+ try {
886
+ await mkdir(binDir, { recursive: true });
887
+ const tmp = join(binDir, `.tmp-${key.slice(0, 8)}-${process.pid}-${Math.random().toString(36).slice(2)}`);
888
+ await copyFile(opts.outPath, tmp);
889
+ await chmod(tmp, 0o755);
890
+ await rename(tmp, cachedBin);
891
+ await pruneCache(root);
892
+ }
893
+ catch {
894
+ /* publishing is best-effort */
895
+ }
896
+ }
897
+ //# sourceMappingURL=cc.js.map