@scriptc/compiler 0.0.26 → 0.0.28

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.
@@ -248,6 +248,28 @@ export interface LibArchiveOptions {
248
248
  * matching compileC's arbitrary-input safety boundary. */
249
249
  cacheIdentity?: string;
250
250
  sanitize?: boolean;
251
+ /** Multi-instance library mode (the profile's abi.localize_runtime): the
252
+ * external symbols to KEEP global — every other scriptc external
253
+ * definition in the archive (the runtime's internals, the program TU's
254
+ * mangled functions and globals, vendor objects) is demoted to a local
255
+ * symbol. Toolchain sanitizer ABI remains external as required,
256
+ * so N archives built under pairwise-distinct prefixes link into one
257
+ * process with no symbol collisions and no shared mutable runtime state.
258
+ * Undefined references (the target C/math runtime and system APIs, plus
259
+ * sanitizer ABI in instrumented builds) keep their global binding. Windows
260
+ * embedders additionally link advapi32, iphlpapi, and ws2_32. Omitted = the
261
+ * classic archive, byte-for-byte. Admitted for darwin/linux/win32 native
262
+ * hosts, linux and windows cross triples from any host, and macos cross
263
+ * triples from a darwin host (compileLibrary owns the refusal fence). */
264
+ localizeSymbols?: readonly string[];
265
+ /** Thread-instanced state (the profile's abi.instance_per_thread): every
266
+ * TU of the archive compiles with -DSCR_THREAD_INSTANCES, moving the
267
+ * runtime units' mutable statics into thread-local storage (SCR_TL in
268
+ * scr_runtime.h) to match the program TU's thread-local globals — one
269
+ * complete instance per embedder thread. The define rides cflags, so
270
+ * every cache tier keys it automatically; omitted = the classic
271
+ * archive, byte-for-byte. */
272
+ threadInstances?: boolean;
251
273
  /** IR-detected link gates (the compileC precedent, refusal-narrowed). */
252
274
  regex?: boolean;
253
275
  assert?: boolean;
@@ -6,6 +6,7 @@ import { access, chmod, copyFile, link, mkdir, mkdtemp, readdir, readFile, realp
6
6
  import { availableParallelism, homedir, tmpdir } from "node:os";
7
7
  import { basename, delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path";
8
8
  import { promisify } from "node:util";
9
+ import { localizeElfObject, mergeAndLocalizeCoffObjects } from "./object-localize.js";
9
10
  const execFileAsync = promisify(execFile);
10
11
  /** Test lanes run against one immutable checkout and one immutable toolchain
11
12
  * for the lifetime of each Vitest worker. Production deliberately rediscovers
@@ -712,9 +713,12 @@ async function ensureTlsArchive(sanitize, driver, buildIdentity, cacheRoot = ven
712
713
  * units a library may reach are the pure-data ones (regex + the vendored matcher, assert,
713
714
  * inspect, symbol, searchParams, emitter+dyn_handle, zlib); every
714
715
  * loop-hooked or ambient unit was refused at SC4005 before emission.
715
- * External-symbol contract: undefined references only to libc/libm — which
716
- * is why zlib rides the VENDORED per-flavor objects here even on hosts
717
- * (the executable lane's system `-lz` cannot ride inside an archive). */
716
+ * External-symbol contract: undefined references only to the target's C/math
717
+ * runtime and system APIs. Windows embedders additionally link advapi32,
718
+ * iphlpapi, and ws2_32; the platform driver supplies its ordinary CRT and
719
+ * kernel imports. Zlib rides the VENDORED per-flavor objects here even on
720
+ * hosts because the executable lane's system `-lz` cannot ride inside an
721
+ * archive. */
718
722
  /** The library base: the executable lane's unconditional sources minus the
719
723
  * fiber/loop and child-process units, plus the library-mode TU. */
720
724
  const LIB_RUNTIME_SOURCES = [
@@ -757,6 +761,7 @@ export async function compileLibArchive(opts) {
757
761
  "-fno-strict-aliasing", // the emitted object model type-puns — see compileC's buildArgs
758
762
  "-Wno-deprecated-declarations",
759
763
  "-DSCR_LIB",
764
+ ...(opts.threadInstances ? ["-DSCR_THREAD_INSTANCES"] : []),
760
765
  ...(opts.textDecoderLegacy ? ["-DSCR_TEXT_DECODER_LEGACY"] : []),
761
766
  "-I", rtDir,
762
767
  ...(regex ? ["-I", vendorEngineDir()] : []),
@@ -815,7 +820,13 @@ export async function compileLibArchive(opts) {
815
820
  }
816
821
  }
817
822
  const vendorBuildIdentity = await currentVendorCacheBuildIdentity(driver, `${toolchainEnv}\0${implicitToolchain ?? "<uncached>"}`);
818
- const cacheCompleteArchive = root !== null && await archiverSupportsPersistentCache(arArgv, driver);
823
+ // Runtime-localized archives skip the completed-archive tier: their bytes
824
+ // additionally depend on the localization toolchain's identity (host ld/
825
+ // objcopy or the cross driver's lld), which the archive key does not
826
+ // fingerprint. The runtime-object tier still serves them (localization
827
+ // consumes the same per-flavor objects).
828
+ const cacheCompleteArchive = opts.localizeSymbols === undefined &&
829
+ root !== null && await archiverSupportsPersistentCache(arArgv, driver);
819
830
  let cachedArchive = null;
820
831
  let compilerVersion = "";
821
832
  let archiverVersion = "";
@@ -968,17 +979,29 @@ export async function compileLibArchive(opts) {
968
979
  }
969
980
  }
970
981
  const objects = [programObject, ...runtimeObjects, ...lreObjects, ...zlibObjects];
971
- // A cacheable build owns a private archive from `ar` through publication.
972
- // The caller-visible output can be shared by another invocation without
973
- // letting that invocation's bytes poison this key.
974
- const archiveOutput = cachedArchive === null ? opts.outPath : join(buildDir, "artifact.lib.a");
982
+ // Multi-instance library mode: the archive's one member becomes the
983
+ // combined, symbol-localized object (cached vendor/runtime objects
984
+ // are read-only inputs here — the combine step never mutates them).
985
+ const archiveMembers = opts.localizeSymbols === undefined
986
+ ? objects
987
+ : [
988
+ await localizeLibraryObjects(driver, arArgv, buildDir, programObject, [...runtimeObjects, ...lreObjects, ...zlibObjects], opts.localizeSymbols, stem),
989
+ ];
990
+ // A cacheable or runtime-localized build owns a private archive from
991
+ // `ar` through publication. Localized archives deliberately bypass the
992
+ // completed-artifact cache, but still need atomic installation so two
993
+ // invocations sharing a caller-visible output cannot race `rm`/`ar` on
994
+ // that path.
995
+ const archiveOutput = cachedArchive === null && opts.localizeSymbols === undefined
996
+ ? opts.outPath
997
+ : join(buildDir, "artifact.lib.a");
975
998
  await rm(archiveOutput, { force: true }); // `ar r` would append into a stale archive
976
999
  await mkdir(dirname(archiveOutput), { recursive: true });
977
1000
  await execFileAsync(arArgv[0] ?? "ar", [
978
1001
  ...arArgv.slice(1),
979
1002
  "rcs",
980
1003
  archiveOutput,
981
- ...objects,
1004
+ ...archiveMembers,
982
1005
  ]);
983
1006
  if (archiveOutput !== opts.outPath)
984
1007
  await installArtifact(archiveOutput, opts.outPath);
@@ -1029,6 +1052,121 @@ export async function compileLibArchive(opts) {
1029
1052
  if (root !== null)
1030
1053
  await pruneCache(root).catch(() => undefined);
1031
1054
  }
1055
+ /* Multi-instance library mode's localization step: combine the program
1056
+ * object with exactly the runtime/vendor members it reaches into ONE
1057
+ * relocatable object, then demote every scriptc external definition except
1058
+ * the profile-declared symbols to a local symbol. The internals are not
1059
+ * renamed apart — they stop being visible to the embedder's linker at all,
1060
+ * so a second archive built under a different prefix brings its own private
1061
+ * copy of the whole runtime (allocator, collector, arena, panic sink) into
1062
+ * the same process. Undefined references (the target C/math runtime and
1063
+ * system APIs, plus sanitizer ABI in instrumented builds) keep their global
1064
+ * binding: those platform services and the sanitizer are the embedder's,
1065
+ * shared by design. Windows embedders additionally link advapi32, iphlpapi,
1066
+ * and ws2_32.
1067
+ *
1068
+ * Member selection matters: a classic archive's unused members (and their
1069
+ * undefined references to units library mode excludes, like the
1070
+ * fs-promises unit's fiber symbols) never reach an embedder's link. A
1071
+ * blind merge of every object would carry those references into the one
1072
+ * combined member. Staging the support objects into an intermediate
1073
+ * archive keeps the linker's own member semantics: `ld -r` pulls only the
1074
+ * members the program object transitively needs (the COFF arm implements
1075
+ * the same member semantics in process).
1076
+ *
1077
+ * Mach-O — one host-ld64 invocation: -r merges program + needed members,
1078
+ * -exported_symbols_list demotes every unlisted global to
1079
+ * private extern, and -r without -keep_private_externs writes
1080
+ * private externs out as non-external symbols. Apple ASan's
1081
+ * image-registration COMMON remains shared so the final Mach-O
1082
+ * image registers its globals once. ld64 reads every macOS
1083
+ * architecture, so darwin-host macos cross triples ride the
1084
+ * same arm; compileLibrary refuses macos targets elsewhere.
1085
+ * ELF, native linux host — ld -r merges with --force-group-allocation
1086
+ * (ASan's instrumented globals ride ELF section groups whose
1087
+ * signatures repeat across archives sharing runtime objects;
1088
+ * resolving the groups into the combined member keeps a later
1089
+ * multi-archive link from discarding one archive's copies),
1090
+ * then binutils objcopy --keep-global-symbols localizes every
1091
+ * other DEFINED global (objcopy leaves undefined symbols
1092
+ * global by its own rule). The exact historical recipe.
1093
+ * ELF, cross triples from any host — `zig cc -target <triple> -r`
1094
+ * merges (zig is already the cross driver's hard requirement),
1095
+ * then localizeElfObject demotes in process and resolves
1096
+ * section groups the way --force-group-allocation does, with
1097
+ * no host binutils/llvm-objcopy dependency.
1098
+ * COFF, native win32 hosts and windows cross triples from any host —
1099
+ * mergeAndLocalizeCoffObjects performs member selection, the
1100
+ * merge, cross-object symbol resolution, and demotion in
1101
+ * process: no linker offers a COFF relocatable mode (lld-link
1102
+ * mirrors MSVC link.exe; zig's COFF driver refuses multi-object
1103
+ * merges) and llvm-objcopy rejects symbol-scope flags for
1104
+ * COFF, so no tool pairing exists to shell out to. */
1105
+ async function localizeLibraryObjects(driver, arArgv, buildDir, programObject, supportObjects, keepSymbols, stem) {
1106
+ const platform = targetPlatform(driver);
1107
+ const combined = join(buildDir, `${stem}.localized.o`);
1108
+ const staging = join(buildDir, `${stem}.localize-staging.a`);
1109
+ const keepFile = join(buildDir, "localize-keep.syms");
1110
+ const run = async (argv) => {
1111
+ try {
1112
+ await execFileAsync(argv[0], [...argv.slice(1)]);
1113
+ }
1114
+ catch (err) {
1115
+ const stderr = err.stderr ?? String(err);
1116
+ throw new Error(`${argv[0]} failed while localizing the library archive's runtime symbols (abi.localize_runtime).\n` +
1117
+ `Runtime symbol localization needs the ${platform === "darwin" ? "host toolchain's ld" : driver.target === null ? "host toolchain's ld and objcopy" : "cross driver's relocatable link"} beside the C compiler.\n\n${stderr}`);
1118
+ }
1119
+ };
1120
+ if (platform === "win32") {
1121
+ // COFF has no relocatable-link tool to stage through; the member
1122
+ // selection and combine+demote happen in process over the object bytes.
1123
+ const [program, ...support] = await Promise.all([programObject, ...supportObjects].map((path) => readFile(path)));
1124
+ try {
1125
+ await writeFile(combined, mergeAndLocalizeCoffObjects(program, support, new Set(keepSymbols), {
1126
+ program: basename(programObject),
1127
+ support: supportObjects.map((path) => basename(path)),
1128
+ }));
1129
+ }
1130
+ catch (err) {
1131
+ throw new Error(`COFF symbol localization failed while localizing the library archive's runtime symbols (abi.localize_runtime).\n\n${err.message}`);
1132
+ }
1133
+ return combined;
1134
+ }
1135
+ await run([arArgv[0] ?? "ar", ...arArgv.slice(1), "rcs", staging, ...supportObjects]);
1136
+ if (platform === "darwin") {
1137
+ await writeFile(keepFile, keepSymbols.map((s) => `_${s}\n`).join(""));
1138
+ await run(["ld", "-r", programObject, staging, "-o", combined, "-exported_symbols_list", keepFile]);
1139
+ }
1140
+ else if (platform === "linux" && driver.target === null) {
1141
+ await writeFile(keepFile, keepSymbols.map((s) => `${s}\n`).join(""));
1142
+ await run(["ld", "-r", "--force-group-allocation", programObject, staging, "-o", combined]);
1143
+ await run(["objcopy", `--keep-global-symbols=${keepFile}`, combined]);
1144
+ }
1145
+ else if (platform === "linux") {
1146
+ // Cross ELF: the cross driver's own lld performs the relocatable merge
1147
+ // (with the staging archive's member semantics); demotion and section-
1148
+ // group resolution happen in process. -nostdlib keeps zig from feeding
1149
+ // libc/compiler-rt inputs into the merge.
1150
+ await run([
1151
+ driver.argv[0] ?? "zig",
1152
+ ...driver.argv.slice(1),
1153
+ "-target", driver.target,
1154
+ "-nostdlib",
1155
+ "-r", programObject, staging,
1156
+ "-o", combined,
1157
+ ]);
1158
+ try {
1159
+ await writeFile(combined, localizeElfObject(await readFile(combined), new Set(keepSymbols)));
1160
+ }
1161
+ catch (err) {
1162
+ throw new Error(`ELF symbol localization failed while localizing the library archive's runtime symbols (abi.localize_runtime).\n\n${err.message}`);
1163
+ }
1164
+ }
1165
+ else {
1166
+ throw new Error(`runtime symbol localization (abi.localize_runtime) has no ${platform} arm; compileLibrary admits darwin, linux, and win32 builds only`);
1167
+ }
1168
+ return combined;
1169
+ }
1032
1170
  /* -------------------------- persistent build cache ---------------------------
1033
1171
  * Content-addressed caches that let repeat builds of unchanged programs skip
1034
1172
  * payload code generation/linking — the test lanes' dominant cost. Executable