Commit Graph
51 Commits
Author SHA1 Message Date
Chris Tate 67247dd26b Support objectMode streams with any value as a chunk
Readable, Writable, Duplex, Transform and PassThrough take the
objectMode option, and a Duplex, Transform or PassThrough the
readableObjectMode and writableObjectMode options, as true or false
literals in an options object literal or in an options value. An
object-mode side holds any value that converts to a checked-dynamic
value: push, unshift, write, end, pipe, pipeline, for await, 'data'
listeners, Readable.from and the transform callback carry values in
TypeScript and JavaScript, and constructor-function streams and options
held in variables do the same. The object-mode high-water mark counts
values and defaults to 16. A readable buffer that mixes strings, Buffers
and other values holds them all, and push(undefined) and other values on
a byte-mode stream report ERR_INVALID_ARG_TYPE through 'error', as in
Node.

In typed code a write or transform callback, or a _write or _transform
method, may declare its chunk with any such type, which converts when
the callback runs. Writes pass the encoding argument to _write and
_transform as Node does: undefined when absent, null when a callback
took its place, otherwise the value unchanged. A parameter declared
string | null | undefined receives that value and one declared string
receives "" for undefined and null. write(chunk, encoding) on a
byte-mode stream encodes a string chunk with any known encoding and
throws ERR_UNKNOWN_ENCODING for an unknown one. Readable.from takes
typed Maps and Sets of any such value, and arrays that mix strings and
Buffers.

for await over a readable-only stream that will emit 'close' now ends
after 'close', as Node's eos does. Buffered writes that complete inside
clearBuffer run their callbacks in order, and 'finish' waits for a
deferred afterWrite that carries a callback, as Node's pendingcb does,
unless a _final callback schedules it.

The 'data' listener adapters take three slots and write and transform
callbacks read their chunk and encoding through runtime accessors, so
RUNTIME_ABI_VERSION is now 26.
2026-10-22 04:21:55 -05:00
Chris Tate d426960f45 Make Cipheriv and Decipheriv Transforms and accept function stages anywhere in pipeline()
Cipheriv and Decipheriv values are Transforms, as in Node: they can be
written and ended, piped into, used as pipeline() and stream/promises
pipeline stages, composed, read, iterated with for await, read by
stream/consumers, passed to finished(), and listened to, and their stream
properties read as in Node. Each chunk goes through the cipher's
update(), and the end pushes final() or reports its error through
'error'; writing after final() throws out of write(). The stream unit
builds a cipher's Transform on its first stream use from operations the
cipher's handle provides (ScrDynHandleOps.transform), so programs that
use ciphers without streams link as before. The cipher is a
collector-headered object behind a traced handle: its Transform's steps
hold it, and the pair is reclaimed as a cycle. Through untyped values, in
a program that uses streams, a cipher's stream properties and its on,
once, off, pipe, unpipe, read, pause, resume, setEncoding, write, end,
destroy, cork and uncork members run on its Transform; other stream
members throw a coded SC2020 error.

A Transform holds a write's callback while its readable side is at the
high-water mark and runs it at the next read, and runs it on a tick once
the readable side has ended, as Transform._write does. write() on a
PassThrough, a Transform, a Hash or a cipher used to return true however
much the readable side buffered. zlib streams applied the rule already
and keep their timing.

pipeline() accepts function stages anywhere among its streams, so runs of
async generator stages can sit on both sides of a stream. A stage can be
named by a function declaration, or a const bound to a function
expression, that nothing reassigns; its body lowers again at the
pipeline with its source typed by the pipeline. The source can also be
an array, a string, a synchronous generator object, or a synchronous
generator function without parameters: an async generator steps the
source's iterator and yields each element, and the next stage receives
each element on the same tick as with Node's for await over the source.
The runtime's function-stage entry points take a bitmask of the function
positions and one (next, read, return) helper triple for each generator
pumped into a stream, so RUNTIME_ABI_VERSION is now 25. In JavaScript,
the chunks a stage receives from the previous stage's generator read as
the strings or Buffers it yields, so their string and Buffer methods
compile.

