Repository navigation
Carry typed-array reads as plain numbers where undefined acts like NaN - #829
Merged
Merged
Conversation
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.
A typed-array element read answers undefined for an index outside [0, length), so the default lowering returns a heap-boxed `number | undefined` and widens every binding, parameter and return the read reaches. In the tsc-ts scanner that boxed every character read and turned every character predicate (isLineBreak, isIdentifierPart, ...) into a function taking a boxed union. Most consumers cannot tell undefined from NaN: relational, arithmetic and bitwise operators, truthiness, Math functions, String.fromCharCode, typed-array indexing, switch over number cases, and equality with a number that is never undefined. The new analysis (nan-coded-reads.ts) follows reads through local bindings, parameters of directly called functions and methods (no overrides, no value uses), and their returns. A slot is NaN-coded when every value it receives is a NaN-coded read or slot, or any other number (converted with ToNumber on entry), and either: - it never holds a genuine NaN (integer elements, integer constants, guarded integer arithmetic), so NaN stands for undefined exactly; any consumer that takes the value whole (an argument, a stored or returned value, a literal element, concatenation) gets the exact `number | undefined` rebuilt, and the optional-read analysis treats that occurrence as optional, so it lowers as before; or - every consumer is NaN-insensitive. Equality of two NaN-coded values that never hold a genuine NaN also answers true for two NaNs, as undefined === undefined does. Any other slot keeps the established union lowering; asserted reads (`t[i]!`) are unchanged. tsc-ts best-v1 (scriptc main + this commit vs main, Linux sandbox, Xeon 2.5 GHz, interleaved, medians of 5, diagnostics identical): | app | parse | total (single-threaded) | wall --checkers 8 | | --- | --- | --- | --- | | tRPC | 1.263 -> 0.866 s (-31.4%) | 3.035 -> 2.527 s (-16.7%) | -13.0% | | Excalidraw | 2.637 -> 1.865 s (-29.3%) | 14.99 -> 14.04 s (-6.3%) | -16.2% | | Playwright | 3.150 -> 2.202 s (-30.1%) | 12.93 -> 12.14 s (-6.1%) | -12.4% | | TypeORM | 2.562 -> 2.036 s (-20.5%) | 10.93 -> 10.46 s (-4.3%) | -8.6% | | self-check | 2.080 -> 1.529 s (-26.5%) | 7.06 -> 6.68 s (-5.4%) | -15.1% | The tsc-ts IR has 646 instead of 697 scr_union_new_f64 sites (the rest are cold), 22 instead of 168 typed-read helper calls, and 217 instead of 572 scr_union_get_f64 calls; the scanner's hot functions allocate no union boxes. Binary size -0.2%.
Every two-argument Math.min/Math.max was an out-of-line call to scr_math_min/scr_math_max (94 sites in tsc-ts, including compareCodePoints and IdentifierTable.intern). LLVM's minimum/maximum intrinsics have exactly their semantics: NaN propagates and -0 orders below +0. On x86-64 they lower to minsd/maxsd with inline NaN and signed-zero handling, on arm64 to fmin/fmax.
The NaN-coded read analysis memoized lookups whose answer can be null (symbols, eligible functions, call sites, plain-number returns) and read them back with `map.get(key)!`. Under Node that returns the cached null, but in the natively compiled compiler `!` is a checked extraction, which threw "null is not representable" while lowering programs (self-hosting-native-frontend, the native CLI bootstrap contracts). Read the cache through an undefined check instead.
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The Node-hosted CLI emits with an empty target triple (the host), while the native CLI passes its toolchain's triple. Inline truncation keyed on the triple text alone, so on x86-64 Linux the Node seed emitted llvm.trunc and the native compiler emitted the inline sequence, and the native bootstrap's LLVM comparison with the Node seed failed (self-hosting-native-driver, rebuild-emit). An empty triple now means the host architecture, as it already does for executable TLS; WASI keeps the intrinsic. The DataView emission test accepts either truncation form, since it runs on hosts of both kinds.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A typed-array read outside
[0, length)answersundefined, so scriptc lowered everyt[i]to a heap-boxednumber | undefinedand widened every binding, parameter and return it reached. In the tsc-ts scanner that boxed every character read and made every character predicate take a boxed union.nan-coded-reads.ts) follows reads through locals, parameters of directly called functions and methods, and their returns.Math.*,String.fromCharCode, typed-array indexing, numericswitch, equality with a number that is never undefined), the read is a plain double holding NaN for an invalid index.number | undefinedrebuilt, so they lower as before.undefined === undefinedis true.t[i]!) are unchanged.Math.truncand DataView offsets use an integer round trip instead of a libmtrunccall. The read helper decides presence for integer element kinds with one inline index check. A truncation that did nothing in the cold ToUint32 path is removed.Math.min/Math.maxviallvm.minimum/llvm.maximum, which match the JS semantics for NaN and −0.tsc-ts, Linux, medians of 5, diagnostics identical: parse tRPC −31%, Excalidraw −29%, Playwright −30%, TypeORM −21%, self-check −27%; total single-threaded −4% to −17%; wall at 8 checkers −9% to −16%. Runtime benchmark suite geomean +0.4% to +0.6% (all within noise), executable sizes unchanged, tsc-ts binary −0.2%.
New corpus program
typed-array-nan-coded-reads.tspins: invalid, NaN, fractional, negative and −0 indexes; reads flowing through bindings, parameters and returns; rebuilt undefined at consumers that observe it; equality of two missing reads; float NaN elements next to missing reads; genuine-NaN slots; subarray views reading past their own length. Plus compiler tests for the IR shape and for inline truncation.Second of a three-PR stack, on top of the TDZ fix.