Keep typed-array reads unboxed in number compares and in-bounds bindings

This commit is contained in:
Chris Tate
2026-10-12 23:05:43 -05:00
parent 89cadd38b0
commit 5a5bfc279d
9 changed files with 610 additions and 3 deletions
@@ -414,6 +414,25 @@ export function tryLowerNumericIndexRead(
};
}
/** Fuse our own typed-array read helper with strict equality against a
* plain number: `t[i] === b` is false when `t[i]` is undefined, and the
* NaN-for-invalid read is never equal to anything either, so the plain
* read answers exactly (`!==` is the negation). The other operand must
* lower to a double that never stands for undefined: a union keeps the
* box (undefined === undefined is true), and so does a NaN-coded value
* (nan-coded-reads.ts), which encodes undefined as NaN. The read keeps
* its arguments, so evaluation order is unchanged. */
export function tryLowerBytesReadNumberEquality(
lowerer: Lowerer,
operand: IrExpr,
other: IrExpr,
otherNode: ts.Expression,
): IrExpr | null {
if (other.type.kind !== "f64" || !isBytesReadHelperCall(lowerer, operand)) return null;
if (lowerer.nanCodedReads?.mayStandForUndefined(otherNode) === true) return null;
return tryLowerNumericIndexRead(lowerer, operand, operand.loc);
}
/** Fuse our own scalar-array read helper with an immediate ToBoolean, so a
* condition such as `if (flags[i])` never boxes `T | undefined`. Numbers
* reuse getNumber: a missing slot reads as NaN, which is falsy exactly like
@@ -110,6 +110,7 @@ import {
lowerSafeIndexRead,
tryLowerIndexTruthiness,
tryLowerNumericIndexRead,
tryLowerBytesReadNumberEquality,
lowerSafeBytesRead,
} from "./array-values.js";
import { strCharsCall } from "./containers/array-construction.js";
@@ -9497,6 +9498,14 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
const storedRight = runtimeOptionalStorageOperand(lowerer, expr.right);
if (storedLeft !== null && right.type.kind !== "union") left = storedLeft;
if (storedRight !== null && left.type.kind !== "union") right = storedRight;
// A typed-array read compared with a plain number needs no
// `number | undefined` box.
const fusedLeft = tryLowerBytesReadNumberEquality(lowerer, left, right, expr.right);
if (fusedLeft) left = fusedLeft;
else {
const fusedRight = tryLowerBytesReadNumberEquality(lowerer, right, left, expr.left);
if (fusedRight) right = fusedRight;
}
}
if (strictEquality && (left.type.kind === "caught" || right.type.kind === "caught")) {
if (left.type.kind === "caught")
@@ -237,6 +237,7 @@ export class NanCodedReads {
private readonly presentReads = new Map<ts.ElementAccessExpression, IrBytesElem>();
private readonly counters = new Map<ts.Symbol, boolean>();
private readonly fixedLengths = new Map<ts.Symbol, ts.NewExpression | null>();
private readonly stableReceivers = new Map<ts.Symbol, boolean>();
private readonly bindings = new Map<ts.Symbol, BindingSlot>();
private readonly notSlots = new Set<ts.Symbol>();
/** Every slot's value occurrences. */
@@ -274,6 +275,12 @@ export class NanCodedReads {
return left.nce && right.nce && !left.nan && !right.nan;
}
/** The value lowers to a double that may stand for undefined (a
* NaN-coded read or slot). */
mayStandForUndefined(node: ts.Expression): boolean {
return this.loweredInfo(node).nce;
}
/** Bindings and returns that are NaN-coded (consistency checks). */
nanCodedBinding(symbol: ts.Symbol): boolean {
const slot = this.bindings.get(symbol);
@@ -437,17 +444,97 @@ export class NanCodedReads {
const index = peelTransparent(read.argumentExpression);
if (!ts.isIdentifier(array) || !ts.isIdentifier(index)) return false;
const arraySymbol = this.symbolOf(array);
if (!arraySymbol || !this.fixedLength(arraySymbol)) return false;
if (!arraySymbol || !this.stableReceiver(arraySymbol)) return false;
const symbol = this.symbolOf(index);
if (!symbol || !this.nonNegativeCounter(symbol)) return false;
const decl = this.host.declarationOf(symbol);
if (!decl || this.containingFunction(decl) !== this.containingFunction(read)) return false;
return indexReadInBounds(read, {
sameLength: (array, other) => this.sameFixedLength(array, other),
lengthAlias: (bound, array) => this.lengthAlias(bound, array),
conditions: true,
});
}
/** A typed-array binding that always names the same typed array: a
* fixed-length allocation, or a parameter or `let`/`const` binding that
* no code ever writes. A typed array's length never changes (static
* code cannot resize or detach a buffer), so a guard on its length
* stays true for that binding. */
private stableReceiver(symbol: ts.Symbol): boolean {
const known = this.stableReceivers.get(symbol);
if (known !== undefined) return known;
this.stableReceivers.set(symbol, false);
if (this.fixedLength(symbol)) {
this.stableReceivers.set(symbol, true);
return true;
}
const decl = this.host.declarationOf(symbol);
if (!decl || isJsSourceFile(decl.getSourceFile()) || decl.getSourceFile().isDeclarationFile)
return false;
let name: ts.Identifier;
if (ts.isParameter(decl)) {
if (!ts.isIdentifier(decl.name) || decl.dotDotDotToken) return false;
// Sloppy-mode `arguments` aliases parameters.
const fn = decl.parent;
let usesArguments = false;
if (fn && ts.isFunctionLike(fn))
ts.walkPreorder(fn, (node) => {
if (ts.isIdentifier(node) && node.text === "arguments") usesArguments = true;
});
if (usesArguments) return false;
name = decl.name;
} else if (
ts.isVariableDeclaration(decl) &&
ts.isIdentifier(decl.name) &&
ts.isVariableDeclarationList(decl.parent) &&
decl.parent.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const) &&
!ts.isForOfStatement(decl.parent.parent) &&
!ts.isForInStatement(decl.parent.parent)
)
name = decl.name;
else return false;
if (this.host.bytesElemOf(name) === null) return false;
for (const id of this.names.get(name.text) ?? []) {
if (id === name || this.symbolOf(id) !== symbol) continue;
if (this.writeOf(id) !== "read") return false;
}
this.stableReceivers.set(symbol, true);
return true;
}
/** `bound` is a `const` binding initialized with `array.length`, where
* `array` is the same stable typed-array binding: it always holds the
* length. */
private lengthAlias(bound: ts.Expression, array: ts.Expression): boolean {
const b = peelTransparent(bound);
const a = peelTransparent(array);
if (!ts.isIdentifier(b) || !ts.isIdentifier(a)) return false;
const symbol = this.symbolOf(b);
const decl = symbol ? this.host.declarationOf(symbol) : undefined;
if (
!decl ||
!ts.isVariableDeclaration(decl) ||
!decl.initializer ||
!ts.isVariableDeclarationList(decl.parent) ||
!(decl.parent.flags & ts.NodeFlags.Const)
)
return false;
const init = peelTransparent(decl.initializer);
if (
!ts.isPropertyAccessExpression(init) ||
init.questionDotToken ||
init.name.text !== "length"
)
return false;
const owner = peelTransparent(init.expression);
if (!ts.isIdentifier(owner)) return false;
const ownerSymbol = this.symbolOf(owner);
return (
ownerSymbol !== null && ownerSymbol === this.symbolOf(a) && this.stableReceiver(ownerSymbol)
);
}
/** A `let`/`const` binding that starts at a non-negative integer literal
* and only ever grows by non-negative integer literals (`i++`, `++i`,
* `i += 2`) or is reset to one (`i = 0`): it always holds a non-negative
@@ -1329,6 +1416,21 @@ export class NanCodedReads {
// Compound assignment reads of a binding: arithmetic.
return NUMERIC_COMPOUND.has(k) || k === ts.SyntaxKind.PlusEqualsToken;
}
if (k === ts.SyntaxKind.EqualsToken && this.typedArrayStoreTarget(p.left)) {
// A typed-array element store converts the value with ToNumber,
// so undefined and NaN store the same element. The assignment's
// own value is the unconverted one.
const outer = transparentParent(p);
const consumer = outer.parent;
if (
!consumer ||
ts.isExpressionStatement(consumer) ||
(ts.isForStatement(consumer) &&
(consumer.incrementor === outer || consumer.initializer === outer))
)
return true;
return this.useSafe(p);
}
if (k === ts.SyntaxKind.EqualsToken) {
const slot = this.sinkOf(e);
if (!slot || !this.eligible.has(slot)) return false;
@@ -1385,6 +1487,16 @@ export class NanCodedReads {
return false;
}
/** `t[k]` on a number typed array, as an assignment target. */
private typedArrayStoreTarget(target: ts.Expression): boolean {
const e = peelTransparent(target);
return (
ts.isElementAccessExpression(e) &&
!e.questionDotToken &&
this.host.bytesElemOf(e.expression) !== null
);
}
/** The value of a logical expression is only tested for truthiness. */
private inConditionContext(node: ts.Expression): boolean {
const e = transparentParent(node);
@@ -112,3 +112,34 @@ test("an if condition reads like a statement, and a same-length fact extends the
});
expect(plain).toEqual([false, false, false, false, false]);
});
test("a length-alias fact lets a cached length bound the read", () => {
const file = parseSourceFile(
"length-alias.ts",
`function f(src: Uint8Array, dst: Uint8Array) {
const n = src.length;
for (let i = 0; i < n; i++) { const c = src[i]; }
for (let i = 0; i < n; i++) { const c = dst[i]; }
}`,
"ts",
);
const results: boolean[] = [];
const facts = {
lengthAlias: (bound: ts.Expression, array: ts.Expression) =>
ts.isIdentifier(bound) &&
bound.text === "n" &&
ts.isIdentifier(array) &&
array.text === "src",
};
ts.walkPreorder(file, (node) => {
if (ts.isElementAccessExpression(node)) results.push(indexReadInBounds(node, facts));
return undefined;
});
expect(results).toEqual([true, false]);
const plain: boolean[] = [];
ts.walkPreorder(file, (node) => {
if (ts.isElementAccessExpression(node)) plain.push(indexReadInBounds(node));
return undefined;
});
expect(plain).toEqual([false, false]);
});
@@ -30,6 +30,10 @@ export interface BoundsFacts {
* fixed-length typed arrays allocated with the same length), so a guard
* `i < other.length` bounds `array[i]` as well. */
sameLength?(array: ts.Expression, other: ts.Expression): boolean;
/** `bound` always holds `array.length` (for example, a `const` copy of
* the length of a typed array whose binding is never reassigned), so a
* guard `i < bound` bounds `array[i]`. */
lengthAlias?(bound: ts.Expression, array: ts.Expression): boolean;
/** A read inside an `if` condition is bounded like one in a statement.
* Opt-in: a caller that does not also prove the index a non-negative
* integer keeps its established decision for such reads. */
@@ -202,6 +206,7 @@ function sameReference(a: ts.Expression, b: ts.Expression): boolean {
function isLengthOf(node: ts.Expression, array: ts.Expression, facts?: BoundsFacts): boolean {
const e = peel(node);
if (ts.isIdentifier(e)) return facts?.lengthAlias?.(e, array) === true;
return (
ts.isPropertyAccessExpression(e) &&
!e.questionDotToken &&
@@ -154,6 +154,7 @@ test("proven-present reads and guarded occurrences keep numeric parameters plain
).toEqual([f64, f64]);
// `shorter` is not as long as `xs`: its read may be undefined.
expect(fn(mod, "probe").params[1]!.type.kind).toBe("union");
// Code holding `zs` could detach its buffer: no proof.
expect(fn(mod, "escaped").params[0]!.type.kind).toBe("union");
// A typed array's length never changes (static code cannot resize or
// detach a buffer), so `zs` escaping to `grab` keeps the proof.
expect(fn(mod, "escaped").params[0]!.type).toEqual(f64);
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/4813-typed-array-read-equality-bindings.ts"
],
"diags": []
}
@@ -0,0 +1,101 @@
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, test } from "vitest";
import { compile, deserializeModule, validateModule } from "../src/index.js";
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
async function emit(source: string): Promise<(name: string) => string> {
const dir = await mkdtemp(join(tmpdir(), "scriptc-typed-read-boxing-"));
try {
const entry = join(dir, "main.ts");
const outPath = join(dir, "main.ir.json");
await writeFile(entry, source);
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
if (!result.ok)
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
const mod = deserializeModule(await readFile(outPath, "utf8"));
expect(validateModule(mod)).toEqual([]);
const ll = emitLlvmModule(mod);
return (name) =>
ll.match(
new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\) #0 \\{([\\s\\S]*?)\\n\\}`),
)![1]!;
} finally {
await rm(dir, { recursive: true, force: true });
}
}
const UNION = /scr_union_(new|get|release)|_bytes_idxOr_/;
test("typed-array reads compared with numbers or bound in bounds are never boxed", async () => {
const body = await emit(`
function countByte(a: Uint8Array, b: number): number {
let c = 0;
for (let i = 0; i < a.length; i++) if (a[i] === b) c++;
return c;
}
function countOther(a: Float32Array, b: number): number {
let c = 0;
for (let i = 0; i <= a.length; i++) if (b !== a[i]) c++;
return c;
}
function lower(src: Uint8Array, dst: Uint8Array): void {
const n = src.length;
for (let i = 0; i < n; i++) {
const c = src[i];
dst[i] = c >= 65 && c <= 90 ? c | 32 : c;
}
}
function kinds(a: Float64Array): string {
let s = "";
for (let i = 0; i < a.length; i++) {
const v = a[i];
s += typeof v;
}
return s;
}
function copyPast(src: Int16Array, dst: Int16Array): void {
for (let i = 0; i < dst.length; i++) {
const c = src[i];
dst[i] = c;
}
}
const a = new Uint8Array([10, 97, 10]);
const d = new Uint8Array(3);
lower(a, d);
const w = new Int16Array(4);
copyPast(new Int16Array([1, 2]), w);
console.log(countByte(a, 10), countOther(new Float32Array([1, NaN]), 1), d[0], kinds(new Float64Array(2)), w[3]);
`);
for (const name of ["countByte", "countOther", "lower", "kinds", "copyPast"])
expect(body(name), name).not.toMatch(UNION);
// The comparison is the plain element compare.
expect(body("countByte")).toMatch(/fcmp oeq double|icmp eq i32/);
});
test("reads that may be undefined on both sides keep undefined === undefined", async () => {
const body = await emit(`
function bothReads(a: Uint8Array, i: number, b: Uint8Array, j: number): boolean {
return a[i] === b[j];
}
function optional(a: Uint8Array, i: number, b: number | undefined): boolean {
return a[i] === b;
}
function outside(a: Uint8Array): string {
let s = "";
for (let i = 0; i <= a.length; i++) {
const v = a[i];
s += typeof v;
}
return s;
}
const a = new Uint8Array(2);
console.log(bothReads(a, 5, a, 6), optional(a, 9, undefined), outside(a));
`);
// Two NaN-coded reads compare equal when both stand for undefined.
expect(body("bothReads")).toMatch(/fcmp uno double/);
// A possibly-undefined operand and an observed out-of-bounds binding
// keep `number | undefined`.
for (const name of ["optional", "outside"]) expect(body(name), name).toMatch(UNION);
});
@@ -0,0 +1,323 @@
// Typed-array element reads compared with numbers and bound to locals:
// in-bounds and past-the-end indices, fractional and negative indices,
// every number typed-array kind, -0 and NaN elements, comparisons with
// values that may be undefined, and bindings observed with typeof.
function countEq(a: Uint8Array, b: number): number {
let c = 0;
for (let i = 0; i < a.length; i++) if (a[i] === b) c++;
return c;
}
function countNe(a: Int16Array, b: number): number {
let c = 0;
for (let i = 0; i <= a.length; i++) if (a[i] !== b) c++;
return c;
}
function eqAt(a: Float64Array, i: number, b: number): string {
return `${a[i] === b}/${a[i] !== b}/${b === a[i]}/${b !== a[i]}`;
}
function eqOptional(a: Uint8Array, i: number, b: number | undefined): string {
return `${a[i] === b}/${a[i] !== b}`;
}
function eqReads(a: Int32Array, i: number, b: Float32Array, j: number): string {
return `${a[i] === b[j]}/${a[i] !== b[j]}`;
}
function lower(src: Uint8Array, dst: Uint8Array): void {
const n = src.length;
for (let i = 0; i < n; i++) {
const c = src[i];
dst[i] = c >= 65 && c <= 90 ? c | 32 : c;
}
}
function lowerPast(src: Uint8Array, dst: Uint8Array): number {
// The last pass reads past the end of `src`: undefined stores as 0.
let stores = 0;
for (let i = 0; i < dst.length; i++) {
const c = src[i];
dst[i] = c >= 65 && c <= 90 ? c | 32 : c;
stores++;
}
return stores;
}
function bindingKinds(a: Float32Array, extra: number): string {
const out: string[] = [];
for (let i = 0; i < a.length; i++) {
const v = a[i];
out.push(`${typeof v}:${v}:${Object.is(v, -0)}`);
}
for (let i = a.length; i < a.length + extra; i++) {
const v = a[i];
out.push(`${typeof v}:${v}:${v === undefined}`);
}
return out.join(" ");
}
function show(value: unknown): string {
return `${typeof value}(${String(value)})`;
}
function bindingEscapes(a: Int8Array, i: number): string {
const v = a[i];
return show(v);
}
function inBoundsEscapes(a: Uint32Array): string {
const out: string[] = [];
for (let i = 0; i < a.length; i++) {
const v = a[i];
out.push(show(v));
}
return out.join(",");
}
function scanI8(t: Int8Array, needle: number): string {
let hits = 0;
let misses = 0;
for (let i = -1; i <= t.length; i += 0.5) {
if (t[i] === needle) hits++;
if (needle !== t[i]) misses++;
}
let typeofs = "";
for (let i = 0; i < t.length + 1; i++) {
const v = t[i];
typeofs += typeof v === "number" ? "n" : "u";
}
for (let i = 0; i < t.length; i++) {
const v = t[i];
if (v === needle) typeofs += "=";
if (Object.is(v, -0)) typeofs += "-0";
if (v !== v) typeofs += "NaN";
}
return `${hits}/${misses}/${typeofs}`;
}
function scanU8(t: Uint8Array, needle: number): string {
let hits = 0;
let misses = 0;
for (let i = -1; i <= t.length; i += 0.5) {
if (t[i] === needle) hits++;
if (needle !== t[i]) misses++;
}
let typeofs = "";
for (let i = 0; i < t.length + 1; i++) {
const v = t[i];
typeofs += typeof v === "number" ? "n" : "u";
}
for (let i = 0; i < t.length; i++) {
const v = t[i];
if (v === needle) typeofs += "=";
if (Object.is(v, -0)) typeofs += "-0";
if (v !== v) typeofs += "NaN";
}
return `${hits}/${misses}/${typeofs}`;
}
function scanU8C(t: Uint8ClampedArray, needle: number): string {
let hits = 0;
let misses = 0;
for (let i = -1; i <= t.length; i += 0.5) {
if (t[i] === needle) hits++;
if (needle !== t[i]) misses++;
}
let typeofs = "";
for (let i = 0; i < t.length + 1; i++) {
const v = t[i];
typeofs += typeof v === "number" ? "n" : "u";
}
for (let i = 0; i < t.length; i++) {
const v = t[i];
if (v === needle) typeofs += "=";
if (Object.is(v, -0)) typeofs += "-0";
if (v !== v) typeofs += "NaN";
}
return `${hits}/${misses}/${typeofs}`;
}
function scanI16(t: Int16Array, needle: number): string {
let hits = 0;
let misses = 0;
for (let i = -1; i <= t.length; i += 0.5) {
if (t[i] === needle) hits++;
if (needle !== t[i]) misses++;
}
let typeofs = "";
for (let i = 0; i < t.length + 1; i++) {
const v = t[i];
typeofs += typeof v === "number" ? "n" : "u";
}
for (let i = 0; i < t.length; i++) {
const v = t[i];
if (v === needle) typeofs += "=";
if (Object.is(v, -0)) typeofs += "-0";
if (v !== v) typeofs += "NaN";
}
return `${hits}/${misses}/${typeofs}`;
}
function scanU16(t: Uint16Array, needle: number): string {
let hits = 0;
let misses = 0;
for (let i = -1; i <= t.length; i += 0.5) {
if (t[i] === needle) hits++;
if (needle !== t[i]) misses++;
}
let typeofs = "";
for (let i = 0; i < t.length + 1; i++) {
const v = t[i];
typeofs += typeof v === "number" ? "n" : "u";
}
for (let i = 0; i < t.length; i++) {
const v = t[i];
if (v === needle) typeofs += "=";
if (Object.is(v, -0)) typeofs += "-0";
if (v !== v) typeofs += "NaN";
}
return `${hits}/${misses}/${typeofs}`;
}
function scanI32(t: Int32Array, needle: number): string {
let hits = 0;
let misses = 0;
for (let i = -1; i <= t.length; i += 0.5) {
if (t[i] === needle) hits++;
if (needle !== t[i]) misses++;
}
let typeofs = "";
for (let i = 0; i < t.length + 1; i++) {
const v = t[i];
typeofs += typeof v === "number" ? "n" : "u";
}
for (let i = 0; i < t.length; i++) {
const v = t[i];
if (v === needle) typeofs += "=";
if (Object.is(v, -0)) typeofs += "-0";
if (v !== v) typeofs += "NaN";
}
return `${hits}/${misses}/${typeofs}`;
}
function scanU32(t: Uint32Array, needle: number): string {
let hits = 0;
let misses = 0;
for (let i = -1; i <= t.length; i += 0.5) {
if (t[i] === needle) hits++;
if (needle !== t[i]) misses++;
}
let typeofs = "";
for (let i = 0; i < t.length + 1; i++) {
const v = t[i];
typeofs += typeof v === "number" ? "n" : "u";
}
for (let i = 0; i < t.length; i++) {
const v = t[i];
if (v === needle) typeofs += "=";
if (Object.is(v, -0)) typeofs += "-0";
if (v !== v) typeofs += "NaN";
}
return `${hits}/${misses}/${typeofs}`;
}
function scanF32(t: Float32Array, needle: number): string {
let hits = 0;
let misses = 0;
for (let i = -1; i <= t.length; i += 0.5) {
if (t[i] === needle) hits++;
if (needle !== t[i]) misses++;
}
let typeofs = "";
for (let i = 0; i < t.length + 1; i++) {
const v = t[i];
typeofs += typeof v === "number" ? "n" : "u";
}
for (let i = 0; i < t.length; i++) {
const v = t[i];
if (v === needle) typeofs += "=";
if (Object.is(v, -0)) typeofs += "-0";
if (v !== v) typeofs += "NaN";
}
return `${hits}/${misses}/${typeofs}`;
}
function scanF64(t: Float64Array, needle: number): string {
let hits = 0;
let misses = 0;
for (let i = -1; i <= t.length; i += 0.5) {
if (t[i] === needle) hits++;
if (needle !== t[i]) misses++;
}
let typeofs = "";
for (let i = 0; i < t.length + 1; i++) {
const v = t[i];
typeofs += typeof v === "number" ? "n" : "u";
}
for (let i = 0; i < t.length; i++) {
const v = t[i];
if (v === needle) typeofs += "=";
if (Object.is(v, -0)) typeofs += "-0";
if (v !== v) typeofs += "NaN";
}
return `${hits}/${misses}/${typeofs}`;
}
const bytes = new Uint8Array([10, 97, 10, 0, 255, 10]);
console.log("countEq", countEq(bytes, 10), countEq(bytes, 0), countEq(bytes, 255), countEq(bytes, NaN));
console.log("countEq-0", countEq(bytes, -0), countEq(bytes, 10.5), countEq(bytes, 266));
console.log("countNe", countNe(new Int16Array([-1, 2, -1]), -1), countNe(new Int16Array(2), 0));
const floats = new Float64Array([0, -0, NaN, 1.5, Infinity]);
for (let i = -1; i < 7; i++) console.log("eqAt", i, eqAt(floats, i, 0), eqAt(floats, i, NaN));
console.log("eqAt-frac", eqAt(floats, 1.5, 0), eqAt(floats, 3, 1.5), eqAt(floats, 4, Infinity));
console.log("eqAt-neg0", eqAt(floats, 0, -0), eqAt(floats, 1, -0), eqAt(floats, 1, 0));
const optional: (number | undefined)[] = [undefined, 10, 97];
for (const b of optional) console.log("eqOptional", show(b), eqOptional(bytes, 1, b), eqOptional(bytes, 99, b));
const ints = new Int32Array([7, -7]);
const singles = new Float32Array([7, -7.5]);
for (const [i, j] of [
[0, 0],
[1, 1],
[5, 0],
[5, 9],
[0.5, 0.5],
]) {
console.log("eqReads", i, j, eqReads(ints, i!, singles, j!));
}
const src = new Uint8Array([72, 101, 76, 76, 79, 32, 90, 64, 91, 65]);
const dst = new Uint8Array(src.length);
lower(src, dst);
console.log("lower", Array.from(dst).join(","));
const wide = new Uint8Array(src.length + 3).fill(1);
console.log("lowerPast", lowerPast(src, wide), Array.from(wide).join(","));
console.log("bindingKinds", bindingKinds(new Float32Array([1.25, -0, NaN, 0]), 2));
for (const i of [0, 1, 2, -1, 0.5, 3]) console.log("bindingEscapes", i, bindingEscapes(new Int8Array([-128, 127, -0]), i));
console.log("inBoundsEscapes", inBoundsEscapes(new Uint32Array([0, 4294967295, 2147483648])));
const needles = [0, -0, 1, 255, NaN, -1, 4294967295];
const i8 = new Int8Array([-1, 0, 1, -128]);
console.log("Int8Array", needles.map((n) => scanI8(i8, n)).join(" "));
const u8 = new Uint8Array([255, 0, 1, 128]);
console.log("Uint8Array", needles.map((n) => scanU8(u8, n)).join(" "));
const u8c = new Uint8ClampedArray([300, -5, 1, 128]);
console.log("Uint8ClampedArray", needles.map((n) => scanU8C(u8c, n)).join(" "));
const i16 = new Int16Array([-1, 0, 1, -32768]);
console.log("Int16Array", needles.map((n) => scanI16(i16, n)).join(" "));
const u16 = new Uint16Array([65535, 0, 1, 128]);
console.log("Uint16Array", needles.map((n) => scanU16(u16, n)).join(" "));
const i32 = new Int32Array([-1, 0, 1, -2147483648]);
console.log("Int32Array", needles.map((n) => scanI32(i32, n)).join(" "));
const u32 = new Uint32Array([4294967295, 0, 1, 128]);
console.log("Uint32Array", needles.map((n) => scanU32(u32, n)).join(" "));
const f32 = new Float32Array([-1, -0, 1, NaN]);
console.log("Float32Array", needles.map((n) => scanF32(f32, n)).join(" "));
const f64 = new Float64Array([-1, -0, 1, NaN]);
console.log("Float64Array", needles.map((n) => scanF64(f64, n)).join(" "));