Commit Graph
65 Commits
Author SHA1 Message Date
Chris Tate 8e9e2136cf Merge main 2026-10-19 03:18:36 -05:00
Chris Tate 1b40157fed Keep pending match metadata in the array's metadata slot
A pending match result now stores its shared match source as a dynamic
handle in ScrArrExt.metadata, next to its start index, instead of in a
separate field with its own release hook. Array teardown already
releases metadata, so scr_arr_ext_free and the publish check keep their
previous code, and a static hello world links the same array code as on
main.
2026-10-18 18:25:56 -05:00
Chris Tate 526415a91c Give matchAll rows and exec results their index, input and groups
Match results record their metadata lazily. The results of one exec or
matchAll call share a source (the subject and the regex), and each
result keeps its start index. index, input and a null-prototype groups
object are built when a result is converted to a dynamic value; .index
and .input on a match slice read the source directly.

matchAll rows are allocated inline, like exec results. A drain whose
rows are read only through their elements, .length, the .groups
projection and the companion index uses matchAllCaptures or
matchAllCapturesInto, whose rows carry no metadata.
2026-10-18 14:43:40 -05:00
Malte UblandClaude Opus 5.5 fd6145f627 Test the dispose hook only in programs that can install it
Every emitted free loaded the runtime's dispose hook and branched on it,
and every runtime array, map and byte-store free made an out-of-line call
to scr_weak_dispose to do the same. The hook has two clients, weak
collections and the kept sources of checked records, and a program that
has neither never installs it.

The emitted frees now test two module constants the emitter defines after
every helper is emitted: whether the program constructs a WeakMap or
WeakSet, and whether some helper can keep a record source (a checked
record builder that notes one, or a JSON schema of a closed record).
Class instances test the first, closed record layouts both; LLVM folds the
test away, so a program without either frees with no hook load at all.
The runtime's hot frees test the hook inline instead of calling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 08:17:24 -07:00
Chris Tate 32061e8ba3 Add Float16Array
Float16Array is a native typed-array kind with IEEE 754 binary16 storage.
Stores round the number to nearest-even in one step from the double, as
Node does, including overflow to Infinity, subnormals, -0 and NaN; reads
widen exactly. The runtime converts with integer operations, so no target
needs half-precision hardware or compiler-rt support.

Construction, element reads and writes, ArrayBuffer views, fill, set,
subarray, slice, both sort orders, constructor values and checked-dynamic
storage work as for the other typed arrays. Math.f16round,
DataView.prototype.getFloat16 and setFloat16, and util.types.isFloat16Array
compile.
2026-10-17 22:45:32 -05:00
Malte UblandClaude Opus 5.5 4aa5d6cbf8 Give dense arrays head room so shift, splice(0, n) and unshift runs cost amortized O(1)
shift() moved every remaining element down one slot, making queue drains
quadratic (100k elements: 0.85 s; 1M: no result in 30 s). Dense arrays of
8-byte slots with their own allocation now remove front elements by
advancing data/present and lowering cap; the vacated slots stay in the
allocation as head room. A flag in `spare` marks it and its size is
implied by the layout (present == data + cap + head), so every reader,
including inline element access in emitted code, is unchanged. Growth
moves the elements back to the start of the allocation (in place only
when that leaves half the moved count free, else geometrically), a
drained array starts over at the front, unshift reuses head room, and an
array unshift already had to move once reserves head room for half its
length. scr_arr_outline drops head room before compiled storage swaps.
Booleans, inline slots, sparse arrays and static arrays keep the old
behavior.

