mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Keep integers visible to LLVM and take the ASCII fast paths of text conversion (#898)
This commit is contained in:
@@ -269,13 +269,29 @@ function emitBytesGetLocal(
|
||||
B.br(done);
|
||||
B.startBlock(done);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi double [ ${loaded.name}, %${ok} ], [ ${f64Lit(NaN)}, %${invalid} ]`);
|
||||
if (loaded.uint32 === undefined) {
|
||||
B.line(`${out} = phi double [ ${loaded.name}, %${ok} ], [ ${f64Lit(NaN)}, %${invalid} ]`);
|
||||
return { name: out, type: F64 };
|
||||
}
|
||||
// The hint is ToUint32 of the value, and ToUint32(NaN) is 0, so it holds
|
||||
// for an invalid index too. Integer ranges (which would claim more) are
|
||||
// never derived for NaN-coded reads.
|
||||
if (loaded.uint32 === undefined) return { name: out, type: F64 };
|
||||
const u32 = B.tmp();
|
||||
//
|
||||
// An integer element joins as an integer and a validity bit, and the
|
||||
// number converts after the join. A join of the numbers, `phi [sitofp x,
|
||||
// NaN]`, hides the integer from LLVM: compares of the number stay floating,
|
||||
// and once `| 0` makes the NaN arm 0, InstCombine moves the conversion into
|
||||
// that join (`phi [sitofp x, 0.0]`) and never folds it back. Converting
|
||||
// the joined integer gives it two users (this conversion and the ToUint32
|
||||
// consumers), so a compare of `x | 0` stays an integer compare.
|
||||
const u32 = B.tmp(),
|
||||
valid = B.tmp(),
|
||||
number = B.tmp();
|
||||
B.line(`${u32} = phi i32 [ ${loaded.uint32}, %${ok} ], [ 0, %${invalid} ]`);
|
||||
B.line(`${valid} = phi i1 [ true, %${ok} ], [ false, %${invalid} ]`);
|
||||
const signed = elem === "i8" || elem === "i16" || elem === "i32";
|
||||
B.line(`${number} = ${signed ? "sitofp" : "uitofp"} i32 ${u32} to double`);
|
||||
B.line(`${out} = select i1 ${valid}, double ${number}, double ${f64Lit(NaN)}`);
|
||||
return { name: out, type: F64, uint32: u32 };
|
||||
}
|
||||
|
||||
@@ -361,47 +377,46 @@ export function emitToUint32(
|
||||
B.br(done);
|
||||
|
||||
B.startBlock(slowLabel);
|
||||
const ordered = B.tmp();
|
||||
const belowInf = B.tmp();
|
||||
const aboveNegInf = B.tmp();
|
||||
const finiteRange = B.tmp();
|
||||
const finite = B.tmp();
|
||||
B.line(`${ordered} = fcmp ord double ${value}, ${value}`);
|
||||
B.line(`${belowInf} = fcmp olt double ${value}, ${F64_INF}`);
|
||||
B.line(`${aboveNegInf} = fcmp ogt double ${value}, ${f64Lit(-Infinity)}`);
|
||||
B.line(`${finiteRange} = and i1 ${belowInf}, ${aboveNegInf}`);
|
||||
B.line(`${finite} = and i1 ${ordered}, ${finiteRange}`);
|
||||
const finiteLabel = B.newLabel("uint32.coerce.finite");
|
||||
const nonfiniteLabel = B.newLabel("uint32.coerce.nonfinite");
|
||||
const slowDone = B.newLabel("uint32.coerce.slow.done");
|
||||
B.condBr(finite, finiteLabel, nonfiniteLabel);
|
||||
|
||||
B.startBlock(finiteLabel);
|
||||
// Only magnitudes above 2^53 reach this block, and every such double is
|
||||
// already an integer: no truncation is needed before the reduction.
|
||||
const residue = B.tmp();
|
||||
// Only NaN, the infinities and magnitudes above 2^53 reach this block.
|
||||
// Such a finite double is the integer mantissa * 2^shift with shift >= 1,
|
||||
// so its low 32 bits are the shifted mantissa's (none once shift >= 64),
|
||||
// negated for a negative value. NaN and the infinities have the maximum
|
||||
// exponent, a shift of 972, and convert to 0 like ToUint32. Integer
|
||||
// arithmetic only: the former `frem` became an `fmod` call that LLVM
|
||||
// could hoist above the range test, onto the fast path.
|
||||
const bits = B.tmp();
|
||||
const exponentBits = B.tmp();
|
||||
const exponent = B.tmp();
|
||||
const fraction = B.tmp();
|
||||
const mantissa = B.tmp();
|
||||
const shift = B.tmp();
|
||||
const inWord = B.tmp();
|
||||
const safeShift = B.tmp();
|
||||
const shifted = B.tmp();
|
||||
const magnitude = B.tmp();
|
||||
const low = B.tmp();
|
||||
const negative = B.tmp();
|
||||
const wrapped = B.tmp();
|
||||
const normalized = B.tmp();
|
||||
const finiteU32 = B.tmp();
|
||||
B.line(`${residue} = frem double ${value}, ${f64Lit(4294967296)}`);
|
||||
B.line(`${negative} = fcmp olt double ${residue}, ${f64Lit(0)}`);
|
||||
B.line(`${wrapped} = fadd double ${residue}, ${f64Lit(4294967296)}`);
|
||||
B.line(`${normalized} = select i1 ${negative}, double ${wrapped}, double ${residue}`);
|
||||
B.line(`${finiteU32} = fptoui double ${normalized} to i32`);
|
||||
B.br(slowDone);
|
||||
|
||||
B.startBlock(nonfiniteLabel);
|
||||
B.br(slowDone);
|
||||
|
||||
B.startBlock(slowDone);
|
||||
const negated = B.tmp();
|
||||
const slowU32 = B.tmp();
|
||||
B.line(`${slowU32} = phi i32 [ ${finiteU32}, %${finiteLabel} ], [ 0, %${nonfiniteLabel} ]`);
|
||||
B.line(`${bits} = bitcast double ${value} to i64`);
|
||||
B.line(`${exponentBits} = lshr i64 ${bits}, 52`);
|
||||
B.line(`${exponent} = and i64 ${exponentBits}, 2047`);
|
||||
B.line(`${fraction} = and i64 ${bits}, 4503599627370495`);
|
||||
B.line(`${mantissa} = or i64 ${fraction}, 4503599627370496`);
|
||||
B.line(`${shift} = sub i64 ${exponent}, 1075`);
|
||||
B.line(`${inWord} = icmp ult i64 ${shift}, 64`);
|
||||
B.line(`${safeShift} = select i1 ${inWord}, i64 ${shift}, i64 0`);
|
||||
B.line(`${shifted} = shl i64 ${mantissa}, ${safeShift}`);
|
||||
B.line(`${magnitude} = select i1 ${inWord}, i64 ${shifted}, i64 0`);
|
||||
B.line(`${low} = trunc i64 ${magnitude} to i32`);
|
||||
B.line(`${negative} = icmp slt i64 ${bits}, 0`);
|
||||
B.line(`${negated} = sub i32 0, ${low}`);
|
||||
B.line(`${slowU32} = select i1 ${negative}, i32 ${negated}, i32 ${low}`);
|
||||
B.br(done);
|
||||
|
||||
B.startBlock(done);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = phi i32 [ ${fastU32}, %${fastLabel} ], [ ${slowU32}, %${slowDone} ]`);
|
||||
B.line(`${out} = phi i32 [ ${fastU32}, %${fastLabel} ], [ ${slowU32}, %${slowLabel} ]`);
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,13 @@ import { BYTES_ELEM_NUM, closureIdentityEqual, f64Lit } from "./common.js";
|
||||
import { emitStringInputs } from "./string-lifetimes.js";
|
||||
import { emitStringParts, numberPart, stringParts } from "./string-construction.js";
|
||||
import { emitBorrowedInputs } from "./borrowed-inputs.js";
|
||||
import { exactInteger, widenInteger, integerNumber } from "./integer-values.js";
|
||||
import {
|
||||
exactInteger,
|
||||
int32Number,
|
||||
integerNumber,
|
||||
narrowInteger,
|
||||
widenInteger,
|
||||
} from "./integer-values.js";
|
||||
import {
|
||||
bitwiseIdentityOperand,
|
||||
integerArithmeticRange,
|
||||
@@ -233,9 +239,20 @@ export function emitOperatorExpr(
|
||||
const li = exactInteger(host, l, e.left),
|
||||
ri = exactInteger(host, r, e.right);
|
||||
if (li && ri) {
|
||||
const range = e.op === "%" ? null : integerArithmeticRange(e.op, li.range, ri.range);
|
||||
if (range && withinInt32(range) && withinInt32(li.range) && withinInt32(ri.range)) {
|
||||
// Int32 operands and result: 32-bit arithmetic that cannot
|
||||
// overflow. The nsw flag survives where a truncated 64-bit
|
||||
// result would lose it, so LLVM can widen loop counters such
|
||||
// as `i = (i + 1) | 0` into 64-bit address arithmetic.
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = ${e.op === "+" ? "add" : e.op === "-" ? "sub" : "mul"} nsw i32 ${narrowInteger(host, li)}, ${narrowInteger(host, ri)}`,
|
||||
);
|
||||
return int32Number(host, result, range, e.type);
|
||||
}
|
||||
const left = widenInteger(host, li),
|
||||
right = widenInteger(host, ri);
|
||||
const range = e.op === "%" ? null : integerArithmeticRange(e.op, li.range, ri.range);
|
||||
if (range) {
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
|
||||
@@ -40,6 +40,12 @@ export function exactInteger(
|
||||
if (value.uint32 !== undefined && range.min >= -2147483648 && range.max <= 2147483647) {
|
||||
return { name: value.uint32, type: "i32", signed: true, range };
|
||||
}
|
||||
if (range.min >= -2147483648 && range.max <= 2147483647) {
|
||||
// An int32 conversion lets LLVM fold it with an int32 source's sitofp.
|
||||
const name = host.B.tmp();
|
||||
host.B.line(`${name} = fptosi double ${value.name} to i32`);
|
||||
return { name, type: "i32", signed: true, range };
|
||||
}
|
||||
const name = host.B.tmp();
|
||||
host.B.line(`${name} = fptosi double ${value.name} to i64`);
|
||||
return { name, type: "i64", signed: true, range };
|
||||
@@ -52,6 +58,26 @@ export function widenInteger(host: LlvmEmitterContext, value: LlInteger): string
|
||||
return wide;
|
||||
}
|
||||
|
||||
/** The i32 of an integer whose range is within int32. */
|
||||
export function narrowInteger(host: LlvmEmitterContext, value: LlInteger): string {
|
||||
if (value.type === "i32") return value.name;
|
||||
const narrow = host.B.tmp();
|
||||
host.B.line(`${narrow} = trunc i64 ${value.name} to i32`);
|
||||
return narrow;
|
||||
}
|
||||
|
||||
/** `integerNumber` for an i32 whose range is within int32. */
|
||||
export function int32Number(
|
||||
host: LlvmEmitterContext,
|
||||
name: string,
|
||||
range: IntegerRange,
|
||||
type: IrType,
|
||||
): LlValue {
|
||||
const number = host.B.tmp();
|
||||
host.B.line(`${number} = sitofp i32 ${name} to double`);
|
||||
return { name: number, type, uint32: name, integer: { name, type: "i32", signed: true, range } };
|
||||
}
|
||||
|
||||
export function integerNumber(
|
||||
host: LlvmEmitterContext,
|
||||
name: string,
|
||||
@@ -60,7 +86,18 @@ export function integerNumber(
|
||||
): LlValue {
|
||||
const number = host.B.tmp();
|
||||
const uint32 = host.B.tmp();
|
||||
host.B.line(`${number} = sitofp i64 ${name} to double`);
|
||||
host.B.line(`${uint32} = trunc i64 ${name} to i32`);
|
||||
// A proven int32 converts from its low half. The value is the same, but
|
||||
// LLVM folds `fptosi (sitofp i32 x)` back to x, also through the phis of
|
||||
// loop-carried locals (whose other incoming values are int32 too, such as
|
||||
// `x | 0`), while a 64-bit source is not known to convert exactly. So a
|
||||
// counter written `i = (i + 1) | 0` stays an integer instead of taking a
|
||||
// double round trip on every iteration.
|
||||
const int32 = range.min >= -2147483648 && range.max <= 2147483647;
|
||||
host.B.line(
|
||||
int32
|
||||
? `${number} = sitofp i32 ${uint32} to double`
|
||||
: `${number} = sitofp i64 ${name} to double`,
|
||||
);
|
||||
return { name: number, type, uint32, integer: { name, type: "i64", signed: true, range } };
|
||||
}
|
||||
|
||||
@@ -6,7 +6,14 @@ import { byteNumberAccess } from "../../ir/byte-numbers.js";
|
||||
* consumers in loops can reuse it. The number remains authoritative for floating
|
||||
* arithmetic, signed zero, returns and zero-iteration loops. Only explicit
|
||||
* local stores maintain these views; captures and implicit binders stay on
|
||||
* the ordinary path. No range or integer assumption is made about inputs. */
|
||||
* the ordinary path. No range or integer assumption is made about inputs.
|
||||
*
|
||||
* A declared local that only ever holds bitwise results (`c = units[i] | 0`)
|
||||
* also gets a view when a loop compares it: its stores already have the
|
||||
* integer, and compares of the view stay integer compares. Without it each
|
||||
* compare converts the stored number back, and LLVM moves that number's
|
||||
* conversion into the join of a NaN-coded typed-array read (`phi [sitofp x,
|
||||
* 0.0]`), where nothing folds it back to the integer. */
|
||||
export function findIntegerViews(fn: IrFunction): Set<string> {
|
||||
const wanted = new Set<string>();
|
||||
if (fn.async || fn.generator) return wanted;
|
||||
@@ -19,7 +26,19 @@ export function findIntegerViews(fn: IrFunction): Set<string> {
|
||||
if (loopDepth > 0 && expr.kind === "varRef" && expr.type.kind === "f64")
|
||||
wanted.add(expr.localId);
|
||||
};
|
||||
const declared = new Set<string>();
|
||||
const compared = new Set<string>();
|
||||
const nonBitwise = new Set<string>(fn.params.map((p) => p.localId));
|
||||
const stored = (localId: string, value: IrExpr | null | undefined): void => {
|
||||
if (!value || !bitwiseValue(value)) nonBitwise.add(localId);
|
||||
};
|
||||
const expr = (node: IrExpr): boolean => {
|
||||
if (node.kind === "assignExpr") stored(node.localId, node.value);
|
||||
else if (node.kind === "incDec") nonBitwise.add(node.localId);
|
||||
else if (loopDepth > 0 && node.kind === "bin" && COMPARES.has(node.op)) {
|
||||
for (const side of [node.left, node.right])
|
||||
if (side.kind === "varRef" && side.type.kind === "f64") compared.add(side.localId);
|
||||
}
|
||||
if (
|
||||
node.kind === "bin" &&
|
||||
(node.op === "&" ||
|
||||
@@ -48,7 +67,11 @@ export function findIntegerViews(fn: IrFunction): Set<string> {
|
||||
node.kind === "while" ||
|
||||
node.kind === "doWhile";
|
||||
if (loop) loopDepth++;
|
||||
if (node.kind === "varDecl") initialized.add(node.localId);
|
||||
if (node.kind === "varDecl") {
|
||||
initialized.add(node.localId);
|
||||
declared.add(node.localId);
|
||||
stored(node.localId, node.init);
|
||||
} else if (node.kind === "assign") stored(node.localId, node.value);
|
||||
if (node.kind === "forOf") excluded.add(node.localId);
|
||||
// Canonical induction is maintained separately by the loop emitter.
|
||||
if (node.kind === "for" && node.init?.kind === "varDecl") excluded.add(node.init.localId);
|
||||
@@ -67,6 +90,7 @@ export function findIntegerViews(fn: IrFunction): Set<string> {
|
||||
// A conversion observed only after a floating loop must not add work to
|
||||
// each iteration. Expression results still carry their existing views.
|
||||
fn.body.every(stmt);
|
||||
for (const id of compared) if (declared.has(id) && !nonBitwise.has(id)) wanted.add(id);
|
||||
const eligible = new Set(
|
||||
fn.locals
|
||||
.filter(
|
||||
@@ -82,3 +106,20 @@ export function findIntegerViews(fn: IrFunction): Set<string> {
|
||||
for (const id of wanted) if (!eligible.has(id)) wanted.delete(id);
|
||||
return wanted;
|
||||
}
|
||||
|
||||
const COMPARES = new Set(["===", "!==", "<", "<=", ">", ">="]);
|
||||
|
||||
/** Whether a value is always a bitwise result, whose ToUint32 view is free. */
|
||||
function bitwiseValue(value: IrExpr): boolean {
|
||||
if (value.kind === "bin")
|
||||
return (
|
||||
value.op === "&" ||
|
||||
value.op === "|" ||
|
||||
value.op === "^" ||
|
||||
value.op === "<<" ||
|
||||
value.op === ">>" ||
|
||||
value.op === ">>>"
|
||||
);
|
||||
if (value.kind === "unary") return value.op === "~";
|
||||
return value.kind === "libCall" && (value.fn === "math.imul" || value.fn === "math.clz32");
|
||||
}
|
||||
|
||||
@@ -37,8 +37,11 @@ console.log(safe(0.5));
|
||||
new RegExp(`define internal [^\\n]+ @sc_f_${name}\\([^\\n]*\\) #0 \\{([\\s\\S]*?)\\n\\}`),
|
||||
)![1]!;
|
||||
expect(llBody("safe")).toContain(" = add nsw i64 ");
|
||||
// Only the arbitrary input's slow ToUint32 path still adds doubles.
|
||||
expect(llBody("safe").match(/ = fadd double /g)).toHaveLength(1);
|
||||
// The arbitrary input's slow ToUint32 path is integer arithmetic: no
|
||||
// double additions and no `frem` (an `fmod` call LLVM could hoist).
|
||||
expect(llBody("safe")).toContain("uint32.coerce.slow");
|
||||
expect(llBody("safe")).not.toContain(" = fadd double ");
|
||||
expect(llBody("safe")).not.toContain("frem");
|
||||
expect(llBody("unknown")).toContain("uint32.coerce.slow");
|
||||
expect(llBody("unknown")).toContain(" = fadd double ");
|
||||
expect(llBody("negativeZero")).toContain(" = fadd double ");
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
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 } from "../src/index.js";
|
||||
import { emitLlvmModule } from "../src/backend/llvm/emitter.js";
|
||||
|
||||
test("int32 counters and typed-array reads keep integers visible to LLVM", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-integer-reads-"));
|
||||
try {
|
||||
const entry = join(dir, "main.ts");
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
await writeFile(
|
||||
entry,
|
||||
`
|
||||
class Text {
|
||||
pos: number;
|
||||
end: number;
|
||||
units: Uint16Array;
|
||||
values: Float64Array;
|
||||
constructor(n: number) {
|
||||
this.units = new Uint16Array(n);
|
||||
this.values = new Float64Array(n);
|
||||
this.pos = 0;
|
||||
this.end = n | 0;
|
||||
}
|
||||
skip(): number {
|
||||
const units = this.units;
|
||||
const end = this.end;
|
||||
let pos = this.pos;
|
||||
while (pos < end) {
|
||||
const c = units[pos] | 0;
|
||||
if (c === 42 || c > 127) break;
|
||||
pos = (pos + 1) | 0;
|
||||
}
|
||||
this.pos = pos;
|
||||
return pos;
|
||||
}
|
||||
sum(at: number): number {
|
||||
return this.values[at] + this.units[at];
|
||||
}
|
||||
}
|
||||
const text = new Text(8);
|
||||
console.log(text.skip(), text.sum(3), text.sum(9));
|
||||
`,
|
||||
);
|
||||
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"));
|
||||
const ll = emitLlvmModule(mod);
|
||||
const body = (name: string): string =>
|
||||
ll.match(
|
||||
new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\) #0 \\{([\\s\\S]*?)\\n\\}`),
|
||||
)![1]!;
|
||||
const skip = body("_x25_Text_skip");
|
||||
// The counter's increment is 32-bit arithmetic that cannot overflow and
|
||||
// converts from i32, so LLVM folds the double round trip of the local.
|
||||
expect(skip).toMatch(/ = add nsw i32 /);
|
||||
expect(skip).not.toMatch(/ = sitofp i64 /);
|
||||
// The read joins its integer and a validity bit; the number converts after the join.
|
||||
expect(skip).toMatch(
|
||||
/ = phi i32 \[ %\w+, %bytes\.read\.ok\d+ \], \[ 0, %bytes\.read\.invalid\d+ \]/,
|
||||
);
|
||||
expect(skip).toMatch(
|
||||
/ = phi i1 \[ true, %bytes\.read\.ok\d+ \], \[ false, %bytes\.read\.invalid\d+ \]/,
|
||||
);
|
||||
expect(skip).not.toMatch(/ = phi double \[ %\w+, %bytes\.read\.ok/);
|
||||
// A floating element has no integer view and keeps the joined number.
|
||||
const sum = body("_x25_Text_sum");
|
||||
expect(sum).toMatch(/ = phi double \[ %\w+, %bytes\.read\.ok\d+ \], \[ 0x7FF8000000000000/);
|
||||
expect(sum).toMatch(/ = phi i1 \[ true, %bytes\.read\.ok/);
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -46,7 +46,7 @@ console.log(mix(-0, new Uint32Array(3)), floating(1.75, false), observedAfter(1.
|
||||
const mix = body(ll, "mix");
|
||||
expect(mix).toContain("integer view state");
|
||||
// The original input is converted once, before entering the loop.
|
||||
expect(mix.match(/^uint32\.coerce\.slow\.[^\n]*:/gm)).toHaveLength(1);
|
||||
expect(mix.match(/^uint32\.coerce\.slow\d+:/gm)).toHaveLength(1);
|
||||
const loopStart = mix.search(/\nloop\.b[._\d]/);
|
||||
expect(loopStart).toBeGreaterThan(0);
|
||||
const loop = mix.slice(loopStart);
|
||||
|
||||
@@ -35,8 +35,9 @@ console.log(remainder(5, 2.5), signed(-7.5), maybeZero(7.5, 2.5));
|
||||
const fn = (name: string): string =>
|
||||
new RegExp(`^define internal [^\\n]*@sc_[bf]+_${name}\\([^]*?^}`, "m").exec(ll)![0];
|
||||
// Four source remainders keep a floating fallback behind a runtime
|
||||
// integer check; three ToUint32 coercion fallbacks stay floating.
|
||||
expect(ll.match(/ = frem double /g)).toHaveLength(7);
|
||||
// integer check; ToUint32 coercion fallbacks use integer arithmetic.
|
||||
expect(ll.match(/ = frem double /g)).toHaveLength(4);
|
||||
expect(ll).toContain("uint32.coerce.slow");
|
||||
expect(ll.match(/ = srem i64 /g)).toHaveLength(5);
|
||||
expect(ll).toMatch(/ = select i1 [^\n]+double [^\n]+double /);
|
||||
// Unproven operands: both round-trip checks, the 2^53 dividend bound,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4730-touint32-large-magnitudes.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4731-string-mixed-ascii-slices.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4732-buffer-write-utf16le-runs.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/4733-typed-array-read-compares.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -2738,6 +2738,32 @@ double scr_bytes_write_str(ScrBytes *b, const ScrStr *s, const ScrStr *enc,
|
||||
size_t i = 0, written = 0, width = utf16 ? 2 : 1;
|
||||
const uint64_t high = UINT64_C(0x8080808080808080);
|
||||
while (i < s->len && budget - written >= width) {
|
||||
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
/* utf16le of an ASCII run: zero-extend each byte to a little-endian
|
||||
* unit, eight per step with two word stores (the generic loop below
|
||||
* stores byte by byte, because the source and destination may alias
|
||||
* as far as the compiler knows). */
|
||||
if (utf16) {
|
||||
uint8_t *d = b->data + o + written;
|
||||
size_t n = 0, room = (budget - written) / 16, left = (s->len - i) / 8;
|
||||
size_t steps = room < left ? room : left;
|
||||
for (; n < steps; n++) {
|
||||
uint64_t w;
|
||||
memcpy(&w, s->data + i + 8 * n, 8);
|
||||
if (w & high) break;
|
||||
uint64_t lo = w & UINT64_C(0xffffffff), hi = w >> 32;
|
||||
lo = (lo | (lo << 16)) & UINT64_C(0x0000ffff0000ffff);
|
||||
lo = (lo | (lo << 8)) & UINT64_C(0x00ff00ff00ff00ff);
|
||||
hi = (hi | (hi << 16)) & UINT64_C(0x0000ffff0000ffff);
|
||||
hi = (hi | (hi << 8)) & UINT64_C(0x00ff00ff00ff00ff);
|
||||
memcpy(d + 16 * n, &lo, 8);
|
||||
memcpy(d + 16 * n + 8, &hi, 8);
|
||||
}
|
||||
i += 8 * n;
|
||||
written += 16 * n;
|
||||
if (n != 0) continue;
|
||||
}
|
||||
#endif
|
||||
/* ASCII runs encode byte-for-unit: widen (or copy) eight bytes per
|
||||
* step while the budget holds them all, else one code point below. */
|
||||
if (s->len - i >= 8 && budget - written >= 8 * width) {
|
||||
|
||||
@@ -1081,13 +1081,44 @@ static void scr_sidx_ascii_run(const ScrStr *s, ScrSidx *e) {
|
||||
e->ascii_end = end;
|
||||
}
|
||||
|
||||
/* How far past its end a forward access may grow the cached ASCII run, and
|
||||
* how far beyond the access the growth looks ahead. */
|
||||
#define SCR_SIDX_ASCII_REACH ((size_t)4096)
|
||||
#define SCR_SIDX_ASCII_AHEAD ((size_t)256)
|
||||
|
||||
/* Grow the cached ASCII run forward until it covers byte `want` (plus a
|
||||
* lookahead) or meets a non-ASCII byte. The run stays one contiguous ASCII
|
||||
* span starting at the unit/byte pair (ascii_cu, ascii_cb), so a scan that
|
||||
* reads a mostly ASCII text front to back (a scanner taking substrings at
|
||||
* increasing offsets) tests each byte once instead of walking from the
|
||||
* cursor and re-centring the run every 128 bytes. Only accesses up to
|
||||
* SCR_SIDX_ASCII_REACH past the run try: the walk below bounds the rest. */
|
||||
static bool scr_sidx_ascii_grow(const ScrStr *s, ScrSidx *e, size_t want) {
|
||||
size_t end = e->ascii_end;
|
||||
if (want < end || want >= s->len || want - end > SCR_SIDX_ASCII_REACH) return false;
|
||||
size_t limit = s->len - want > SCR_SIDX_ASCII_AHEAD ? want + SCR_SIDX_ASCII_AHEAD : s->len;
|
||||
while (limit - end >= 8) {
|
||||
uint64_t word;
|
||||
memcpy(&word, s->data + end, 8);
|
||||
if (word & UINT64_C(0x8080808080808080)) break;
|
||||
end += 8;
|
||||
}
|
||||
while (end < limit && (unsigned char)s->data[end] < 0x80) end++;
|
||||
e->ascii_end = end;
|
||||
return want < end;
|
||||
}
|
||||
|
||||
static size_t scr_u16_to_byte_walk(const ScrStr *s, ScrSidx *e, size_t u16,
|
||||
bool *mid);
|
||||
|
||||
/* Convert a UTF-16 index to a byte offset from the closest sparse anchor or
|
||||
* hot cursor. If u16 addresses the second (low-surrogate) unit of
|
||||
* an astral char, *mid is set and the returned offset is the START of that
|
||||
* 4-byte sequence. u16 at or past the end returns s->len with *mid false.
|
||||
* Same contract as a from-scratch scan. */
|
||||
static size_t scr_u16_to_byte_c(const ScrStr *s, ScrSidx *e, size_t u16,
|
||||
bool *mid) {
|
||||
* Same contract as a from-scratch scan. The identity and ASCII-run cases
|
||||
* inline into callers; everything else is out of line. */
|
||||
static inline size_t scr_u16_to_byte_c(const ScrStr *s, ScrSidx *e, size_t u16,
|
||||
bool *mid) {
|
||||
if (s->ascii || e->u16len == s->len) { /* all ASCII: identity mapping */
|
||||
*mid = false;
|
||||
return u16 < s->len ? u16 : s->len;
|
||||
@@ -1098,6 +1129,17 @@ static size_t scr_u16_to_byte_c(const ScrStr *s, ScrSidx *e, size_t u16,
|
||||
*mid = false;
|
||||
return e->cb;
|
||||
}
|
||||
return scr_u16_to_byte_walk(s, e, u16, mid);
|
||||
}
|
||||
|
||||
static __attribute__((noinline)) size_t scr_u16_to_byte_walk(const ScrStr *s, ScrSidx *e,
|
||||
size_t u16, bool *mid) {
|
||||
if (u16 >= e->ascii_cu && scr_sidx_ascii_grow(s, e, e->ascii_cb + (u16 - e->ascii_cu))) {
|
||||
e->cb = e->ascii_cb + (u16 - e->ascii_cu);
|
||||
e->cu = u16;
|
||||
*mid = false;
|
||||
return e->cb;
|
||||
}
|
||||
scr_sidx_extend_to_u16(s, e, u16);
|
||||
/* Extending a far-end lookup can just have proved identity. Do not
|
||||
* materialize an index that the identity fast path will never consult. */
|
||||
@@ -1151,16 +1193,29 @@ static size_t scr_u16_to_byte_c(const ScrStr *s, ScrSidx *e, size_t u16,
|
||||
return cb;
|
||||
}
|
||||
|
||||
static size_t scr_byte_to_u16_walk(const ScrStr *s, ScrSidx *e, size_t byte_off);
|
||||
|
||||
/* Convert a byte offset (must be a char boundary) to a UTF-16 index from
|
||||
* the closest sparse anchor or hot cursor. */
|
||||
static size_t scr_byte_to_u16_c(const ScrStr *s, ScrSidx *e,
|
||||
size_t byte_off) {
|
||||
* the closest sparse anchor or hot cursor. As above, the identity and
|
||||
* ASCII-run cases inline into callers. */
|
||||
static inline size_t scr_byte_to_u16_c(const ScrStr *s, ScrSidx *e,
|
||||
size_t byte_off) {
|
||||
if (s->ascii || e->u16len == s->len) return byte_off; /* all ASCII */
|
||||
if (byte_off >= e->ascii_cb && byte_off < e->ascii_end) {
|
||||
e->cu = e->ascii_cu + (byte_off - e->ascii_cb);
|
||||
e->cb = byte_off;
|
||||
return e->cu;
|
||||
}
|
||||
return scr_byte_to_u16_walk(s, e, byte_off);
|
||||
}
|
||||
|
||||
static __attribute__((noinline)) size_t scr_byte_to_u16_walk(const ScrStr *s, ScrSidx *e,
|
||||
size_t byte_off) {
|
||||
if (byte_off >= e->ascii_cb && scr_sidx_ascii_grow(s, e, byte_off)) {
|
||||
e->cu = e->ascii_cu + (byte_off - e->ascii_cb);
|
||||
e->cb = byte_off;
|
||||
return e->cu;
|
||||
}
|
||||
/* A byte search result may be far beyond the existing prefix. Build the
|
||||
* same sparse intervals first, then choose the closest boundary anchor. */
|
||||
if (byte_off > e->indexed_cb) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
// ToInt32/ToUint32 of doubles beyond 2^53, NaN and the infinities (the slow
|
||||
// path of every bitwise operator and integer typed-array store), and int32
|
||||
// loop counters written `i = (i + 1) | 0`, whose values convert back and
|
||||
// forth between integers and doubles.
|
||||
|
||||
const big: number[] = [
|
||||
2 ** 53 + 2,
|
||||
-(2 ** 53 + 2),
|
||||
2 ** 53 + 4,
|
||||
2 ** 53 * 3 + 6,
|
||||
2 ** 54 + 4,
|
||||
-(2 ** 54 + 12),
|
||||
2 ** 60 + 2 ** 33 + 2 ** 12,
|
||||
-(2 ** 60 + 2 ** 31 + 2 ** 9),
|
||||
2 ** 62 + 2 ** 31,
|
||||
2 ** 63,
|
||||
-(2 ** 63),
|
||||
2 ** 63 + 2 ** 11,
|
||||
2 ** 64 - 2 ** 11,
|
||||
2 ** 64,
|
||||
2 ** 64 + 2 ** 12,
|
||||
-(2 ** 64 + 2 ** 12),
|
||||
2 ** 84 + 2 ** 33,
|
||||
2 ** 85 + 2 ** 33,
|
||||
2 ** 1023,
|
||||
1.7976931348623157e308,
|
||||
-1.7976931348623157e308,
|
||||
1e21,
|
||||
-1e21,
|
||||
123456789012345680000,
|
||||
0 / 0,
|
||||
1 / 0,
|
||||
-1 / 0,
|
||||
];
|
||||
|
||||
function show(label: string, value: number): string {
|
||||
return `${label}=${value}`;
|
||||
}
|
||||
|
||||
for (const x of big) {
|
||||
const parts = [
|
||||
show("|0", x | 0),
|
||||
show(">>>0", x >>> 0),
|
||||
show("~", ~x),
|
||||
show("&ffff", x & 0xffff),
|
||||
show("^1", x ^ 1),
|
||||
show("<<1", x << 1),
|
||||
show(">>1", x >> 1),
|
||||
show(">>>1", x >>> 1),
|
||||
show("1<<x", 1 << x),
|
||||
];
|
||||
console.log(String(x), parts.join(" "));
|
||||
}
|
||||
|
||||
// Integer typed-array stores convert with ToUint32 (modulo the element width).
|
||||
const i32 = new Int32Array(big.length);
|
||||
const u32 = new Uint32Array(big.length);
|
||||
const u16 = new Uint16Array(big.length);
|
||||
const i8 = new Int8Array(big.length);
|
||||
for (let i = 0; i < big.length; i++) {
|
||||
const x = big[i]!;
|
||||
i32[i] = x;
|
||||
u32[i] = x;
|
||||
u16[i] = x;
|
||||
i8[i] = x;
|
||||
}
|
||||
console.log("Int32Array", Array.from(i32).join(","));
|
||||
console.log("Uint32Array", Array.from(u32).join(","));
|
||||
console.log("Uint16Array", Array.from(u16).join(","));
|
||||
console.log("Int8Array", Array.from(i8).join(","));
|
||||
|
||||
// Values computed at run time, so no conversion folds at compile time.
|
||||
let seed = 7;
|
||||
function next(): number {
|
||||
seed = (seed * 1103515245 + 12345) % 2147483648;
|
||||
return seed;
|
||||
}
|
||||
let mixed = 0;
|
||||
for (let k = 0; k < 200; k++) {
|
||||
const scale = 2 ** (53 + (next() % 40));
|
||||
const value = (next() + 1) * scale * (next() % 2 === 0 ? 1 : -1);
|
||||
mixed = (mixed + (value | 0) + (value >>> 0) + (value >> 3)) | 0;
|
||||
}
|
||||
console.log("mixed", mixed);
|
||||
|
||||
// int32 counters: the loop variable converts to a double for every number use.
|
||||
function sumCounters(start: number, end: number): number {
|
||||
let sum = 0;
|
||||
for (let i = start | 0; i < end; i = (i + 1) | 0) sum += i * 0.5;
|
||||
return sum;
|
||||
}
|
||||
console.log("counters", sumCounters(0, 1000), sumCounters(-500, 500), sumCounters(2147483000, 2147483647));
|
||||
|
||||
function scanUntil(units: Uint16Array, pos: number, stop: number): number {
|
||||
let i = pos | 0;
|
||||
while (i < units.length && (units[i] | 0) !== stop) i = (i + 1) | 0;
|
||||
return i;
|
||||
}
|
||||
const units = new Uint16Array(64);
|
||||
for (let i = 0; i < units.length; i++) units[i] = 97 + (i % 26);
|
||||
console.log("scan", scanUntil(units, 0, 122), scanUntil(units, 26, 122), scanUntil(units, 60, 0));
|
||||
|
||||
// A counter that wraps at 2^31: its increment is not an int32, so it stays a
|
||||
// double until `| 0` wraps it.
|
||||
const wrapped: number[] = [];
|
||||
let w = 2147483645;
|
||||
for (let n = 0; n < 6; n++) {
|
||||
wrapped.push(w);
|
||||
w = (w + 1) | 0;
|
||||
}
|
||||
console.log("wrap", wrapped.join(","));
|
||||
|
||||
// An index that goes negative: integer reads past either end stay undefined.
|
||||
const small = new Uint16Array([10, 20, 30]);
|
||||
const reads: string[] = [];
|
||||
for (let i = -2; i < 5; i = (i + 1) | 0) reads.push(String(small[i]));
|
||||
console.log("reads", reads.join(","));
|
||||
@@ -0,0 +1,121 @@
|
||||
// Mostly ASCII strings with rare non-ASCII characters (2-, 3- and 4-byte
|
||||
// UTF-8, the last a surrogate pair), read the way a scanner reads a source
|
||||
// text: forward substrings and character codes, searches whose results are
|
||||
// UTF-16 offsets, backward reads and random jumps. Short and long strings
|
||||
// (the native index keeps them in different caches) of several shapes.
|
||||
|
||||
const words = ["alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta", "iota", "kappa"];
|
||||
const rare = ["\u00e9", "\u20ac", "\ud83d\ude00", "\u00df\u00e4", "\u4e2d"];
|
||||
|
||||
let seed = 12345;
|
||||
function rand(n: number): number {
|
||||
seed = (seed * 1103515245 + 12345) % 2147483648;
|
||||
return (seed >>> 8) % n;
|
||||
}
|
||||
|
||||
function makeText(lines: number, rareEvery: number): string {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < lines; i++) {
|
||||
let line = "";
|
||||
const count = 3 + rand(8);
|
||||
for (let j = 0; j < count; j++) {
|
||||
if (j > 0) line += rand(5) === 0 ? ", " : " ";
|
||||
line += words[rand(words.length)]!;
|
||||
if (rareEvery > 0 && rand(rareEvery) === 0) line += rare[rand(rare.length)]!;
|
||||
}
|
||||
out.push(line);
|
||||
}
|
||||
return out.join("\n");
|
||||
}
|
||||
|
||||
function fold(hash: number, s: string): number {
|
||||
for (let i = 0; i < s.length; i++) hash = (Math.imul(hash, 31) + s.charCodeAt(i)) | 0;
|
||||
return (Math.imul(hash, 17) + s.length) | 0;
|
||||
}
|
||||
|
||||
function isWordUnit(c: number): boolean {
|
||||
return (c >= 97 && c <= 122) || c > 127;
|
||||
}
|
||||
|
||||
// Forward: extract every word with substring, as a scanner takes token values.
|
||||
function forward(text: string): string {
|
||||
let hash = 0;
|
||||
let count = 0;
|
||||
let pos = 0;
|
||||
while (pos < text.length) {
|
||||
const c = text.charCodeAt(pos);
|
||||
if (!isWordUnit(c)) {
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
const start = pos;
|
||||
while (pos < text.length && isWordUnit(text.charCodeAt(pos))) pos++;
|
||||
hash = fold(hash, text.substring(start, pos));
|
||||
count++;
|
||||
}
|
||||
return `words=${count} hash=${hash}`;
|
||||
}
|
||||
|
||||
// Searches: line breaks and a word, with offsets fed back into slices.
|
||||
function searches(text: string): string {
|
||||
let hash = 0;
|
||||
let lines = 0;
|
||||
for (let i = text.indexOf("\n"); i >= 0; i = text.indexOf("\n", i + 1)) {
|
||||
lines++;
|
||||
// Native strings are UTF-8: keep surrogate pairs whole (a lone surrogate would become U+FFFD).
|
||||
let from = Math.max(0, i - 6);
|
||||
const unit = text.charCodeAt(from);
|
||||
if (unit >= 0xdc00 && unit <= 0xdfff) from--;
|
||||
hash = fold(hash, text.slice(from, i));
|
||||
}
|
||||
let found = 0;
|
||||
for (let i = text.indexOf("theta"); i >= 0; i = text.indexOf("theta", i + 5)) {
|
||||
found++;
|
||||
hash = (hash + i) | 0;
|
||||
}
|
||||
const last = text.lastIndexOf("kappa");
|
||||
return `lines=${lines} theta=${found} lastKappa=${last} hash=${hash}`;
|
||||
}
|
||||
|
||||
// Backward reads and random jumps; a position inside a surrogate pair moves to its start.
|
||||
function jumps(text: string): string {
|
||||
let hash = 0;
|
||||
for (let i = text.length - 1; i >= 0; i -= 7) hash = (Math.imul(hash, 3) + text.charCodeAt(i)) | 0;
|
||||
for (let k = 0; k < 400; k++) {
|
||||
let a = rand(text.length);
|
||||
const lowA = text.charCodeAt(a);
|
||||
if (lowA >= 0xdc00 && lowA <= 0xdfff) a--;
|
||||
let b = Math.min(text.length, a + 1 + rand(40));
|
||||
const lowB = text.charCodeAt(b);
|
||||
if (b < text.length && lowB >= 0xdc00 && lowB <= 0xdfff) b++;
|
||||
hash = fold(hash, text.substring(a, b));
|
||||
}
|
||||
return `hash=${hash}`;
|
||||
}
|
||||
|
||||
const shapes: [string, number, number][] = [
|
||||
["short ascii", 20, 0],
|
||||
["short mixed", 40, 6],
|
||||
["medium mixed", 900, 40],
|
||||
["long sparse", 9000, 400],
|
||||
["long dense", 6000, 3],
|
||||
["long ascii", 7000, 0],
|
||||
];
|
||||
for (const [name, lines, rareEvery] of shapes) {
|
||||
const text = makeText(lines, rareEvery);
|
||||
console.log(name, `length=${text.length}`, forward(text), searches(text), jumps(text));
|
||||
}
|
||||
|
||||
// A long ASCII prefix before the first non-ASCII character, read from both ends.
|
||||
const prefix = "x".repeat(70000) + "\u00e9" + "y".repeat(5000) + "\ud83d\ude00" + "z".repeat(100);
|
||||
console.log(
|
||||
prefix.length,
|
||||
prefix.indexOf("\u00e9"),
|
||||
prefix.indexOf("y"),
|
||||
prefix.indexOf("z"),
|
||||
prefix.substring(69998, 70003),
|
||||
prefix.substring(75000, 75004),
|
||||
prefix.charCodeAt(75001),
|
||||
prefix.charCodeAt(75002),
|
||||
prefix.lastIndexOf("x"),
|
||||
);
|
||||
@@ -0,0 +1,61 @@
|
||||
// Buffer#write with 'utf16le' (and latin1/ascii) of strings whose ASCII runs
|
||||
// have every length around the eight-byte steps, mixed with non-ASCII
|
||||
// characters, at offsets and byte budgets that cut runs, units and surrogate
|
||||
// pairs; plus the scanner's use: a Uint16Array filled through its buffer.
|
||||
|
||||
function hex(bytes: Uint8Array, count: number): string {
|
||||
let out = "";
|
||||
for (let i = 0; i < count; i++) out += bytes[i]!.toString(16).padStart(2, "0");
|
||||
return out;
|
||||
}
|
||||
|
||||
const pieces = ["", "a", "\u00e9", "\u20ac", "\ud83d\ude00", "\u007f", "\u0080"];
|
||||
const strings: string[] = [];
|
||||
for (let run = 0; run <= 34; run++) {
|
||||
const ascii = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGH".slice(0, run);
|
||||
for (const piece of pieces) strings.push(ascii + piece + ascii.slice(0, run % 9));
|
||||
}
|
||||
|
||||
let lines = 0;
|
||||
let checksum = 0;
|
||||
for (const s of strings) {
|
||||
const size = 2 * s.length + 6;
|
||||
const out: string[] = [];
|
||||
for (const [offset, budget] of [
|
||||
[0, -1],
|
||||
[1, -1],
|
||||
[3, 7],
|
||||
[0, 16],
|
||||
[2, 17],
|
||||
[0, 2 * s.length - 1],
|
||||
]) {
|
||||
const buf = Buffer.alloc(size, 0xee);
|
||||
const written =
|
||||
budget < 0 ? buf.write(s, offset, "utf16le") : buf.write(s, offset, Math.min(budget, size), "utf16le");
|
||||
out.push(`${written}:${hex(buf, size)}`);
|
||||
}
|
||||
const latin = Buffer.alloc(s.length + 2, 0xee);
|
||||
const writtenLatin = latin.write(s, 1, "latin1");
|
||||
const ascii = Buffer.alloc(s.length + 2, 0xee);
|
||||
const writtenAscii = ascii.write(s, 0, "ascii");
|
||||
const line = `${out.join(" ")} ${writtenLatin} ${hex(latin, latin.length)} ${writtenAscii} ${hex(ascii, ascii.length)}`;
|
||||
for (let i = 0; i < line.length; i++) checksum = (Math.imul(checksum, 31) + line.charCodeAt(i)) | 0;
|
||||
if (lines++ % 16 === 0) console.log(JSON.stringify(s), line);
|
||||
}
|
||||
console.log("strings", strings.length, "checksum", checksum);
|
||||
|
||||
// The scanner's conversion: the code units of a text, through the bytes of a Uint16Array.
|
||||
function toCodeUnits(text: string): Uint16Array {
|
||||
const chars = new Uint16Array(text.length + 4);
|
||||
Buffer.from(chars.buffer).write(text, "utf16le");
|
||||
return chars;
|
||||
}
|
||||
let text = "";
|
||||
for (let i = 0; i < 3000; i++) text += i % 97 === 0 ? "\u00e9\u20ac" : i % 211 === 0 ? "\ud83d\ude00" : "abcdefgh"[i % 8];
|
||||
const units = toCodeUnits(text);
|
||||
let same = units.length === text.length + 4;
|
||||
for (let i = 0; i < text.length; i++) if (units[i] !== text.charCodeAt(i)) same = false;
|
||||
for (let i = text.length; i < units.length; i++) if (units[i] !== 0) same = false;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < units.length; i++) sum = (sum * 33 + units[i]!) % 1000000007;
|
||||
console.log("units", text.length, units.length, same, sum);
|
||||
@@ -0,0 +1,122 @@
|
||||
// Typed-array reads at unproven indices, past the end (undefined, so NaN,
|
||||
// so 0 after `| 0`), and compares of bitwise locals in loops: signed and
|
||||
// unsigned elements, values above 2^31, and counters that leave the array.
|
||||
|
||||
function scanUntil(units: Uint16Array, start: number, stop: number): number {
|
||||
let pos = start | 0;
|
||||
const end = (units.length + 2) | 0;
|
||||
while (pos < end) {
|
||||
const c = units[pos] | 0;
|
||||
if (c === stop || c === 0) break;
|
||||
pos = (pos + 1) | 0;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
function classify32(values: Int32Array, extra: number): string {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < values.length + extra; i = (i + 1) | 0) {
|
||||
const v = values[i] | 0;
|
||||
const u = values[i] >>> 0;
|
||||
let kind = "";
|
||||
if (v < 0) kind += "neg";
|
||||
else if (v === 0) kind += "zero";
|
||||
else if (v <= 127) kind += "small";
|
||||
else kind += "big";
|
||||
if (u > 2147483647) kind += "/high";
|
||||
out.push(`${i}:${kind}`);
|
||||
}
|
||||
return out.join(" ");
|
||||
}
|
||||
|
||||
function classifyU32(values: Uint32Array, extra: number): string {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < values.length + extra; i = (i + 1) | 0) {
|
||||
const v = values[i] | 0;
|
||||
const u = values[i] >>> 0;
|
||||
let kind = "";
|
||||
if (v < 0) kind += "neg";
|
||||
else if (v === 0) kind += "zero";
|
||||
else if (v <= 127) kind += "small";
|
||||
else kind += "big";
|
||||
if (u > 2147483647) kind += "/high";
|
||||
out.push(`${i}:${kind}`);
|
||||
}
|
||||
return out.join(" ");
|
||||
}
|
||||
|
||||
function classify16(values: Int16Array, extra: number): string {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < values.length + extra; i = (i + 1) | 0) {
|
||||
const v = values[i] | 0;
|
||||
const u = values[i] >>> 0;
|
||||
let kind = "";
|
||||
if (v < 0) kind += "neg";
|
||||
else if (v === 0) kind += "zero";
|
||||
else if (v <= 127) kind += "small";
|
||||
else kind += "big";
|
||||
if (u > 2147483647) kind += "/high";
|
||||
out.push(`${i}:${kind}`);
|
||||
}
|
||||
return out.join(" ");
|
||||
}
|
||||
|
||||
function classify8(values: Int8Array, extra: number): string {
|
||||
const out: string[] = [];
|
||||
for (let i = 0; i < values.length + extra; i = (i + 1) | 0) {
|
||||
const v = values[i] | 0;
|
||||
const u = values[i] >>> 0;
|
||||
let kind = "";
|
||||
if (v < 0) kind += "neg";
|
||||
else if (v === 0) kind += "zero";
|
||||
else if (v <= 127) kind += "small";
|
||||
else kind += "big";
|
||||
if (u > 2147483647) kind += "/high";
|
||||
out.push(`${i}:${kind}`);
|
||||
}
|
||||
return out.join(" ");
|
||||
}
|
||||
|
||||
function countBetween(bytes: Uint8Array, lo: number, hi: number): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i <= bytes.length; i = (i + 1) | 0) {
|
||||
const b = bytes[i] & 255;
|
||||
if (b >= lo && b <= hi) n = (n + 1) | 0;
|
||||
const x = bytes[i] ^ 0;
|
||||
if (x !== b) n = (n + 1000) | 0;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function mixed(units: Uint16Array): number {
|
||||
let h = 0;
|
||||
let i = 0;
|
||||
while (i < units.length + 3) {
|
||||
const c = units[i] | 0;
|
||||
const d = ~units[i + 1];
|
||||
if (c > 64 && d < -65) h = (Math.imul(h, 31) + c) | 0;
|
||||
else if (c === 0) h = h ^ 0x5bd1e995;
|
||||
i = (i + 1) | 0;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
const text = "abc/*x*/def\nghi*jk\u00e9\u4e2d\uffff";
|
||||
const units = new Uint16Array(text.length);
|
||||
for (let i = 0; i < text.length; i++) units[i] = text.charCodeAt(i);
|
||||
console.log(scanUntil(units, 0, 42), scanUntil(units, 3, 10), scanUntil(units, 0, 7), scanUntil(units, -2, 99));
|
||||
console.log(scanUntil(units, 2.5, 42), scanUntil(units, units.length - 1, 42), scanUntil(units, 1e10, 42));
|
||||
|
||||
const i32 = new Int32Array([0, -1, 1, 127, 128, -2147483648, 2147483647, 65536]);
|
||||
const u32 = new Uint32Array([0, 1, 4294967295, 2147483648, 2147483647, 300]);
|
||||
const i16 = new Int16Array([-32768, 32767, -1, 0, 5]);
|
||||
const i8 = new Int8Array([-128, 127, -1, 0]);
|
||||
console.log(classify32(i32, 2));
|
||||
console.log(classifyU32(u32, 1));
|
||||
console.log(classify16(i16, 0));
|
||||
console.log(classify8(i8, 3));
|
||||
|
||||
const bytes = new Uint8Array(64);
|
||||
for (let i = 0; i < bytes.length; i++) bytes[i] = (i * 37 + 11) & 255;
|
||||
console.log(countBetween(bytes, 0, 0), countBetween(bytes, 32, 126), countBetween(bytes, 200, 255));
|
||||
console.log(mixed(units), mixed(new Uint16Array(0)), mixed(new Uint16Array([65, 66, 0, 120])));
|
||||
Reference in New Issue
Block a user