The nested trampoline never returns (setcontext back), so the redzones
around its locals stayed poisoned in the shadow of Node's stack, and
Node's own frames tripped over them later (stack-buffer-underflow in
__bzero, sanitized corpus 6822/6823/6831/6832/6894 on macOS). Linux's
ASan clears a context's stack in its swapcontext interceptor; macOS's has
none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A nested entry below the mark makes its context with ni_make_context,
so macOS clears only the top slice of the range instead of the whole of
Node's stack below the mark on every nested call (17 ms per call, and
the cross-heap GC test ran past its timeout).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The libnode-linux action becomes .github/actions/libnode, which serves
the Linux x64 and macOS lanes: the libnode-packs cache for the runner's
platform, else the development pack.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
The AST reader accepts protocol 9, TypeScript 7.1's encoding, as well as
protocol 5 (7.0's), so scriptc can read either checker's responses:
- the 64-byte header of protocol 9;
- its node kinds, which count 7.1's SourceKeyword: each kind above it
reads as 7.0's, and node handles name the response's own kinds;
- a module declaration's child mask, whose attributes slot moves the
body to slot 3.
A protocol 9 response with a node 7.0 has no form for (a SourceKeyword
node, import attributes on a module declaration, or an `import source`
clause) throws an AstDecodeError that names it.
Each chunk a readable side delivered looked up its 'data' listeners by
name twice: once to decide whether a flowing stream emits directly, and
again in emit('data'), which scans the stream's event names even when a
piped stream has no 'data' listener at all. The stream now keeps a
data_listeners flag, set from the emitter registry's empty/nonempty
transitions through scr_emitter_observe, and reads it on those paths;
emit('data') is skipped when the stream has no 'data' listener.
The native bootstrap's package checks copy the seed's distribution into a
package directory outside the workspace and, since the CLI packages depend
on their LLVM helper and runtime pack, drop the bundled copies. Nothing in
that directory resolved @scriptc/llvm-linux-x64-gnu, so
verify-native-cli.mjs failed with 'Cannot find module'. The test now links
both packages into the directory's node_modules, as pnpm links a workspace
package's dependencies.
The teardown of a type with children of its own type called itself for
each child: a call, its register saves and the segment bookkeeping per
node, 45 instructions per node in a binary tree.
A record or standalone class whose every reference field holds its own
type and that has no collector header now tears down in one loop. A child
whose last reference the dead node held goes onto a pending list linked
through its own count word, which it no longer needs, and the loop takes
the list's first node next, so nodes are visited parent first. Each node
frees its block as before (dispose observers, the RC audit, blocks of
other arenas), the calling thread's blocks go into the teardown segment
in visit order, and the segment goes onto the free list in one step, so a
structure built parent first takes the blocks in its allocation order.
Shared and immortal children keep their release. The loop needs no depth
budget. Other teardowns keep the recursive form.
A read within IDENTITY_SETTLE_MS of a file's last write records no
identity, and a later read of the same bytes in the same frontend does.
FrontendInputTracker.record compared the two probes as JSON and marked
the snapshot unstable. A build or coverage run that read its entry
before and after that point then published no cache entry: the exact
repeat of a build lowered again, and a coverage verdict was not replayed.
Two file probes with the same digest now merge into one that keeps the
settled identity; any other difference still marks the snapshot
unstable.
forEach, map, filter, some, every, find, findIndex, findLast,
findLastIndex and flatMap on a dynamic array, and through
Array.prototype.<method>.call on a native array, called the callback
without its thisArg, so `this` in the callback was not the second
argument. These methods now bind `this` to the thisArg, or undefined
when there is none, once per call, around all of the method's callbacks.
A counted loop whose body branches on `(i & M) === C`, with M one of 1, 3
and 7 (a check every second, fourth or eighth iteration), now asks LLVM to
unroll it by M + 1 (llvm.loop.unroll.count on its back edge). In each
unrolled copy the counter's low bits are known, so the guard folds to a
constant and only one copy per period keeps the check. Before, LLVM tested
the guard on every iteration and, when the guarded condition was cheap,
evaluated that condition on every iteration too.
The loop must step by 1 from a multiple of the period, have no nested
loop, and stay under 4096 IR nodes once unrolled. Loops with the same
period share one loop ID node, which the module emits only when a back
edge refers to it.
- 'this' in a class body maps through the nominal table (it is the class).
- An unbound type parameter maps through the lowerer's IR-level binding,
which is how generic class instances bind their parameters.
- An intersection that refines a class (a mixin instance, 'T & Class') is
that class, not a merged record.
- An object whose members do not map structurally (an interface over Map
or Set, method-only interfaces) keeps its per-type recognition.
On a 160-program sample of the corpus (generic classes, interfaces, Maps),
the shadow census goes from 78.7% equal (2,936 canonical failures, 943
divergences) to 98.0% equal (25 failures, 343 divergences).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>