Scaling probes: shift/BFS/splice(0,1)/deque drains and unshift runs go
from exponent ~2.0 to ~1.0 (shift 1e5 847 ms -> 1.6 ms); runtime bench
geomean 1.0045 (neutral).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:13:53 -07:00
Chris Tate f19b2cffe1 Make regex exec results, parseInt of captures, and UTF-8 file reads cheaper (#1128) 2026-10-16 08:11:29 -05:00
Chris Tate 7b83558f52 Run Atomics on shared storage without a lock
Atomics operations on 8-, 16- and 32-bit integer arrays are sequentially
consistent atomic instructions on the element, and no longer take the
process-wide shared-memory lock. Other reads and writes of shared storage
are relaxed atomic accesses: element reads and writes access the element,
and copies, fills, conversions, searches and sorts read shared bytes into
a private copy and write back only the range they change, so concurrent
accesses to the same bytes are free of data races. Non-shared storage
keeps its plain memory accesses. Atomics.wait and Atomics.notify queue
waiters in buckets chosen by location, each with its own lock and
condition variable, so waiting threads no longer serialize other Atomics.

Atomics.store answers +0 for a value that truncates to -0, as Node does.
The shared storage runtime test also runs under ThreadSanitizer.
2026-10-16 00:06:31 -05:00
Chris Tate ab0d4f0cd3 Give small exact arrays room for one append and keep growth on constant slot sizes
Exact constructions (literals, slice, concat results) round their request
to 16 bytes before the size class, so arrays of 2, 4 and 6 slots get one
spare slot and an append right after a copy no longer reallocates. Arrays
of 8-byte slots with their own storage and no sparse entries grow below
the dense cutoff on a path with a constant slot size, and dense copies of
8-byte slots use a constant size.
2026-10-15 20:14:59 -05:00
Malte Ubl b99e2d2558 Visit only selected captures when building regex exec results
A spans result (the compiler proved only some captures are read) used to
run the full per-capture decision for every group. Slots now start as
present undefined with a zero payload, and only the selected and lazy
capture bits are visited; full results visit every capture as before.
The hole-free prefix is measured after the fill.

String-array teardown keeps its storage pointers in registers across the
release calls (releasing a string runs no user code), and regex group
parseInt takes the short-decimal path inline (shared with
scr_parse_int_bytes as scr_parse_int_short_digits) with signed
conversions where the values are known to be far below 2^53.
2026-10-15 04:47:04 -07:00
Malte Ubl c863eaa5a7 Speed up regex exec results and parseInt of numeric captures
exec/match results now allocate their slots inline with the array header
(scr_arr_new_inline): one allocation and one free per match instead of a
header plus a separately grown dense block. The capture fill loop keeps
the storage pointers in registers instead of reloading them after every
slot store.

parseInt(digits, 10) of a short all-digit string (1-15 digits, the shape
of numeric fields and regex captures) now converts directly. The general
path no longer divides per digit to detect u64 overflow (one bound per
call; the exact bignum fallback is unchanged) and skips fmod for integral
radixes in 2..36.

The new corpus program pins match-array mutation past the captured length
(push, length, unshift, sort, splice, reverse, slice) with index/input,
and parseInt across lengths, signs, prefixes, overflow and radix forms.
2026-10-15 04:46:38 -07:00
Chris Tate 1344e372f6 Run V8's TimSort for comparator sorts (#1002) 2026-10-15 00:44:24 -05:00
Chris Tate 229e336382 Hold TimSort gallop bounds in ptrdiff_t and check gallop results
The gallop searches in the comparator sort held their search bounds in
long, which is 32 bits on Windows x64, so a run of more than 2^31 slots
gave a truncated bound and the merges could copy outside the array. The
bounds are now ptrdiff_t; every other index, length, offset, hint, run
length and run-stack entry in the sort was already size_t. Each gallop
result is checked against its run length (0 <= k <= length) before any
merge uses it, and a result outside the run is an internal error rather
than a copy.

A new runtime unit test calls gallop and the merge step, through test-only
entry points built with SCR_SORT_TEST, at indices past 2^32 and over runs
of more than 2^32 slots in a sparse mapping, and checks that an
out-of-range gallop result traps.
2026-10-14 23:13:45 -05:00
Chris Tate 76dee28d8a Add 8-byte allocator size classes below 128 bytes 2026-10-14 21:28:04 -05:00
Chris Tate da89261676 Run V8's TimSort for comparator sorts
Array.prototype.sort and toSorted with a comparator now run V8's
TimSort (array-sort.tq) step for step in the runtime, in the generated
sort for booleans and unions with undefined, and in the checked-value
sort. The comparator is called as f(x, y) < 0 in Node's argument order,
so a comparator sees Node's calls in Node's order and an inconsistent
comparator yields Node's permutation. Undefined values, including a
union's undefined arm, are set aside before sorting and never reach the
comparator, and a checked comparator's result converts with ToNumber.
Structured inputs take fewer comparator calls than the previous merge
sort. Each thread reuses one sort buffer of up to 1 MiB.
2026-10-14 21:22:39 -05:00
Chris Tate d2a84e4a41 Write typed JSON.stringify arrays from their slots and append fixed text inline
The emitted JSON writers read each array element through scr_arr_state
and scr_arr_get_ref, retaining and releasing it, and appended every
bracket, comma and key through a runtime call. Arrays whose indices below
the length all hold values now serialize from their slots
(scr_arr_dense_slots) with borrowed elements; other arrays keep the
element walk. Fixed text is stored inline when it fits the buffer's spare
capacity, booleans write their literal inline, and numbers are formatted
directly into the buffer.
2026-10-14 18:05:50 -05:00
Chris Tate 23124ccdb0 Lower typed-array sort and toSorted 2026-10-14 11:15:37 -05:00
Chris Tate 6fe81f0ce6 Sort by key for recognized numeric comparators 2026-10-14 06:48:48 -05:00
Chris Tate e932714bfa Shrink the cycle-collector object header to 8 bytes 2026-10-14 04:53:07 -05:00
Chris Tate ea6428bad6 Keep the first elements of small appended arrays in the array's block 2026-10-13 23:36:22 -05:00
Chris Tate 844ef14771 Keep lone surrogates in strings 2026-10-13 19:48:11 -05:00
Chris Tate 947f665896 Store boolean arrays as one byte per element 2026-10-13 18:45:47 -05:00
Chris Tate 1334a8acd2 Lend arguments to closure bodies and comparators 2026-10-13 10:35:18 -05:00
Chris Tate 6790082742 Keep hole-free prefix and int32 parameter upkeep off hot paths 2026-10-13 09:11:57 -05:00
Chris Tate 5df589706b Skip array state bytes below a hole-free prefix 2026-10-13 02:13:46 -05:00
Chris Tate b10c0af911 Sort dense arrays without the comparator sort snapshot 2026-10-12 18:01:14 -05:00
Malte Ubl e1ccfeeb84 codegen: signed conversions for array counters and lengths
Integer induction counters live in unsigned 64-bit slots, so every read
as a number was a uitofp i64 (a 6-7 instruction sequence on x86-64) and
every nested start an fptoui. The runtime's push path also round-tripped
the array length through a double for its bound check.

The values are at most 2^32 - 1: on 64-bit targets convert counters with
sitofp and loop starts with fptosi, convert array lengths and validated
indices through int64_t in scr_array.c, and test the push bound on the
integer length. 32-bit targets keep the unsigned conversions.

tsc-ts Excalidraw: uitofp i64 sequences 32.5 M -> 4.0 M, -0.66%
instructions on top of the throw-path change.
2026-10-12 06:40:50 -07:00
Chris Tate 72cc828ab0 Merge main into perf/compact-array-header-2 2026-10-12 01:12:21 -05:00
Malte Ubl fa55ce6b07 Keep the RC entry points of array elements in one shared descriptor
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.
2026-10-11 19:46:48 -07:00
Malte Ubl 6778826d8f Keep the array growth sequence; round only exact construction to its block
Rounding every dense capacity up to its allocator block changed the growth sequence (a 48-byte block holds 5 slots, so storage went 48/96/192/384 B instead of 48/80/144/288/576 B), which moved array storage into other size classes: +3.6% simulated L1 misses on the tsc-ts self-check and +2.6%/+4.3% wall on the self-check and TypeORM. Growth is back to 4, 8, 16, ...; only exactly sized construction fills its block. Adds a runtime test for exact construction followed by appends, index writes and length extension over the spare capacity.
2026-10-11 16:30:22 -07:00
Malte Ubl ead9f4ecba Size array storage to its construction count and its allocator block
Arrays constructed with a known element count (literals, slices, map/filter results) reserved at least four slots, so a one-element literal took a 48-byte block for 9 bytes of content. Construction now allocates exactly the requested slots, and any dense capacity is rounded up to fill its small-object allocator block (1 slot -> 16 B, 2-3 -> 32 B). Appends below four slots grow straight to four and double from there as before. The first storage of an array is a plain allocation with its states cleared, without the realloc, state move and sparse absorption of later growth. New capacity still starts as holes.
2026-10-11 16:30:21 -07:00
Malte Ubl def6f86a0e Let collector types bypass the generations where they never die
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.
2026-10-11 09:39:57 -07:00
Malte Ubl 85aa553ee4 Skip out-of-line array storage work for arrays without it
Destroying an array released its metadata and freed its sparse and
property storage through calls that took the shared empty ext, so every
array paid four calls on NULL. Check the ext once and free it in one
place; truncating length likewise skips the sparse search without one.
2026-10-11 09:38:28 -07:00
Malte Ubl 30a4d8b284 Allocate an array's out-of-line storage from the small-object allocator
The ext struct came from the system calloc, which every regex match result
(whose metadata lives there) paid for: regex-logs ran 1.9% slower in the
runtime suite. It now comes from scr_mem_calloc and returns through
scr_mem_free, like the array itself.
2026-10-11 09:38:27 -07:00
Malte Ubl 1a14263b5f Move rarely used array storage out of line
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.
2026-10-11 09:38:27 -07:00
Malte Ubl 6f488e13aa Shrink the cycle-collector header to 16 bytes
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.
2026-10-11 09:38:26 -07:00
Malte Ubl bf508eb70f Publish typed object graphs to workers by reference (@scriptc/threads)
The new @scriptc/threads package exports publish(value) and
sharesPublishedGraphs. Node.js runs its JavaScript: publish validates
the whole graph (plain objects, arrays, class instances, Maps, Sets and
primitives other than symbols), then deep-freezes it, and published Maps
and Sets reject mutation. scriptc builds lower both exports to
intrinsics and never compile the package body.

In scriptc, publish makes the graph immutable and immortal: a
compiler-emitted walker per static type (llvm/publish.ts) marks every
object rc == SIZE_MAX through a runtime worklist (scr_publish.c), so
retains, releases and the collector skip it and nothing frees it, and
the objects leave the collector's buffers. Hierarchy members dispatch on
the concrete class; arrays, maps, sets, strings and dyn subgraphs have
runtime walkers. Static types over-approximate a graph, so an
unpublishable type gets a refusing walker that fails only when a value
is present, and every mark is logged so a refusal restores the whole
graph, exactly like the Node implementation's validate-first order.

A typed-ref capsule over a published object travels through postMessage
and workerData as a pointer (a fresh capsule in the receiver; vtables,
adapters and type keys are process-wide); immortal dyn graphs inside a
message are shared the same way. Iterating a published map no longer
writes its iteration depth.

Stores into the static types a program publishes (computed by
backend/publication.ts, a class with its whole hierarchy) carry a guard
that throws Node's frozen-object TypeErrors, and may-throw counts them.
The corpus program pins the contract against Node: shared and cyclic
class graphs with arrays, maps, sets and records read identically on
both threads, every write form fails with Node's message, unpublished
instances of the same classes stay writable, a refused publish leaves
its graph unchanged, and a worker-published graph reaches the main
thread. packages/threads/README.md documents the contract and the
differences (prototypes and identity across messages).

Each mark records its object kind, so a successful publish takes the
objects out of the RC audit's live counts (immortal objects are never
freed).

