Version for loops on the positions they advance (#2415)

This commit is contained in:
Chris Tate
2026-10-27 09:16:42 -05:00
committed by GitHub
13 changed files with 385 additions and 47 deletions
@@ -2992,7 +2992,12 @@ function emitProvenLoop(
}
const deferred = new Set<string>();
for (const id of plan.locals.keys()) deferred.add(id);
emitter.speculation = { writes: plan.writes, resume: new Map(), deferred };
emitter.speculation = {
writes: plan.writes,
resume: new Map(),
deferred,
increments: plan.increments,
};
try {
const leave = B.newLabel("integer.leave");
emitCopy(leave);
@@ -1775,6 +1775,9 @@ export class LlEmitter {
ran?: string;
/** Writes that speculate a rewritten value (rewriteSpeculable). */
values?: ReadonlyMap<IrStmt, SpeculativeValue>;
/** A proven integer loop's expression-position `++`/`--` of its
* shadowed bindings (ProvenLoopPlan.increments). */
increments?: ReadonlySet<IrExpr>;
} | null = null;
/** Speculative loop sites numbered so far in the module. */
speculativeLoopSites = 0;
@@ -517,6 +517,29 @@ export function emitOperatorExpr(
// Expression-position ++/-- over an f64 binding (locals, module
// globals, capture boxes): read, write ±1, yield old (postfix) or
// new (prefix).
const shadow = host.integerLoopBindings.get(e.localId);
if (shadow !== undefined && host.speculation?.increments?.has(e)) {
// A proven integer loop's shadow: the loop range holds every value
// the binding takes, so the step cannot overflow.
const old = B.tmp();
const next = B.tmp();
const number = B.tmp();
B.line(`${old} = load i64, ptr ${shadow.slot}`);
B.line(`${next} = ${e.op === "+" ? "add" : "sub"} nsw i64 ${old}, 1`);
B.line(`store i64 ${next}, ptr ${shadow.slot}`);
const value = e.prefix ? next : old;
B.line(`${number} = sitofp i64 ${value} to double`);
return {
name: number,
type: e.type,
integer: {
name: value,
type: "i64",
signed: true,
range: shadow.range ?? { min: -Number.MAX_SAFE_INTEGER, max: Number.MAX_SAFE_INTEGER },
},
};
}
const b = host.binding(e.localId);
if (host.int32Locals.has(e.localId)) {
if (!host.int32LocalWrites.has(e))
@@ -1,4 +1,4 @@
/* Loop-entry range versions (ir/loop-entry-ranges.ts): a while loop whose
/* Loop-entry range versions (ir/loop-entry-ranges.ts): a while or for loop whose
* integer work depends on locals entering it without range facts runs as
* two copies. A test on entry checks that each such local holds an integer
* in its assumed range; the fast copy is emitted under the facts that hold
@@ -16,7 +16,7 @@ interface LoopEntryState {
fn: IrFunction | null;
facts: IntegerRangeFacts | null;
slots: IntegerSlotFacts;
/** Found on the first while loop the function emits. */
/** Found on the first while or for loop the function emits. */
versions: Map<IrStmt, LoopEntryVersion> | null;
/** Loops whose copies are being emitted: a loop is versioned once. */
readonly versioning: Set<IrStmt>;
@@ -61,7 +61,7 @@ export function beginLoopEntryVersions(
* its locals: plain double slots of the current function, outside a
* speculative copy and a coroutine. */
export function loopEntryVersion(emitter: LlEmitter, loop: IrStmt): LoopEntryVersion | null {
if (loop.kind !== "while") return null;
if (loop.kind !== "while" && loop.kind !== "for") return null;
const state = ENTRY_VERSIONS.of(emitter);
if (state.fn === null || state.facts === null || state.versioning.has(loop)) return null;
if (
@@ -773,6 +773,9 @@ export interface ProvenLoopPlan {
locals: Map<string, IntegerRange>;
/** Statement-level writes computed as integers. */
writes: Set<IrStmt>;
/** Expression-position `++`/`--` of shadowed bindings (`out[o++] = v`),
* computed on the shadows. */
increments: Set<IrExpr>;
}
/** Plan a proven integer loop. `cursors` are the loop's bounded bindings,
@@ -791,10 +794,17 @@ export function planProvenIntegerLoop(
): ProvenLoopPlan | null {
if (cursors.size === 0) return null;
const writes = new Map<string, IrStmt[]>();
const increments = new Map<string, IrExpr[]>();
const refused = new Set<string>();
let rejected = false;
const expr = (e: IrExpr): boolean => {
if (e.kind === "assignExpr" || e.kind === "incDec") refused.add(e.localId);
// Every value a cursor holds in the loop lies in its loop range, so an
// increment of one writes an integer of that range.
if (e.kind === "incDec" && cursors.has(e.localId)) {
const list = increments.get(e.localId) ?? [];
list.push(e);
increments.set(e.localId, list);
} else if (e.kind === "assignExpr" || e.kind === "incDec") refused.add(e.localId);
return everyExprChild(e, expr, nested);
};
const nested = (s: IrStmt): boolean => {
@@ -827,12 +837,13 @@ export function planProvenIntegerLoop(
for (const s of loop.body) stmt(s);
if (rejected) return null;
const locals = new Map<string, IntegerRange>();
for (const [id, list] of writes) {
for (const id of new Set([...writes.keys(), ...increments.keys()])) {
if (refused.has(id) || !eligible(id)) continue;
const exact = list.every((s) => {
const value = speculativeWriteTarget(s)!.value!;
return isSpeculableValue(value, false) && proven(value);
});
const exact =
(writes.get(id) ?? []).every((s) => {
const value = speculativeWriteTarget(s)!.value!;
return isSpeculableValue(value, false) && proven(value);
}) && (increments.get(id) ?? []).every(proven);
if (exact) locals.set(id, cursors.get(id)!);
}
// Every binding a shadowed write reads is shadowed, already an integer,
@@ -844,15 +855,19 @@ export function planProvenIntegerLoop(
for (let changed = true; changed;) {
changed = false;
for (const id of [...locals.keys()])
if (!writes.get(id)!.every((s) => grounded(speculativeWriteTarget(s)!.value!))) {
if (!(writes.get(id) ?? []).every((s) => grounded(speculativeWriteTarget(s)!.value!))) {
locals.delete(id);
changed = true;
}
}
if (locals.size === 0) return null;
const planned = new Set<IrStmt>();
for (const id of locals.keys()) for (const s of writes.get(id)!) planned.add(s);
return { locals, writes: planned };
const plannedIncrements = new Set<IrExpr>();
for (const id of locals.keys()) {
for (const s of writes.get(id) ?? []) planned.add(s);
for (const e of increments.get(id) ?? []) plannedIncrements.add(e);
}
return { locals, writes: planned, increments: plannedIncrements };
}
const SAFE = Number.MAX_SAFE_INTEGER;
+13 -2
View File
@@ -23,7 +23,8 @@ export interface IntegerRangeFacts {
ranges: IntegerRanges;
loopRanges: ReadonlyMap<IrStmt, ReadonlyMap<string, IntegerRange>>;
}
/** Ranges assumed for locals on entry to while loops (loop-entry-ranges.ts):
/** Ranges assumed for locals on entry to while and for loops
* (loop-entry-ranges.ts), after a for loop's initializer:
* the facts inside such a loop hold in a copy of it that runs only after a
* test proves every assumed local is an integer in its range. After the
* loop, its assumed locals and its writes carry no facts. */
@@ -744,8 +745,11 @@ export function analyzeIntegerRangeFacts(
set(facts, e.localId, range);
break;
case "incDec": {
const before = facts.get(e.localId) ?? null;
// A binding a for loop bounds holds a value in its loop range at
// every point of the loop, also where a statement around this
// increment has discarded its fact.
const bound = counterRanges.get(e.localId);
const before = facts.get(e.localId) ?? bound ?? null;
const sum = before ? integerArithmeticRange(e.op, before, { min: 1, max: 1 }) : null;
const after =
bound && sum
@@ -1005,6 +1009,9 @@ export function analyzeIntegerRangeFacts(
}
case "for": {
if (s.init) body([s.init], facts);
// The initializer writes none of the assumed locals.
const assumed = entries?.get(s);
if (assumed) for (const [id, range] of assumed) if (!facts.has(id)) set(facts, id, range);
const counter = induction(s, facts);
const bounded = boundedIntegerLoopFacts(s, loopLocals, facts);
const loop = new Map(facts);
@@ -1076,6 +1083,10 @@ export function analyzeIntegerRangeFacts(
body([s.update], entry);
}
havoc(facts, [s], facts);
if (assumed) {
invalidate(facts, [s]);
for (const id of assumed.keys()) facts.delete(id);
}
break;
}
case "while":
@@ -1,7 +1,7 @@
import { expect, test } from "vitest";
import { analyzeIntegerRangeFacts } from "./integer-ranges.js";
import { findLoopEntryVersions, LOOP_ENTRY_RANGE } from "./loop-entry-ranges.js";
import { BOOL, F64, type IrExpr, type IrFunction, type IrStmt } from "./ir.js";
import { BOOL, BYTES_U8, F64, type IrExpr, type IrFunction, type IrStmt } from "./ir.js";
const loc = { file: "test.ts", start: 0, end: 0 };
const num = (value: number): IrExpr => ({ loc, kind: "numLit", value, type: F64 });
@@ -141,3 +141,77 @@ test("loops without a remainder the assumption proves keep one copy", () => {
const known = fn([set("acc", ref("unknown")), set("i", num(0)), proven], ["acc", "i"]);
expect(findLoopEntryVersions(known, analyzeIntegerRangeFacts(known)).size).toBe(0);
});
const step = (localId: string): IrExpr => ({
loc,
kind: "incDec",
op: "+",
prefix: false,
localId,
type: F64,
});
const bytes = (localId: string): IrExpr => ({ loc, kind: "varRef", localId, type: BYTES_U8 });
/** i = unknown; o = unknown; line = unknown;
* for (let j = 0; j < line; j = j + 1) out[o++] = src[i++];
* observe(o) */
function cursors(init?: IrStmt) {
const read: IrExpr = {
loc,
kind: "bytesIntrinsic",
method: "get",
receiver: bytes("src"),
args: [step("i")],
type: F64,
};
const store: IrStmt = { loc, kind: "bytesSet", arr: bytes("out"), index: step("o"), value: read };
const loop: IrStmt = {
loc,
kind: "for",
init: init ?? { loc, kind: "varDecl", localId: "j", init: num(0) },
cond: condition("<", ref("j"), ref("line")),
update: set("j", bin("+", ref("j"), num(1))),
body: [store],
};
const after = ref("o");
const f = fn(
[
set("i", ref("unknown")),
set("o", ref("unknown")),
set("line", ref("unknown")),
loop,
observe(after),
],
["i", "o", "line", "j", "unknown"],
);
f.locals.push(
{ id: "src", name: "src", type: BYTES_U8, mutable: false },
{ id: "out", name: "out", type: BYTES_U8, mutable: false },
);
return { f, loop, store, read, after };
}
test("versions a for loop on the positions it advances and the passes it runs", () => {
const { f, loop, store, read, after } = cursors();
const facts = analyzeIntegerRangeFacts(f);
expect(facts.ranges.get((store as Extract<IrStmt, { kind: "bytesSet" }>).index)).toBeNull();
const version = findLoopEntryVersions(f, facts).get(loop)!;
expect([...version.entry.keys()].sort()).toEqual(["i", "line", "o"]);
// Every pass advances each position once, and the passes stay below `line`.
expect(version.loopRanges?.get("o")).toEqual({
min: LOOP_ENTRY_RANGE.min,
max: 2 * LOOP_ENTRY_RANGE.max,
});
expect(version.ranges.get((store as Extract<IrStmt, { kind: "bytesSet" }>).index)).toBeDefined();
expect(
version.ranges.get((read as Extract<IrExpr, { kind: "bytesIntrinsic" }>).args[0]!),
).toBeDefined();
// No fact leaves the loop.
expect(version.ranges.has(after)).toBe(false);
});
test("leaves a for loop whose initializer writes an advanced position as it is", () => {
const init: IrStmt = { loc, kind: "assign", localId: "o", value: num(0) };
const { f, loop } = cursors(init);
expect(findLoopEntryVersions(f, analyzeIntegerRangeFacts(f)).has(loop)).toBe(false);
});
+81 -29
View File
@@ -1,5 +1,6 @@
import type { IrExpr, IrFunction, IrStmt } from "./ir.js";
import { everyExpr, everyStmt, everyStmtList } from "./traverse.js";
import { byteNumberAccess } from "./byte-numbers.js";
import {
analyzeIntegerRangeFacts,
type IntegerRange,
@@ -8,14 +9,12 @@ import {
type LoopEntryAssumptions,
} from "./integer-ranges.js";
type WhileLoop = Extract<IrStmt, { kind: "while" }>;
/** The range a versioned loop requires of each assumed local on entry. A
* loop's counters stay below a typed array or string length (at most 2^48),
* so sums of a few such values and assumed locals stay exact integers. */
export const LOOP_ENTRY_RANGE: IntegerRange = { min: -(2 ** 48), max: 2 ** 48 };
/** A copy of a while loop that runs when every local in `entry` holds an
/** A copy of a loop that runs when every local in `entry` holds an
* integer in its range on entry (never -0). `ranges` are the facts that
* hold for the loop's expressions in that copy and that the function's own
* analysis does not prove; `loopRanges` are the loop's bounded bindings
@@ -26,17 +25,22 @@ export interface LoopEntryVersion {
loopRanges: ReadonlyMap<string, IntegerRange> | undefined;
}
/** Versions of innermost while loops whose integer work depends on locals
* that enter the loop without range facts: copies of module globals,
/** Versions of innermost while and for loops whose integer work depends on
* locals that enter the loop without range facts: copies of module globals,
* parameters, fields or call results (`const base = offset;` before a loop
* that computes `(count + base) % 60`).
* that computes `(count + base) % 60`), or positions carried from one loop
* to the next (`out[o++] = f(src[i++])` in a loop of `line` passes).
*
* The function is analyzed again with each number local the loop reads
* without a fact assumed in LOOP_ENTRY_RANGE at the loop's entry. A loop is
* versioned when that analysis proves both operands of a remainder that the
* function's own analysis leaves unproven. Only remainders qualify: the
* operands of a product or a sum of such locals are as often fractional,
* and a loop whose test fails runs the ordinary copy in a larger function.
* versioned when that analysis proves what the function's own analysis
* leaves unproven: both operands of a remainder, or the index of a typed
* array read or write. Only those qualify: the operands of a product or a
* sum of such locals are as often fractional, while an index that is not
* an integer reads undefined, and a loop whose test fails runs the
* ordinary copy in a larger function. A for loop qualifies only when its
* initializer writes none of the assumed locals, so that they hold their
* entry values when the test reads them.
* The entry test covers only the assumed locals that such an operation reads, directly,
* through the loop's writes of other locals, or through the loop condition;
* when that drops any, the function is analyzed once more with the smaller
@@ -58,19 +62,21 @@ export function findLoopEntryVersions(
.map((l) => l.id),
);
const known = (e: IrExpr): boolean => !!facts.ranges.get(e);
let candidates = new Map<WhileLoop, ReadonlyMap<string, IntegerRange>>();
// Keyed by the statement (a while or for loop).
let candidates = new Map<IrStmt, ReadonlyMap<string, IntegerRange>>();
everyStmtList(fn.body, {
expr: () => true,
stmt: (s) => {
if (s.kind !== "while" || containsLoop(s.body)) return true;
const assumed = entryCandidates(s, eligible, known);
if (assumed.size > 0) candidates.set(s, assumed);
const loop: IrStmt = s;
if ((s.kind !== "while" && s.kind !== "for") || containsLoop(s.body)) return true;
const assumed = entryCandidates(loop, eligible, known);
if (assumed.size > 0) candidates.set(loop, assumed);
return true;
},
});
if (candidates.size === 0) return result;
let versioned = analyzeIntegerRangeFacts(fn, slots, assumptionsOf(candidates));
const pruned = new Map<WhileLoop, ReadonlyMap<string, IntegerRange>>();
const pruned = new Map<IrStmt, ReadonlyMap<string, IntegerRange>>();
let dropped = false;
for (const [loop, entry] of candidates) {
const roots = gainedOperands(loop, known, versioned);
@@ -104,7 +110,7 @@ export function findLoopEntryVersions(
}
function assumptionsOf(
candidates: ReadonlyMap<WhileLoop, ReadonlyMap<string, IntegerRange>>,
candidates: ReadonlyMap<IrStmt, ReadonlyMap<string, IntegerRange>>,
): LoopEntryAssumptions {
const result = new Map<IrStmt, ReadonlyMap<string, IntegerRange>>();
for (const [loop, entry] of candidates) result.set(loop, entry);
@@ -112,9 +118,10 @@ function assumptionsOf(
}
/** Number locals the loop reads without a range fact and does not declare,
* when the loop has a remainder without one. */
* when the loop has a remainder or a typed array index without one. A for
* loop whose initializer writes such a local has none. */
function entryCandidates(
loop: WhileLoop,
loop: IrStmt,
eligible: ReadonlySet<string>,
known: (e: IrExpr) => boolean,
): Map<string, IntegerRange> {
@@ -123,29 +130,64 @@ function entryCandidates(
let integerWork = false;
everyStmt(loop, {
expr: (e) => {
if (e.kind === "varRef" && eligible.has(e.localId) && !known(e)) read.add(e.localId);
if ((e.kind === "varRef" || e.kind === "incDec") && eligible.has(e.localId) && !known(e))
read.add(e.localId);
else if (e.kind === "bin" && e.op === "%" && !known(e)) integerWork = true;
const index = bytesIndex(e);
if (index && !known(index)) integerWork = true;
return true;
},
stmt: (s) => {
if (s.kind === "varDecl") declared.add(s.localId);
if (s.kind === "bytesSet" && !known(s.index)) integerWork = true;
return true;
},
});
const result = new Map<string, IntegerRange>();
if (!integerWork) return result;
for (const id of read) if (!declared.has(id)) result.set(id, LOOP_ENTRY_RANGE);
const initialized = loop.kind === "for" && loop.init ? writtenIn(loop.init) : new Set<string>();
for (const id of read) {
if (initialized.has(id)) return new Map();
if (!declared.has(id)) result.set(id, LOOP_ENTRY_RANGE);
}
return result;
}
/** Operands of the loop's remainders that the versioned analysis proves
* where the function's own analysis does not prove both. */
/** The index of a typed array element read (`a[i]`, `buf.readInt16LE(i)`). */
function bytesIndex(e: IrExpr): IrExpr | undefined {
if (e.kind !== "bytesIntrinsic") return undefined;
const numeric = byteNumberAccess(e);
if (numeric) return e.args[numeric.offsetArg];
return e.method === "get" ? e.args[0] : undefined;
}
/** The locals `node` assigns, declares or increments. */
function writtenIn(node: IrStmt): Set<string> {
const ids = new Set<string>();
everyStmt(node, {
expr: (e) => {
if (e.kind === "assignExpr" || e.kind === "incDec") ids.add(e.localId);
return true;
},
stmt: (s) => {
if (s.kind === "assign" || s.kind === "varDecl") ids.add(s.localId);
return true;
},
});
return ids;
}
/** Operands of the loop's remainders and typed array indexes that the
* versioned analysis proves where the function's own analysis does not
* prove them. */
function gainedOperands(
loop: WhileLoop,
loop: IrStmt,
known: (e: IrExpr) => boolean,
versioned: IntegerRangeFacts,
): IrExpr[] {
const operands: IrExpr[] = [];
const gained = (e: IrExpr | undefined): boolean =>
e !== undefined && !known(e) && !!versioned.ranges.get(e);
everyStmt(loop, {
expr: (e) => {
if (
@@ -156,23 +198,32 @@ function gainedOperands(
!!versioned.ranges.get(e.right)
)
operands.push(e.left, e.right);
const index = bytesIndex(e);
if (gained(index)) operands.push(index!);
return true;
},
stmt: (s) => {
if (s.kind === "bytesSet" && gained(s.index)) operands.push(s.index);
return true;
},
stmt: () => true,
});
return operands;
}
/** The locals `roots` read, closed over the loop's writes (a local written
* from others reads them) and over its condition (a local the condition
* reads makes every local of the condition relevant). */
function relevantLocals(loop: WhileLoop, roots: readonly IrExpr[]): Set<string> {
* reads makes every local of the condition relevant). A for loop's
* condition bounds its passes, and with them the positions its body
* advances (`o++` in a loop of `line` passes), so its locals are always
* relevant. An increment or assignment reads its own local. */
function relevantLocals(loop: IrStmt, roots: readonly IrExpr[]): Set<string> {
const relevant = new Set<string>();
const readsOf = (e: IrExpr): string[] => {
const readsOf = (e: IrExpr | null | undefined): string[] => {
const ids: string[] = [];
if (!e) return ids;
everyExpr(e, {
expr: (x) => {
if (x.kind === "varRef") ids.push(x.localId);
if (x.kind === "varRef" || x.kind === "incDec") ids.push(x.localId);
return true;
},
stmt: () => true,
@@ -193,7 +244,7 @@ function relevantLocals(loop: WhileLoop, roots: readonly IrExpr[]): Set<string>
return true;
},
});
const condition = readsOf(loop.cond);
const condition = readsOf(loop.kind === "while" || loop.kind === "for" ? loop.cond : null);
const pending: string[] = [];
const add = (id: string): void => {
if (relevant.has(id)) return;
@@ -201,6 +252,7 @@ function relevantLocals(loop: WhileLoop, roots: readonly IrExpr[]): Set<string>
pending.push(id);
};
for (const root of roots) for (const id of readsOf(root)) add(id);
if (loop.kind === "for") for (const id of condition) add(id);
while (pending.length > 0) {
const id = pending.pop()!;
for (const value of written.get(id) ?? []) for (const other of readsOf(value)) add(other);
@@ -191,8 +191,10 @@ test("a loop over a counter array tests its kind once on entry", async () => {
/icmp eq i8 %\w+, 7\n\s+br i1 %\w+, label %kind\.fast\d+, label %kind\.wide/,
);
const rest = counter.slice(counter.search(/^kind\.fast\d+:/m));
// The fast copy is the last one, before the loop's exit.
const fast = rest.slice(0, rest.search(/^loop\.e\d+:/m));
// The fast copy is the last one, before the loop's exit or, when the
// loop is also versioned on `n`'s entry range, before the copy for other
// entry values.
const fast = rest.slice(0, rest.search(/^(loop\.e|entry\.slow)\d+:/m));
expect(fast).not.toBe("");
expect(fast).not.toMatch(/icmp eq i8 %\w+, 7/);
expect(fast).toMatch(/add nsw i64 %\w+, 1/);
@@ -43,3 +43,33 @@ test("while loops test their entry values and run integer remainders in the fast
await rm(dir, { recursive: true, force: true });
}
});
test("for loops test the positions they advance and step them as integers in the fast copy", async () => {
const dir = await mkdtemp(join(tmpdir(), "scriptc-entry-cursors-"));
try {
const entry = join(import.meta.dirname, "../../../tests/corpus/for-entry-range-cursors.ts");
const outPath = join(dir, "cursors.ir.json");
const result = await compile(entry, { outPath, outDir: dir, outputKind: "ir" });
if (!result.ok) throw new Error(result.diagnostics.map((d) => d.message).join("\n"));
const mod = deserializeModule(await readFile(outPath, "utf8"));
const ll = emitLlvmModule(mod, { pointerBits: 64 });
const body = (name: string): string =>
ll.match(
new RegExp(`define internal [^\\n]+ @sc_bf_${name}\\([^\\n]*\\{([\\s\\S]*?)\\n\\}`),
)![1]!;
for (const name of ["lines", "reverse", "find"]) {
const fn = body(name);
const fast = fn.slice(fn.indexOf("\nentry.fast"), fn.indexOf("\nentry.slow"));
const slow = fn.slice(fn.indexOf("\nentry.slow"));
// The fast copy steps i64 shadows of the positions; the ordinary copy
// steps their doubles.
expect(fast, name).toMatch(/= (add|sub) nsw i64 %t\d+, 1\n/);
expect(fn, name).toMatch(/alloca i64 ; integer [io]\n/);
expect(slow, name).toMatch(/= f(add|sub) double %t\d+, 0x3FF0000000000000\n/);
}
// The initializer writes a position, so the loop has one copy.
expect(body("restart")).not.toContain("entry.fast");
} finally {
await rm(dir, { recursive: true, force: true });
}
});
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/for-entry-range-cursors.ts"
],
"diags": []
}
+116
View File
@@ -0,0 +1,116 @@
// For loops that advance positions carried in from outside the loop
// (`out[o++] = f(src[i++])` in a loop of `line` passes). Entry positions and
// pass counts cover integers inside and outside [-2^48, 2^48], fractions,
// -0, NaN and infinities.
function code(rand: number): number {
if (rand < 0.25) return 97;
if (rand < 0.5) return 99;
if (rand < 0.75) return 103;
return 116;
}
// Lines of `line` codes, each followed by a newline, from `start` on.
function lines(input: Float64Array, output: Uint8Array, count: number, start: number): number {
let i = start;
let o = start;
while (count > 0) {
const line = Math.min(count, 6);
for (let j = 0; j < line; j++) output[o++] = code(input[i++]!);
output[o++] = 10;
count -= line;
}
return o + i / 1000;
}
// Writes backwards from `o`, reading forwards from `i`, with prefix steps.
function reverse(input: Float64Array, output: Uint8Array, line: number, i: number, o: number): string {
for (let j = 0; j < line; j++) output[--o] = code(input[++i]!);
return `${i} ${o}`;
}
// Leaves the loop with a return that reads both positions.
function find(input: Float64Array, line: number, i: number, o: number): number {
for (let j = 0; j < line; j++) {
if (input[i++]! > 0.9) return i * 1000 + o;
o++;
}
return -(i * 1000 + o);
}
// The initializer writes a carried position.
function restart(input: Float64Array, output: Uint8Array, line: number, o: number): number {
let i = 3;
for (let j = (i = 0); j < line; j++) output[o++] = code(input[i++]!);
return i * 100 + o;
}
// A callee that throws partway through the loop.
function guarded(input: Float64Array, output: Uint8Array, line: number, i: number): number {
let o = 0;
for (let j = 0; j < line; j++) {
const rand = input[i++]!;
if (rand > 0.91) throw new RangeError(`stop at ${i} ${o}`);
output[o++] = code(rand);
}
return o;
}
const input = new Float64Array(64);
let seed = 42;
for (let k = 0; k < input.length; k++) {
seed = (seed * 3877 + 29573) % 139968;
input[k] = seed / 139968;
}
function show(bytes: Uint8Array): string {
let text = "";
for (const b of bytes) text += b === 10 ? "|" : b === 0 ? "." : String.fromCharCode(b);
return text;
}
const starts = [0, 3, -0, -2, 2.5, NaN, Infinity, -Infinity, 2 ** 48, 2 ** 48 + 2, 2 ** 53, 60];
for (const start of starts) {
const output = new Uint8Array(40);
const end = lines(input, output, 20, start);
console.log("lines", start, end, show(output));
}
for (const count of [0, 1, 6, 7, 2.5, -3, NaN]) {
const output = new Uint8Array(24);
console.log("count", count, lines(input, output, count, 0), show(output));
}
for (const [line, i, o] of [
[5, 0, 10],
[5, -1, 4],
[3.5, 2, 8],
[NaN, 0, 5],
[4, 0.5, 6],
[4, 2 ** 48, 2 ** 48],
[4, 1, -0],
] as const) {
const output = new Uint8Array(12);
console.log("reverse", line, i, o, reverse(input, output, line, i, o), show(output));
}
for (const [line, i, o] of [
[10, 0, 0],
[64, 5, -3],
[3, 2 ** 48 - 1, 7],
[8, 1.5, 0],
] as const) {
console.log("find", line, i, o, find(input, line, i, o));
}
for (const [line, o] of [
[4, 0],
[4, 2],
[2, NaN],
] as const) {
const output = new Uint8Array(8);
console.log("restart", line, o, restart(input, output, line, o), show(output));
}
for (const start of [0, 20, 40, 2.5]) {
const output = new Uint8Array(64);
try {
console.log("guarded", start, guarded(input, output, 30, start), show(output));
} catch (error) {
console.log("guarded", start, error instanceof RangeError, (error as Error).message);
}
}
@@ -0,0 +1 @@
{ "plain": 5800, "san": 6664 }