Release executables now import small runtime functions from runtime
bitcode, as speed executables do, so LLVM can inline them into program
code. Release units keep the runtime compiler's own objects, unchanged,
and additionally ship the bitcode of the same compilation reduced by the
helper's prune-import. Their unit-local symbols stay local, so only
bodies that need none of them can be imported, which keeps the import
small: about 1.3 MB per program.
Only unsanitized programs small enough for one codegen partition import.
Every partition reads the bitcode again; on the Effect fixtures (64
partitions) import cost +2..18% compile time and +4.3..4.9% size, so
partitioned release builds stay exactly as before. Speed is unchanged.
Measured against main in Linux Vercel Sandboxes, same-VM interleaved
medians (bench-runtime --layouts=4 --runs=16), on two VMs:
- bench-runtime (31 workloads): -1.3% (8 faster, 0 slower) and -1.0%
(1 slower, typed-array-scan-workers +3.2%); size +0.0%
- js-compiled suite (22 programs): -2.2% and -1.8%; size -0.1%
- cold builds of all 53 programs, interleaved: +3.1%; helper time per
build +40..235 ms
- Effect fixtures: byte-identical to main
Release builds now emit the string retain and the dyn retain and release
fast paths inline. The dyn release decides the cases scr_dyn_release
decides by kind and calls scr_dyn_release_survivor for capsules and
handles. String releases and the other families stay runtime calls, as
string-heavy programs have many release sites. The speed posture keeps
every fast path and gains the dyn release. The runtime ABI version moves
to 21 because program objects call the new runtime entry point.
Every SCR_ELEM_REF array stored its element type's retain, release and trace entry points in its own header (24 bytes), so ScrArr was 80 bytes: an 80-byte block for arrays of acyclic elements and a 96-byte block for cycle-capable ones. The three pointers now live in one constant ScrArrOps descriptor per element type, which the compiler emits once per module (@sc_arrops.*) and the runtime keeps static, and the header holds a pointer to it plus a 4-byte `cyc` flag beside `elem` that the inline RC fast paths test. ScrArr is 64 bytes: arrays take 64- and 80-byte blocks.
scr_arr_new_ref(retain, release, trace, cap) becomes scr_arr_new_ops(ops, cap); scr_set_to_arr_ref takes the result's descriptor from its caller instead of building an array from the set's key adapters. The backend names the %ScrArr fields in SCR_ARR_FIELD. The layout change bumps the runtime ABI marker to scr_runtime_abi_v13.
Nearly every long-lived object becomes a cycle candidate once, soon after
it is built: the constructing frame's release leaves it held only by the
structure it was stored into. A nursery pass then walks it (about 75 ns per
object), it survives, and it is re-buffered for the mature level, whose
next pass walks it again. In tsc-ts single-threaded, nursery passes walked
6.0M objects on Excalidraw and 36.5M on Sentry and freed 7%; 99% of what
they walked was promoted. Most of that walking goes to types whose objects
never die in a young cycle: SymbolLinks, NodeLinks and Symbol were 2.0M of
Excalidraw's candidates and the collector freed none of them, while young
garbage comes from a few types (inference contexts and mappers, closures,
some arrays). Types with a collector yield under 0.5% are 42-55% of what
nursery passes walk.
So the collector now keeps per-thread statistics per type and level. Each
ScrCycType gains a `kind` index: emitted descriptors number themselves from
32 (FIRST_EMITTED_CYC_KIND / SCR_CYC_KIND_EMITTED), the runtime's own
families have fixed kinds below that, 0 keeps no statistics. For the
nursery and mature levels, a pass counts the candidates of each kind it
walks and the objects of each kind it frees. After every 8192 walked
candidates a kind bypasses that level for the next window if fewer than 1
in 128 of them were freed. A release that buffers an object at a level its
type bypasses promotes it past the level first, and a nursery pass that
re-buffers a survivor into a bypassed mature level moves it to the old
level. One candidate in 16 stays put, so the yield keeps being measured
and a type whose objects start dying young again is walked again after
one window.
Only the generation an object is judged in changes. Any assignment is
sound (a restricted pass counts edges from older objects as external), a
bypassing candidate stays buffered, and full passes walk everything they
did before, so the old level's bounds hold unchanged. Cyclic garbage made
only of objects of a bypassing type waits for the next pass at an older
level. Programs that never complete a window (8192 walked candidates of one
type) are collected exactly as before.
The descriptor layout is part of the emitted-code/runtime ABI, so the
program-object marker moves to scr_runtime_abi_v12.
Pass log (instrumented build, single-threaded, one run each, with the
preceding null-slot change): all collector passes Excalidraw 606 -> 313 ms
(nursery 435 -> 153), TypeORM 629 -> 383 ms, tsc-ts self-check 377 -> 215
ms, Sentry 7.61 -> 5.75 s (nursery 2.92 -> 1.04 s, mature 1.17 -> 1.02 s;
the full pass is unchanged at 3.6 s).
tsc-ts (Linux sandbox, interleaved, medians; 10 rounds, Sentry 5 rounds
pinned to 8 CPUs; both changes against int/best-v6): single-threaded
Excalidraw -8.0%, TypeORM -4.2%, self-check -4.3%, Sentry -5.3%;
--checkers 8 -12.2%, -0.8%, -2.3%, -1.6%. Peak RSS -0.8% to +0.6%.
Diagnostics identical.
Tests: test_cycle gains check_nursery_bypass, check_mature_bypass and
check_rebuffer_past_mature (a test-only entry point, scr_cyc_pass_at under
SCR_CYC_TESTING, runs passes at exact levels); each fails under the
corresponding mutation (no bypass, a sticky decision, no sampling, no freed
or walked counts, a bypassing candidate left unbuffered, survivors
re-buffered into a bypassed level). Corpus 4690 builds a long-lived
declaration graph large enough for both levels to bypass and lets objects
of the same classes die in cycles; under --sanitize the exit RC audit
proves they are reclaimed.
Every ScrArr carried sixteen words, seven of them (sparse slots, their
length and capacity, noncanonical numeric properties with length and
capacity, and match-result metadata) used only by sparse arrays, arrays
with numeric non-index properties and regex match results. They now live in
a ScrArrExt the array points at, allocated on first use: a dense array
shrinks from 128 to 80 bytes, and a cycle-capable one (with its 16-byte
collector header) from 144 to 96.
Runtime readers go through a shared empty ext; writers allocate it. The
backend mirror loses the seven fields, tagged-template arrays are emitted
with a NULL ext, live stream commits swap the ext pointer (which swapped all
seven fields before), and the cached typed-view refresh swaps sparse slots
and properties through scr_arr_swap_indexed_ext, keeping each array's
metadata as before. The program-object ABI marker moves to
scr_runtime_abi_v11.
A corpus program (4610-array-ext-dyn-refresh.ts, with its order baseline) pins numeric non-index properties of a dynamically typed array through its refreshed typed view, sparse slots written directly and through a live reference, truncation, and regex match metadata read directly, through `any` and after a copy.
Every cycle-capable object carried a 32-byte header: trace and teardown function pointers, a 32-bit color, the buffered flag, the generation and a 64-bit candidate index. The two function pointers are the same for every object of a type, so the header now points at one constant ScrCycType { trace, free_fn } descriptor per type, and the remaining fields pack into the second word: u8 color, u8 gen, u16 buffered, u32 buf_index (candidate buffers stop at 2^31 entries). wasm32 pads before the descriptor, so every object-relative offset is the same on all targets.
Emitted shapes define their descriptor next to the trace and teardown functions; inline allocation stores one header word; the inline release reads buffered at obj-6 and the inline free reads the generation as an i8 at obj-7. Runtime families allocate with static descriptors. The program-object ABI marker moves to scr_runtime_abi_v10.
tsc-ts (interleaved, 5 rounds): peak RSS -8% to -11% on every app and mode; single-threaded wall -0.3% to -5.5% on top of the typed-rest-spread change.
- Synchronize published packages and the compiler surface manifest at v0.2.8, and bump the runtime ABI marker to v9.
- Add release notes for worker graph sharing, catchable deep recursion, parallel code generation, runtime performance, and compatibility fixes.
Worker runtime variants link only into executables, whose own TLS block
holds every runtime thread-local, but they were compiled with the
default general-dynamic model. The linker relaxes each __tls_get_addr
call to a %fs-relative address, yet the compiler had already treated it
as a call: hot runtime leaves saved and restored extra registers around
every thread-local access (scr_mg_visit pushed five registers instead
of one). With -ftls-model=initial-exec each access is one %fs-relative
operand. Darwin, Windows and wasm variants are unchanged.
tsc-ts self-check, Linux sandbox, Worker build vs non-worker build:
1.04x -> 1.03x (9 interleaved runs).
The default build's largest size contributor on the stack was H23's
-falign-functions=64 on every x86-64 runtime unit (2-11 KB per program;
results/h45-size-budget). Release and library flavors now keep the
compiler's default function alignment; the opt-in speed flavor keeps the
blanket 64-byte alignment. Vendored archives stay 64-byte aligned
(lre_exec keeps its pinned placement) except libunicode.c, whose small
table helpers were most of the regex programs' padding. The runtime-first
link order is unchanged.
Docs and usage text describe speed without the reverted runtime profile.
Re-lands perf/h02-runtime-lto (6d373317, b62448f3) behind the speed
posture.
- Runtime packs gain an executable `speed` flavor whenever the host LLVM
helper can run. Its static units are emitted through the helper's
`runtime-unit` command (promoted unit-local symbols, object and import
bitcode from one module); its SCR_DYNAMIC variants reference the release
objects. Release, dev, and library flavors and the vendor archives are
built exactly as before (the rebuilt macOS pack's flavors and archives
are identical to the baseline's).
- Speed program builds load the speed flavor's bitcode and pass it to the
helper (`--import-bitcode`); release builds never do.
loadRuntimeBitcode only reads a real speed flavor, never release.
- The helper drops the emitter's inert sanitize_address attributes only
when it imports, so release modules reach the pipeline unchanged.
- Tests: runtime-pack.test.ts covers speed-variant bitcode selection,
verification, and that release-flavor bitcode is never imported;
native-codegen.test.ts covers helper arguments and cache keys.
Release executables stay byte-identical to the stack tip with the rebuilt
helper (14/14 workloads, macOS).
Zig cc keeps frame pointers by default, so every packaged runtime function
on Linux paid push/mov/pop of %rbp, including tiny hot leaves such as
scr_exc_pending. Linux release flavors now pass -fomit-frame-pointer
(unwinding still uses .eh_frame; dev flavors, Darwin, and the sanitized
lane are unchanged).
On top of the runtime-unit alignment from the link-order change. The
separate NDEBUG change (34fbc5a4 on perf/h24-release-hygiene) is not part
of this stack: zig cc already defines NDEBUG at -O2 on Linux, and it
measured neutral on macOS.
Linkers lay out code in input order, and executables linked the program
object first, so any change in program size shifted every runtime
function. The vendored archives build at -Os, which drops function
alignment, so the regex interpreter (lre_exec) landed at a different
offset within its cache line after every unrelated edit and swung
template-render by up to 17% and regex-logs by up to 8% on x86-64.
Executable inputs are now runtime objects, runtime archives, the
program object, its partitions, then FFI libraries. Generated code only
references runtime-object symbols, so single-pass GNU ld still resolves
every archive member; when FFI libraries are present, non-Darwin links
list the runtime archives again after them. The native-link-info input
order follows the same plan.
Vendored archives build with -falign-functions=64 on native targets.
On x86-64 the release runtime units do too: generated loops that call
runtime helpers otherwise swing by up to 13% with the program's
position relative to 16-byte-aligned helpers (numeric-kernels). arm64
keeps its runtime units unaligned, where the size cost is higher and
the effect was not measured.
A no-op 32-byte growth of the program object moved the baseline by
+10.8% (template-render, CI +9.3..+12.9%) and is neutral with this
change on every measured workload (Linux, 15 runs).
- Preserve callback identity, receiver binding, and class references across unions and collections.
- Track record field presence independently of undefined values and improve native runtime behavior.
- Expand differential coverage and streamline CI bootstrap and compiler rebuild checks.
- Add native worker execution with isolated module state, structured messages, and lifecycle controls.
- Support shared buffers and integer atomics with coordinated runtime ownership and cancellation.
- Extend compiler, runtime-pack, differential, and compatibility coverage for native concurrency.
- Enforce oxlint and oxfmt for owned JavaScript and TypeScript with independent CI checks.
- Preserve compiler import boundaries and protected fixtures while cleaning up lint findings.
- Standardize source formatting and validate import, fixture, and embedded-source protections.
* Expand static compilation for common Effect 4 patterns
* Run every Effect 4 fixture slice in CI
* Preserve existing corpus behavior across Effect 4 lowering
* Preserve Effect platform callbacks and native stream state
* Record new corpus baselines and retain child stdin refusals
* Retire the obsolete stateful regex diagnostic
* Preserve promise reference count layout for cycle collection
* Make native installs portable and smaller
- Support glibc 2.34 with portable compiler binaries and static LLVM helpers.
- Bundle the host runtime and discover optional cross-target packs from project dependencies.
- Add application build benchmarks covering latency, executable size, and memory.
* Keep bootstrap tools available in Sandbox validation
- Isolate Zig for native compiler bootstrap and glibc entropy contracts.
- Preserve the existing toolchain scope for cache and corpus tests.
* Fix portable compiler bootstrap linking
- Keep native support objects compatible with the destination host linker.
- Resolve installed targets using the host declared by the toolchain manifest.
- Remove C emission, debugging options, and automatic backend fallback.
- Complete LLVM lowering and native bootstrap coverage across supported targets.
- Link executables and libraries with precompiled C runtime packs, including iOS and Android.
- Preserve fresh constructor identity and shared captures across local class instances.
- Carry constructors through unknown values without a JavaScript engine.
- Support runtime-length spreads into fixed and rest parameters.
- Compile the production validator and exercise native IR construction and validation on C and LLVM.
- Preserve Error causes, wider callbacks, and discriminated JSON layouts in static programs.
- Version the IR and runtime ABI and cover the new compiler paths with differential and sanitizer tests.
- Pin GNU runtime packs to glibc 2.36 Zig targets and reject incompatible imports.
- Provision verified Zig builds and exercise published artifacts on Debian Bookworm.
Fixes#349
Co-authored-by: Cruel <383999+Cruel@users.noreply.github.com>
* Expand LLVM native output targets
- Add owned helper and runtime-pack contracts for macOS x64, Linux glibc/musl, Windows x64, and WASI.
- Generalize LLVM target selection, package validation, linker plans, and release/CI matrices.
- Document linker boundaries and add focused native-output and runtime-pack coverage.
* Pin Windows LLVM helper CI to VS 2022
The official LLVM archive needs the Visual Studio 2022 CMake generator, which is not guaranteed by windows-latest.
* Align bootstrap cache with LLVM runtime-pack builds
Use the same target-specific helper and linker identity before and after compiler loading so routed executable cache hits remain lightweight.
* Install Windows LLVM CI tools with Chocolatey
windows-2022 includes Visual Studio but not winget; use its available Chocolatey bootstrap for CMake and Ninja.
* Prevent Zig version probes from leaving runtime-pack objects
Run runtime-pack compiler version probes in a private temporary directory and remove the accidental tracked WASI a.o file.
* Use 7-Zip for Windows LLVM setup
Extract the official LLVM development archive with two-stage 7-Zip instead of Windows tar.exe, which timed out while materializing the toolchain tree.
* Define ssize_t for the MSVC runtime pack
Clang's MSVC target does not expose POSIX ssize_t through sys/types.h; define the pointer-sized runtime type without affecting MinGW.
* Build the Windows runtime pack with Zig
Use the MinGW-compatible Windows sysroot required by the runtime while retaining COFF helper output and installing Zig in the Windows native CI lane.
* Give macOS LLVM differential shard time to finish
Shard 1 passed its setup and focused contracts but was cancelled during its cold LLVM differential slice at the 20-minute job limit.