Dates (time values) and process streams (descriptors) are scalars in
scriptc, so their walkers refuse a date field or root directly instead
of pushing its bits as a pointer; the refusal corpus program pins the
statically typed refusals (date, function, RegExp, typed array and Error
values in class and record fields, and a date root) against Node.

Main lowers a discarded or union-typed `xs.pop()` to a length
truncation; those stores now carry a `pop` marker so a published array
fails with pop's own TypeError ("Cannot delete property 'N'", or the
length message for an empty array, whose `length = 0` store the emitter
drops unless the array type can be published).
2026-10-10 01:00:36 -05:00
Malte Ubl 5a51b9f0ea Inline the buffered checks of runtime and emitted collector hooks
Emitted releases of class and record objects already test `buffered`
before calling scr_cyc_on_release, but the runtime's own release paths
(arrays, maps, boxes, closures, unions, errors, promises, streams) called
it unconditionally, and every death of a headered object called
scr_cyc_on_dead, which returns at once for an object that was never
buffered. On the tsc-ts self-check 13M of 18.5M scr_cyc_on_release calls
found the object already buffered (10.7M from traced arrays,
3.2M from maps).

scr_cyc_released/scr_cyc_dead are the inline halves for runtime code,
and the emitted noinline slow paths test `buffered` before calling
scr_cyc_on_dead (cycleDeadLines). The out-of-line hooks keep their own
checks, so other callers are unaffected.

