Commit Graph
276 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 444274e53b test(runtime): give the glob oracle build a 180 s hook budget
A validation lane failed with a 60 s hook timeout in
packages/runtime/test/glob.test.ts. The `beforeAll` build compiles about
thirty C sources with `-fsanitize=address` and the RC audit, which takes
about 48 s on 8 cores when run alone, so 60 s flakes once many suites
compile at once. Only that hook moves; the oracle run keeps its own
120 s budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 05:33:25 -07:00
Chris Tate 5a95e8a076 Compile scr_intl.c with the ICU headers in the COMMON-symbol test
common-symbols.test.ts compiles every runtime source with the vendored
include paths only. scr_intl.c includes ICU's C API headers, which the
runtime pack build takes from the fetched ICU release, so on a host
without system ICU headers both of the test's compiles failed with
"'unicode/ubrk.h' file not found", and the aarch64-macos compile failed
on every host. The test now passes the same source/common and
source/i18n include paths for scr_intl.c, through icu-data.mjs's
ensureIcuSources.
2026-10-30 03:43:48 -05:00
Chris Tate 3b1a2c41b6 Merge main 2026-10-29 15:55:05 -05:00
Chris Tate 16f3c16752 Match Node's node:path behavior from Node 24's path tests (#2495) 2026-10-29 15:10:50 -05:00
Chris Tate c5f423b464 Decode malformed UTF-8 to U+FFFD in the ICU converter and at directory and argument boundaries
The ICU unit's WTF-8 to UTF-16 conversion counted units from lead bytes
alone. A 4-byte-shaped encoding of a BMP value counted two units and wrote
one, so ICU received a length past the units written, and word segments
built from those ranges could return prior heap contents. The conversion now
decodes each maximal ill-formed subsequence to U+FFFD, as the WHATWG UTF-8
decoder does, keeps WTF-8's 3-byte lone surrogates as their unit, and
reports the units it wrote. The decoder lives in scr_intl_utf16.h.

Directory entry names (readdir, opendir) and process.argv entries now decode
as UTF-8 with U+FFFD for malformed bytes, as Node decodes them, instead of
entering strings as raw bytes.

test_intl_utf16.c checks the decoder against Node's TextDecoder over
malformed sequences. intl-malformed-utf8.test.ts compares a program that
segments malformed directory names and an argument against Node.
2026-10-29 08:07:53 -05:00
Chris Tate 5f7a9cc8c7 Merge main 2026-10-29 03:34:56 -05:00
Chris Tate a35bf813db Keep the pointer when a narrowing drops a pointer union's unit arm
A union retype between two shape unions (or two class unions) shares the
pointer when both encode their unit arms the same way. Narrowing a field
read past `=== undefined` or `=== null` retypes a union with a unit arm
to one without it, so the encodings differ and each listed arm was
rebuilt: a retain, a tag switch, a second retain and a release, for the
same pointer. The unit encodings only matter when the value may hold a
unit arm. When the retype lists none of the source's unit tags, every
value is a record or class pointer whose arm the target derives from the
record or class itself, so the pointer is the target value.

The rebuild's release also made the pointer a nursery cycle candidate,
and in the native compiler those candidates kept the nursery bypass's
probes of closures and boxes succeeding. Without them, a probe window can
open while young cycles of the type are held by older garbage that the
bypass promoted earlier. The window then frees nothing, the type stays
bypassed, and the garbage waits for a mature pass, which on a cold tsc-ts
build raised the compiler's peak RSS by about 100 MB. A nursery probe
window that opens now makes the next release-triggered pass that would
run at the nursery run at the mature level, which reclaims that garbage
before the window is judged.
2026-10-28 23:23:07 -05:00
Chris Tate abb87f4dd7 Grow the ASCII run before walking for forward index reads
A scanner reading a mostly ASCII text front to back (slices and
positioned startsWith at increasing positions) walked from the hot
cursor on every read within 64 units of it, since the near-cursor walk
came before the ASCII-run growth. A forward read past the run now grows
the run first. When a non-ASCII character ends the run, a nearby read
walks from the run's end or the cursor and starts a new run where it
lands.
2026-10-28 21:13:01 -05:00
Chris Tate 7a7fd8ce1d Match Node's node:path behavior from Node 24's path tests
path.posix, path.win32 and the bare path module are one module object
each, the handle process.getBuiltinModule answers, so require('path/posix'),
path.posix and the target platform's bare path compare as Node's do, a
namespace beside other values in one slot is untyped, and path[family]
reads that object. Functions read through them take checked arguments:
each string parameter passes Node's validateString under its own name
(basename's suffix first), resolve validates paths[i] only as far as its
last-first walk reads them, parse answers an untyped { root, dir, base,
ext, name }, format follows _format over any object, and
toNamespacedPath answers a non-string as it is. A JavaScript call whose
arguments are not strings, or omit one, goes through the same checks.
Typed slots keep the typed function values.

path.win32.relative lowercases both resolved paths with
String.prototype.toLowerCase and walks them by UTF-16 unit, comparing
segments instead when lowercasing changed a length, as Node does. It
folded ASCII only, so a path with İ or Ⱥ in a different case gave a
longer relative path. The function moves into its own runtime unit,
scr_path_win32_relative.c, which links with the regex unit whose case
mapping it uses. scr_path.c stays free of it, so library and
cross-target builds that link the path functions without the regex unit
still link.

A JavaScript binding whose inferred type holds a union of functions and
arrays (a test table of [fn, cases] rows, or one destructured row) takes
the checked-dynamic kind that dynFallbackType gives the unmappable
spellings of such tables. Its rows keep their functions' names, and
for-of, forEach and destructuring read the items as untyped values.
TypeScript bindings keep their typed layout.
2026-10-28 19:45:36 -05:00
Chris Tate a484390e07 Merge main 2026-10-28 09:50:05 -05:00
Chris Tate d3e8ab41ef Publish unwind plans without a plain write to their pc
A new plan was copied into its slot as a whole struct, which wrote the
slot's pc with a plain store while other threads' lookups read it
atomically: a data race. The insert now copies the fields after pc and
publishes pc only with the release store.

The unwinder test also runs its threaded cases under ThreadSanitizer,
which reports the race without this change.
2026-10-28 00:36:18 -05:00
Chris Tate d5ac31f812 Raise through cached unwind plans on x86-64 Linux
scr_unwind_raise walks the stack with plans cached per return address.
The first raise through a return address finds the function's FDE
through the .eh_frame_hdr of the object that contains it, runs the CFI
up to the call, and finds the call site's landing pad in the LSDA; the
plan records the frame's CFA rule, where its callee-saved registers are
saved, and the landing pad. Later raises look the plans up, restore the
registers, and jump to the first landing pad. Plans are inserted under
a lock and read without one.

Landing pads continue the unwind with scr_unwind_resume instead of
`resume`, so the walk goes on from their frame. A frame whose CFI,
pointer encodings or personality the plans do not express, or a return
address without an FDE, leaves the unwind to libgcc's forced unwind
from the frame the walk started at, as before. Other Linux targets keep
libgcc's unwinder; their scr_unwind_resume calls _Unwind_Resume.

bench:runtime gains layered-errors, a workload whose errors are thrown
below small functions that hold references and caught per entry in a
loop, and the corpus gains unwind-landing-pad-chains, which pins landing
pads that continue the unwind, including in frames LLVM inlines into a
caller whose handler takes the exception.
2026-10-28 00:22:04 -05:00
Malte Ubl ad45f05ca8 Merge branch 'ni/i4-deferred' into ni/i5-shared-modules 2026-10-27 19:14:39 -07:00
Chris Tate 1a86f94239 Merge main 2026-10-27 16:45:14 -05:00
Chris Tate 0c954ebfd6 Reserve fewer worker arenas before smaller class slices on hosts that limit address space
Worker runtimes reserve one region for 32 arenas of 40 class slices. When
the host refused the full region (32 arenas of 1 GiB slices, 1.28 TiB of
address space), the allocator kept 32 arenas and shrank every slice. Under
valgrind, which maps at most 56 GiB, slices dropped to 16 MiB, and every
class whose live blocks outgrew 16 MiB took the system allocator.
Callgrind profiles then counted system allocations that a native run
never makes: tsc-ts checking VS Code made 10.3 M small calloc calls under
valgrind and 0.06 M natively.

The reservation now tries arena counts from 32 down to 4 at each slice
size, from 1 GiB down, before smaller slices, and 2 and 1 arena only when
no slice size fits 4. Arenas that are not reserved stay claimed, so
threads beyond them use the system allocator, and the region bounds of
foreign frees use the arenas reserved. Hosts that map the full region keep
32 arenas of 1 GiB slices.

A runtime test limits RLIMIT_AS to 60 GiB and checks that the first thread
gets 256 MiB slices for 400,000 blocks of 56 bytes, that three more
threads get arenas and a fourth uses the system allocator, and that blocks
freed across threads keep their contents.
2026-10-27 10:43:05 -05:00
Malte UblandClaude Opus 5.5 3a8aee738a Merge the restacked base into ni/i5-shared-modules
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 05:41:22 -07:00
Chris Tate 659a74acc4 Merge main 2026-10-27 02:27:10 -05:00
Chris Tate 47904df3c2 Decode short non-ASCII strings directly in charCodeAt
charCodeAt on a non-ASCII string mapped the UTF-16 index to a byte offset
through a string index entry, which the first read claimed and the
string's release purged. For a string of one character, such as each
character of a for-of loop that codePointAt(0) classifies, the entry cost
more than the read.

A non-ASCII receiver of up to 16 bytes is now decoded from its start on
each read, as scr_str_utf16_len already counts short receivers. Longer
receivers keep the index entry and its cursor.
2026-10-27 01:07:00 -05:00
Chris Tate df77671e33 Merge main 2026-10-26 23:13:34 -05:00
Chris Tate 48ccee523e Allocate template literals at their exact length
A template literal of strings and numbers reserved 32 bytes for each
number, the longest number text, and kept that capacity for the result's
lifetime: an 8-byte name built from two numbers used a 96-byte block. The
numbers now format into a stack buffer first, so the result is allocated
once at its exact length.
2026-10-26 21:01:36 -05:00
Chris Tate 3c83fcb89c Allocate object groups that die together in arenas (#2348) 2026-10-26 16:54:31 -05:00
Chris Tate c5e8a876ec Store ordinary objects as a layout plus value slots (#2353) 2026-10-26 16:18:38 -05:00
Chris Tate f12b2b041d Cheaper trial deletion in the cycle collector 2026-10-26 14:34:49 -05:00
Malte Ubl 01a87c968a Merge remote-tracking branch 'origin/ni/i4-deferred' into HEAD
# Conflicts:
#	packages/compiler/src/frontend/lowering/lower-modules.ts
2026-10-26 11:27:04 -07:00
Chris Tate 9c7d552fdc Link the arena runtime into the runtime C tests 2026-10-26 13:26:29 -05:00
Malte UblandClaude Opus 5.5 dc1e8acc1d Store ordinary objects as a layout plus value slots
A checked object (SCR_DYN_OBJ) now starts in layout mode: v.obj.layout names
its key sequence, a node of the thread's interned transition tree, and
v.obj.slots holds one 8-byte value per key, where every property used to take
a 48-byte entry. All properties of a layout-mode object are writable,
enumerable, configurable data properties. An accessor, a non-default
attribute, a delete, more than 64 keys, spent layout budgets, publication to
other threads, or any code that reads entries directly moves the object to
dictionary mode (today's entries) for good, so unported paths stay correct.

Layouts replace the dynamic tier's lazily computed hidden classes (ScrJsShape)
and keep their budgets. Inline caches key on the layout pointer and read the
slot directly; JSON.parse builds objects along layouts; stringify, key
enumeration, release, trace and the runtime read cache read slots in place.
The emitted index-signature checks read members through runtime accessors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 09:18:53 -07:00
Malte UblandClaude Opus 5.5 7ac0542aa9 Read ordinary objects' properties through accessors outside the object core
Runtime units outside the object core (scr_json.c, scr_js_object.c) read and
adjusted SCR_DYN_OBJ entries in place: v.obj.len, v.obj.entries[i] and the
48-byte ScrDynEntry fields. The single object model's generic objects
(stage S3) change that storage, so these units now go through own-property
accessors in scr_dyn.h:

- scr_dyn_obj_count, scr_dyn_obj_key_at, scr_dyn_obj_value_at and
  scr_dyn_obj_prop_at (a ScrDynProp view: key, value, getter, setter and
  attributes) walk the properties in creation order;
- scr_dyn_obj_index_of finds one by key;
- scr_dyn_obj_set_attrs_at restricts a member a builtin object just added;
- scr_dyn_own_symbol_prop and scr_dyn_copy_prop_at wrap the symbol-entry
  and copy primitives.

The accessors are inline over today's entries, so behavior is unchanged.
21 units move (inspect, assert, util compare, console, http, http2, fetch,
tls, fs, os, lib, array, message, publish, region, symbol, worker events,
handle properties, builtin values, declaration adoption). A package test
keeps entry storage reads inside the core.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 05:39:34 -07:00
Chris Tate c3b265f68c Merge main 2026-10-26 04:39:20 -05:00
Chris Tate feb4501013 Keep a candidate's generation when a limited cycle pass reaches it
A limited pass (scr_cyc_collect_set, which freeing a LifetimeRegion runs
over the objects the region owned) takes only the candidates in its set.
Its walk can still reach other candidates, and one that is referenced from
outside is blackened by scan and promoted to the next generation while it
keeps its slot in its old generation's buffer. scr_cyc_on_dead finds a
candidate's buffer by its generation, so when that object died it removed
another buffer's entry and left its own slot pointing at the freed block,
which the next pass then read.

Such a candidate is no longer promoted: it keeps the generation of the
buffer it is in until a pass takes it.
2026-10-26 00:24:05 -05:00
Chris Tate 307f9e0009 Keep one heap-trim test block per four pages of any size
The test kept one live 48-byte block every 1,000 allocations, which on a
system with 64 KiB pages leaves a live block on every page, so no page held
only free blocks and the resident-memory check failed. It now keeps one
block per four pages of blocks, from the system's page size.
2026-10-25 22:47:07 -05:00
Chris Tate 5b672087c6 Return the freed heap pages to the system after the frontend is disposed
The native compiler's frontend-dispose sweep frees about 450 MB on a cold
tsc-ts build, but the size-class allocator keeps the freed blocks on their
classes' free lists, which only later blocks of the same size reuse. Code
generation allocates other sizes, so its pages stacked on top of the freed
heap and set the build's peak resident memory.

- scr_heap_trim (scr_alloc.c) returns the pages of the calling thread's
  size-class slices that hold only free blocks (MADV_DONTNEED) and the
  system allocator's free pages (malloc_trim). In the single-threaded
  allocator the blocks of a returned page leave the free list and the
  class carves past its frontier instead; the remaining free blocks are
  relinked in address order. Worker runtimes return the calling thread's
  arena pages as a parking thread does.
- scr_collect_cycles_phase, the full sweep after a phase's data died,
  calls it; the native compiler calls that sweep when it disposes the
  frontend.
- test_heap_trim.c checks that the pages go back, that the blocks still
  allocated keep their contents, and that new blocks overlap none of
  them, with and without worker arenas (another thread allocating and
  freeing remotely throughout).

Cold tsc-ts build, 12 interleaved rounds: peak RSS 1407 -> 1247 MB
(-160 MB, -11.4%, every round); wall 87.1 -> 85.9 s (median).
2026-10-25 22:36:11 -05:00
Chris Tate efca230de9 Merge ni/i4-deferred and apply parity and security fixes 2026-10-25 22:26:54 -05:00
Chris Tate cbe4a9387b Merge main 2026-10-25 15:24:25 -05:00
Chris Tate cab0968510 Keep holes and values written through unknown[] views in typed arrays (#1866) 2026-10-25 14:01:03 -05:00
Chris Tate 90605110a5 Adapt the nursery trigger to where cyclic garbage dies (#2231) 2026-10-25 14:00:41 -05:00
Chris Tate e8a64267cb Call toJSON on foreign elements and merge main 2026-10-25 13:13:02 -05:00
Chris Tate e24ed12933 Decode string storage within its length in Buffer encodings
String storage can hold bytes that are not well-formed UTF-8: an HTTP
header value keeps the raw bytes a client sent. Encoding such a string
read past its end.

- scr_bytes_next_cp decoded any high byte as the lead of a sequence and
  read up to three bytes after it without a bound. It now takes the
  storage length, decodes only complete, well-formed sequences, and
  returns any other byte as U+0000..U+00FF, advancing by one. That is
  each such byte's value in Node, which decodes header values as latin1.
  This covers latin1, ascii, binary and utf16le encoding, hex and base64
  of non-ASCII text (which narrow through latin1), Buffer#write and
  TextEncoder#encodeInto.
- base64 decoding read one byte past its input when the eight-character
  groups ended exactly at the end.

The runtime's bytes unit test (built with ASan) encodes ill-formed
storage at every offset within a word and random high bytes, and
compares latin1, ascii and utf16le with a scalar reference. A corpus
program sends such header bytes to a server and encodes them as Node
does.
2026-10-25 12:48:21 -05:00
Chris Tate b0c01dc341 Adapt the nursery trigger to where cyclic garbage dies
A program's nursery trigger starts at 256 candidates and doubles, up to
4096, after each scheduled mature or full pass whose dead set held more
mature and old objects than an eighth of what the nursery passes freed
since the previous one; it halves after one that held less than a
thirty-second of that. SCR_CYCLE_THRESHOLD and
scr_cyc_set_nursery_threshold pin the trigger as before. The
scheduled-age rule keeps counting collection points in the pinned or
default trigger.
2026-10-25 12:48:07 -05:00
Chris Tate 18fd46be1c Merge main 2026-10-25 10:43:36 -05:00
Chris Tate 63a99cc4d9 Convert a loop's string receiver to UTF-16 at once only when the loop can read enough of it
A versioned loop over a non-ASCII string passes the range of indexes it
reads: a counted loop's start and limit, or a while loop's counter and
bound. The runtime converts the string at once when that range has at
least one unit per 16 bytes of it, and otherwise counts the loop as one
indexed read, so a loop over a line of a long source text no longer
keeps a UTF-16 copy of all of it. The publish call is weighted as
unlikely, since loops re-entered over one string skip it.
2026-10-25 07:37:50 -05:00
Chris Tate 6157fa93ab Merge base branch and fix 2026-10-25 07:02:48 -05:00
Chris Tate b3c98139e4 Move small number-key maps to the direct index from the second key
A number-keyed Map or Set with keys the direct index allows now moves to it
at its second entry rather than its fifth, so a map over a few small
integer keys (a histogram over 0..3) reads through the inline direct index
instead of the out-of-line linear scan.
2026-10-25 05:48:40 -05:00
Chris Tate 7141af5cc9 Merge main 2026-10-25 00:07:23 -05:00
Chris Tate 37031fa7c2 Convert a loop's string receiver to UTF-16 at once, and publish it only when the scan holds another string (#2180) 2026-10-24 23:34:50 -05:00
Chris Tate 6a29947998 Run the native compiler's nursery passes every 1024 candidates
The native compiler now starts with the cycle collector's nursery trigger
at 1024 new candidates instead of 256. scr_cyc_set_nursery_threshold sets
the trigger that SCR_CYCLE_THRESHOLD sets, on the pattern of
scr_cyc_set_growth_cap; the environment variable still takes precedence.

The compiler sets it from a constructor in its host object, before its
modules initialize. Set from main, the trigger leaves the build's
collection work where it was: the cost depends on the collector's first
passes. Programs keep the default trigger.

A cold tsc-ts build takes 4% less wall time (12 paired runs) and its peak
RSS goes from 1.41 GB to 1.31 GB. The program's IR is unchanged.
2026-10-24 22:59:02 -05:00
Chris Tate add5f4869b Convert a loop's string receiver to UTF-16 at once, and publish it only when the scan holds another string 2026-10-24 22:53:15 -05:00
Chris Tate e0f1de0a2b Merge main 2026-10-24 20:00:43 -05:00
Chris Tate 9d08b301fc Merge main 2026-10-24 17:50:19 -05:00
Chris Tate 12e16cdb32 Find weak collection entries through hashed indexes (#2004) 2026-10-24 16:40:37 -05:00
Chris Tate 190fb8d58e Mix weak-collection hashes in 64 bits on 32-bit targets
uintptr_t is 32 bits on wasm32, where the weak-collection hashes shifted it by 33 and 56 bits. The hashes now widen keys, owners and kinds to uint64_t before mixing; 64-bit targets compute the same values as before.
2026-10-24 16:01:33 -05:00