console.log(new F(2)) printed `{ a: 2 }` where Node prints `F { a: 2 }`.
A construction through a plain function now stamps the first prototype
on the receiver's chain whose own `constructor` data property holds a
named function with that name, as Node's getConstructorName finds it.
The dyn printer already reads such runtime class names, so its code is
unchanged; stamped prototypes stay alive and are cleared before release
at exit. `new F()` boxes F under its name so the prototype's constructor
carries it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JSON.stringify of a class instance, or of a record, array or union that
holds one, boxes the value and serializes it with the runtime JSON
traversal. The output matches Node: own enumerable properties in
creation order, properties added after construction, toJSON on the
instance or a base class, and no methods, accessors, #private fields or
static fields. A class whose declared field cannot be read through the
checked-dynamic property view, such as a Date field, is refused at
compile time with SC1090 naming the field.
Boxed Map and Set values now stringify as {}, as Node does, instead of
throwing.
`scriptc coverage` now lists every WebAssembly module whose bytes the
build can see, with its size, whether it validates, its function, import
and export counts, and the WebAssembly proposals it uses. Nothing is
translated yet; builds are unchanged.
- packages/compiler/src/wasm/binary.ts decodes and validates a module in
plain TypeScript (the native compiler compiles it too) at the feature
level scriptc means to translate: the MVP plus sign-extension,
non-trapping float-to-int, bulk memory, multi-value, mutable globals,
reference types and extended constant expressions. Where WebAssembly
3.0 and V8 relaxed a 2.0 rule (global initializers reading defined
globals, memory indices), it follows V8, the behavior oracle. Modules
that use SIMD, threads, exception handling, tail calls, function
references or GC, memory64 or multiple memories still decode, and are
reported as needing that proposal instead of as invalid.
- packages/compiler/src/wasm/discover.ts finds modules from four sources:
paths the compiler resolves from the source text (require.resolve,
createRequire(...).resolve, import.meta.resolve,
new URL(..., import.meta.url), __dirname joins), inline base64 and byte-array
literals, the .wasm files of reached packages whose code mentions
WebAssembly, and a `scriptc.wasm` list in package.json or the new
coverage-only `--wasm <file>` flag. Modules are reported once, by
SHA-256, with every source that found them; module files are input
probes of the coverage verdict cache. The JSON envelope
(--print=diagnostics) carries the same records under `wasmModules`.
Evidence: packages/compiler/test/wasm-spec.test.ts runs the decoder over
the 3,017 distinct binary modules of the WebAssembly 2.0 core test suite
(without SIMD) and the extended-const proposal's tests, generated by
gen-wasm-spec-cases.mjs with wabt's wast2json, and requires V8's verdict
for each; 8,000 seeded mutations of those modules must also agree with
V8, as must undici's inline llhttp modules (scalar valid, SIMD reported)
and es-module-lexer's. src/wasm/binary.test.ts checks each feature and
each error class against V8. On the survey's real modules the decoder
agrees with V8 for quickjs-wasi (637 KB, 36 ms) and photon (1.9 MB,
121 ms). tests/coverage-fixtures/wasm pins the four discovery sources.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Packages whose declarations name WebAssembly types (quickjs-wasi,
photon-node, and therefore pi) failed scriptc's type check with SC0001
"Cannot find namespace 'WebAssembly'", because scriptc's ES-only type
world never loads lib.dom, so `scriptc coverage` could not analyze them.
- scriptc.d.ts declares lib.dom's WebAssembly namespace and BufferSource
(admitting every ArrayBufferView, so Node's Buffer types fit).
- The type mapper gives every type declared in the namespace, and unions
over one, the checked-dynamic representation, so declarations that only
mention them compile.
- Every reached use of the API value refuses at its site with a named
SC2020 diagnostic: `WebAssembly.<member>` reads and calls,
`new WebAssembly.<Class>`, `instanceof WebAssembly.<Class>`, the bare
namespace, `globalThis.WebAssembly`, and the global object viewed
through a cast to a type with a WebAssembly member (pi codemode's
spelling). TypeScript sources fail the build; JavaScript sources get a
located runtime fence.
- `typeof WebAssembly` and `typeof WebAssembly.<member>` need no value and
fold to Node's answers ("object", "function"), including inside
`typeof x === "..."` tests, so feature probes in JavaScript glue behave
as in Node. Before, a JavaScript probe answered "string" (the identity
token) and silently took the non-wasm branch.
- The surface manifest projects the refusal (stdlib.webassembly), and the
compatibility ledger's `WebAssembly` global row moves from
not-implemented to refused; no other row changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Appending a ScrDynHandleTag used to grow the always-linked scr_dynh_ops
table, which shifted .bss and code addresses in every program, including
programs that never box the new handle (Blob and File paid this when
they were added). The table is now sized by SCR_DYNH_CAPACITY (128; 52
tags are in use), so a link-gated unit can add a tag without changing
any other program's executable. This change is the one layout shift that
makes later tags free.
Structured clone between threads (postMessage, workerData) rejected
every handle other than MessagePorts and buffers. A handle's ops can now
carry a ScrDynHandleTransfer (share on the sender, adopt on the
receiver, drop with the message), which scr_message.c uses for any tag,
so a gated unit such as a future WebAssembly.Module needs no reference
from the message encoder and no per-tag registry. No handle declares the
hooks yet; scr_message.test.c exercises them across threads through a
test handle in the last capacity slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A JavaScript class that extends Error and declares `set name(v)` or
`set message(v)` (quickjs-wasi's JSException) made the Error property
write dispatcher, which exists once any program writes `.name` or
`.message` on an Error, pass a string where the untyped setter takes a
dynamic value. IR validation failed with SC9001 "expected dyn, got
string". The dispatcher now coerces the value to the setter's parameter
type, which leaves TypeScript setters (string parameters) unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`class Counter extends base(42) {}` with `function base<T>(initial: T) {
return class { value: T = initial; }; }` failed with SC1090 "extending
computed expressions": the call's result type is the factory's local class
instantiated with T = number, which only maps inside the factory's own
generic instance.
A call of a generic function whose result is one of its local classes (or
an instance of one) now keys the instance on its type bindings and maps the
result inside that instance, where the local class takes the instance's
name and bindings. The class value then works as a base (`extends
base(42)`), as a `const` binding, through `new`, and for method calls and
field reads on its instances. Each instantiation has its own class, so
`base(1) === base(1)` is false and `instanceof` tells them apart, as in
Node. Calls inside another generic instance bind through the declaration;
calls elsewhere also record the instantiated checker type, so later uses of
it name the same class.
Fixes#1241
Co-authored-by: BaoLei <50226652+blurname@users.noreply.github.com>
The exports-assignment cache moves from a module-level WeakMap onto the
Lowerer (cjsExportsAssignments), and the inline require() calls that close
a require cycle move from a module-level WeakSet into preflight's result:
checkPreflight stores them on the LoadResult, and lowering receives them
with the other preflight facts (LowerOptions.inlineRequireCycleCalls).
6110's call and apply wrappers now compile `root === module.exports` and
`typeof module.exports`, which print what Node prints when the wrappers'
untaken branches run. The class base's `console.log(module.exports)` is the
one statement left as a runtime fence, and its body is lowered once, so
the program's deferred-fence count is 1.
pnpm test:ts7 --baselines-only reported the fifteen new corpus programs as
missing. Recorded with SCRIPTC_UPDATE_BASELINES=1 from the native frontend;
every entry has no preflight diagnostics and a single-module order, and no
existing baseline changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A base method calling `this.set()` where only a subclass is constructed
devirtualizes to the subclass override. With a polymorphic `this` return
type, the override's lowered return type is the subclass object while the
site keeps the base method's type, and the validator reported an internal
compiler error (SC9001: "call %Sub.set type object != return object"). The
Next.js extract hit this once its placeholder ResponseCookies instance was
gone: MiddlewareResponseCookies became the only constructed class.
The direct call now carries the callee's call-site return type and upcasts
to the site's class; any other mismatch keeps the virtual call. Calls whose
types already matched are emitted exactly as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Date.prototype.toUTCString and the static Response.json operation are
static surfaces now (pnpm manifest). pnpm node-compat leaves the Node
ledger and the public artifact unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A getter that only throws (`get page(): never`, or Next.js's unannotated
removed-API getters) or has no return statement declared a VOID property
type, so the class was refused (SC2001). No read of such a getter produces
a value other than undefined; the property now takes the checked-dynamic
slot, and each read throws the getter's error or answers undefined.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`id(undefined)` binds T = undefined, which the type mapper answers as VOID
(the return convention), so the instance's parameter was refused (SC2001)
although a declared `v: undefined` parameter compiles. Instantiated
parameters are value positions and now take the unit-only slot, as
declared ones do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`JSON.stringify(new Url(...))` was refused (SC1090) for every program
class, although JSON.stringify of a class with a toJSON hook serializes
whatever toJSON(key) returns. A class whose chain declares toJSON now
crosses into the runtime JSON walker through its checked-dynamic class
view, which calls the hook with the root key "" and handles nested hooks;
an undefined hook result keeps the dyn-root undefined rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Spreading a falsy primitive copies nothing, so `...(cond && { k: v })` is
the optional-field idiom `...(cond ? { k: v } : {})`. The `&&` spelling
(the @edge-runtime/cookies parseSetCookie shape) was refused with a record
shape mismatch (SC2002).
When the spread's type mixes the record with primitive arms, the literal
lowering now uses the existing conditional-spread desugar: cond evaluates
once, v only when taken, and the absent arm leaves the optional field
undefined. Record and record-or-unit spreads keep the source-copy path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`const [[k, v], ...rest] = map` was refused (SC1031). The source now
snapshots once into its iteration order (Map entries as pairs, Set
values), the spread's own machinery, and the pattern lowers as an array
pattern over that snapshot, as string sources already do. Element defaults
keep a fence because they run between iterator steps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A class field typed `Headers`, `Request`, or `Response` (or an array or
nullable form of them) maps to checked-dynamic storage, and the class-field
rule refused it (SC1090) although `unknown`, Map values, and closures
already store the same native handles.
Such fields now take the checked-dynamic slot, like the worker handles;
reads keep the handle's identity and the static fetch member lowering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Response.json(data, init)` was refused (SC2020). It now builds a native
Response the way undici does: ResponseInit converts, the data serializes
through the checked-dynamic JSON serializer (toJSON hooks included;
TypeError "Value is not JSON serializable" when JSON.stringify answers
undefined), then the response initializes like the constructor with
"application/json" as the default content type, so status, statusText,
header, and null-body-status errors keep their order.
The fetch compatibility profile moves Response.json from unsupported to a
static operation with the static-response-json differential fixture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`new Date(ms).toUTCString()` was refused (SC2020). It now lowers to a
runtime formatter over the stored time value with V8's output: "Www, DD
Mmm YYYY HH:mm:ss GMT" in UTC, a leading "-" and at least four digits for
negative years, and "Invalid Date" for an invalid time value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A computed TypeScript field without an initializer (`[INTERNALS]: T`)
was refused even with a stable Symbol()/Symbol.for() or literal key, which
contradicts the documented rule that TypeScript fields need an initializer
or an unconditional constructor assignment.
Such a field now takes its declared type and the identifier fields'
definite-assignment rule: an unconditional top-level `this[key] = ...` in
the constructor, otherwise the checked deferred-initialization slot. Types
that admit undefined (optional fields) start undefined.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JSON.stringify answers undefined for an undefined, function, or symbol
root. Through `unknown` (or a generic instantiated with it) the dyn-root
lowering converted the runtime result to a string, so the result was the
text "undefined": `text === undefined` was false and `typeof text` was
"string".
Where the result reaches a use that distinguishes undefined from that text
(undefined/null comparisons, typeof, ??, ?., truth tests, a return from a
function declaring undefined, or an unannotated local binding used that way), the
call now keeps the checked `string | undefined` result the replacer path
already produces; later string uses of such a binding narrow by the
checker's flow type. Other uses keep the existing string ABI, so programs
that cannot observe the difference compile unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`h.getSetCookie?.()` on a native Headers handle failed with SC2020: the
optional-chain lowering first reads the callee as a value, and native
handle methods have no first-class value. A required standard-library
method on a never-nullish receiver always exists, so `?.()` is the plain
call.
When that method read has no lowering, the optional chain now re-dispatches
the plain call. Reads that lower keep the existing path unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`headers.host.toString()` after `!Array.isArray(headers.host)` narrows a
`string | number | string[]` slot to `string | number`, and the union
toString lowering accepted the narrowed checker type but emitted the toString
node over the wider storage union. The validator rejected the non-stringable
arm as an internal compiler error (SC9001).
The lowering now retags the read to the proven union (the same checked
coercion other narrowed reads use) before the per-union ToString helper.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
undici's Request constructor wraps the URL parser's failure: TypeError
"Failed to parse URL from <input>" with the parser's TypeError as its
non-enumerable cause. fetch() rejected, and new Request() threw, with the
bare "Invalid URL" error instead.
Both paths now replace the pending parser error with the wrapping
TypeError (sf_throw_url_parse_failure).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Headers stores each Set-Cookie value separately, but undici dispatches its
header list's combined value: one line with the values joined by ", " at
the position of the first one. fetch() wrote one line per value.
The request serializer now joins later Set-Cookie values into the first
entry and skips the rest, for every HeadersInit form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fetch's "sort and combine" never combines Set-Cookie: Node's Headers
iterator yields one entry per Set-Cookie value, in insertion order, while
get() still joins them with ", ". The native forEach walked the sorted
unique names and read the combined value, so repeated cookies reached the
callback as one joined string.
forEach now indexes the expanded list per step (sf_headers_sorted_entry),
keeping the live re-sort so callback mutations shift later entries as in
Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
quickjs-wasi's JSException constructor runs `delete this.stack` so V8's
own stack property stops shadowing the class's stack accessor; scriptc
fenced the delete, so every guest exception reaching the host threw the
fence error. Compiled errors keep the trace internally and class dispatch
already reads the accessor, so the delete now evaluates its receiver and
nothing else, for Error subclasses whose chain declares a stack getter.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three gaps on pi-tui's terminal start path:
- `if (process.stdin.setRawMode)` read the method as a value (SC2020).
The read now answers the stream object's member: the method on a TTY,
undefined otherwise, as on Node.
- process.stdin.setEncoding had no runtime method, and a string 'data'
listener was fenced. stdin now decodes with the shared StringDecoder
once per read (split multibyte sequences stay whole, end() flushes the
tail before 'end'), string listeners receive the text, and dynamic
listeners receive what Node delivers. A string listener with no
encoding set throws the setEncoding-state TypeError, as child streams
do.
- process.stdout.on("resize", cb) (once/off/removeListener, stderr too)
registers on the stream object, which listens for SIGWINCH and emits
when a TTY's size changed; the listener never keeps the loop alive.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node checks its builtins before node_modules for a bare core-module name,
so an installed npm package named string_decoder (a transitive dependency
of pi) never answers `import ... from "string_decoder"`. scriptc
attributed the import to the package and refused it. The npm resolution
now answers null for bare builtin names (prefix-only builtins such as
node:test keep their bare names for packages), for ES imports and CommonJS
requires alike.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1309 let a checker-untyped receiver call a member named like a
String.prototype-only method (`bold`, `italics`, ...), refusing only a
string receiver at run time. TypeScript importing chalk's JavaScript
source types the default export as a plain function, so `chalk.bold` is
an `any` member on a typed receiver and still fenced; pi calls it in
nine places. A checker-untyped member now takes the same path. The chalk 5
fixture pins root, aliased, and Chalk-instance `bold` calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Weak collections already lower to checked-dynamic storage as locals, but a
class field of those types was fenced as a dynamic-typed field (pi's
AgentSession keys entry ids by message in a WeakMap field). Declared,
optional, and constructor-assigned fields of those types now store the
native collection like the locals do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pi-ai consumes every provider stream with `for await` over an EventStream
whose `async *[Symbol.asyncIterator](): AsyncIterator<T>` is an async
generator method; that was SC1070. The loop now opens the iterator with
the method call and drives the returned async generator like any other
(break and throw close it). An `async *` declaration annotated with the
protocol interfaces (AsyncIterator, AsyncIterableIterator, AsyncIterable)
maps like AsyncGenerator. Boxing a class instance whose fields hold a
promise now links the dynamic async unit (the materializer boxes the
promise), which EventStream's own `this.isComplete(event)` calls need.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under @types/node, process.getBuiltinModule("node:os") types as the
module namespace, which lowered as an unchecked dynamic value and failed.
A string-literal id naming a supported builtin now lowers to the namespace
value an `import * as` binding has, including pi's cast-and-probe spelling
`(process as P).getBuiltinModule?.("node:os") ?? null`; a nullable
namespace binding narrowed present keys the same member tables, and a
namespace is truthy. process.features read as a whole value (`"sea" in
process.features`) is an object holding none of the flags, matching the
existing member reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pi's CLI setup replaces process.emitWarning to silence warnings. The
assignment now stores the replacement: later process.emitWarning(...)
calls apply it to their arguments, and runtime-built warnings (Node's own
modules call process.emitWarning) reach it as (message, type[, code])
instead of the report and the 'warning' listeners. process.title writes
already lowered; the corpus program pins both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`a[0]?.b.c` read `.c` from undefined when the element was missing: the
tail only joined the guard when the checker typed the receiver nullable,
and an array or index-signature read is typed present while it lowers to
an undefined-armed value. The tail check now also accepts receivers that
can lower to a hidden undefined; present receivers re-dispatch the plain
lowering. A non-null assertion stays inside the chain (`a?.b!.c`), and
parentheses end it for checked-dynamic reads (`(a?.b).c` throws).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage S3 of the two-tier design: dynamic-tier functions compute with
jsval, a new IR type holding a 64-bit NaN-boxed word whose numbers,
booleans, undefined and null are immediates, so a number between two
dynamic operators is no longer a heap node.
- IR: the jsval type and js.* builtins (operators and the boundary
conversions fromDyn/toDyn/fromF64/fromBool/fromStr/fromBigInt/toF64).
The jsval pass rewrites dynamic-tier functions after tier assignment:
operator chains, comparisons, number checks and truthiness of operator
results run on jsvals; dyn locals become jsval locals when that saves
boxing; dynamic functions called only from dynamic functions return a
jsval. IR version 17 serializes each function's tier.
- Runtime: scr_js.h and scr_js_value.c behind the dynTier feature
(invariant I3); static runtime units never include scr_js.h.
- Backend: number cases inline (immediate doubles and NUM cells), always
inlined jsval retain/release; 32-bit targets keep boxed cells.
- Interprocedural analyses see jsval conversions as the dyn forms they
replace, so no fact about a static function changes.
Static functions are untouched: typed programs and the self-hosted
compiler's static functions compile to byte-identical LLVM.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tsc opens every CommonJS file with `exports.A = exports.B = void 0;` and
later aliases `exports.A = A;`. Collection registered an export global
for the chain's first name at the preamble, while the preamble lowered
to nothing and the class alias registered and stored nothing. Importers
then read `lib.A` from a global that was never assigned, and `new
lib.A()` crashed with a segfault (chardet's encoding modules).
The preamble detection moves to program.ts so collection and lowering
share it. A dropped preamble whose real assignment is a class alias now
registers no storage, so importers resolve to the class.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`list.map((rec) => rec.match(x))` over an array of class instances in a
JavaScript file lowers the callback with a checked-dynamic parameter and
checks the boxed closure into the native callback type. The instances
cross into the dynamic tier only when the callback is called, inside
that function boundary, so the native-capsule dispatch never learned
their classes and `rec.match` threw "rec.match is not a function". One
class or several, inherited methods or own, all failed (chardet's
recogniser list, any `.map`/`.filter`/`.forEach` with an untyped arrow).
A dynamic value checked into a function type now registers the classes
its parameters carry, as a dynFrom of those values already does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With object identity exact, dequal(1, 2) went from a fenced error to a
silent `true`: `(1).constructor` read undefined natively, so
`(ctor = a.constructor) === b.constructor` held for any two primitives.
Constant-key `.constructor` reads on dynamic values now answer the same
interned closure the Number, String, Boolean and BigInt identifiers read
for primitives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The zero-cost gate showed every benchmark executable changing: the new
string literals and floating-point constants grew the merged constant
pools of scr_json.o and scr_lib.o, which every program linking those units
carries, and edits to scr_dyn_fn_get and scr_dyn_has_own changed code the
typed benchmarks reach.
- The builtin constructor values, the inherited Object.prototype members
and the stored timer functions move to scr_dyn_invoke.c, which links only
into programs that use them: the frontend now selects that unit for
dyn.globalConstructor, dyn.builtinFunction, dyn.objectTag and
constant-key reads of constructor/valueOf/toString/hasOwnProperty/
propertyIsEnumerable. Those reads call scr_dyn_inherited_read after the
ordinary read (and scr_dyn_object_prototype_member before it, since the
ordinary read fences Object.prototype itself); computed-key reads keep
their old answer. The unit's new strings live in their own section so
programs linking it for other reasons keep their bytes.
- scr_date_parse_get_time keeps its exact body; Date.parse and
new Date(string) call the new scr_date_parse (bounded grammars, then
V8's parser), and JavaScript `new Date(value)` calls
scr_dyn_native_date_new_value, so fs.Stats dates are untouched. The
parser and UTC(t) avoid new pooled constants.
- Object.prototype.toString's "[object Error]" moves to
scr_dyn_object_tag_of; scr_json.c is unchanged.
- Calling or constructing through stored constructor values and
own-property checks on Object.prototype itself are not supported in
this change (they would touch shared code).
Also pins the IMF-fixdate (cookie Expires) parse and Date#toUTCString
round-trip in the corpus.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The tree-shaking test failed on Linux x64 after main was merged into
this branch: "expected 84496 to be less than 81920". Measured with the
release runtime pack (hello = console.log("hello", "world")):
tree file R+E segment
main b866ae323 80288 0x5fe9
989d0d6bc 79928 0x5fe9
merge 474012f04 84488 0x6579
No runtime family comes back: hello's absence checks still pass. The
code that grows is the cycle collector and the release paths that feed
it, which hello's exit reaches (scr_lib_init -> scr_lib_cleanup ->
scr_arr_release -> scr_cyc_on_release -> scr_cyc_collect_due -> the
pass). Main grew it by 1,447 bytes (the death rule and
scr_mem_order_for_free), this branch by 1,386 (the inline nursery
appends in the runtime's release functions, scr_cyc_route_bypassed and
the window). Main left .text 23 bytes under the executable segment's
24 KiB page, so together they cross it, and every later segment and the
file move by 4 KiB. 96 KiB, the Mach-O ceiling, restores several pages
of slack.