- 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.
The natively built emitter records an allocation stack for every object
under ASan, so emitting its own module takes several times longer than
the plain build and now runs close to the 300 s per-stage limit on shared
runners. Sanitized runs get a 900 s per-stage limit and a matching test
budget; plain runs keep the existing limits.
Deferred collection diagnostics now stay attached to their declaration until a
copy reaches the final report, so a discarded probe buffer or a successful
nested collection can no longer drop them. Uses of a binding whose type names
a rejected class report that class's diagnostic before the inherited-blocker
cascade.
Compiler caches keyed by programs and lowering passes move onto the
objects that own their lifetime, and the field-creation pass recognizes
its own writes by their receiver temporaries. A source test keeps weak
collections out of the compiler. Records and arrays that reach an
untyped slot through a union now keep their identity, and empty object
literals are keyed by their native object, so both work as WeakMap and
WeakSet keys instead of failing at runtime.
Two self-hosting tests compile large parts of the compiler under the
sanitizer, and on loaded macOS runners they regularly come close to their
limits. Backend analysis ranged from 165 s to over 300 s against a 300 s
limit with no code change behind the spread (134 s locally, base and
current alike), and the native-frontend compile ranged from 643 s to over
900 s against a 900 s limit. Backend analysis now allows 600 s, and the
native-frontend compile 1500 s within an 1800 s test limit.
Virtual slots whose implementations all borrow a parameter now store the
borrowing body (`@sc_bf_...`), or a virtual adapter, instead of the owned
entry (`@sc_f_...`). Three inheritance codegen tests still required the
owned entry. They now accept either entry for the same implementation,
so they keep checking which method fills each slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vtables store the borrowing body of a method whose slot borrows its
parameters, but that decision lived in private emitter state, so the
self-hosting layout fixture kept emitting owned entries and no longer
matched production (`@sc_f_...` vs `@sc_bf_...`).
Move the decision into VirtualBorrows (virtual-borrows.ts), used by the
emitter and by the fixture, which now runs the same call-lifetimes
analysis. The slot lookup narrows explicitly instead of combining a
union with `&&`/`||`, which the self-hosted subset rejects (SC1090). The
abstract-slot emission case now expects the borrowing body, since its
implementation borrows its receiver.
The remainder and bitwise-arithmetic fixtures relied on parameters
staying arbitrary, but each was only called with integers, so the new
call-site proofs removed the floating fallbacks they count. A fractional
call per function restores the arbitrary inputs and keeps every original
expectation. The self-hosting range cases now expect the int32-closed
range where a region's writes are all int32 literals (for-of still binds
the local to unproven elements and keeps no fact).
`x as Sub` lowered to a call to an interned %class.assert helper that
unwrapped a union box, retained the payload twice, compared the class-id
range, and threw on mismatch; every use paid a call, three retains, two
releases and a pending check (2,152 sites in tsc-ts). Plain operands
(locals and field reads, which isSafeToRepeat already admits) now lower
to the check itself: per compatible union arm a tag test plus, where the
arm is not already the target or a subclass, an instanceOf range test,
selecting the downcast payload, with the helper's exact TypeError as the
final arm. Other operands keep the helper so they are evaluated once.
The backend borrows through the result: instanceOf reads its operand as
a receiver instead of retaining a temporary, and downcast/upcast are
borrowable, edge-preserving projections, so `(node as Sub).field` and
method receivers on a cast no longer touch reference counts. The mismatch
arm unwinds directly (previous commit) and stays out of the hot path.
Corpus 4432 pins the Node-observable behavior of failing casts (a
TypeError before any later read, unwinding through finally blocks and
callbacks) and of successful casts from bases, subclasses and unions; a
harness test pins the exact messages, which Node cannot oracle.
The record edge cases typed linked | undefined now lower to a nullable pointer released through the record's NULL-tolerant helper; a new case with a second non-unit arm keeps the tagged-box trace and release helpers pinned.
Every console.log/error/warn line and process.stdout/stderr.write chunk
is still submitted before the call returns, as in Node. On POSIX
executables, each one now goes out as exactly one write(2) on fd 1 or 2.
Before, it went through C stdio, which cost a copy into the stdio
buffer, one stream call per argument, separator, and newline, and a
flush. stderr is unbuffered, so console.error made one write(2) per
piece: 3 writes per line on average in the probe.
- The line renders into a 2 KiB stack buffer (heap above that). A large
single string goes out with writev and no copy.
- Short writes continue and EINTR retries. Other errors (EAGAIN, EPIPE,
EBADF) return errno with the rest dropped, which is what the stdio
flush did. console still ignores them, and SIGPIPE behavior is
unchanged.
- Runtime-internal bytes still in the C stdout buffer are flushed first
(__fpending on Linux, the FILE fields on Darwin, an unconditional
flush elsewhere), so stdout order and merged 2>&1 order are kept.
- Windows, WASI, and library artifacts keep the C stream path, now fed
one formatted chunk per call.
Tests: corpus 4294 covers line sizes across the stack buffer and the
64 KiB stdio buffer. New console-io harness cases cover stdout and
stderr merged into one pipe and into one file (byte-compared with
Node), and console.log after the stdout reader closes (EPIPE).
Linux sandbox, ns per line (median of 9 launches, 200k lines; scriptc
includes ~1 ms startup). All outputs byte-identical to Node.
probe target node prev cand
p-str pipe 4040 1323 1239
p-str file 3092 849 790
p-str null 2193 319 283
p-args pipe 6168 1440 1228
p-args file 4896 925 802
p-args null 3916 389 292
p-write pipe 2889 1328 1197
p-write file 2267 809 769
p-write null 1577 297 265
p-err pipe 4672 3253 1132
p-err file 3478 2042 717
p-err null 2614 562 232
log-lines pipe 4958 1490 1356
log-lines file 3885 966 877
log-lines null 2983 425 368
strace -c (pipe): p-err went from 600,000 to 200,000 write calls. The
other probes stay at one write per line.
Re-lands perf/h30-inline-rc (00051ca0, 40398773, 07972360) behind the
speed posture. With inline RC on, every mirrored runtime RC family
retains inline and releases inline down to the rc == 1 / new cycle
candidate slow path (shapes.ts INLINE_RC_FAMILIES). Release emission is
unchanged: ShapeHost.rcHelpers is null unless LlvmTargetOptions.inlineRc
is set, and every retain/release (including capture boxes) stays a
runtime call; the %ScrMapRc type is only emitted with inline RC.
- index.ts and native/prepare.ts pass inlineRc for speed executables,
speed object/asm emission, and speed library profiles.
- scr_runtime.h keeps H30's static asserts for the mirrored header and
trace-slot offsets (no code change; the rebuilt macOS pack's manifest is
identical to the baseline's).
- Tests: shapes.test.ts pins release symbols and H30's speed fast paths
and header offsets; emitter.test.ts pins that release IR is unchanged
and speed routes releases through the helpers. The differential harness
gains `// @optimization: speed` (and a SCRIPTC_TEST_OPTIMIZATION=speed
lane); corpus 4032 pins every mirrored family under speed.
Release executables stay byte-identical to the stack tip (14/14
workloads, macOS).
The full emitter inlines the small-object allocator on 64-bit host targets,
so the self-hosting layout fixture's host enables inlineAlloc for 64-bit
requests (as emitLlvmModule does), and the recursive-records case no longer
asserts the width-specific free path (64-bit pushes onto the allocator's
free list; 32-bit calls scr_cyc_free).
(Split from the stack's later "Update self-hosting emission cases for
acyclic proofs and inline allocation"; the acyclic-proof half moves to the
construction-only acyclic change.)
Constructor-only references are now proven acyclic (cycle-analysis.ts), so
the self-hosting cases that meant to exercise the collector through such
edges no longer saw a cycle:
- emission: the nullable-union and mutually-recursive-class cases add an
array edge (always mutable) to keep the traced path, and a new case pins
the acyclic form of the nullable-union record.
- backend analysis: "removing a leaf preserves another cyclic branch"
closes its cycle through an array edge.
(Split from the stack's later "Update self-hosting emission cases for
acyclic proofs and inline allocation"; the inline-allocation half moves to
the inline-allocation change.) tests/harness/self-hosting-emission.test.ts
and self-hosting-backend-analysis.test.ts pass at this point of the stack.
Seventeen Order inputs whose shape disagrees with the target (wrong leaf
kinds at the top level, in nested records, record arrays and string
arrays; null for object and number; missing fields; object vs array at a
field and at the root; duplicate keys; truncated JSON; 1e400 and a lone
surrogate; extra nested members; reordered keys) plus an Item[] root.
The expected lines are the output of the checked-route-only build
(perf/harness); the fast path must decline or agree on every one.
The tree-shaking test failed on Linux x64 with "expected 68344 to be less
than 65536". Measured with the release helper/runtime-pack pipeline in a
Linux x64 sandbox (hello = console.log("hello", "world")):
PR file .text R+E segment
#743 64256 15794 0x3f11
#744 68416 16050 0x4011
#764 68520 16546 0x4221
The allocator adds 256 bytes of .text to hello: the inline scr_mem_free
fast path in the release functions hello already links (scr_str_release,
scr_arr_destroy, scr_arr_gc_free, scr_cyc_free, scr_dyn_dispose). Its
slow path and address-space reservation (scr_sa_slow, scr_sa_reserve)
are already stripped from hello. ELF segments are page aligned, and #743
left only 239 bytes before the executable segment's 16 KiB page, so 17
bytes over the boundary move every later segment, and the file, by 4 KiB.
Shrinking hello back under the old ceiling is not a reasonable trade.
At the top of the stack the executable segment is 545 bytes past the
page boundary. The extra bytes come from hot-path code the later PRs add
on purpose (number-to-string fast paths in scr_f64_to_str, inline-alloc
release paths, the direct fd write), all reached through console.log's
generic argument rendering. Outlining them to save one page would undo
the optimizations those PRs add.
The ceiling's documented intent is "several native pages of
linker-version slack" above a hello that the comment put at about 41KB.
That no longer held before this stack (64256 bytes on #743, 1.2 KiB of
slack). 80 KiB restores about three pages of slack over the stack's
largest hello and still catches lost section GC (the old always-linked
runtime was about 400KB). Symbol absence remains the primary
reachability contract. The Mach-O ceiling is unchanged.
Verified in the Linux x64 sandbox: pnpm test
tests/harness/runtime-tree-shaking.test.ts fails on #744 before this
change (68416 vs 65536) and passes after it, on #744 and on #764.
Move the allocator into scr_alloc.c with inline fast paths in scr_runtime.h;
strings, array headers/slot buffers, and emitted acyclic records/classes
(scr_rt_calloc/scr_rt_free) now use it. Adds scr_alloc.test.{c,ts}.
- 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.
* Compose class references across unions and collections
- Compare class, function and native references held in differently represented unions with `===`, `!==` and `Object.is` while preserving subclass identity, and refuse comparisons that a structural union arm leaves undecidable
- Keep subclass values intact through base-typed array and Set insertion spreads, `concat`, and inferred class-predicate `filter`/`find`/`findLast`, and allow checked class refinement of captured optional values with several class arms
- Split the sanitized compiler self-rebuild CI job at the IR boundary, after giving sort helpers distinct IR nodes so serialized and in-memory IR emit the same LLVM
* Keep compact comparisons for plain union arms
- Compare a plain operand with a union that already holds its type through the existing inline union comparison, preserving established parameter representations
- Update the coverage expectation for the accepted class refinement
- Preserve narrowed class and generic values, abstract properties, and standalone caught-class identity.
- Support identity-preserving array concatenation and typed-array collection values, with focused differential and sanitizer coverage.
- Share revision-matched bootstrap seeds across CI jobs while retaining command, rebuild, packaging, and sanitizer checks.
- Keep narrowed class fields and shared union writes native.
- Support covariant returns and method extraction through abstract class views.
- Preserve missing numeric values through local, static, and namespace updates.
- Keep typed callbacks on native paths while preserving receivers, evaluation order, and method replacement.
- Preserve caught class identity, collection narrowing, and missing indexed values across typed code.
- Add standard terminal access and filesystem error metadata with differential coverage and versioned runtime compatibility.
- 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.
- Avoid materializing scalar string slices and preserve index metadata.
- Specialize known callbacks and numeric call boundaries.
- Lower proved integer remainders without changing number semantics.
- Preserve live class identity through interface receivers and object methods.
- Extend synchronous text reads and regular expression replacement callbacks.
- Keep unsupported generic dispatch boundaries explicit.
- Support concrete generic inheritance, forward references, and nullable substitutions.
- Bound recursive specialization demands while preserving independent concrete uses.
- Preserve class identity through checked assertions and contextual lowering.
- Sort primitive arrays with stable native ordering and pure conversion keys.
- Preserve sparse semantics and argument effects while avoiding callback overhead.
- Verify Unicode ordering and ownership across native and WASI paths.
- Split builtin lowering into focused API owners and keep module dispatch separate.
- Share byte conversion, receiver checks, and filesystem option parsing; remove redundant forwarding and clarify names.
- Preserve compiler output and diagnostics, with import boundaries enforced by the code-quality checks.
- Speed up string-keyed collections and wide object property lookup.
- Share scalable numeric-key ordering across typed and checked objects.
- Preserve property, iteration, and ownership behavior across native and WASI targets.