Commit Graph
3145 Commits
Author SHA1 Message Date
Chris Tate 1f04c0b544 Group Sets, Maps and generators with Object.groupBy and Map.groupBy
Object.groupBy and Map.groupBy accepted only arrays. They now also take a
Set (its elements), a Map (its [key, value] entries) and a synchronous
generator. Sets and Maps are walked live, as forEach walks them, and a
generator is pulled one element per callback call and closed when the
callback throws. In JavaScript sources, unannotated callback parameters
take the element and its index.
2026-10-17 16:23:24 -05:00
Chris Tate 6ce668b213 Compile switch statements over object values (#1211) 2026-10-17 16:21:43 -05:00
Chris Tate a1d5d9e1b9 Expose Promise.prototype methods on promises in untyped bindings
Reading then, catch or finally from a promise held in a JavaScript or
checked-dynamic binding now answers the shared method values, so
typeof checks, thenable detection and "then" in p match Node. Calling a
method value uses its receiver; a receiver that is not a promise throws
Node's incompatible-receiver TypeError.

Fixes #978
Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com>
2026-10-17 16:18:56 -05:00
Chris Tate b91882d684 Use asserted key types in the method replacement census
An element write whose key is asserted to a number, such as
`slots[at as number] = x` or `slots[at!] = x`, was judged by the type of
the key before the assertion. A `number | undefined` key counted as a key
that can name any string, so calls of every method in the program went
through the checked property lookup. The census now reads the type of the
key with its assertions, so these writes only mark methods with numeric
names.

Fixes #1148
Co-authored-by: Alan Leal <33952+alantriesagain@users.noreply.github.com>
2026-10-17 16:15:37 -05:00
Chris Tate 4e979b2c7e Lower Promise.try with arguments for the callback (#1212) 2026-10-17 16:15:20 -05:00
Chris Tate 6b5adcf6e5 Store unknown and object class fields in checked-dynamic storage (#1208) 2026-10-17 16:08:57 -05:00
Chris Tate 2f2f26d0d5 Report program type errors instead of npm package refusals
When the program still fails to typecheck against a refused package's own
declarations, the type errors are the program's. Report them at their
locations instead of an SC2013 refusal for every imported package.

Fixes #1155
Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com>
2026-10-17 16:06:08 -05:00
Chris Tate d7623938c0 Keep declared methods of generic class instances after a computed-key write
When a write with a computed string key can replace any method, calls of
every method name read the receiver's property bag without checking for
the key, and the bag falls back to the class's prototype data. Generic
class instantiations, mixin classes and their subclasses have no prototype
data, so their methods read as undefined and calls threw "x.status is not
a function". Reads on these classes now check for an own property and
otherwise use the declared method.

Fixes #963
Co-authored-by: Quim T. <26749475+espetro@users.noreply.github.com>
2026-10-17 16:03:33 -05:00
Chris Tate e161dc4446 Treat type-asserted values as present in the optional-value analysis
`hot(xs[i] as number)` and `hot(stack.pop() as number)` counted as
possibly undefined, so `hot(n: number)` took a boxed `number | undefined`
parameter and callers boxed every number they passed. An assertion to a
type without undefined already checks the value and throws a TypeError
for undefined, so its result is now treated as present, like `x!`.

Fixes #985
Co-authored-by: Tim Kendall <1427378+timkendall@users.noreply.github.com>
2026-10-17 16:00:54 -05:00
Chris Tate d00ccbe89e Widen Result<T, never> and Result<never, E> unions
A union arm whose object type has a required property of type never has
no values. When a conversion's source checker type proves an arm
uninhabited, the re-tag traps on that arm instead of requiring a matching
destination layout, so Result<T, never> widens into Result<T, E>.

Fixes #1170
Co-authored-by: BaoLei <50226652+blurname@users.noreply.github.com>
2026-10-17 15:58:16 -05:00
Malte UblandClaude Opus 5.5 250097dd32 Represent unions of records as the record pointer itself
A union of two or more record arms and at most two unit arms (the
discriminated unions of object literals behind Result, token, shape and
AST types) now holds the record pointer instead of a reference-counted
%ScrUnion box, like nullable unions already do. NULL and the existing
null sentinel encode the unit arms.

Every record shape that is an arm of such a union carries its
module-wide shape number in a hidden i32 after the reference count; the
arm of a value is that word mapped through a per-union tag helper, which
LLVM folds into the consumer's switch. Records share one retain; release
and trace dispatch on the arm, and every arm of a union with a traced arm
carries a collector header. Wraps move the record, retypes between such
unions reuse the pointer, and equality is identity. Record field indices
come from the shape's field base.

Unions over records that are also arms of runtime-inspected unions (Map
keys, Set elements), library builds, lifetime-region programs and debug
builds keep boxes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:57:35 -07:00
Chris Tate cd977bf200 Lower Promise.try with arguments for the callback
Promise.try(f, ...args) evaluates the arguments at the call and passes
them to f, which still runs synchronously; its result, a returned
promise or a synchronous throw settle the returned promise as in the
one-argument form. A callback with more parameters than arguments, or
an arguments list with a spread, is refused.
2026-10-17 15:55:37 -05:00
Chris Tate 6f4b569cb6 Compile switch statements over object values
A switch whose discriminant is an object, array, record, function or
other reference value was refused. It now lowers like the union and
checked-dynamic switches: the discriminant is evaluated once and each
case expression, in source order and only until one matches, is compared
with it by the same strict-equality lowering as ===, which compares
references by identity. The === and !== lowering is now a function the
switch lowering shares.
2026-10-17 15:52:58 -05:00
Chris Tate 0210e54e49 Lower filter, find, findIndex, some and every on tuples
Fixed tuples, readonly or mutable, lower these predicate methods like map
and flatMap: each position is read when it is visited, find, findIndex,
some and every stop at the first deciding position, and predicate results
and narrowing follow the array methods' rules.

Fixes #1171
Co-authored-by: BaoLei <50226652+blurname@users.noreply.github.com>
2026-10-17 15:50:17 -05:00
Chris Tate 6470c870ef Store unknown and object class fields in checked-dynamic storage
TypeScript class fields declared `unknown` or `object`, also with `null`
or `undefined` or as arrays of those, were refused with SC1090. They now
use the checked-dynamic storage that JavaScript class fields without a
declared type already use. This covers instance fields, static fields and
parameter properties. Fields declared `any` stay refused.

Fixes #1147
Co-authored-by: Alan Leal <33952+alantriesagain@users.noreply.github.com>
2026-10-17 15:47:37 -05:00
Chris Tate 318209abe6 Keep the identity of checked-dynamic objects placed in JavaScript literals
JavaScript file-scope object literals are stored as checked-dynamic
objects. An array literal holding one, directly or in a nested literal,
was built with a static element type, so the element was a copy:
arr[1] === marked was false and writes through the array missed the
original. Such array literals now build as checked-dynamic arrays, a
file-scope binding initialized with one stores it unchanged, and an
object literal whose property is a nested literal holding one builds as
a checked-dynamic object.
2026-10-17 15:44:45 -05:00
Chris Tate 4018a1d957 Compile async functions that suspend only at their awaits to stackless coroutines (#1204) 2026-10-17 15:38:36 -05:00
Chris Tate e331d7e53b Copy callback helpers per known callback target (#1206) 2026-10-17 15:36:18 -05:00
Malte UblandClaude Opus 5.5 83517ecd82 Accept a frame-stored tuple receiver in the tuple for-of test
A for-of over a tuple producer's result only reads its elements, so the
tuple now lives in frame storage and the producer runs through its
result-slot entry. The test still checks that the receiver is computed
once and that no array snapshot is allocated, whichever entry runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:32:48 -07:00
Malte UblandClaude Opus 5.5 1d1e934b4c Copy callback helpers per known callback target
Direct calls whose callback arguments resolve to known functions call a
copy of the helper made for that tuple of targets, so the constant-callback
analysis devirtualizes and LLVM inlines the callback. Constant callbacks
also resolve through recursion that forwards the callback, through other
consumers' parameters, and through unwritten captures.

Copies are limited to bodies of at most 300 IR nodes and 4 copies per
function, which keeps the self-hosted compiler within 1% of its size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:14:49 -07:00
Chris Tate d7ac988fec Return primitive receivers from Object.setPrototypeOf for any object prototype (#1202) 2026-10-17 15:06:23 -05:00
Chris Tate 8e3b32ee3c Convert class instances with their own valueOf and toString in dynamic operators (#1201) 2026-10-17 15:00:18 -05:00
Chris Tate 0b7e777689 Support net.SocketAddress and net.BlockList (#1200) 2026-10-17 14:56:25 -05:00
Chris Tate 8e061e6859 Keep power-of-two remainders on integer paths where truncation is a round trip (#1198) 2026-10-17 14:51:44 -05:00
Chris Tate fe611d3663 Add Int32[] element contracts (#1199) 2026-10-17 14:49:24 -05:00
Chris Tate 1ed65c27b6 Remove retain/release pairs through private-array precision and last-use moves (#1196) 2026-10-17 14:43:49 -05:00
Chris Tate 4e3138ceda Keep optional, nullable, and dictionary JSON members on the direct schema parser (#1193) 2026-10-17 14:39:47 -05:00
Chris Tate bff94f18ce Return primitive receivers from Object.setPrototypeOf for any object prototype
Object.setPrototypeOf with a boolean, number, string, bigint or symbol
receiver now returns the receiver when the prototype is null or any object
or function, including class instances, classes, Maps, Sets, typed arrays,
promises and Object.prototype. Other prototypes throw a TypeError, as in
Node.
2026-10-17 14:38:11 -05:00
Chris Tate cffe4123d2 Give dense arrays head room so shift, splice(0, n) and unshift runs cost amortized O(1) (#1195) 2026-10-17 14:37:36 -05:00
Chris Tate 18b50c4b44 Convert class instances with their own valueOf and toString in dynamic operators
Dynamic operators converted a class instance held as `any` or `unknown`
through its plain-object snapshot, so a class's own valueOf and toString
were never called: `c + 1` gave "[object Object]1" and `c * 3` gave NaN.

When a boxed class declares valueOf or toString, the operands of the
dynamic conversions pass through a generated helper first. For an instance
of such a class the helper runs OrdinaryToPrimitive with the compiled
methods, in the order of the conversion's hint, and returns the primitive
result; it throws Node's TypeError when both return objects. Other values
are returned unchanged for the runtime operation to convert.

The conversions covered are the arithmetic, bitwise and relational
operators, `+`, `++`, unary `+`, Number(), BigInt(), template literals,
String(), loose equality and computed property keys. Operands are
evaluated before the first conversion, loose equality converts an object
only against a primitive, and property keys convert only for receivers
that admit a key, in the order Node uses.
2026-10-17 14:35:22 -05:00
Chris Tate 0db6d27c7c Support net.SocketAddress and net.BlockList
net.SocketAddress and net.BlockList were refused at compile time. They
are now native checked-dynamic handles in scr_net.c, so their getters
and methods also work on untyped values, and they match Node 24:

- new SocketAddress(options) checks the options in Node's order
  (ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE for the family,
  ERR_SOCKET_BAD_PORT, ERR_OUT_OF_RANGE for the flow label) and throws
  ERR_INVALID_ADDRESS for an address that does not parse. parse() reads
  URL.parse(`http://${input}`)'s hostname and port and answers undefined
  on failure; isSocketAddress(), toJSON() and util.inspect match.
- BlockList takes address, range, subnet and CIDR rules (addAddress,
  addAddresses, addRange, addSubnet, addCIDR, addCIDRs), removes them
  (removeAddress, removeRange, removeSubnet, removeCIDR, clear) and lists
  them in Node's order: IPv4 addresses, IPv6 addresses, subnets, then
  ranges, each newest first, with a repeated address kept once.
- check() compares across families through the IPv4-mapped IPv6 form,
  as Node's SocketAddress::compare and is_in_network do, and answers
  false for a string that does not parse in the given family.
- rules, size, toJSON(), fromJSON() (Node's rule patterns), isBlockList(),
  PRIVATE_RANGES, instanceof and util.inspect match, with the validation
  errors of lib/internal/blocklist.js.

Corpus 5659 and the node-types fixture net-blocklist.ts cover both
classes.

The compatibility registry lists the BlockList methods and
SocketAddress.parse as supported, and both classes as partial: their
structured clone and worker transfer forms are not modeled.
2026-10-17 14:32:06 -05:00
Malte UblandClaude Opus 5.5 9aae9e41b5 Keep power-of-two remainders on integer paths where truncation is a round trip
`x % c` for a constant power of two c computes `x - trunc(x / c) * c`.
On x86-64 without SSE4.1, trunc is itself an integer round trip, so a
division, a multiplication, the round trip and a sign fix sit on the
dividend's dependency chain. Loops that feed the remainder back, such as
`s = (s + x) % 65536`, ran up to twice as slow as with the integer
remainder they used before.

Where truncation is a round trip, dividends below 2^62 now take integer
paths:

- A proven integer dividend takes the i64 remainder.
- A computed dividend (`+`, `-` or `*`) takes the i64 remainder when it
  is an integer. Otherwise, below 2c it is x when |x| < c, behind a
  predicted branch, and x - copysign(c, x) when not, which is exact.
  Larger fractions take the path below.
- Any other dividend computes (trunc(x) srem c) + (x - trunc(x)) without
  a branch on whether x is an integer.

Huge, infinite and NaN dividends, divisors above 2^52 and targets with a
rounding instruction keep the double sequence. No path calls fmod, whose
cost grows with the quotient.

On a Linux x86-64 sandbox (median ms, main -> this change): an
accumulator by 65536 2667 -> 1343, a phase accumulator by 1 2210 -> 432,
a fractional generator by 1024 1449 -> 491, an LCG by 2^31 1561 -> 1016,
random integers and fractions 593 -> 451. Statically proven operands and
other divisors are unchanged.

Corpus 5776 covers the bounds of each path, mixed streams at one
remainder site, and computed dividends around c and 2c of both signs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:28:03 -07:00
Malte UblandClaude Opus 5.5 63dae8775e Define the region fiber hooks in an always-linked runtime unit
scr_async.c reads scr_region_fiber_hooks, but several runtime test links
(cipher, bytes, sha-kernels, runtime) take scr_async.c without
scr_region.c and failed with an undefined reference. scr_object.c is in
every runtime link, so the pointer now lives there; scr_region.c still
installs the hooks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:24:47 -07:00
Malte UblandClaude Opus 5.5 95602b88e4 Move owned bindings into their last read and hand stored arguments to constructors
An owned local's or parameter's read in the last statement of its extent
retained the value, and the scope released the binding's own reference
later. When that statement reads the binding exactly once, outside any loop
or try statement nested in it, assigns it nowhere and mentions no binding
derived from it (which could borrow from the moved reference), the read now
takes over the reference: it loads the slot and clears it, and the scope's
NULL-tolerant release does nothing. The read node must occur once in the
function, since lowering can share varRef nodes. Parameters also count
locals rebound from them as derived, and bodies with walk borrows keep
their parameters. (Ported from the unmerged rc-round3 branch and extended
to parameters.)

A parameter whose last use stores it (a field, record or element store, a
record literal field, a push) now takes the owned convention instead of the
borrowed one, unless it is only projected: `new Node(build(), build())`
hands the temporaries straight into the fields instead of retaining a
stored copy in the constructor and releasing the temporaries in the caller.
Callers that still use their argument retain it, as for any owned
parameter.

Executed RC operations (with the private-array change): alloc-trees
8.10M -> 1.83M, optional-fields 18.76M -> 2.68M, region-builds
7.94M -> 5.89M, cyclic-graphs 6.93M -> 5.28M, intern-table-map
17.9M -> 16.0M; no workload increased.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:19:01 -07:00
Malte UblandClaude Opus 5.5 f49c38e082 Run never-suspending callees in place from stackless async bodies
A stackless coroutine body cannot use the await-call entries, which park
the caller's stack, so `await f(...)` of an async f that never suspends
spawned f's promise and awaited it: 4M such awaits took 142 ms against
main's 91 ms. The coroutine body now calls f's inline await entry
(sc_ai_*), which runs the body in place between scr_async_inline_enter
and exit and leaves a throw pending. The coroutine then takes the await's
turn itself. A throw becomes the rejected promise the await re-throws
from on the same turn, and an adopting `return f(...)` first takes the
then-job turn, exactly as the await-call entries do (80 ms).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:16:58 -07:00
Malte UblandClaude Opus 5.5 4aa5d6cbf8 Give dense arrays head room so shift, splice(0, n) and unshift runs cost amortized O(1)
shift() moved every remaining element down one slot, making queue drains
quadratic (100k elements: 0.85 s; 1M: no result in 30 s). Dense arrays of
8-byte slots with their own allocation now remove front elements by
advancing data/present and lowering cap; the vacated slots stay in the
allocation as head room. A flag in `spare` marks it and its size is
implied by the layout (present == data + cap + head), so every reader,
including inline element access in emitted code, is unchanged. Growth
moves the elements back to the start of the allocation (in place only
when that leaves half the moved count free, else geometrically), a
drained array starts over at the front, unshift reuses head room, and an
array unshift already had to move once reserves head room for half its
length. scr_arr_outline drops head room before compiled storage swaps.
Booleans, inline slots, sparse arrays and static arrays keep the old
behavior.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:13:53 -07:00
Malte UblandClaude Opus 5.5 06b0bb2931 Write borrowed string slices into stack buffers
A slice read only by lending consumers (string equality and ordering,
length/charCodeAt/indexOf/includes/startsWith/endsWith, Number/unary
plus/parseInt/parseFloat, typed string-keyed Map/Set get/has/set/add/
delete keys, string switch discriminants, and string parameters their
callee only lends) is written by scr_str_slice_scratch into a per-site
stack buffer as an immortal ASCII ScrStr marked as scratch (cap < len),
instead of a heap allocation. Stable locals initialized by such a slice
qualify when every read lends. Map/Set insertion stores a heap copy of a
scratch key, and probes cache the key hash in the scratch header, so the
counting idiom allocates only for new keys. Longer, non-ASCII, and
interned results keep the owned slice; coroutines and debug builds are
unchanged. Slice bounds are normalized without a libm trunc call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:13:11 -07:00
Malte UblandClaude Opus 5.5 24c54fc5d3 Add Int32[] element contracts
`Int32` from @scriptc/types now also annotates the element type of a
declared number array (`Int32[]`, `readonly Int32[]`, `Array<Int32>`,
`ReadonlyArray<Int32>`) on parameters, variables, class instance fields
and function results. The contract promises that every element stored
into the array, through any reference, is an int32 by construction.

contracts/int32-elements.ts checks it over a whole-program flow graph of
the places array references live (locals, parameters, globals, results,
class field families, record fields, container element slots), with one
node per allocation and per opaque origin. Allocations that can reach an
annotated slot are constrained and every element store into an array that
may be constrained must be range-proven an int32 (SC7002); element copies
constrain their sources by fixpoint; arrays from or escaping to code the
IR does not show refuse the contract (SC7003). Slots only constrained
arrays reach are recorded as IrContracts.int32Elements.

Element reads that find a value through those slots carry the int32
range, `getNumber` reads are integer-or-NaN operands of wrapping sums, and
ToInt32 of any integer-or-NaN operand is branch-free. Holes, missing
indices and length changes keep their JavaScript behavior.

Integer kernels over a 2^20-element array: `(s + a[i]) | 0` 1086 -> 175 ms,
`(Math.imul(h, 31) + a[i]) | 0` 602 -> 150 ms, prefix sums 575 -> 155 ms;
programs without the annotation compile to identical binaries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:08:06 -07:00
Malte UblandClaude Opus 5.5 1689404912 Keep records, tuples and spread updates that own references in frame storage
Frame storage for non-escaping records was limited to number, boolean and
inline-union fields. Records with string, array, record or class fields,
tuples, and same-shape spread updates (`{ ...state, count }`) were always
heap-allocated, even when every use only projected them.

Closed record and tuple shapes now qualify regardless of field types. The
frame owns the reference fields: they start empty in entry-addressed slots
and are registered before anything is stored, with the binding's lexical
scope for locals and with the statement frame for call-argument
temporaries, so an exception while the record is being built releases
exactly what was stored. Interned string literal fields need no owner.
A reassigned binding builds the new value apart, copies it over its slot
and then releases the replaced fields. Result-slot entries cover producers
returning such records, tuples and spreads of a binding, and store into
the zeroed caller storage without releasing an old value. A spread of a
binding is a projection for the lifetime analysis. Published shapes and
switch-case bindings of reference-owning shapes keep heap storage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:07:26 -07:00
Malte UblandClaude Opus 5.5 da8ccadf6d Keep stores into a function's own unreached arrays out of its reference writes
Reference-write analysis merged every reference-array element into one
location, so the map, filter and fused-pipeline helpers, which fill a fresh
result array, counted as writing all array elements. A borrow of an input
element across their callbacks (and of any element across a call into
them) was refused, and each element was retained and released per call.

An array local declared once from an array literal or `new Array(n)`,
never rebound, unboxed, and otherwise only the receiver of element stores,
length updates, `length`, `push` and presence tests, or the value of a
`return`, is private to its activation: no code reads its elements or
reaches its storage another way before the function returns it. Its stores
cannot release a value a borrow depends on, so they no longer add the
array-element location, and they stay out of the function's summary (a
caller's borrows predate the array).

Executed RC operations: ast-interp 4.49M -> 2.31M, optional-fields
18.76M -> 10.72M, template-render 0.40M -> 0.36M, the same as an unsound
oracle that drops every array-element write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:52:49 -07:00
Chris Tate 3032248c11 v0.3.6 2026-10-17 13:47:41 -05:00
Malte UblandClaude Opus 5.5 a40138c8b0 Keep union and dictionary members out of the direct parser's record loop
The record loop inlined the union reader and the overflow insert, which
slowed records whose members are all plain leaves by about 2%
(json-records). Scalar members test their kind first again, and the union
reader, the overflow insert, and the absent fill stay out of line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:37:58 -07:00
Malte UblandClaude Opus 5.5 74a9cfb8cc Compile async functions that suspend only at their awaits to stackless coroutines
Native async bodies whose only suspension points are their own awaits now
compile to LLVM switched coroutines, the lowering WASI async bodies and
native synchronous generators already use. A suspended call keeps a heap
frame of the values live across its awaits instead of a fiber with its own
stack. Bodies that reach other suspension points (async generator
consumption, dyn awaits, node:test subtests), module evaluation bodies,
dyn results, worker and library programs, and Windows keep their fibers.

The emitter selects bodies by emitting them as usual first: that text may
suspend only through await runtime calls and await-call entries, and may
call no function that parks. The coroutine form is then emitted and
checked again. Each await prepares its continuation in the runtime (a
pending promise's waiter list, or one READY-queue turn, continuing in place
exactly where a fiber's hop would) and extracts the settled result after
resuming. The eager prefix runs on the spawner's stack and moves to a fresh
stack near the stack guard, so deep async recursion keeps working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:29:05 -07:00
Malte UblandClaude Opus 5.5 3fcef83d38 Carry lifetime regions across await and reuse freed region chunks
An async function called inside LifetimeRegion.run now keeps its region
after every await: its fiber records the region that was current at spawn,
and the event loop makes that region current while it resumes the fiber.
A region is not freed (or attempted) while a fiber that carries it exists,
so a region released while its async work is pending keeps taking that
work's objects and is freed once the work ends. Before, objects created
after an await went to the default region, which is never freed, and kept
the request's region allocated for good, so per-request regions in async
servers grew without bound.

Freed region chunks now go to a per-thread cache (at most 8 MiB resident,
the first 256 KiB of each kept cleared, fresh pages past it, no huge-page
chunks) instead of being unmapped, so a region per request or per batch no
longer maps and faults in new memory every time.

Adds the requests.ts region benchmark (concurrent async handlers), corpus
5023 and its region-stats checks, and a chunk-reuse runtime unit check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:26:12 -07:00
Malte UblandClaude Opus 5.5 c8c7553487 Record TypeScript order baselines for the direct JSON parse fixtures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:25:23 -07:00
Malte UblandClaude Opus 5.5 e415d8fc63 Parse JSON dictionaries on the direct schema parser
Records with a string index signature (`Record<string, string>`,
`{ name: string; [key: string]: number }`) still sent the whole document
through the dynamic tree and the checked cast. Their undeclared members now
join the record's overflow map in source order straight from the text, read
as the index value type, exactly as the checked route copies the parsed
object's entries. Index value types that are unions, `unknown`, a repeated
key, or a `__proto__` member keep the checked route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 11:10:43 -07:00
Malte UblandClaude Opus 5.5 da36100ecc Keep optional and nullable JSON members on the direct schema parser
`JSON.parse(text) as T` parses straight into the native layout only when
every record member is a number, boolean, string, record or array, so one
optional or nullable member sent the whole document through the dynamic
tree and the checked cast. Record members typed as unions of those kinds
with null and undefined now parse directly too, provided each JSON value
kind selects at most one arm (at most one record and one array arm, no
discriminant), so the checked route's first full match is that arm.

Each union describes its slot representation: the nullable reference
payload, the inline scalar tag and bits, or a tagged box built through a
compiler-emitted box function. A missing undefined-armed member takes its
ABSENT state, exactly as the checked route's missing key does; any other
missing member, a value without a matching arm, or a duplicate key still
declines to the checked route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 10:55:47 -07:00
Chris Tate 4530547f01 Return owned copies of the worker thread name
The thread record keeps the Worker's name as an immortal string and frees
it with the record. worker.threadName and the in-thread threadName
returned that buffer as a string value, whose retains do not extend an
immortal string's lifetime, so a name read before the Worker was released
pointed at freed memory once the record was destroyed. Both getters now
copy the name into a new string.

Corpus 5992 keeps worker.threadName past the worker's exit and the
Worker's release and then uses it; it matches Node, plain and with
SCRIPTC_SAN=1, and fails under ASan without this change.
2026-10-17 12:36:51 -05:00
Chris Tate 8620fa533a Keep record views of class instances only where a copy is observable (#1188) 2026-10-17 12:30:36 -05:00
Chris Tate 79dd586b82 Treat switch, assert and identity-keyed constructors as identity observations
A class instance that flows into a structural type keeps a live view when
the program can tell a copy from it. switch statements match their cases
with ===, node:assert's named comparison exports and util's
isDeepStrictEqual compare their arguments, and new Set, Map, WeakSet,
WeakMap and WeakRef key their arguments by identity; these now count as
observations of the values' types.
2026-10-17 12:08:58 -05:00