tsc-ts self-check (median of 7): 1.652 -> 1.643 s (-0.5%); in the profile
scr_cyc_on_release + scr_cyc_on_dead fall from 2.75% to under 0.5% self
time. Binary +16 KB.
2026-10-09 10:10:25 -07:00
Malte Ubl de2100515a Splice in place when the removed elements are discarded
A statement-position `a.splice(start, deleteCount, ...items)` built a
one-element (or n-element) items array, then scr_arr_splice_insert built
the removed-elements array that the caller released straight away: three
or more allocations per sorted insert in tsc-ts's insertType. When the
result is discarded and the items are plain evaluated values, the emitter
now evaluates receiver, start, count, and items in order and calls
scr_arr_splice_drop with the owned value slots. It applies the same
clamping and state shifts on dense receivers, releases removed values
only after the receiver is consistent again, and keeps the general
implementation for sparse receivers and sparse-sized results.
2026-10-09 10:10:23 -07:00
Malte Ubl c75cc167dd Append runtime-optional push values in place
`a.push(v)` where v may hold undefined at run time (an unchecked array
read or a hole-observing for-of binding, as in tsc-ts's filterType loop)
built a temporary one-element array, stored v or present undefined into
it, and spread it onto the receiver: an allocation, a growth, a copy and
two releases per append. Such pushes now evaluate every argument first,
then append each present value through the ordinary push (and its inline
dense fast path) and each missing one as a present-undefined slot at the
current length, which is what Node stores. Pure receivers and arguments
are re-read instead of copied into owned temporaries.

Call arguments read from arrays of nullable references now take the
inline dense lookup like other projections (the value is the element
pointer itself, so the path is a few loads), and scr_arr_peek_ref answers
dense in-range indices before the general lookup. compareSymbols'
`declarations[0]!` arguments no longer leave compiled code.
2026-10-09 10:10:22 -07:00
Malte Ubl 439964a693 Inline the small-object allocator into emitted constructors and releases
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.
2026-10-08 15:36:07 -05:00
Malte Ubl 064277f297 Prefetch upcoming reference elements in the comparator sort kernel
On the x86_64 lane 41% of records-sort samples sat on the first load of
each record header (the comparator argument retain): a merge step touches
one element it has not seen before per comparison. Prefetching the
element four positions ahead on the advancing side (and during run
detection) overlaps that miss with the current comparator call: ~4%
faster on Linux (156 vs 162 ms median), neutral on macOS.
2026-10-08 15:31:04 -05:00
Malte Ubl 5ad1b976fd Decide string comparisons inline before calling the runtime
records-sort compares interned literals (`key === "department"`, equal
departments) and names that differ in their first bytes, yet every
comparison was an out-of-line call (and memcmp through a stub).

- sc_str_eq: identity, then byte length, then scr_str_eq.
- sc_str_cmp / sc_str_cmp_u16: identity; then the first differing byte
  of one 8-byte word when both strings are that long, otherwise of the
  first bytes; the UTF-16 order is decided inline only for ASCII bytes.
  Everything else calls the runtime.

The helpers are internal module functions that LLVM inlines; they are
emitted whenever the runtime entry is declared.

The comparator sort kernel also holds one undefined-`this` window for the
whole sort (each emitted call restores the receiver stack it pushes) and
resolves the element retain function once.
2026-10-08 15:31:04 -05:00
Malte Ubl bf10875927 Sort comparator snapshots in the runtime without per-move reference counting
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.
2026-10-08 15:31:04 -05:00
Malte Ubl 70dfdbd61e Route strings, arrays and emitted records through the small-object allocator
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}.
2026-10-08 15:01:26 -05:00
Malte Ubl ecd9643eb0 Remove the large-array cliff (1.2M-element fill and update: 10.4 s to 0.03 s) (#743)
* 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
2026-10-08 15:01:21 -05:00
Chris Tate 1f10e83720 Promote canary to main (#738)
- 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.
2026-10-08 11:00:13 -05:00
Chris Tate 0b168d5c23 Borrow stable loop references and bound deep cleanup
- Borrow stable loop references and share guarded dense array reads.
- Bound recursive cleanup across generated objects and runtime containers.
- Preserve ownership through mutation, callbacks, sparse reads, and abrupt exits.
2026-10-07 14:31:14 -05:00
Chris Tate 4ec55cdf64 Accelerate default primitive array ordering (#721)
- 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.
2026-10-07 08:15:08 -05:00
Chris Tate f22013f177 Improve array storage and bulk transformations
- Share dense array storage and move owned ranges without rebuilding containers.
- Batch array construction and insertion while preserving evaluation order and bounded temporary storage.
- Lower indexed mutations natively and preserve typed-array reversal across numeric element types.
2026-10-06 01:05:40 -05:00