`scripts/refresh-cli-profile.mjs --context-sensitive` runs the training twice. The second run uses a compiler that the first profile optimizes and that is instrumented again after inlining, and it merges those counts into the first profile. Builds with the result also order each function's blocks by the counts and split cold blocks off (context-sensitive profiles). `--base-profile` runs only the second phase. `scripts/build-native-cli.mts` builds such a compiler when `SCRIPTC_NATIVE_PROFILE_GENERATE` and `SCRIPTC_NATIVE_PROFILE_USE` are both set. The release workflow's cli-profile job collects the profile this way, with a 120-minute timeout.
Shard 1 of "test (macOS arm64 LLVM artifacts, no clang)" ran the no-clang
object, helper parity, packaged helper and packed npm installation
contracts around its LLVM differential slice: about 12 minutes on top of
a 35-minute slice, 45 to 49 minutes against the job's 50-minute timeout.
The contracts now run in "test (macOS arm64 LLVM artifacts, no clang,
contracts)", which the all-lanes check requires like the shards, and
every shard runs only its differential slice.
macOS binds libnode's own calls to v8::base::Stack inside libnode, so the
bridge's definitions cannot give V8 Node's stack as they do on Linux; V8
took the main thread's stack start and its conservative scan faulted in
the gap above Node's stack. While Node starts on its stack, the main
thread's pthread record now names that stack, which V8 reads once and
keeps.
macOS's makecontext clears the whole stack it is given, so it gets only
the top slice of Node's stack (no 64 MiB pass per context).
CI: dev-pack.json pins mirrored darwin-arm64 and darwin-x64 libnode
builds of the pinned Node 24.22.0 from the libnode-packs workflow, the
dev-pack action serves either platform, and the macOS lanes provision it,
so island tests fail rather than skip on macOS CI. The arm64 LLVM job
runs the island harness; the Intel job runs it and the island corpus.
bridge-smoke loads the .dylib bridge on macOS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each @scriptc/cli-<platform> package bundled its platform's LLVM helper
and runtime pack, which are also published as @scriptc/llvm-<platform> and
@scriptc/runtime-<platform>. The CLI packages now depend on those packages
at the same version (workspace:*, which pnpm pack writes as the exact
version) and leave them out: cli-linux-x64-gnu goes from 115 MB to 60 MB
packed.
- build-native-cli.mts --platform-packages, which each CLI package's
build:native passes, writes a manifest without llvm_package, runtime_pack
and runtime_packs.
- loadNativeToolchain resolves a missing helper or host runtime pack as
Node resolves the package from the compiler's directory: nested in the
platform package, beside it (hoisted, or pnpm's virtual store) or higher
up. It requires the compiler's version, and an absent or mismatched
package fails with an error that names it.
- installNativeCli records where the package manager put both packages in
the installed manifest, with the same checks.
- verify-native-cli.mjs requires the exact dependencies of a platform
package and checks the resolved packages' versions.
- package-native-cli.mjs bundles the helper and runtime pack into the
standalone archives, which stay self-contained.
- smoke-native-install.mjs installs the helper and runtime pack tarballs
with the platform package; the bootstrap's exported package drops its
bundled copies first.
- release.yml publishes the CLI packages only once every helper and
runtime pack package resolves on npm, and scriptc only once the CLI
packages resolve, each wait at most 25 minutes. A rerun skips the
uploads npm already accepted and waits again.
The Linux helpers linked LLVM 22.1.8's release archive. The helper's code
generation is LLVM's code, so building that LLVM from its release sources
with ThinLTO and with a profile of the helper's own work makes code
generation 5-9% faster than the helper as released, with identical objects.
- native/llvm-codegen/llvm.profdata.gz is the profile, collected by
scripts/refresh-llvm-profile.mjs: it builds LLVM with IR instrumentation
and an instrumented helper, and runs that helper on tsc-ts, five Effect
fixtures, the runtime benchmark workloads and every 50th corpus program
(scripts/llvm-helper-inputs.mjs). It follows LLVM's sources, so it needs
a refresh when LLVM is bumped.
- scripts/build-llvm-dist.mjs builds LLVM with that profile and ThinLTO,
and packs it with a checksum. The llvm-dist workflow, run by hand, builds
it for x64 and arm64, requires a helper linked against it to give the
same objects as one linked against the release archive
(scripts/check-llvm-dist.mjs), prints both helpers' times and the
profile's mismatch count, and attaches the archive to the LLVM_DIST_TAG
release of this repository.
- The release workflow's Linux jobs, and the wasm32-wasi job, which also
uploads the x64 GNU helper, download that archive and build the helper
with SCRIPTC_LLVM_THINLTO=1: clang, lld and ThinLTO, and the profile for
the helper's own sources. Without the archive, or if that build fails,
the helper links the release archive as before.
- The four Linux helper packages share their build in
native/llvm-codegen/linux-helper.mjs. Without SCRIPTC_LLVM_THINLTO it
builds the same executable as before.
The release workflow publishes each package and moves on once npm accepts
the upload. A package whose processing on npm stalls was visible only on
npmjs.com.
A new npm-available job runs after "Publish to npm". It checks each package
of the release with `npm view <name>@<version> version --prefer-online`
every 2 minutes (scripts/wait-npm-available.mjs) until every package
resolves, and reports each package's time from upload to available in the
log and the job summary. When a package still does not resolve after 2
hours, the job fails and names each missing package and version. It only
reads from the registry.
The publish job lists every package of the release and records the time
npm accepted each upload, keeps those times with its upload receipts, and
passes both to the new job. Create GitHub Release now waits for the new
job, so the release is created and marked complete only once every
package resolves; a later release run checks again and skips uploads npm
already accepted.
A warm rebuild ran about 3.5 MB of the native compiler's code, but the code
was spread over its 69 MB text, so 64 KB fault-around mapped about 20 MB of
it. The emitter can now take a function layout: a list of functions in
tiers. It writes the listed definitions, generated helpers included, first
and by tier, each in a `layout-<tier>` partition group. The LLVM helper
compiles each partition's definitions in text order, so the listed ones
come first in their partitions, and a helper that places functions by
group keeps a tier's definitions in the same partitions. The rest keep
their order and their source-file groups.
- compile() takes a `functionLayout` file (`<tier> <symbol>` lines,
gzip-compressed when the name ends in .gz). Keys leave out module
numbers.
- build-native-cli.mts uses the committed packages/compiler/native/cli.layout.gz,
or the file SCRIPTC_NATIVE_LAYOUT names; an empty value and instrumented
builds use none.
- scripts/refresh-cli-layout.mjs builds an instrumented compiler, builds
the runtime workloads and a corpus slice cold and then unchanged, and
reads the functions each run executed from the raw profiles. Tier 1 is
`--version` and rebuilds that hit both caches; tier 2 is the other builds.
- The release workflow's cli-profile job also collects a layout on the
release commit, and the CLI builds use it. Names of closures and
generated helpers change with nearby edits, so a committed layout
matches fewer functions as main moves on.
- Forked lowering and emission workers reset the profile counters after
fork and write their profile before _exit, so instrumented builds also
count the work done in workers. This applies to refresh-cli-profile.mjs
too.
Executables the compiler produces are unchanged.
On the san 1/20 shard the sanitized nodes build shares the runner with the
corpus's other two workers and the side queue. It took 944 s and 860 s on
#2194's runs and passed 1200 s on main's 7fa88083, failing the test timeout.
The Linux san corpus now skips that one case
(SCRIPTC_THREE_SANITIZED_NODES_JOB), and a three_nodes_san job runs it alone
with the corpus's LeakSanitizer settings. The required test check waits for
the new job. Every other three.js case, the plain nodes build and the macOS
suites are unchanged.
The release workflow's new cli-profile job runs
scripts/refresh-cli-profile.mjs on Linux x64 and uploads the profile.
Every platform's native CLI build uses it through
SCRIPTC_NATIVE_PROFILE_USE, and the GitHub release attaches it as
scriptc-<version>-cli.profdata.gz. When the job fails, the CLI builds
use the committed profile.
build-native-cli.mts accepts a gzip-compressed profile in
SCRIPTC_NATIVE_PROFILE_USE when the name ends in .gz.
refresh-cli-profile.mjs creates the directory of --output before it
starts.
The sanitized self-hosting native-frontend build needs about 15 GB by
itself: its clang compile of the whole frontend under ASan peaks at
10.5 GB beside a 3.8 GB compile in Node. In the 14 GB scope, with the
shard's other work running beside it, the kernel killed that clang
intermittently, so SC5004 reported clang's warnings and no error (SAN
16/20 and 19/20). The scope now caps RAM at 15 GB, still below the
runner's 16 GB, and may still swap past it.
The Linux test shards run their suites in a transient systemd scope with
MemoryMax=13G and no swap, below the runner's 16 GB. A process tree that
grows past the cap loses its largest process to the kernel, and the job
log names the failing test, where the runner itself ran out of memory
and left no log. Hosts without systemd run the suites directly.
The side-queue balancer also takes a table of memory-heavy files, with
their measured peak. They run whole in the cost-balanced task, and each
goes to the shard with the least memory-heavy work so far, after the
time-balanced files. decl-boundary.test.ts, which peaks at about 1.8 GB
alone and had no time measurement, so vitest's file sharding put it in
shard 11, is the first entry. It now runs in shard 1 (plain) and 6 (san).
tsc-ts 0.0.8's API server serves on the named pipe the Node host
connects over on Windows (tsc-ts --api --pipe), and the native compiler
already starts it over its standard streams there, so tsc-ts is the
default checker on every platform. SCRIPTC_CHECKER=tsgo still selects
tsgo.
The compiler depends on tsc-ts 0.0.8, which the release-age exclusions
admit with its platform packages and @scriptc/threads 0.4.2.
The Windows CLI smoke test runs with both checkers, and
verify-native-cli.mjs --run checks that a distribution's tsc-ts runs on
its platform (tsc-ts.exe on Windows).
Its release pass alone takes about 15 minutes, so the dev pass is regularly
cancelled at the 30-minute limit on main and on PRs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sanitized seed compiles the compiler's 700 MB LLVM module with one clang
process under AddressSanitizer. The compile peaks near 14 GB while the Node
process that emitted the module still holds about 6 GB, which exceeds the
runner's 16 GB of memory. The job now enlarges swap to 24 GB before it builds
the seed, as the sanitized emitter job does.
Scope-B milestone NI-1, API side: the explicit call API that replaces the
build container's hand-written node subprocess (api patch 09).
- The new package @scriptc/node exports callSync(specifier, name, ...args)
and call(...). The module loads with require() from the calling module;
arguments and results cross as JSON copies; a thrown Error crosses as its
nearest intrinsic class with its message and string code, other thrown
values as JSON copies; callSync refuses a function that returns a
promise. Its Node implementation runs the calls in-process with the same
copies, so a program prints the same under Node and compiled.
- The compiler lowers calls to the named or namespace imports
(node-api-import.ts, lower-node-island.ts) to the nodeIsland.callSync
and nodeIsland.call libCalls: the calling module's real path, the
specifier, the export name and the boxed argument array, with the result
checked against the declared type like `JSON.parse(s) as T`. Arguments
and results whose types are not JSON data are refused at build time, and
the functions and the namespace refuse as values.
- Corpus programs 6800-6803 (sync and async results, errors, process.exit
from Node with the program's exit listeners, Node's pending timers
keeping the program alive) run under Node and as binaries that load
libnode; `// @requires-libnode` skips them where SCRIPTC_LIBNODE names no
pack (until the pack CI matrix publishes packs). The node-island harness
covers loading Node only on the first call, the beside packaging, a
missing pack, a pack of another Node version or bridge ABI, and the
executable's symbols.
- Release tooling publishes @scriptc/node with the other packages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calls into Node now run on one dedicated 64 MB stack with a guard page,
reached by a context switch, and the bridge sets V8's stack limit on it
(bridge ABI 2). Before, a call from an async fiber (512 KB stacks without a
guard page) failed to bootstrap Node, which the dev-mode LLVM differential
showed. The runtime's stack guard follows the switch, and the switch
carries ASan's fiber annotations.
The Node island tests load Node from a libnode pack. Until the NI-13 packs
are published, .github/actions/libnode-dev-pack assembles one for the
Linux x64 test lanes (plain and sanitized): a pinned, checksum-verified
prebuilt libnode of the repository's Node release (24.22.0) and Node's
official headers (packages/libnode-common/dev-pack.json), with the bridge
built against them by build-pack.mjs. The pack is cached by its inputs and
the bridge source, so only a cache miss downloads and builds; the normal
build path never does.
tests/harness/libnode.ts makes a missing pack a failure in those lanes:
island tests skip only off Linux x64 CI without a configured pack. The
bridge smoke test (packages/libnode-common/test/bridge-smoke.c) loads the
pack the way scr_node_island.c does and makes synchronous, throwing and
asynchronous calls through the bridge with the runtime's bootstrap.
IR, LLVM and object outputs of programs that call into Node no longer need
a pack at build time; only executables record one.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The darwin-arm64, linux-x64-musl and linux-arm64-musl CLI builds failed with
"tsc-ts checker: ... has no @tsc-ts/<platform> executable":
- The lockfile had no entry for @tsc-ts/darwin-arm64@0.0.5, so the
darwin-arm64 install skipped it. The lockfile now lists all seven
platform packages.
- A CLI ships the tsc-ts executable of its target. The musl builds run on
glibc runners, where pnpm installs only the glibc packages. Those jobs now
add musl to pnpm's supported libc before installing.
Pushes to main now run every lane except macos, so a merged commit no
longer queues the macOS native build, the darwin test slices and the
macOS LLVM artifact jobs. The nightly sweep and manual runs still run
every lane. Pull requests keep running the macOS lane when
scripts/ci-lanes.mjs selects it.
- The iOS runtime pack builds move from mobile_runtime_packs (linux lane)
to mobile_runtime_packs_ios (macos lane), since they run on macOS
runners. The Android pack stays in the linux lane.
- The macos_full dispatch input is replaced by macos_suite (full or
darwin, default full). Manual runs run the complete macOS suite unless
macos_suite is darwin.
- ci-lanes.mjs also selects the macos lane for paths naming kqueue, the
runtime's platform header, and every file of the darwin suite in
scripts/ci-test-plan.mjs (adding tests/harness/differential.test.ts
and the darwin runtime pack's archive test).
- The required test check counts a skipped macOS job as passing when the
macos lane is off, as it already did for pull requests.
The Linux test jobs set ASAN_OPTIONS=detect_leaks=0 for both flavors and
left leak detection to the RC audit, which does not see runtime tables
allocated with calloc. A worker thread's record-source table leaked that
way, and only local runs reported it. The san shards' case-sharded corpus
task now sets detect_leaks=1 and an LSAN_OPTIONS suppressions file,
tests/harness/lsan.supp, which starts empty. Plain shards, the side queue,
the Effect jobs and macOS, whose ASan has no LeakSanitizer, keep leak
detection off.
scriptc's frontend talks to a TypeScript 7 API server. tsc-ts serves the
same API as tsgo, and it is now the default checker; SCRIPTC_CHECKER=tsgo
selects tsgo, which scriptc still includes.
- @scriptc/compiler depends on tsc-ts 0.0.3. The native distribution
ships its platform package as lib/tsc-ts, named by a new tsc_ts field
of the toolchain manifest; a manifest without it takes the executable
of an installed tsc-ts package.
- The executable cache key includes the tsc-ts executable's identity, in
the compiler and in the CLI's startup route.
- tsc-ts keeps the syntax trees it builds for the life of its process,
so the source parser starts a new server after 256 KB of parsed
source instead of letting one server grow with every file.
- A CI job compiles every corpus program with both checkers, in release
and dev, and requires the same LLVM IR and diagnostics
(scripts/checker-identity.mjs).
- Tests that compare with the TypeScript 7 SDK, the checker-panic
diagnostics, and the protocol tests run against tsgo.
Since pushes to main run to completion, each ran the complete suite on macOS:
20 test jobs and about 350 macOS job-minutes per push (run 38831529977), which
at about four pushes an hour exceeded the repository's macOS runners. On
2026-10-16 at 02:40 UTC 41 macOS jobs were running, 95 were queued (the oldest
for 35 minutes), and main runs took over an hour.
Pushes to main now run the darwin suite on macOS, as pull requests do: 6 jobs
and about 100 minutes (run 38835202089). The nightly schedule still runs the
complete suite, and a manual run (workflow_dispatch) with macos_full set runs
it on demand, for example on a release candidate commit. Manual runs get a
concurrency group of their own, so they neither cancel nor wait for the push
run of the same commit. The policy test evaluates the macOS matrix expressions
for each event.