Corpus 7055-7059 cover cipher streams in TypeScript and JavaScript, their
errors, close order and backpressure, the pipeline stages and sources
with their tick order, and Transform backpressure.
2026-10-22 01:55:38 -05:00
Chris Tate 63001cbc13 Await a suspending stackless callee without its promise 2026-10-21 23:55:37 -05:00
Chris Tate 1d6bd1ea04 Shrink the array header to 48 bytes
ScrArr stores len and cap as 32-bit fields and no longer stores the
state pointer. The dense states share the values' allocation and run
backward from its end: the state of slot i is at
data + cap * (slot width + 1) - 1 - i. Removing slots from the front
(shift, splice(0, n)) advances data and lowers cap, which leaves every
remaining state at its address; the head room size, needed only to free
or reallocate the block, is kept in the vacated slot before data. On
64-bit targets the header goes from 64 to 48 bytes, and from 36 to 32
bytes on wasm32. Array lengths were already limited to 2^32 - 1.

The emitted code loads len and cap as i32 and zero-extends them, and
computes state addresses from data and cap. Runtime loops compute the
end of the states once per call. Local array reads load data once for
both the state and value paths. A local or strict element read whose
index is a loop's induction variable reads slots above the hole-free
prefix through the runtime: an inline state check there makes LLVM keep
-1 - i as a second induction variable, which costs a register in the
loop.

Bulk operations do not read back states they just wrote: an append to
an array whose hole-free prefix reaches its length moves the prefix bound
to the new length, a copy from below the source's prefix writes value
states without reading the source's, and releasing a string array skips
the state checks below the prefix. Copies of up to eight slots and of
short state runs use overlapping word moves instead of a call. Emitted
element access computes the state address only on the state-checked
path. Without these, programs that build many small arrays with split or
slice ran 2-4% slower than with the separate state pointer.

Integer loops over an array's length compare the counter with the i32
length field in 32 bits; the counter never exceeds 2^32 - 1 there. The
array-length stability proof accepts Map and Set reads (get, has, size)
and checked union extractions (`x!`), which run no user code, so loops
that look up maps keep the dense fast copy, which tests the hole-free
prefix once before the loop.

Small constructions with the small allocator request 6 slots, so their
blocks stay 112 bytes, now with 7 inline slots (6 after a collector
header). With 96-byte blocks of 5 slots, programs that free and reuse
many small arrays ran 15-20% slower.

