A sum of products of integer typed-array reads that only reaches a
bitwise operator, such as `(p[j] * 77 + p[j + 1] * 150) >> 8`, now adds
in 32-bit integers like a sum of the reads themselves: a product whose
every result is a safe integer, and never -0, is an exact integer or NaN.
A NaN-coded read passes its validity bit up through those sums, so the
integer result selects 0 for a missed read without a floating compare,
and a loop over such sums vectorizes without converting to doubles.
`length` and `byteLength` loaded the typed array's header with
`!invariant.load` (#2016). LLVM may move such a load down to its use,
past the release that frees the view, so `const slots = this.slots;
this.slots = next; return slots.length` returned a garbage number in
release builds. These reads are now ordinary loads that keep the
header's alias tag. Header loads that only bound or address an element
access stay invariant: their values reach only that access, which runs
while the view is alive, so typed-array loops still hoist the length
and data pointer.
Corpus typed-array-length-after-release covers a replaced field, a
deleted map entry and a rebound global.
A loop that reads one typed array and writes another kept every element
in memory: separate views can share a buffer, so LLVM had to assume a
store to one could change the other's elements. A loop of eight lanes
held in Float64Arrays (zr[k], zi[k], tr[k], ti[k]) ran as scalar loads
and stores of each lane on every step.
When a typed-array loop version covers at least two views and writes at
least one of them, its guard now also requires every pair of the views'
byte ranges to be disjoint: one ends at or before the other starts. In
the fast copy, each view's element reads and writes carry an alias scope
of their own and no-alias with the others' scopes, so LLVM keeps the
elements in registers and vectorizes across lanes. Views that share
bytes, or one array passed twice, take the checked copy as before. The
scopes are string-named, written inline, and named after the function
and the version, so every copy of a loop has its own. Worker programs
scope only views the guard proves thread-owned.
Corpus 8182 runs such loops over disjoint, adjacent, overlapping and
identical views, views of different element types over one buffer, and
empty views.
A sum or product of exact integers whose result range is not proven, such
as `child + 1` of a speculated loop counter, computes its double and, beside
it, an i64 wide view (`wideArithmetic`). Array indexes already used that
view, but number compares and typed-array indexes did not:
`child + 1 < end` compared doubles, and `a[child + 1]` converted the double
back to an integer and tested the round trip before its bounds compare.
`alwaysExactInteger` gives a value's exact integer, or its wide view when
the view's magnitude bound is within the magnitude below which the view
equals the double. A number compare whose two operands both have one
compares the i64s, and a typed-array index uses it directly. Views that
may differ from their double, such as `2 * root + 1` of a counter that may
reach 2^53, keep the double compare.
emitInlineAtomicsOp read its typed array with emitExpr, so every Atomics
call on a local array retained the array before the atomic instruction and
released it after, two out-of-line calls around one instruction. The
array now goes through emitBorrowedInput: an unchanged, unboxed local owns
it across the later operands and the operation, and the instruction and
scr_atomics_op only read it. Fields, globals, captures and bindings an
operand can reassign keep their own reference.
An array or typed-array index that is an exact integer or NaN, such as an
element read that may miss (`t[i]!`), a `charCodeAt`, or a sum of those, is
a valid index when it is ordered and below the length. The index check now
tests that with one ordered compare and the integer, taken from the value's
32-bit view or from one conversion, instead of converting the index back to a
double and comparing it.
Number locals that are declared once with an initializer, are not captured
or boxed, and are only written such values carry the same fact, so
`const c = t[i]!; table[c]` and `(h * 31 + c) | 0` take the integer paths
that the inline read already takes.
ToInt32 and ToUint32 of a double that no range proves an integer took the
i64 conversion only up to 2^53 in magnitude, and decoded the double's bits
above that. Every double between 2^53 and 2^63 is an integer, so the i64
conversion is exact there too, and its low 32 bits are the result. The bit
decoding now handles only NaN, the infinities and magnitudes from 2^63.
Multiplicative hashes such as `((h ^ b) * 16777619) >>> 0` and
`(h * 1000003 + b) | 0`, whose products usually pass 2^53, convert on the
fast path.
A Uint8ClampedArray store called scr_bytes_to_u8_clamp for every element,
which kept loops of clamped stores scalar. The store now computes
ToUint8Clamp in line: NaN and values at most 0 store 0, values at least
255 store 255, and values between round through (v + 2^52) - 2^52, which
rounds a value in (0, 255) to the nearest integer, ties to even, as the
runtime function does.
In worker-enabled programs, every typed-array element access tests the
view's shared storage and branches between a relaxed atomic access and
the ordinary one, so loops over thread-owned views kept one branch per
access. A view's shared storage is fixed at construction, like its length,
so the typed-array loop version guard now tests it once for each invariant
receiver, and the fast copy reads and writes those views directly. In
worker programs a loop is versioned for this even when no bound needs a
guard. When any receiver is shared, the checked copy runs unchanged.
Programs without workers emit the same code as before.
Adds a Uint8ClampedArray brighten kernel to the typed-array-kernels
workload.
Typedarray element stores could alias every header and module-global load,
so each access reloaded the length, data pointer and global receiver slot and
kept its own bounds branch. Element accesses now share one TBAA category
(views of any element type may share a buffer), header loads another, and
whole-slot loads of bytes/number globals their own, so LICM keeps them in
registers. The data pointer loads beside the length, ahead of the branch.
ScrBytes lengths and data pointers are fixed at construction, so innermost
synchronous for loops whose accesses use a statically ranged index or an
affine counter index (k * counter + invariant + c) are versioned: one test
before the loop proves every access in bounds (bounding the counter so every
affine operand stays an exact safe integer), and the copy behind it runs
without per-element checks. The checked loop keeps exact out-of-bounds,
NaN, fractional and DataView-throw behavior.
Float16Array is a native typed-array kind with IEEE 754 binary16 storage.
Stores round the number to nearest-even in one step from the double, as
Node does, including overflow to Infinity, subnormals, -0 and NaN; reads
widen exactly. The runtime converts with integer operations, so no target
needs half-precision hardware or compiler-rt support.
Construction, element reads and writes, ArrayBuffer views, fill, set,
subarray, slice, both sort orders, constructor values and checked-dynamic
storage work as for the other typed arrays. Math.f16round,
DataView.prototype.getFloat16 and setFloat16, and util.types.isFloat16Array
compile.
`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>
Buffer.from of strings, arrays and Uint8Arrays, Buffer.allocUnsafe and
Buffer.concat now return views into a per-thread pool slab of
Buffer.poolSize bytes (64 KiB by default) when the size is below
Buffer.poolSize >>> 1, following Node's allocate, fromStringFast and
fromArrayLike: 8-byte aligned offsets, a new slab when a request does not
fit, and the four-bytes-per-unit reservation for strings. Buffer.poolSize
can be read and set, and Buffer.allocUnsafeSlow returns an unpooled Buffer.
A slab lives while any of its views does. Its ArrayBuffer cannot be
transferred, and structuredClone of a typed array view copies the whole
ArrayBuffer and keeps the view's offset. Buffer.alloc, allocUnsafe and
allocUnsafeSlow validate sizes with Node's ERR_OUT_OF_RANGE error.
The first pooled allocation finds the startup slab, which has the
default 64 KiB size. A request it cannot hold gets a slab of the current
Buffer.poolSize, as in Node, and the room is checked after every new
slab, so a Buffer.poolSize raised before the first pooled allocation
cannot place a reservation past the end of a 64 KiB slab. A reservation
larger than a fresh slab traps instead of writing past it.
Corpus 5531 (typed) and 5532 (untyped) raise Buffer.poolSize before the
first pooled allocation and allocate just under the new threshold
through each pooled path, each case in a fresh child process, and also
shrink Buffer.poolSize and change it between allocations. The
node-types fenced fixture no longer lists Buffer.poolSize, which now
compiles.
Atomics.load, store, exchange, compareExchange, add, sub, and, or and xor
on an integer typed array compile to one sequentially consistent LLVM
atomic instruction on the element. An index that is not an integer in
range takes the runtime operation, which applies ToIndex and raises its
errors.
In worker-enabled executables a typed-array access tests whether the view
is shared. The test used to come before the index check, and the shared
path called scr_bytes_get_or_nan/scr_bytes_set with the index as a
double, then converted the element back to an integer. Both paths now
use the index the inline check produced: the shared test follows the
check (a view's length never changes), and the shared path calls new
runtime entries that take the index as a size_t
(scr_bytes_shared_get_int, scr_bytes_shared_get_f64,
scr_bytes_shared_set). Integer elements join as one 32-bit value and
convert to a number after the join, so compares and ToInt32/ToUint32
of the result stay integer operations in every element kind.
The amortized worker cancellation poll moves from the loop header to
the loop's back edge (the continue target). In the header it was a loop
exit ahead of the body, which kept LLVM from rotating the loop and left
loads such as the view's length inside it.
Adds the typed-array-scan and typed-array-scan-workers runtime
benchmarks: Uint8Array, Uint16Array, Int32Array and Float64Array scan
loops in an ordinary and a worker-enabled executable.
The backend converted size_t lengths and indexes to numbers with sitofp. On
wasm32 size_t is 32 bits, and an array length reaches 2^32 - 1, so a sparse
array of length 4294967295 read -1. sizeToDouble picks uitofp for a 32-bit
size_t and keeps sitofp for 64 bits, whose sign bit a length never reaches,
so native output is unchanged.
An unsettled top-level await always exited 13. Node exits with
`process.exitCode || 13` and runs the exit listeners first, which can set
process.exitCode again. scr_promise_finish_top_level now notes 13 only when
the program set no nonzero exit code, and the pending exit returns the
exit code after the listeners, as the other exits do.
Three WASI tests still exercised the embedded-JS fallback that native-only
compilation removed: an embedded-source fetch scan and two npm fixtures
that every target now refuses. They now check the same behavior with
static programs: fetch named only in text compiles, a global fetch call
reports SC3002, and CommonJS and ESM packages compile and match Node.
A typed-array read at an unproven index joins the element with NaN for an
invalid index. For an integer element that join was `phi double [sitofp
x, NaN]`, which hides the integer from LLVM, and once `| 0` turned the NaN
arm into 0, InstCombine moved the `sitofp` of the result into the join
(`phi double [sitofp x, 0.0]`) where nothing folds it back: a loop over
`const c = units[i] | 0` converted every element to a double and back,
and compared it as a double.
An integer element now joins as `phi i32 [x, 0]` plus a validity bit and
converts after the join (`select valid, sitofp/uitofp x, NaN`). A declared
local whose every store is a bitwise result, and that a loop compares, also
keeps an i32 view: its stores already carry the integer, and compares of
the view stay integer compares, which also leaves the integer join with
more than one user, so the conversion is not folded into it.
The slow path of ToUint32 (NaN, the infinities and magnitudes above 2^53)
reduced the value with `frem`, which lowers to an `fmod` call. The call is
pure, so LLVM sometimes hoisted it above the range test onto the fast path
(65,865 calls in one tsc-ts parse).
A finite double above 2^53 is its integer mantissa shifted left by
exponent - 1075 >= 1, so its low 32 bits are the shifted mantissa's (none
once the shift reaches 64), negated for a negative value. NaN and the
infinities have the maximum exponent and give 0. The slow path is now
integer arithmetic on the double's bits with no branches.
Conflicts, all resolved to this branch's side:
- emitter.ts (numeric-table get, map index probe), expr-bytes.ts, local-array-reads.ts, string-reads.ts: the base only respelled the size-to-double conversions in the old range checks (uitofp nneg, now sitofp); exactIndexLines replaces those checks entirely. The base's other hunks in these files (host inline truncation, the remaining sitofp conversions) merge as they are.
- int32-slots.ts: #837 and #843 both extracted dynamicFieldStores with identical bodies; keep this branch's signature, whose optional records/unions maps also serve #837's single-argument callers.
The `nneg` flag on `uitofp` is LLVM 19 syntax. Sanitized builds hand the textual module to the host clang, and Ubuntu 24.04's clang 18 rejects it ("expected type"), which failed every Linux sanitized Effect shard. These operands are size_t values below 2^63, so `sitofp` converts them identically, is accepted by every supported LLVM, and still lowers to one cvtsi2sd on x86-64 (LLVM 19+ canonicalizes it to `uitofp nneg` where it can prove the sign).
The hint is ToUint32 of the value, and ToUint32(NaN) is 0, so a read whose
index may be invalid keeps it (the invalid arm already contributed 0); it was
dropped unless the index was proven in bounds. Worker programs also dropped it
at the join with the shared-buffer path, which now converts its integer result
exactly. `hash ^ chars[i]` and similar operators then use the loaded integer
instead of re-deriving ToInt32 from the double.
A typed-array, inline ASCII charCodeAt, local array, constant-table or
number-map read whose index is not a proven integer decided validity with
two float compares against the converted length, then an fptoui/uitofp round
trip. On x86-64 both unsigned conversions expand to long sequences, and the
tsc-ts scanner pays them on every character it reads by position.
exactIndexLines replaces them with fptosi, freeze, sitofp, one float equality
and one unsigned integer compare against the length. A frozen out-of-range or
NaN conversion is an arbitrary integer that never converts back to the index
or is not below the length; below the length the round trip is exact, and -0
converts to 0 like ToPropertyKey(-0).
Array, string and byte lengths, counts, capacities and range-checked indices are size_t values below 2^63, but they were converted with a plain uitofp. On x86-64 without AVX-512 an unsigned 64-bit conversion expands to a five-instruction sequence with two constant-pool loads (unpcklps/subpd/unpckhpd/addsd), and it showed up in the hottest tsc-ts checker loops (compareTypeLists, getPropertyOfTypeEx) between a length load and the conversion back to an integer. `uitofp nneg` states what is already true, so the backend emits one cvtsi2sd. arm64 converts unsigned values in one instruction either way. Byte-number indices, which admit values up to 2^64, keep the unsigned conversion.
Baseline x86-64 has no rounding instruction, so every llvm.trunc became
a call to libm's trunc. The typed-array read helper paid that call on
every read: it decided presence with `i >= 0 && i < length &&
Math.trunc(i) === i` before the inline element load.
- Math.trunc (and the DataView offset truncation) now take an integer
round trip on x86-64 targets: values of magnitude below 2^52 go through
fptosi/sitofp with the sign restored by copysign (so -0.5 and -0 give
-0); larger values, NaN and the infinities are already integral and
keep their value. Other targets keep llvm.trunc, which is one
instruction there.
- The ToUint32 slow path is only reached for magnitudes above 2^53 (and
non-finite values), which are integers, so its truncation was a no-op
and is gone.
- For integer element kinds, the read helper performs the element read
that yields NaN for an invalid index (one inline index check) and
answers undefined exactly when that read is NaN: an integer element is
never NaN. Float element kinds keep the range and integrality test.
Worker programs sent every typed-array element read and write through
scr_bytes_get/scr_bytes_set, which take the shared-memory lock guard
even for buffers no other thread can see (4% of a Worker build of the
tsc-ts self-check). A view's `shared` storage is fixed before script
code can see it, so the emitted access now tests it: SharedArrayBuffer
views keep the locked runtime call, every other view takes the ordinary
inline path of non-worker programs.
The new corpus program reads and writes own and shared views through
the same functions (clamping, wrapping, float narrowing, out-of-range
and negative indices) on both threads.
- Add native worker execution with isolated module state, structured messages, and lifecycle controls.
- Support shared buffers and integer atomics with coordinated runtime ownership and cancellation.
- Extend compiler, runtime-pack, differential, and compatibility coverage for native concurrency.
- Enforce oxlint and oxfmt for owned JavaScript and TypeScript with independent CI checks.
- Preserve compiler import boundaries and protected fixtures while cleaning up lint findings.
- Standardize source formatting and validate import, fixture, and embedded-source protections.
- Specialize standard Buffer and DataView numeric reads and writes while preserving checked errors, byte order, and unaligned access.
- Carry integer values through binary-processing loops and remove proven field bounds and receiver ownership overhead.
- Strengthen numeric IR validation and cover binary pipelines, edge cases, and native compiler parity.
- Infer stable heap inputs and local aliases while preserving ownership of escaping values.
- Remove redundant ownership work from collection and byte operations.
- Cover mutation, exceptions, indirect calls, and native bootstrap behavior.
- Carry exact integer facts through branches, arithmetic, and computed indexes.
- Lower ascending, descending, and strided loops with guarded integer induction.
- Remove proven typed-array bounds checks while preserving number semantics and target-width safety.
- Reuse integer values across local updates, branches, and numeric operations.
- Preserve typed-array integer values through reads and writes.
- Cover number semantics, evaluation order, and native emission boundaries.
* feat: support static buffer constructors and prototype methods
- Preserve typed-array constructor identity and shared storage across JavaScript class APIs.
- Support chained indexed assignments and mutable prototype methods with native receiver dispatch.
- Compile Three.js geometry merging, instancing, and keyframe interpolation without a JavaScript engine.
* fix: preserve native collection and dispatch semantics
- Preserve typed-array identity in collections and JavaScript argument evaluation.
- Keep symbol-backed fields and unreachable methods on their existing static paths.
- Restore compiler bootstrapping and deferred class diagnostics.
* fix: preserve inferred returns in native compilation
- Update recorded return statements in place so native compilation preserves mixed JavaScript return values.
* test: update inferred array coverage expectations
- Recognize heterogeneous writes supported by shared JavaScript array storage.
- Support integer widths and clamped arrays with shared storage and exact runtime brands.
- Preserve typed-array values through untyped fields, iteration, and byte I/O.
- Compile native ArrayBuffer response bodies and checked Number predicates.
- Preserve nullable class fields and module callbacks in native storage with correct initialization behavior.
- Support typed-array construction, bulk copying, and in-place compaction through checked native values.
- Preserve receiver identity and evaluation order in nested field updates.
- Run the production LLVM backend natively and reproduce its output across compiler generations.
- Replace unchecked emitter contexts with explicit shared state and callback hosts.
- Preserve NaN in serialized IR and respect optional record storage during spreads.
- Add Float64Array construction, storage, and access across compiler backends and runtime.
- Cover precision, views, methods, and dynamic boundaries with differential fixtures.