Since #784, an array whose elements are proven present passes each element
itself to its callback instead of the optional element value. A callback
whose parameter is a structural supertype of the element
(`refs.find((r: { pos: number }) => ...)` over a wider record) has no
adapter from the plain element record, so lowering rejected it with SC2011.
Before #784 the optional element ABI converted it through a union retag.
The optional-read prepass now treats a site as present only when the
callback's element parameter accepts the stored element through a
mechanical conversion. Otherwise the site keeps the optional element visit,
as before #784. Unannotated and generic parameters are unaffected, so every
site #784 made precise stays precise.
The method-replacement census marks a method name as an observed slot
when some write might replace it, and calls of observed methods go
through a checked property lookup whose result converts back to the
declared return type. Two kinds of data writes over-approximated that:
- A computed write into a string-keyed dictionary marked every method in
the program unless the dictionary's values were primitives. Class
instances have no implicit index signature, so such a dictionary is a
class view only when the class declares the index signature, and then
every method must be assignable to its value type. Any value type no
function can inhabit now qualifies: primitives, or non-callable object
types that require a property functions lack, or weak types sharing no
property with functions.
- A named write like `this.literal = token` marked `literal` for every
class. The write can only replace a method when the written property
can hold a function, so property writes are now judged by the written
property's type (prefetched in one batch).
Compiling vercel-labs/tsc-ts failed after the canary promotion: one
`changedProjects[id] = {...}` write marked every method, OptionNameMap.get
and the JSON parser's literal() calls returned unknown, and their Map-typed
results could not convert back (SC1100/SC1101, 61 errors).
Also skip abstract methods when materializing observed prototype slots.
An abstract declaration emits no prototype member, so lookup continues
to the ancestors; building its value instead threw an uncaught
PoisonError out of the reachability fixed point (the tsc-ts crash).
Tests: corpus 3171 (fails with SC1100/SC1101 before this change) matches
Node in the plain and sanitized lanes; the 112 corpus programs matching
abstract|prototype|method|callback pass; class-dynamic-dispatch unit
tests pass. tsc-ts now builds and type-checks itself with output
identical to tsgo 7.0.2.
scr_console_capacity compared the raw tag with SCR_ARG_STR, but string
parts of a concatenation argument carry SCR_ARG_GLUE. A glued string part
therefore reserved only 32 bytes while scr_console_render, which masks
with SCR_ARG_KIND, copied its full length, overflowing the on-stack (or
heap) line buffer for any glued part longer than 32 bytes.
Mask with SCR_ARG_KIND in the capacity loop and in the single-string
fast path, the two remaining kind tests in scr_console.c.
Tests: corpus 4296 logs template literals and `+` chains with glued
string parts of 31..70000 bytes, several long parts per argument, and
multi-byte UTF-8 parts. Before this change the --sanitize build aborted
with an AddressSanitizer stack-buffer-overflow; it now matches Node in
the plain and sanitized lanes.
console.log(`${method} ${path} ${status}`) used to build the joined string
first (one string per number, scr_str_concat_parts, then releases), and
the console then copied that string into the line it writes. The LLVM
backend now flattens a strConcat argument with stringParts and passes
each part as a ScrLogArg:
- Later parts carry SCR_ARG_GLUE, so no separating space is added.
- A number conversion passes the raw double as SCR_ARG_NUM (String()
spelling, so -0 prints "0"; a bare number argument keeps inspect's
"-0").
- A boolean conversion passes the bool.
The runtime renders the parts directly into the line buffer. Parts are
evaluated in source order through emitStringInputs, which applies the
same borrowing rules as concatenation. Calls that use the new tags go to
new scr_console_log_parts / scr_console_error_parts entry points, so a
program object linked against an older runtime pack fails at link time
instead of misprinting.
Tests: corpus 4295 covers -0/NaN/exponent spellings inside templates
next to bare arguments, empty parts, more than 16 parts, evaluation
order, a later part reassigning an earlier string, union and optional
values, and async code. A new co-located IR unit test checks the
emitted tags and entry points.
Linux sandbox, ns per line (median of 9 launches, 200k lines), prev =
stack tip, cand = this commit (with the previous one). All outputs
byte-identical to Node.
probe target node prev cand
p-str pipe 4022 1364 1149
p-str file 3376 849 744
p-str null 2360 330 247
p-args pipe 6687 1441 1343
p-args file 5135 969 821
p-args null 3831 389 292
p-write pipe 2866 1244 1168
p-write file 2329 839 782
p-write null 1622 309 275
p-err pipe 4656 3253 1073
p-err file 3659 2099 721
p-err null 2749 591 219
log-lines pipe 5007 1476 1314
log-lines file 3949 965 877
log-lines null 3145 446 378
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).
tests/harness/coverage.test.ts requires every corpus program to compile
100% statically. timers-clear-many.ts cleared `handles[i]!`, and the
non-null assertion does not narrow an indexed element for the static
lowering, so the three clearTimeout calls reported SC2020 ("clearTimeout of
'number | undefined' handles"). Read the element into a local and test it
against undefined instead. Node output is unchanged; scriptc coverage now
reports 46/46 statements static and the native binary matches Node.
clearTimeout/clearInterval, refresh, ref/unref and hasRef scanned the
whole timer heap for the handle, so arming and clearing many timers was
quadratic (1e5 cancels: 1.0 s vs Node 46 ms). An id-to-slot hash kept
exact by every heap write makes the lookup O(1); heap order is unchanged.
exec/test rescanned the whole subject for ASCII (and re-converted a
non-ASCII subject to UTF-16) on every call, so lastIndex-driven exec
loops and sticky tokenizers were quadratic (1e5 matches: 7.9 s vs Node
43 ms). A single-entry cache keeps the last long subject's decoding; it
retains the subject so the address cannot be recycled and the string is
never appended in place. Only exec/test use it (no user code runs while
their input is live).
new RegExp recompiled its pattern on every construction (22x Node). A
bounded never-evicting cache keyed by (flags, source) shares bytecode
through the literal-template mechanism; invalid patterns still throw on
every construction.
`i in arr` with a non-literal number key boxed the typed array into the
checked-dynamic tree and materialized every element per evaluation, so
`for (...) if (i in arr)` loops were quadratic (1e4 elements: 5.3 s vs
Node 0.7 ms). A number key over an array receiver now lowers to arrayHas
(scr_arr_has), the query the literal fold and Object.hasOwn already use;
the key is stabilized first to keep JS evaluation order.
`k in dict` with a runtime string key over an index-signature record
snapshotted and scanned every overflow key per probe (1e4 keys: 436 ms,
1e5: timeout). The helper now ends in recordOvfHas (scr_map_has_str).
Multi-megabyte end(data) and chunked write() bodies under a slow raw
reader force partial vectored sends; the driver checks framing, length,
and digest of every response against Node.
On 64-bit little-endian targets ScrStr's capacity word is split into a
32-bit cap and a 32-bit cached Map key hash (header stays 24 bytes).
Heap strings fill the hash on first lookup; every in-place mutation path
(unique concat, split scratch reuse, regrow, spare reuse) clears it.
Immortal strings are never written: compiler literals and the runtime's
one-character/empty statics carry the hash precomputed (string-key-hash.ts
mirrors scr_key_hash; both sides pin the same vectors).
scr_key_hash seeds by length multiplicatively: XORing the length into the
low bits cancelled against the last byte ("ab" and "abc" collided).
Map string lookups inline the probe into each getter and reject linear
entries by hash; set specializes its probe/overwrite by lookup kind with a
shared append path.
Tests: runtime map.test.ts (pinned vectors, statics, in-place append
invalidation), string-key-hash.test.ts, corpus 3141-map-string-key-hash
(plain + sanitized; mutation-checked against both invalidation paths).
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.)
Emitted record and class constructors called scr_rt_calloc/scr_cyc_alloc
with a constant size, so the size class, the zeroing length and the
collector header setup were recomputed per object behind a call (plus an
empty scr_obj_alloc_note call and a variable-length memset). llvm/alloc.ts
now emits the fast paths inline: pop the class free list (clearing only
the bytes the helper does not store itself) or bump, with the old calls as
the cold slow path. Releases inline the weak-dispose hook check, the live
count, and the free-list push; blocks outside the reservation still go to
scr_rt_free.
The emitted IR does not know which runtime it links against (the sanitized
lane only differs at link time), so the runtime now defines scr_sa in
every build. Where the allocator is compiled out (ASan, SCR_RC_AUDIT,
wasm32, Windows, 32-bit, thread-instanced libraries) it stays all-zero and
the inline paths always take the slow path, which keeps the RC-audit
object notes. ScrSaState's layout is pinned with static asserts. Inline
emission is additionally gated off for 32-bit, WASI, Windows triples and
thread-instanced libraries.
Runtime callers with hot constant-size allocations (union boxes, arrays,
boxes, closures, dyn nodes) use new static inline twins
scr_cyc_alloc_inline/scr_cyc_free_inline; scr_cyc_live is exported.
The ABI guard now recognizes function-pointer extern data
(scr_weak_dispose_hook). New corpus program 4410 pins recycled-block
clearing, size classes, objects above the largest class, cycles and weak
observers.
A whole-value read of a nullable-pointer field allocated a heap box per
read, so list walks (`p = p.next`) regressed: 0.07 s -> 0.35 s on a 100k
list walked 200 times. Such reads now retain the instance pointer into
the current frame and wrap it in a private stack box with that
independent owner:
- local union storage accepts nullable field reads and copies of another
binding of the same union as sources (the payload is retained; scalar
and unit arms own nothing; retains branch on the tag because not every
retain entry point is NULL-tolerant);
- arguments to projection-only parameters of calls and constructors.
The list walk now takes 0.05 s (baseline 0.07 s). Corpus 4061 adds a copy
that outlives a store to the field and copies across string, number,
-0 and undefined arms.
A class field typed `C | null` or `C | undefined` (one unit arm plus one
emitted class arm) now holds the instance pointer itself: NULL is the unit
arm. The struct slot stays `ptr`, so layouts are unchanged.
- Teardown, trace and gcFree use the slot's storage type (the class arm);
every class retain/release/visit entry point is NULL-tolerant.
- Projections of a field read (tag tests, narrowing, truthiness, unionDisc,
nullish, borrowed equality operands) use a private stack box over the
borrowed pointer; whole-value reads build an owned heap box (or the
immortal unit instance); generic borrowed receivers get that owned box.
- Stores take the payload of a wrap directly, or the retained payload of
a borrowed union; a store of a union binding is a projection use, so
constructor parameters stored this way are projection-only.
- `new` calls the borrowed constructor body when the lifetime analysis
allows it, so projection-only arguments become private stack boxes.
- The dyn bridges (materialize, commit, symbol fields) convert between
the pointer and a temporary union.
alloc-trees no longer allocates a union box per tree edge. Corpus 4061
pins tests, narrowing, nullish, optional chaining, equality with
mutation, escapes, reassignment, subclass instances, unionDisc, `any`
reads/writes, JSON, console output, cycles, and a 300k chain teardown.
macOS arm64 (H23 runner, --layouts=4 --runs=16, vs perf/harness):
alloc-trees -45.5%, records-sort -72.8%, numeric-kernels -29.4%,
ast-interp -21.3%, word-graph -12.4%; template-render +1.9% and
functional-pipeline +0.8% (both within 2%); geomean -17.6%.
Accepting nullish and unionEq as projection uses also proves parameters
projection-only, so callers pass private stack boxes to them. The narrowed
nullish shape now always borrows a binding (no code runs between the read
and the payload extraction), and unionEq routes proven parameters through
the borrowed receiver path, so a stack box never reaches union RC entry
points. Found by the Linux corpus (free(): invalid pointer in 1535 and
others); pinned by a stack-unions unit test and corpus 4060.
Union values consumed only by projections no longer allocate heap boxes:
- closure bodies get local lifetime facts (parameters stay unproven), so
for-of bindings and optional reads inside comparators use stack boxes;
- unionEq and the narrowed nullish shape are projection uses; their
wrapped-literal, optional array read, and Map.get operands are emitted
as private stack boxes;
- optional array reads consumed by tests/narrowing/comparisons use a
stack box with the inline dense lookup; f64/bool elements are supported
(slot bits copied, runtime state check kept for holes and sparse slots);
- scalar union payload reads are inline loads so LLVM can scalarize the
stack boxes.
Corpus 4060 pins holes, sparse/noncanonical indices, -0/NaN/false,
reference identity, closure locals, and mutation inside ?? defaults.
After the union fix, records-sort spent most of its time in the IR merge
sort's element moves: every src[i] read retained the swapped `src` array
and the element (scr_arr_get_ref), every dst[k] write released the old
slot, and `dst` grew from an empty array.
scr_arr_sort_values runs the IR helper's exact algorithm (same comparator
call order) over borrowed slot copies of the private, compacted snapshot:
- comparator calls use the callValue ABI (owned element arguments, an
undefined `this` window, pending-exception check);
- the array keeps owning every element and is untouched while user code
runs, so the cycle collector and a throwing comparator see the original
snapshot; the sorted permutation is published only after success;
- f64 and reference-counted element types qualify (isSortValuesElement);
unions with an undefined arm and checked/island values keep the IR.
The new internal arrIntrinsic `sortValues` is validated, may throw, and
is not reference-preserving.
The records-sort comparator spent ~70% of its samples allocating and
freeing union boxes: `for (const key of keys)` binds `key` as
`string | undefined` (array holes), and every `key === "department"`
heap-wrapped the literal as well.
- call-lifetimes: strict union equality borrows a local operand, like
unionIsTag/unionNarrow; closure bodies get local facts (their captured
cells are boxed and already rejected), parameter facts stay limited to
environment-free synchronous bodies.
- unionEq emission: a freshly wrapped operand gets a stack box.
% now checks at run time whether both operands are integers (dividend
|x| <= 2^53, nonzero integer divisor) when the integer-range analysis
cannot prove it, and then uses i64 srem with JS sign-of-zero semantics;
fractions, NaN, Infinity, zero divisors and huge dividends keep frem.
Proven integers whose divisor may be zero now only check for zero.
push() appends inline while len < cap (the runtime append is exactly a
dense store at len plus len+1 there) and keeps a cold runtime call that
grows storage otherwise. Element writes were already inlined by H04.
Built on the H04 commits cherry-picked from perf/h04-array-reads
(d88b58a1, a6057a52, 9109cb5a, 657aa3fb). New corpus programs
3150-integer-remainder and 3151-array-push-writes.
Marginal vs perf/h04-array-reads, 15 runs:
| workload | macOS | Linux (focused) |
| numeric-kernels | -18.3% | -10.3% |
| functional-pipeline | -8.8% | -4.3% |
| template-render | -2.9% | (layout, sweep -0.6%) |
| alloc-trees | -2.0% | (layout, sweep +0.3%) |
| records-sort | -1.5% | -0.3% |
| csv-numbers | -0.3% | +0.1% |
geomean marginal: macOS -2.8%, Linux full -0.6%.
vs perf/harness (incl. H04): macOS -14.4% (1.222x Node),
Linux -16.0% (1.160x Node). Linux .text growth <= 3.6%.
Tests: LLVM differential corpus 2008/2008 (Linux, 6 shards, release+dev),
sanitized 3140/3150/3151 pass, backend+ir unit tests pass except one
load-flaky zig COFF cache test that passes in isolation.
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.
A reference edge that can only be stored while its owner is still under
construction points to an object that existed before the owner, so a heap
cycle made only of such edges is impossible. computeTraced now builds the
shape/union graph with per-edge mutability, finds strongly connected
components, and marks a component cycle-capable only if it holds an
intrinsic reference (closure, dyn, class value, caught, promise) or a
mutable internal edge; units that reach such a component stay traced. With
every edge mutable this is exactly the previous fixpoint.
Mutability comes from the final IR (cycle-mutability.ts), not from the TS
readonly modifier: every fieldSet outside a constructor's own this,
fieldIncDec, recordSet, recordKeySet and recordKeyDelete counts; a
constructor where this escapes (anything but a field store target, a field
read, or the receiver of the direct base constructor call, including
closure/class captures) makes its hierarchy mutable, as does any other call
of a constructor function; every type that can cross into checked-dynamic
code (dynFrom operands including boxed closure signatures, thrown values,
library/intrinsic/JSON/FFI arguments) is mutable with its whole type
closure, because typed-ref capsules commit dynamic writes into native
fields; runtime-rooted hierarchies stay mutable; library builds and
dynamic-island programs disable the refinement. Union arms are immutable
(boxes are), collection edges are always mutable.
New corpus programs pin the acyclic case (DAG sharing, a 200k-node chain,
derived constructors, records) and try to defeat the proof (constructor
escapes, readonly bypass through casts/mapped types/any/Reflect/aliases,
subclass rewrites, readonly arrays, untyped writes through capsules).
Security review finding (#747): rn_first recursed into every split target
(split_next/split_goto/loop/loop_split). `visited` stops cycles but not
depth, so a flat runtime pattern with a long run of alternatives, such as
new RegExp("(?:" + "|".repeat(n) + "x)"), chains one split per
alternative and recursed once per alternative while the regex was being
constructed (scr_re_native_build runs on every compile), before any
subject is matched. Deep enough patterns could exhaust the C stack of the
thread compiling them.
The analysis is now iterative: split targets go on a heap worklist of
nops + 1 entries allocated next to `visited`. Every op is processed once,
so each split pushes at most one entry, and the accumulated first-byte set,
empty-match and anchor flags do not depend on the visiting order. If the
worklist cannot be allocated the native build fails and the regex keeps
using libregexp, like the other native-build failures. No other function in
scr_regex_native.h recurses on pattern structure.
Tests: corpus 3139 constructs runtime patterns with 30,000-alternative runs
(empty, consuming, word-bounded, capturing, nested-but-flat, global) and
matches ASCII subjects; it matches Node in the plain and sanitized lanes.
libregexp compiles every non-sticky pattern with a `.*?` search prefix and
interprets its bytecode one opcode at a time, so an unanchored search pays a
backtrack push/pop and ~7 dispatches per subject position, and each `x+`
iteration pushes a backtrack state. Profiles of regex-logs and
template-render showed 65-85% of samples inside lre_exec.
scr_regex_native.h translates the compiled bytecode body (the bytecode stays
the source of truth for parsing, flags, case folding and capture numbering)
into a small program for one-byte subjects:
- single-character opcodes become 128-entry tables computed by evaluating the
interpreter's own predicate (lre_canonicalize, lre_is_space, range pairs)
for each ASCII code unit, so /i and /u are exact on these subjects;
- quantifiers over one character become counted loops that backtrack by
count; greedy loops whose continuation cannot start with a byte of the
loop's class (or is just saves + match) are possessive;
- runs of literal bytes become one memcmp;
- splits, gotos, saves, capture resets, counted group loops and empty-check
registers keep libregexp's exact backtracking order and undo semantics;
- unanchored searches start only at bytes that can begin a match (memchr for
a single byte) and only at 0 for non-multiline `^` patterns.
Lookaround and back references, and all non-ASCII subjects, keep lre_exec.
scr_exec is the single dispatch point, so every match loop is shared. The
program is built with the bytecode and cached on ScrRegex (new `native` slot;
the emitter's %ScrRegex type and literal initializer gain one null pointer).
Tests: packages/runtime/test/test_regex_native.c fuzzes the matcher against
lre_exec (random patterns over the subset and its bail-outs, every start
index, all capture slots; ASan+UBSan), wired into regex.test.ts; a sweep of
8 seeds x 100k patterns (~575k translated, ~70.7M checks) found no mismatch.
tests/corpus/regex-native-matcher.ts pins each construct through the public
APIs on ASCII subjects and non-ASCII twins.
Squashes 1d210a03: build the fuzz test with -D_GNU_SOURCE on Linux; the
vendored cutils.h needs CLOCK_MONOTONIC, which strict -std=c11 hides on glibc.
Number() of an optional string element read no longer boxes string | undefined
into a dynamic value: plain-operand reads test the slot state and convert the
element with num.fromString, answering NaN for missing slots.
Read-only fallbacks (get_f64/bool/number, state, has, borrow) carry
memory(read, inaccessiblemem: readwrite) at the call site and the guard loads
the storage bases before branching, so LLVM hoists header loads out of
loops. Conditions on number[]/boolean[] element reads no longer box
T | undefined: numbers reuse getNumber (missing is NaN, falsy like undefined),
booleans test the slot state then read the value.
Strict element reads (scr_arr_get_f64/bool/ref, borrow), scr_arr_get_number,
scr_arr_state/has and element writes (scr_arr_set_f64/bool/ref) now check
inline for a canonical integer index inside dense storage (below cap, and
below len for reads, present-value state for strict reads) and load or
store the slot directly. Every other case calls the unchanged runtime entry
with a cold call-site attribute. Proven integers (exactInteger) skip the
double round-trip. for...of also reads the length inline.
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}.
* Remove the large-array cliff past 2^20 elements
Arrays longer than 2^20 kept every element beyond the cutoff in a sorted
side store, and each indexed write to an existing entry deleted and
reinserted it, so fill + indexed read/write loops went quadratic
(n=1.2M: 10.4s on macOS arm64, 12.6s on Linux x64).
- Rewrite existing sparse entries in place.
- Let dense storage grow past the cutoff for contiguous extension of a
populated prefix, bulk range writes, and sparse tails that reach 3/16
density, migrating side-store entries so index < cap stays dense.
- Make the side store a deque with front slack so descending first-touch
writes and prefix migration are amortized O(1).
- Bound range-derived slice/splice capacity hints for hole runs.
scr_arr_new now reserves the live count, so a 2^20+3 element Set
converts into dense storage instead of a sparse tail; the white-box
test_lib check that pinned the old policy now asserts dense storage.
* Share immortal boolean union boxes
Maybe-undefined reads of boolean[] elements (`if (composite[i])` in a
sieve) box the element in a boolean|undefined union and free it at once:
one calloc/free pair per read, about a quarter of numeric-kernels samples
at large scales. Unions are immutable and boolean arms own no payload, so
low tags now return headerless immortal instances, the same convention as
the compiler's static unit-arm constants.
* Record preflight/order baselines for corpus 3138