Index loops over arrays left the counter a double in several common
shapes, and every element access at the counter tested the array's
hole-free prefix on each pass:
- A syntactic proof (ir/array-length-stability.ts) shows that a loop never
changes an array's length: the body runs no user code, never rebinds the
array or lets it escape, and only resizes local arrays that no other
binding can hold. Element writes at a counter kept below the length are
accepted.
- Counted loops take a stable `a.length` as their limit, and an element
access at the counter makes an unversioned counted loop worthwhile, so
strided (`i += 2`) and descending (`i = a.length - 1; i >= 0`) loops get
an integer counter.
- Array lengths bound while-loop and for-loop counters by their range,
which needs no stability proof.
- A loop whose array length is stable and whose indexed arrays keep their
storage runs as two copies. One test before the loop (the length is at
most 2^31 - 1 and at most each indexed array's `filled` bound) selects a
fast copy whose element accesses at the counter skip the prefix test and
whose counter is an int32. LLVM can vectorize that copy.
- Loop counters whose ranges fit in 32 bits convert to double from their
low half, so an inlined callee's index conversion folds away.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
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.
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>
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>
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>
`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>
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>
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>
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.
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.