mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-25 06:24:27 +08:00
Keep typed-array reads unboxed in number compares and in-bounds bindings
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
+6
@@ -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(" "));
|
||||
Reference in New Issue
Block a user