Commit Graph
438 Commits
Author SHA1 Message Date
Chris Tate 3e41bcbfd0 Make compile blockers and Node differences actionable (#786) 2026-10-09 10:30:31 -05:00
Chris Tate 96ee89412b Keep exception polls inline in worker programs (#785) 2026-10-09 10:01:27 -05:00
Chris Tate 3a0eda638e Schedule cycle collection by yield and borrow indexed reads (#781) 2026-10-09 07:55:10 -05:00
Chris Tate 2476844e13 Respect runtime optional values and initialization order in more places (#775) 2026-10-09 00:23:28 -05:00
Chris Tate 9131a3498c Release v0.2.7
- Synchronize all published packages and the compiler surface manifest at v0.2.7.
- Record native features, runtime performance improvements, and compatibility fixes since v0.2.6; leave Unreleased empty.
- Verify version and manifest contracts, compatibility output, and cross-target library conformance locally; use CI for full validation.
2026-10-08 21:18:31 -05:00
Chris Tate 8bd17409db Sync runtime and compiler updates into canary
- Merge main's runtime optimizations and CI changes with canary's frontend improvements.
- Preserve adaptive cycle reclamation with inline allocation and deallocation.
- Repair native bootstrap callback narrowing and emitter expectations.
2026-10-08 17:41:20 -05:00
Malte Ubl 62ff176ad7 Mask console argument tags when sizing the line buffer
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.
2026-10-08 15:51:42 -05:00
Malte Ubl 0cd28f7d45 Pass string-concatenation console arguments as parts
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
2026-10-08 15:51:42 -05:00
Malte Ubl c48f059071 Write console and process stream output directly to the descriptor
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.
2026-10-08 15:51:42 -05:00
Malte Ubl e7eef59ec1 Fast paths for number<->string conversion (Number/parseFloat, toFixed, two-decimal String)
String to number: Number(str), unary +, and parseFloat now convert short
decimal spellings with Clinger's exact fast path (scr_decimal_scan in
scr_number.c): significant digits m <= 2^53 and a decimal exponent within
+-22 (plus the folded range up to 22 + 15) need one correctly rounded
multiply or divide, bit-identical to strtod. ToNumber validates and
accumulates in the same pass; everything else still goes through the copied
span and strtod. Whitespace trimming in Number(), parseInt, and parseFloat
skips the UTF-8 decode for ASCII non-whitespace.

toFixed: the exact-binary-value digit generation moves from scr_lib.c into
scr_number.c (scr_f64_to_fixed). For f <= 22, mantissa * 5^f fits a
64x64->128 multiply, so the spec's n is one shift plus the dropped-half bit
whenever it fits 64 bits. Digits render two at a time instead of through
snprintf. The 16-limb bignum remains the fallback.

Number to string (H44): a non-integral x below 2^46 that is the double
nearest to m/100 prints as m/100 directly instead of running Ryu. Below 2^46
ulp(x) < 0.01, so m/100 is the unique shortest round-tripping decimal. The
correctly rounded division (double)m / 100 == x is the proof. This covers
JSON.stringify, templates, String(), join, and console output.

Tests: Node oracles for every path. number-cases.txt gains money-like values,
the 2^46 cutoff, and one-ulp neighbors. tonumber-cases.txt gains fast-path
boundaries plus 20k seeded short decimals. The new parsefloat-cases.txt pins
scr_parse_float, and the new fixed-cases.txt/test_fixed.c pin toFixed,
including the 2^64 fast-path limit and f up to 100. Fuzz modes cover all four
paths. Existing case lines are unchanged. Corpus 3170 pins the paths
end-to-end against Node.

Linux x64 sandbox, A/B against perf/stack/06-h23-layout-noise,
--layouts=4 --runs=16 (run 3 of 3; geomean -6.0% / -6.7% / -6.3%):

| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 236.2 | 204.2 | -13.5% | -19.8% .. +2.1% | neutral | +0.0% | 488.62 | 0.42x |
| regex-logs | 176.2 | 168.8 | -4.2% | -10.1% .. +0.3% | neutral | +0.0% | 281.23 | 0.60x |
| ast-interp | 280.0 | 273.1 | -2.5% | -5.6% .. +1.4% | neutral | +2.8% | 273.05 | 1.00x |
| records-sort | 952.4 | 939.4 | -1.4% | -5.0% .. +5.6% | neutral | +0.0% | 289.87 | 3.24x |
| template-render | 78.9 | 77.3 | -2.1% | -6.2% .. +1.2% | neutral | +0.0% | 246.71 | 0.31x |
| functional-pipeline | 338.7 | 348.8 | +3.0% | -6.5% .. +24.0% | neutral | +0.1% | 339.84 | 1.03x |
| async-pipeline | 207.3 | 207.8 | +0.2% | -5.3% .. +3.8% | neutral | +0.0% | 105.57 | 1.97x |
| word-graph | 302.0 | 300.1 | -0.6% | -5.1% .. +12.9% | neutral | +3.0% | 600.48 | 0.50x |
| alloc-trees | 336.3 | 332.4 | -1.1% | -6.2% .. +2.6% | neutral | +0.1% | 144.73 | 2.30x |
| numeric-kernels | 49.7 | 49.8 | +0.1% | -3.4% .. +6.5% | neutral | +4.0% | 143.73 | 0.35x |
| validate-errors | 179.1 | 182.9 | +2.1% | -5.8% .. +9.1% | neutral | +0.0% | 727.78 | 0.25x |
| csv-numbers | 349.0 | 233.8 | -33.0% | -37.5% .. -26.5% | faster | +0.1% | 541.77 | 0.43x |
| log-summary | 171.6 | 135.7 | -20.9% | -23.3% .. -17.7% | faster | +1.8% | 313.2 | 0.43x |
| inventory-report | 411.9 | 384.7 | -6.6% | -18.1% .. +5.5% | neutral | +2.7% | 521.27 | 0.74x |

Size: +64..+136 bytes per executable. The +1.8..+4.0% rows are one 4 KiB
page crossing (two for inventory-report).
2026-10-08 15:49:53 -05:00
Malte Ubl 2d6a239abb Skip buffering untraced union boxes in the inline union release
The speed posture's inline scr_union_release fast path predates the
runtime change that buffers a surviving union box as a cycle candidate
only when its arm_trace can reach a cycle. It still enqueued every
surviving box: sound (an untraced box visits nothing and the runtime death
path reads the header), but it gave back that change's collector savings
under --optimization=speed. The fast path now loads arm_trace and returns
when it is NULL, exactly like scr_union_release; scr_runtime.h pins the
offset.
2026-10-08 15:46:34 -05:00
Malte Ubl 213cf287b9 Emit inline retain/release fast paths under --optimization=speed (H30)
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).
2026-10-08 15:46:34 -05:00
Malte Ubl 14bd48fd94 Keep the timer slot index and regex constructor cache per worker
Upstream native workers (095b6193) run one runtime instance per thread and
make the timer heap thread-local (SCR_TL). The timer slot index added for
O(1) handle lookups indexes that heap, so it becomes thread-local too;
scr_timers_teardown already frees it during worker context shutdown.

The RegExp constructor cache stores retained templates that live instances
borrow and that are never evicted. Its table is thread-local, and worker
executables leave it empty (constructions compile privately as before),
because a worker context has no teardown point after every borrowing
instance. Ordinary executables are unchanged.
2026-10-08 15:44:26 -05:00
Malte Ubl 1152644a9c Write each console.error line with one write
stderr is unbuffered, so the piecewise stdio calls issued one write(2)
per argument, separator and newline (two syscalls per simple line), and
the line was not atomic against other writers sharing the descriptor.
Render the line first and emit it with a single fwrite, as Node writes
one chunk.
2026-10-08 15:44:26 -05:00
Malte Ubl 260aad64a7 Index timer handles by heap slot
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.
2026-10-08 15:44:26 -05:00
Malte Ubl 536deb5904 Cache regex subject decoding for exec/test and compiled RegExp patterns
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.
2026-10-08 15:44:26 -05:00
Malte Ubl b84f31e65a Send HTTP response heads with their bodies and trim per-request server work
- scr_net: scr_net_sock_writev_native sends ordered slices with one
  sendmsg when the bytes would go out immediately (plain socket, nothing
  buffered); every other state takes the per-slice write_raw path, so
  buffering, transports, and teardown are unchanged.
- scr_http: end(data) before the head, write() before the head, chunk
  framing, and the final chunk now leave as one write (head + body +
  framing), as Node does. A typical response was a head segment plus a
  body segment; it is now one segment and one syscall.
- scr_net: stop reading after a short read (libuv UV_HANDLE_READ_PARTIAL);
  the level-triggered poller reports later bytes or EOF. Removes the
  EAGAIN read per request (0.7 per request on http-api). TLS transports
  keep draining.
- scr_http: Date header formatted once per second; head built in one
  pass without snprintf; request headers parsed with geometric array
  growth and one shared string when the name is already lowercase.

The bytes on the wire are unchanged (the runner digests every response
against Node).
2026-10-08 15:42:37 -05:00
Malte Ubl f7ac13e87f Cache string Map key hashes in the string header
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).
2026-10-08 15:40:53 -05: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 18a2eddf80 Emit live-object audit notes only for --sanitize builds
scr_obj_alloc_note/scr_obj_free_note exist for the SCR_RC_AUDIT live-object
audit and are empty in release and dev runtime packs, yet every emitted
record/class new and free helper called them. The LLVM emitter takes an
objectAudit option (default on for direct emission); the production
executable and library pipelines pass it only for --sanitize builds. Runtime
TUs compile their own call sites away outside SCR_RC_AUDIT while scr_object.c
keeps the no-op symbols for objects that still reference them.
2026-10-08 15:29:36 -05:00
Malte Ubl e520c9f3b6 Buffer only union boxes whose trace can reach a cycle
scr_union_release buffered every union box left alive by a release as a
cycle-collector candidate, including boxes whose arm_trace is NULL (scalar
arms, and ref arms whose type the compiler proved acyclic, such as
TreeNode | null once TreeNode is constructor-only). Such a box's trace visits
nothing, so it can never be on a collectable cycle: skip the candidate
buffer for it. Deaths check the header's buffered bit inline before calling
scr_cyc_on_dead, because typed-ref cache commits swap arm contents between
boxes of one union type, so a box can have been buffered under an earlier
arm.

test_closure.c checks that string- and f64-armed boxes are never buffered,
that closure-armed boxes are, and that a closure -> box -> union -> closure
cycle is still collected under the RC audit.
2026-10-08 15:26:31 -05:00
Malte Ubl 2539fd9ac3 Keep fiberless async calls out of worker programs
Upstream native workers (095b6193) start each worker thread with a NULL
scr_als_active (SCR_ALS_SLOT() supplies the main slot) and give the fiber
epilogue its own context-termination path. The fiberless frame now goes
through SCR_ALS_SLOT(), and worker programs keep the fiber path, so
termination and AsyncLocalStorage behave exactly as upstream defines them.
Ordinary executables are unchanged.
2026-10-08 15:22:27 -05:00
Malte Ubl 363fb69f07 Run await-free async functions without a fiber
An async function whose body can never park its execution context
completes before its call returns, exactly like a fiber that never
suspends. The LLVM emitter now proves that from the emitted IR: a body
qualifies when neither its own text nor that of any module function it
mentions (transitively) contains a suspension entry point (scr_await_*,
scr_module_await, scr_promise_await_settled, scr_test_sub, generator
yields, async-generator operations, fiber accessors). wasm32, programs
embedding the dynamic engine, module-evaluation bodies, dyn results, and
programs using node:test subtests (t.test awaits on the current fiber
even from sync code) keep fibers. Qualifying spawn wrappers call the body
on the caller's stack between scr_async_inline_enter/leave, which save
and restore what a fresh fiber isolates (the Error.stack frame chain and
the AsyncLocalStorage context), then settle a fresh promise from the
outcome through the same fulfill/reject paths the fiber trampoline uses.
A park or hop while a fiberless frame is on a fiber's stack traps as an
internal error rather than reordering.

In async-pipeline, loadUser and authorize become fiberless: one fiber
per request instead of five. New corpus programs pin microtask ordering,
rejections (handled and unhandled), finally, nested suspending spawns,
closures, methods, Error.stack, AsyncLocalStorage, and a fiber-switch
stress test.

Final benchmarks for the branch (three commits) vs perf/harness, runs=15.

macOS arm64 (local):
| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 279.2 | 280.7 | +0.5% | -1.8% .. +3.0% | neutral | +0.0% | 230.93 | 1.22x |
| regex-logs | 305.8 | 306.0 | +0.1% | -0.8% .. +2.0% | neutral | +0.0% | 132.42 | 2.31x |
| ast-interp | 235.3 | 235.1 | -0.1% | -0.6% .. +0.4% | neutral | +0.0% | 133.55 | 1.76x |
| records-sort | 621.6 | 619.0 | -0.4% | -1.1% .. +0.6% | neutral | +0.0% | 131.56 | 4.71x |
| template-render | 202.8 | 201.9 | -0.4% | -1.7% .. +2.6% | neutral | +0.0% | 99.28 | 2.03x |
| functional-pipeline | 226.0 | 226.4 | +0.2% | -0.4% .. +0.6% | neutral | +0.0% | 120.24 | 1.88x |
| async-pipeline | 267.0 | 21.6 | -91.9% | -92.2% .. -91.4% | faster | +0.0% | 58.58 | 0.37x |
| word-graph | 417.6 | 566.8 | +35.7% | -49.6% .. +171.5% | neutral | +0.0% | 944.76 | 0.60x |
| alloc-trees | 311.1 | 311.3 | +0.1% | -4.8% .. +2.3% | neutral | +0.0% | 87.37 | 3.56x |
| numeric-kernels | 79.0 | 80.5 | +1.9% | -4.1% .. +8.4% | neutral | +0.0% | 94.05 | 0.86x |
| validate-errors | 169.9 | 169.4 | -0.3% | -12.0% .. +12.3% | neutral | +0.0% | 517.13 | 0.33x |
| csv-numbers | 379.3 | 372.8 | -1.7% | -27.8% .. +12.5% | neutral | +0.0% | 360.69 | 1.03x |
| log-summary | 180.7 | 184.0 | +1.8% | -14.4% .. +9.1% | neutral | +0.0% | 181.51 | 1.01x |
| inventory-report | 231.7 | 239.0 | +3.2% | -11.1% .. +10.9% | neutral | +0.0% | 245.91 | 0.97x |
(word-graph was host noise from concurrent agents; the immediate recheck
measured -2.0%, neutral.)

Linux x86_64 (sandbox):
| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 1031.8 | 1019.7 | -1.2% | -5.5% .. +3.2% | neutral | +0.0% | 627.39 | 1.63x |
| regex-logs | 534.1 | 533.6 | -0.1% | -5.5% .. +1.9% | neutral | +0.0% | 336.44 | 1.59x |
| ast-interp | 538.1 | 536.8 | -0.2% | -1.7% .. +2.3% | neutral | +0.0% | 324.66 | 1.65x |
| records-sort | 1798.6 | 1749.9 | -2.7% | -4.8% .. +0.5% | neutral | +0.0% | 326.49 | 5.36x |
| template-render | 379.2 | 371.0 | -2.1% | -9.3% .. +1.0% | neutral | +0.0% | 287.51 | 1.29x |
| functional-pipeline | 608.5 | 596.7 | -1.9% | -3.9% .. +3.2% | neutral | +0.0% | 390.16 | 1.53x |
| async-pipeline | 250.3 | 35.9 | -85.7% | -86.2% .. -85.3% | faster | -0.0% | 123.43 | 0.29x |
| word-graph | 462.1 | 461.7 | -0.1% | -3.3% .. +2.4% | neutral | +0.0% | 690.81 | 0.67x |
| alloc-trees | 690.4 | 699.1 | +1.3% | -0.9% .. +4.1% | neutral | +0.0% | 174.67 | 4.00x |
| numeric-kernels | 176.0 | 172.8 | -1.8% | -3.4% .. +0.6% | neutral | +0.0% | 176.9 | 0.98x |
| validate-errors | 304.4 | 301.0 | -1.1% | -4.9% .. +1.6% | neutral | +0.0% | 945.13 | 0.32x |
| csv-numbers | 602.8 | 605.0 | +0.4% | -4.7% .. +1.9% | neutral | +0.0% | 696.66 | 0.87x |
| log-summary | 332.9 | 332.6 | -0.1% | -4.0% .. +1.4% | neutral | +0.0% | 384.79 | 0.86x |
| inventory-report | 601.0 | 583.5 | -2.9% | -6.5% .. +5.6% | neutral | +0.0% | 585.31 | 1.00x |
2026-10-08 15:22:27 -05:00
Malte Ubl 2c94164dd2 Keep the fiber register file inside ScrCtx
scr_ctx_swap now stores the callee-saved registers, stack pointer, and
resume address into the outgoing ScrCtx and loads the incoming one,
instead of pushing a frame onto the fiber's own stack. The register file
lives where ucontext_t kept it: inside the heap ScrFiber for fibers and on
the C stack for a resumer, so a suspended fiber's register-held pointers
stay visible to heap scanners such as LeakSanitizer (fiber stacks are
anonymous mappings they do not scan). Fresh fibers are seeded by writing
the ScrCtx fields; only the top word of the new stack is touched.
Layout asserts pin the struct offsets the assembly uses.

The switch cost is unchanged or slightly lower (async-pipeline scale 10 on
macOS arm64: 0.17-0.19 s vs 0.20-0.21 s).
2026-10-08 15:22:27 -05:00
Malte Ubl 5f12a16f8b Switch fibers with a register-only context swap
On x86_64 and AArch64 POSIX targets, fiber switches now save and restore
only the ABI's callee-saved state (scr_ctx_swap) instead of going through
swapcontext/getcontext/makecontext. swapcontext saves and restores the
process signal mask on every switch: one rt_sigprocmask syscall on glibc,
and sigprocmask + sigaltstack + getrlimit on Darwin, plus a 16 KiB memset
in Darwin's makecontext per spawn. Those dominated await-heavy programs
(async-pipeline: most samples in sigprocmask/__sigaltstack/__getrlimit on
macOS, 0.11 s of 0.23 s in the kernel on Linux). The runtime never uses a
context to change the signal mask. Other POSIX targets keep ucontext;
Windows and WASI are unchanged. The sanitizer fiber annotations wrap the
new switch exactly as they wrapped swapcontext.

The spare fiber-stack pool grows from 4 to 64 entries. With fan-out
(Promise.all over a few batches) finished and new fibers alternate in
waves wider than 4, so most spawns paid an mmap, a munmap, and fresh-page
faults. Spares are still unmapped before the loop sleeps.
2026-10-08 15:22:27 -05:00
Malte Ubl b424092446 Walk native regex first-byte analysis with a heap worklist
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.
2026-10-08 15:19:12 -05:00
Malte Ubl 84e18fbce7 Run ASCII regex subjects on a native matcher translated from libregexp bytecode
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.
2026-10-08 15:19:12 -05:00
Malte Ubl 0c2232deb0 Parse typed JSON straight into native records
`JSON.parse(text) as T` used to build a checked-dynamic ScrDyn tree, copy
it into the native layout with the generated dynCheck builder, and then
dispose the tree. Together that was about 68% of json-records.

For record/array targets with number, boolean and string leaves, the
backend now emits a ScrJsonSchema (field offsets in declaration order,
constructors, releases) and a wrapper. The wrapper calls the new
scr_json_parse_schema, a one-pass parser that writes directly into native
records and arrays. Whenever it declines (syntax errors anywhere, kind
mismatches, missing or duplicate declared keys, excess depth) it releases
its partial value without throwing, and the wrapper reruns the unchanged
scr_json_parse + dynCheck route. Every error message and position stays
byte-identical. The lexer helpers are shared through a quiet mode and a
factored number scanner, so values decode bit-identically.

Tests: new differential corpus program json-typed-parse-direct.ts; a
dyncheck harness case comparing typed and untyped error messages through
the fused path; white-box C tests for accept/decline and leak-freedom in
scr_json.test.c; an LLVM emission test (64-bit and wasm32).

Linux (sandbox x86_64, runs=15):
| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 713.2 | 249.3 | -65.0% | -65.6% .. -64.1% | faster | +2.2% | 439.04 | 0.57x |
| regex-logs | 350.8 | 353.7 | +0.8% | -0.9% .. +2.3% | neutral | +0.0% | 238.42 | 1.48x |
| ast-interp | 379.1 | 376.8 | -0.6% | -1.8% .. +0.4% | neutral | +0.0% | 227.45 | 1.66x |
| records-sort | 1200.5 | 1207.8 | +0.6% | -2.4% .. +5.7% | neutral | +0.0% | 232.17 | 5.20x |
| template-render | 248.3 | 248.1 | -0.1% | -2.3% .. +1.0% | neutral | +0.0% | 202.16 | 1.23x |
| functional-pipeline | 402.0 | 395.3 | -1.6% | -3.8% .. +0.5% | neutral | +0.0% | 254.57 | 1.55x |
| async-pipeline | 202.4 | 201.7 | -0.3% | -1.4% .. +2.0% | neutral | +0.0% | 90.14 | 2.24x |
| word-graph | 366.9 | 348.2 | -5.1% | -12.9% .. +4.3% | neutral | +0.0% | 576.56 | 0.60x |
| alloc-trees | 513.5 | 525.5 | +2.3% | -2.1% .. +4.0% | neutral | +0.0% | 132.99 | 3.95x |
| numeric-kernels | 121.4 | 122.5 | +0.8% | -1.9% .. +3.6% | neutral | +0.0% | 135.91 | 0.90x |
| validate-errors | 206.0 | 209.2 | +1.5% | -3.6% .. +4.1% | neutral | +0.0% | 703.47 | 0.30x |
| csv-numbers | 450.9 | 448.6 | -0.5% | -2.2% .. +2.0% | neutral | +0.0% | 499.25 | 0.90x |
| log-summary | 253.5 | 252.6 | -0.3% | -1.5% .. +2.0% | neutral | +0.0% | 279.67 | 0.90x |
| inventory-report | 433.7 | 433.2 | -0.1% | -6.0% .. +3.6% | neutral | +0.0% | 413.79 | 1.05x |
geomean change -7.4%; geomean candidate/node 1.321x -> 1.223x

macOS (arm64, runs=15):
| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 292.3 | 153.0 | -47.7% | -50.1% .. -46.6% | faster | +0.4% | 248.7 | 0.61x |
| (all 13 other workloads neutral; see results/h16-typed-json/local-1.json) |
geomean change -4.8%; geomean candidate/node 1.497x -> 1.425x
2026-10-08 15:05:00 -05:00
Malte Ubl 69e26b417c Define the small-object allocator state as an ordinary global
`ScrSaState scr_sa;` was a tentative definition, which the runtime build
emits as a COMMON symbol. Library localization (abi.localize_runtime)
deliberately keeps COMMON symbols global (the ASan image-registration guard
relies on that), so every runtime-localized library archive exported
scr_sa next to its declared API, failing tests/harness/library-multi.test.ts
M1 ("external definitions equal the declared set exactly").

Initialize it explicitly so it is a regular zero-initialized definition that
localization demotes like every other runtime global. No other global added
by the stack is a tentative definition. The library-multi suite passes
(it fails M1 without this change).
2026-10-08 15:01:26 -05:00
Malte Ubl 78fef71f09 Keep the small-object allocator out of worker executables
Upstream native workers (095b6193) build every runtime unit with
-DSCR_WORKERS and run one runtime instance per thread. The small-object
allocator state is process-global and unsynchronized, so worker builds
fall back to the system allocator, exactly like thread-instanced library
archives. Ordinary executables keep the allocator unchanged.
2026-10-08 15:01:26 -05:00
Malte Ubl 1477a4037d Allocate dynamic object entry and array item buffers from the small-object allocator 2026-10-08 15:01:26 -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 d2461b0ef1 Allocate cycle-headered objects from a size-class free-list allocator
WIP step 1: scr_cyc_alloc/scr_cyc_free use a region-backed small-object
allocator (scr_mem_*), bypassed under SCR_RC_AUDIT/ASan.
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 93482f22a8 Keep the cycle collector small in static executables (#768) 2026-10-08 13:28:14 -05:00
Chris Tate c903daefcf Back off cycle scans for long-lived object graphs (#741) 2026-10-08 11:54:45 -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 fcee23f398 Preserve function identity through adapted callbacks (#737) 2026-10-08 10:15:30 -05:00
Chris Tate c48e5bea09 Improve native callbacks, value narrowing, and runtime metadata
- Keep typed callbacks on native paths while preserving receivers, evaluation order, and method replacement.
- Preserve caught class identity, collection narrowing, and missing indexed values across typed code.
- Add standard terminal access and filesystem error metadata with differential coverage and versioned runtime compatibility.
2026-10-07 21:54:58 -05:00
Chris Tate b9bac85441 Release v0.2.6 (#729)
- Release native workers, shared memory, and broader language and library support.
- Include runtime and compiler improvements in the release notes.
- Synchronize all published packages and the surface manifest at 0.2.6.
2026-10-07 19:15:55 -05:00
Chris Tate 095b619330 Add native workers and shared memory
- Add native worker execution with isolated module state, structured messages, and lifecycle controls.
- Support shared buffers and integer atomics with coordinated runtime ownership and cancellation.
- Extend compiler, runtime-pack, differential, and compatibility coverage for native concurrency.
2026-10-07 19:07:02 -05:00
Chris Tate a8bc0f6abf Optimize scalar string access and numeric calls
- Avoid materializing scalar string slices and preserve index metadata.
- Specialize known callbacks and numeric call boundaries.
- Lower proved integer remainders without changing number semantics.
2026-10-07 15:39:05 -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 13291c68d3 Preserve interface identity and extend native library calls
- Preserve live class identity through interface receivers and object methods.
- Extend synchronous text reads and regular expression replacement callbacks.
- Keep unsupported generic dispatch boundaries explicit.
2026-10-07 13:55:11 -05:00
Chris Tate e936346215 Parallelize large native builds and adapt cycle collection (#722) 2026-10-07 10:47:31 -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 c484422113 Optimize split keys and Map storage (#718) 2026-10-07 01:57:01 -05:00