RUNTIME_ABI_VERSION goes from 22 to 23.
2026-10-20 00:21:45 -05:00
Chris Tate 8e507160c4 Add BroadcastChannel and the remaining Node 24 worker_threads surfaces 2026-10-17 11:06:47 -05:00
Chris Tate de08ab1920 v0.3.5 2026-10-16 10:33:14 -05:00
Chris Tate eb48e1d2e4 Pool small Buffers as Node does (#1101) 2026-10-16 03:50:17 -05:00
Chris Tate d3dda8271d Leave the main thread's heap to the operating system at exit
When the main thread's script ends, the executable released every module
global and ran a full cycle collection before exiting, which takes time
proportional to everything the program still holds (about 15 ms for
1.5 million live objects) and frees memory the operating system reclaims
anyway. The main thread now skips those releases, the exit-time
collection and the collector's buffer cleanup. Exit listeners and output
flushing run as before. Worker threads still free their heaps when they
exit, since later threads reuse their arenas, and builds that check for
leaks (the RC audit, AddressSanitizer) free everything as before, as does
any process run with SCRIPTC_EXIT_FREE=1 for external leak checkers.

SCRIPTC_WORKER_STATS=1 reports, as each worker thread exits, how long it
took to start, to run its script and to exit.
2026-10-16 01:58:45 -05:00
Chris Tate 0d59abe143 Pool small Buffers as Node does
Buffer.from of strings, arrays and Uint8Arrays, Buffer.allocUnsafe and
Buffer.concat now return views into a per-thread pool slab of
Buffer.poolSize bytes (64 KiB by default) when the size is below
Buffer.poolSize >>> 1, following Node's allocate, fromStringFast and
fromArrayLike: 8-byte aligned offsets, a new slab when a request does not
fit, and the four-bytes-per-unit reservation for strings. Buffer.poolSize
can be read and set, and Buffer.allocUnsafeSlow returns an unpooled Buffer.
A slab lives while any of its views does. Its ArrayBuffer cannot be
transferred, and structuredClone of a typed array view copies the whole
ArrayBuffer and keeps the view's offset. Buffer.alloc, allocUnsafe and
allocUnsafeSlow validate sizes with Node's ERR_OUT_OF_RANGE error.

The first pooled allocation finds the startup slab, which has the
default 64 KiB size. A request it cannot hold gets a slab of the current
Buffer.poolSize, as in Node, and the room is checked after every new
slab, so a Buffer.poolSize raised before the first pooled allocation
cannot place a reservation past the end of a 64 KiB slab. A reservation
larger than a fresh slab traps instead of writing past it.

Corpus 5531 (typed) and 5532 (untyped) raise Buffer.poolSize before the
first pooled allocation and allocate just under the new threshold
through each pooled path, each case in a fresh child process, and also
shrink Buffer.poolSize and change it between allocations. The
node-types fenced fixture no longer lists Buffer.poolSize, which now
compiles.
2026-10-16 01:39:10 -05:00
Chris Tate 97a9257fa5 Inline string retains and dynamic value reference counting in release builds
Release builds now emit the string retain and the dyn retain and release
fast paths inline. The dyn release decides the cases scr_dyn_release
decides by kind and calls scr_dyn_release_survivor for capsules and
handles. String releases and the other families stay runtime calls, as
string-heavy programs have many release sites. The speed posture keeps
every fast path and gains the dyn release. The runtime ABI version moves
to 21 because program objects call the new runtime entry point.
2026-10-15 05:15:33 -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 124e3204c1 Merge main into lone surrogate strings 2026-10-13 21:07:27 -05:00
Chris Tate 2a7bf8b0f1 Lower the rest of the global console (#950) 2026-10-13 20:46:49 -05:00
Chris Tate 844ef14771 Keep lone surrogates in strings 2026-10-13 19:48:11 -05:00
Chris Tate 12e3da9fee Lower the rest of the global console 2026-10-13 18:55:55 -05:00
Chris Tate 947f665896 Store boolean arrays as one byte per element 2026-10-13 18:45:47 -05:00
Chris Tate bf40cf4255 Compile regex literals to native matchers 2026-10-13 11:30:19 -05:00
Chris Tate 1334a8acd2 Lend arguments to closure bodies and comparators 2026-10-13 10:35:18 -05:00
Chris Tate 5df589706b Skip array state bytes below a hole-free prefix 2026-10-13 02:13:46 -05:00
Chris Tate 7e6294ca0f Make scriptc compilation native-only 2026-10-12 12:25:30 -05:00
Chris Tate 85736170ae Merge main into feat/program-lifetime-publication 2026-10-12 08:13:15 -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 40a2c2adf2 ProgramLifetime in sanitized builds: skip the RC audit's leak report and scan instances as LeakSanitizer roots
Program-lifetime instances are never freed, and neither is anything they hold, so the RC audit of --sanitize builds reported every one of them, and every string, array and map they reference, as live at exit (4633-contract-program-lifetime failed the sanitized differential lane: 306 strings, 15 arrays, 338 objects). Like a program that abandons fibers, a program that keeps program-lifetime objects now skips the leak report with "scriptc RC audit skipped: N program-lifetime object(s) never freed", which the harnesses already filter.

Sanitized constructors call scr_obj_lifetime_note(obj), which counts the instance and registers it with LeakSanitizer as an intentional root, as publication does for published objects: once the audit no longer exits early, LeakSanitizer would otherwise report the instances the program drops. Plain builds emit nothing.
2026-10-11 17:07:03 -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 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
Chris Tate c67e08a9cf Prepare v0.2.8
- Synchronize published packages and the compiler surface manifest at v0.2.8, and bump the runtime ABI marker to v9.
- Add release notes for worker graph sharing, catchable deep recursion, parallel code generation, runtime performance, and compatibility fixes.
2026-10-10 10:08:39 -05:00
Chris Tate 96ee89412b Keep exception polls inline in worker programs (#785) 2026-10-09 10:01:27 -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 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
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 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 e2d58f60d2 Expand static support for common Effect 4 patterns (#600)
* Expand static compilation for common Effect 4 patterns

* Run every Effect 4 fixture slice in CI

* Preserve existing corpus behavior across Effect 4 lowering

* Preserve Effect platform callbacks and native stream state

* Record new corpus baselines and retain child stdin refusals

* Retire the obsolete stateful regex diagnostic

* Preserve promise reference count layout for cycle collection
2026-10-01 17:36:29 -05:00
Chris Tate c257368c5e Compile Effect generators statically (#589)
* Compile Effect generators statically

* test: declare the dormant array write's expected fence
2026-09-30 14:25:29 -05:00
Chris Tate 3f56576644 feat(compiler): rebuild the frontend with native compiler stages (#568)
- Share frontend loading, package selection, contracts, and FFI parsing between Node and native clients.
- Preserve collection layouts and optional method returns while lowering long nullish chains without recursive stack growth.
- Rebuild the frontend from TypeScript through native lowering and C emission with matching output.
2026-09-29 07:45:18 -05:00
Chris Tate a36b356a6c Support native callable properties and terminal streams
- Preserve callable properties, descriptors, and shared module bindings across native JavaScript code.
- Add stored process streams with input control, output callbacks, and write interception.
- Preserve Error identity and classification through native rethrows and promise rejection.
2026-09-29 00:52:01 -05:00
Chris Tate 31332427b6 Compile local class factories and spread calls natively
- Preserve fresh constructor identity and shared captures across local class instances.
- Carry constructors through unknown values without a JavaScript engine.
- Support runtime-length spreads into fixed and rest parameters.
2026-09-28 20:11:25 -05:00
Chris Tate f7d217f0d5 Support native ReferenceError, EvalError, and URIError (#501)
* Support additional native Error subclasses

* fix error identity across island callback bridge

* record new Error family parity entry
2026-09-27 20:27:50 -05:00
Chris Tate ffe5e27563 feat(compiler): run the IR validator natively (#493)
- Compile the production validator and exercise native IR construction and validation on C and LLVM.
- Preserve Error causes, wider callbacks, and discriminated JSON layouts in static programs.
- Version the IR and runtime ABI and cover the new compiler paths with differential and sanitizer tests.
2026-09-27 13:11:56 -05:00
Chris Tateandeuxaristia 472748956b Render UTF-8 output in Windows consoles (#340)
- Configure attached Windows consoles for UTF-8 while preserving redirected bytes and restoring host state.
- Add cross-platform output coverage and a native pseudo-console regression.

Fixes #321

Co-authored-by: euxaristia <25621994+euxaristia@users.noreply.github.com>
2026-09-16 14:48:27 -05:00
Chris Tate 401338f437 feat(compiler): add native LLVM object emission (#242)
* feat(compiler): add native LLVM object emission

- ship the pinned macOS arm64 LLVM helper and platform package
- add assembly/object CLI outputs, caching, diagnostics, and runtime ABI checks
- validate helper artifacts through CI, packaging, docs, and differential coverage

* fix(compiler): harden native LLVM emission

* fix(compiler): harden LLVM helper packaging

* fix(compiler): stabilize LLVM helper package size

* fix(compiler): harden native helper validation

* fix(compiler): harden LLVM helper verification

* fix(compiler): harden native object emission
2026-08-27 10:30:40 -05:00
Chris Tate 6f7a1a08fe feat: support thread-instanced library state (#137)
* feat: support thread-instanced library state

- Add the abi.instance_per_thread profile field: the archive's TUs compile
  with -DSCR_THREAD_INSTANCES, and the SCR_TL qualifier in scr_runtime.h
  moves every runtime unit's mutable statics into thread-local storage;
  both backends emit the program TU's module globals, run-once guards, and
  lazily-compiled regex literal caches thread-local to match
- ONE linked archive then serves one independent instance per embedder
  thread through the unchanged entry family: a thread registers its sink
  and calls the init entry, and owns its own collector, result arena,
  poison flag, and program state — a trap poisons only the instance it
  fired in while sibling threads' instances keep answering
- Document the contract beside the profile spec: one instance per thread,
  selected implicitly by the calling thread; instance lifetime is the
  thread's lifetime; the never-entered-from-two-threads rule is unchanged;
  independent of and composable with abi.localize_runtime
- Immutable interned data (string literals, unit arms, template arrays,
  vtables) stays shared — Darwin ASan's image-registration common
  included, keeping the one-registration-per-image discipline; the
  exception cell's current pointer resolves a NULL sentinel in this mode
  because a thread-local address is not a constant initializer
- Add the four-thread acceptance probe (distinct per-thread workloads,
  concurrent instance-local inits, per-instance collects, a trap delivered
  to its own thread's sink exactly once), the composition probe pairing a
  thread-instanced localized archive with a second different-prefix
  localized archive, an explicit ASan rerun beside the suite-flavor
  sanitized builds, and profile-shape coverage; schedule the new lane
  contracts beside M1/M2
- Non-opted builds are byte-for-byte unchanged: the qualifier expands to
  nothing outside -DSCR_LIB -DSCR_THREAD_INSTANCES (verified object-level
  over every touched runtime TU in both default and library flavors)

* fix: keep sanitized runtime-localized archives linkable on ELF

ASan's instrumented globals ride ELF section groups; archives built from
shared runtime objects carry groups with REPEATED signatures, so a process
linking two runtime-localized sanitized archives kept one archive's group
and discarded the other's — whose now-local references then dangled at the
embedder's link. Resolving the groups into the combined relocatable member
(ld -r --force-group-allocation) keeps every archive's copies; the
localization step then demotes them per archive exactly like unsanitized
state. Plain builds carry no section groups, so the flag is inert there.

With the groups resolved, ELF surfaces the same deliberate exception
Mach-O already documented: the image-wide registration guard COMMON stays
shared so the final image registers its ASan globals exactly once — M1/M7
now pin that spelling on both platforms. The explicit ASan pairing (M8)
additionally points Linux LSan away from contractually thread-lifetime
instance state, exactly as the sanitized suite lanes do.

* fix: isolate inspect state per thread

* fix(runtime): share uptime anchor across threads
2026-08-12 09:49:43 -05:00
Chris Tate 481dd9c974 fix: flush console output promptly
- Submit console, process, readline, and island writes before returning.
- Add live-pipe and SIGKILL parity coverage against Node 24.
2026-07-26 01:30:42 -05:00
Chris Tate b5e3d019fc Rename the capability: library mode, never "core", surface and internals
- CLI: the core verb becomes `scriptc build --lib --profile <p.json>` (no positional; profile names the entry); artifact renames to <name>.lib.a with .lib.ll/.lib.c/.lib.ir.json siblings
- SC4xxx diagnostic text and hints say library, never core; trap-message strings likewise (LIBRARY RC AUDIT, library reset registry, library inbound bytes)
- internals: compileLibrary/compileLibArchive, IrModule.lib + IrLibSection/IrLibExport, moduleLibAsyncSurface, libRoots, loadLibraryProfile/LibraryProfile under src/library/, frontend/lib-exports.ts, emitLibEntries/emitLibDefs
- runtime: scr_core.c -> scr_library.c with scr_library_* symbols and the SCR_LIB macro (scr_lib_* was taken by the stdlib unit); scr_lib_session_cleanup
- tests: tests/library-mode/ fixtures (entries lib.ts), library-*.test.ts harness files
2026-07-23 09:24:44 -05:00
Chris Tate fc2a467408 Funnel home fix + K8 fiber/loop symbol audit
- non-core scr_trap/scr_trap_fmt move from scr_console.c to scr_exception.c: the failure-channel unit is in every link, the runtime's own unit-test TU subsets included (the console unit is not)
- K8 additionally asserts no scr_loop/fiber/timer/child symbol is defined in or referenced by the archive — the design's mechanical backstop for the narrowed core base set
2026-07-23 02:00:43 -05:00
Chris Tate 50676fc9dd K-fixture conformance suite: K1-K10 over both emissions
- five generic fixtures under tests/core-mode (scalars, buffers, reinit, traps, init-trap) with C probe drivers; the harness flips the profile emission and pins identical outputs across the two lanes
- K1 nm symbol exactness both directions; K8 ambient audit (no sigaction/signal/pthread_create/atexit/setvbuf references — scr_init and scr_lib_init compile out under SCR_CORE)
- K3 covers both arena postures over one core; K5-K7 cover sink-exactly-once, init traps, pre-registration abort, Uncaught rendering, poisoned-core aborts; K10 reruns K4/K5/K7 under ASan + the RC audit's re-init zero-heap seam
- K9 drives SC4002/SC4003/SC4004/SC4005/SC4007 end to end with the profile-teaching rider verbatim
2026-07-23 01:37:42 -05:00
Chris Tate 59274de9d7 Trap funnel: scr_trap/scr_trap_fmt over every core-linkable trap site
- executable expansion in scr_console.c is byte-identical fputs/vfprintf + abort; core expansion in the new scr_core.c poisons, delivers to the registered sink exactly once, aborts pre-registration or if the sink returns
- 89 fputs/fprintf+abort pairs and 10 bare OOM aborts across the 20 core-linkable TUs now funnel; lazy atexit teardowns (json errdyn, regex bytecodes, symbol registry) reroute to the core reset registry via scr_atexit
- scr_core.c owns sink/poison/result-arena/reset-registry/marshalling helpers; scr_exception.c renders escaped exceptions as "Uncaught ..." into the funnel; scr_lib exposes its cleanup to the reset seam
2026-07-23 01:14:32 -05:00