Commit Graph
2303 Commits
Author SHA1 Message Date
Malte UblandClaude Opus 5.5 714ef5aa09 Prove array lengths stable in index loops and version dense array loops
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>
2026-10-17 14:48:38 -07:00
Chris Tate 6ce668b213 Compile switch statements over object values (#1211) 2026-10-17 16:21:43 -05:00
Chris Tate 4e979b2c7e Lower Promise.try with arguments for the callback (#1212) 2026-10-17 16:15:20 -05:00
Chris Tate 1f999c7cf0 Print BlockList address rules in a platform-independent order (#1210) 2026-10-17 16:12:37 -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 b666b6b1b1 Lower filter, find, findIndex, some and every on tuples (#1209) 2026-10-17 16:08:26 -05: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 8a2d9356c4 Print BlockList address rules in a platform-independent order
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.
2026-10-17 15:52:14 -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 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 3008edde94 Sequence the timer turns in the stackless async corpus programs
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>
2026-10-17 13:14:54 -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 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 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 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 fab1a29b33 Merge pull request #1192 from vercel-labs/ctate/v0-3-6-814afa
v0.3.6
v0.3.6
2026-10-17 13:50:36 -05: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 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 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
Chris Tate 9c924818b8 Reserve heap growth for worker threads that allocate large blocks (#1191) 2026-10-17 11:47:44 -05:00
Chris Tate 5d02923bba Add readline's terminal mode, cursor helpers and emitKeypressEvents (#1178) 2026-10-17 11:45:52 -05:00
Chris Tate 82ee707ce5 Support request signals, destroy(err), servername, Expect events and maxHeaderSize in http (#1076) 2026-10-17 11:41:29 -05:00
Chris Tate 348b52c3d9 Keep while-loop counters bounded by a string's length in int32 (#1190) 2026-10-17 11:30:19 -05:00
Chris Tate 81142b99c5 Reserve heap growth for worker threads that allocate large blocks 2026-10-17 11:24:45 -05:00
Chris Tate f36767199e Check every TLS client certificate against the request's name
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.
2026-10-17 11:17:59 -05:00
Chris Tate 2840a92124 Keep while-loop counters bounded by a string's length in int32
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.
2026-10-17 11:09:48 -05:00
Chris Tate 23056949da Keep readline's terminal cursor on character boundaries, and merge main 2026-10-17 11:06:19 -05:00
Chris Tate 287119c449 Keep int32 class fields next to arithmetic on dynamic operands (#1187) 2026-10-17 10:42:20 -05:00