Corpus 5659 printed blockList.rules and the inspected BlockList in the
order Node lists them. Node lists the address rules first, in the
iteration order of the std::unordered_map that holds them, and that order
differs between platforms, so the expected output on macOS differed from
Linux. The subnet and range rules that follow keep Node's fixed order
(newest first).
The program now sorts the address rules before printing the rules lists,
and inspects a BlockList that holds a single address rule.
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>
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>
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.
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>
stackless-async-context.ts woke two concurrent workers from 2 ms and 1 ms
timers. The deadlines were taken when each worker scheduled its timer, so
on a slow build both could be due on the same tick in either order. Those
workers now wake from gates that chained timer callbacks open one per
turn in a fixed order. Each woken body still crosses a timer and
nested awaits with its own AsyncLocalStorage context. The sequential
chain workers keep one pending timer at a time.
stackless-async-ordering.ts scheduled setTimeout(0) and setImmediate from
the main module, where Node leaves their order to timing; the lines never
printed only because main finished first. Main now awaits a timer whose
callback schedules the immediate, a microtask and a tick, so the
macrotask order is pinned and printed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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.
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.
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.
`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>
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>
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>
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>
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>
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>
`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>
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>
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>
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>
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>
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>
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>
`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>
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.
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.
The LLVM backend can emit calls to scr_worker_broadcast_channel (new
BroadcastChannel), so llvm-runtime-abi.test.ts requires a prototype in a
header scr_runtime.h includes; it now sits next to
scr_worker_message_channel in scr_bytes.h.
A TLS client set the name mbedTLS verifies only when it also sent that
name as SNI, so the certificate name check depended on SNI:
- https.request to an IP-literal host with servername: "" checked no name,
so a certificate for another name was accepted.
- tls.connect with servername: "" sent SNI "localhost" and checked the
certificate against localhost instead of the host.
- A verified request to an IP-literal host always failed the handshake,
because mbedTLS refuses to verify without a name.
Each client now keeps the name it checks apart from the SNI name. The
checked name is a non-empty servername, otherwise the host; SNI carries
the same name except for an IP literal or servername: "", which send
none, as in Node. When no SNI goes out, mbedTLS is told to skip its own
name check and the verify callback checks the leaf instead: an IP literal
must be one of the certificate's IP SANs (Node's checkServerIdentity), and
any other name follows mbedTLS's DNS SAN and CN rules. A missing name
fails the check. A mismatch fails with ERR_TLS_CERT_ALTNAME_INVALID, or,
for tls.connect with rejectUnauthorized: false, reads as authorized false
with that authorizationError.
Corpus 5664 covers IP-literal hosts with and without servername: "",
tls.connect with servername: "", and that no SNI is sent.
A while loop that advances a counter through a string, such as
while (pos < text.length) {
const ch = text.charCodeAt(pos);
pos++;
if (ch === 13 && pos < text.length && text.charCodeAt(pos) === 10) pos++;
...
}
kept the counter as a double, so every read converted it and every
charCodeAt took the general index path. The guarded while-counter facts
accepted only a literal or a binding the loop never writes as the
bound. They now also accept the length of a string binding the loop
never writes, which is at most 2^31 - 1 units.
The counter's maximum is also tracked along each path of the body
instead of summing every increment: an increment inside an `if` whose
condition compares the counter below such a bound starts from that
bound, and exits end their path. The second increment above therefore
keeps the counter within int32, and the counter gets int32 storage and
integer charCodeAt reads.