diff --git a/packages/compiler/src/frontend/lowering/array-values.ts b/packages/compiler/src/frontend/lowering/array-values.ts index 3fe8a8858..8f2b28a3f 100644 --- a/packages/compiler/src/frontend/lowering/array-values.ts +++ b/packages/compiler/src/frontend/lowering/array-values.ts @@ -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 diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts index ce3e6eb07..9f26b0058 100644 --- a/packages/compiler/src/frontend/lowering/lower-exprs.ts +++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts @@ -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") diff --git a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts index a716cb2bb..2a537d6de 100644 --- a/packages/compiler/src/frontend/lowering/nan-coded-reads.ts +++ b/packages/compiler/src/frontend/lowering/nan-coded-reads.ts @@ -237,6 +237,7 @@ export class NanCodedReads { private readonly presentReads = new Map(); private readonly counters = new Map(); private readonly fixedLengths = new Map(); + private readonly stableReceivers = new Map(); private readonly bindings = new Map(); private readonly notSlots = new Set(); /** 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); diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.test.ts b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.test.ts index fafacd265..fd21dbb89 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.test.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.test.ts @@ -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]); +}); diff --git a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts index 3768949ab..d087a2d61 100644 --- a/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts +++ b/packages/compiler/src/frontend/lowering/runtime-optional-bounds.ts @@ -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 && diff --git a/packages/compiler/test/nan-coded-reads.test.ts b/packages/compiler/test/nan-coded-reads.test.ts index 8d18b62f4..e312880eb 100644 --- a/packages/compiler/test/nan-coded-reads.test.ts +++ b/packages/compiler/test/nan-coded-reads.test.ts @@ -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); }); diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4813-typed-array-read-equality-bindings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4813-typed-array-read-equality-bindings.ts.json new file mode 100644 index 000000000..5ee982d9b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/4813-typed-array-read-equality-bindings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/4813-typed-array-read-equality-bindings.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/typed-array-read-boxing.test.ts b/packages/compiler/test/typed-array-read-boxing.test.ts new file mode 100644 index 000000000..7d809f94e --- /dev/null +++ b/packages/compiler/test/typed-array-read-boxing.test.ts @@ -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); +}); diff --git a/tests/corpus/4813-typed-array-read-equality-bindings.ts b/tests/corpus/4813-typed-array-read-equality-bindings.ts new file mode 100644 index 000000000..781006f49 --- /dev/null +++ b/tests/corpus/4813-typed-array-read-equality-bindings.ts @@ -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(" "));