mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Merge main
This commit is contained in:
@@ -47,7 +47,8 @@ interface DenseGuard {
|
||||
* then extends the length over hole states exactly the same way). An index
|
||||
* below the hole-free prefix bound branches to `filledLabel` first; both
|
||||
* that block and the returned (state-checked) dense block may use the
|
||||
* guard's values. */
|
||||
* guard's values. A `proven` index is known to lie below the bound (a dense
|
||||
* array loop's fast copy, dense-array-loops.ts) and always takes it. */
|
||||
function emitDenseGuard(
|
||||
host: LlvmEmitterContext,
|
||||
array: string,
|
||||
@@ -56,6 +57,7 @@ function emitDenseGuard(
|
||||
slow: string,
|
||||
requireBelowLen: boolean,
|
||||
filledLabel: string,
|
||||
proven = false,
|
||||
): DenseGuard {
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
@@ -137,8 +139,10 @@ function emitDenseGuard(
|
||||
B.line(`${offset} = trunc i64 ${wide} to i32`);
|
||||
}
|
||||
// The bound never exceeds the length or the dense capacity.
|
||||
let inFilled = B.tmp();
|
||||
B.line(`${inFilled} = icmp ult i64 ${wide}, ${filledWide}`);
|
||||
// The proof covers the counter's integer value only.
|
||||
const below = proven && integral === null;
|
||||
let inFilled = below ? "true" : B.tmp();
|
||||
if (!below) B.line(`${inFilled} = icmp ult i64 ${wide}, ${filledWide}`);
|
||||
if (integral) {
|
||||
const t = B.tmp();
|
||||
B.line(`${t} = and i1 ${integral}, ${inFilled}`);
|
||||
@@ -272,6 +276,7 @@ export function emitDenseArrayGet(
|
||||
element: IrType,
|
||||
getter: string,
|
||||
retain: boolean,
|
||||
proven = false,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
@@ -285,7 +290,7 @@ export function emitDenseArrayGet(
|
||||
const slow = B.newLabel("arr.get.slow"),
|
||||
join = B.newLabel("arr.get.join"),
|
||||
valueLabel = B.newLabel("arr.get.value");
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, valueLabel);
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, true, valueLabel, proven);
|
||||
const state = loadState(host, guard);
|
||||
const present = B.tmp();
|
||||
B.line(`${present} = icmp eq i8 ${state}, ${SCR_ARR_VALUE}`);
|
||||
@@ -331,6 +336,7 @@ export function emitDenseArrayNumber(
|
||||
array: string,
|
||||
index: LlValue,
|
||||
indexExpr: IrExpr | undefined,
|
||||
proven = false,
|
||||
): string {
|
||||
return emitDenseArrayGet(
|
||||
host,
|
||||
@@ -341,6 +347,7 @@ export function emitDenseArrayNumber(
|
||||
{ kind: "f64" },
|
||||
"scr_arr_get_number",
|
||||
false,
|
||||
proven,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -399,6 +406,7 @@ export function emitDenseArraySet(
|
||||
acc: ArrayAccess,
|
||||
element: IrType,
|
||||
value: string,
|
||||
proven = false,
|
||||
): void {
|
||||
const B = host.B;
|
||||
const ty = acc === "f64" ? "double" : acc === "bool" ? "i1" : "ptr";
|
||||
@@ -413,7 +421,7 @@ export function emitDenseArraySet(
|
||||
const slow = B.newLabel("arr.set.slow"),
|
||||
join = B.newLabel("arr.set.join"),
|
||||
filledLabel = B.newLabel("arr.set.filled");
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, false, filledLabel);
|
||||
const guard = emitDenseGuard(host, array, index, indexExpr, slow, false, filledLabel, proven);
|
||||
const { offset, len } = guard;
|
||||
const storeValue = (valuePtr: string): void => {
|
||||
if (acc === "f64") {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { BOOL, F64, arrayOf, type IrExpr, type IrLocal, type IrStmt } from "../../ir/ir.js";
|
||||
import {
|
||||
countedDenseShape,
|
||||
matchDenseArrayLoop,
|
||||
type DenseLoopShape,
|
||||
} from "./dense-array-loops.js";
|
||||
|
||||
const loc = { file: "dense-array-loops.ts", start: 0, end: 0 };
|
||||
const ARR = arrayOf(F64);
|
||||
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc });
|
||||
const arr = (localId: string): IrExpr & { kind: "varRef" } => ({
|
||||
kind: "varRef",
|
||||
localId,
|
||||
type: ARR,
|
||||
loc,
|
||||
});
|
||||
const method = (
|
||||
receiver: string,
|
||||
name: Extract<IrExpr, { kind: "arrIntrinsic" }>["method"],
|
||||
args: IrExpr[] = [],
|
||||
): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: name,
|
||||
receiver: arr(receiver),
|
||||
args,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const read = (receiver: string, index = ref("i")): IrExpr => method(receiver, "getNumber", [index]);
|
||||
const write = (receiver: string, value: IrExpr, index = ref("i")): IrStmt => ({
|
||||
kind: "arraySet",
|
||||
arr: arr(receiver),
|
||||
index,
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
const locals = new Map<string, IrLocal>([
|
||||
["i", { id: "i", name: "i", type: F64, mutable: true }],
|
||||
["a", { id: "a", name: "a", type: ARR, mutable: false }],
|
||||
["b", { id: "b", name: "b", type: ARR, mutable: true }],
|
||||
["c", { id: "c", name: "c", type: ARR, mutable: false }],
|
||||
]);
|
||||
|
||||
/** for (let i = 0; i < a.length; i++) body */
|
||||
function loop(body: IrStmt[]): IrStmt & { kind: "for" } {
|
||||
return {
|
||||
kind: "for",
|
||||
loc,
|
||||
init: { kind: "varDecl", localId: "i", init: num(0), loc },
|
||||
cond: { kind: "bin", op: "<", left: ref("i"), right: method("a", "length"), type: BOOL, loc },
|
||||
update: {
|
||||
kind: "assign",
|
||||
localId: "i",
|
||||
value: { kind: "bin", op: "+", left: ref("i"), right: num(1), type: F64, loc },
|
||||
loc,
|
||||
},
|
||||
body,
|
||||
};
|
||||
}
|
||||
const shape: DenseLoopShape = {
|
||||
counter: "i",
|
||||
length: arr("a"),
|
||||
range: { min: 0, max: 4294967294 },
|
||||
};
|
||||
const match = (body: IrStmt[], initialized = new Set(["a", "b"])) =>
|
||||
matchDenseArrayLoop(loop(body), shape, locals, new Set(), initialized);
|
||||
|
||||
test("arrays indexed at the counter of a length-preserving loop get an int32 fast copy", () => {
|
||||
const scale = write("b", {
|
||||
kind: "bin",
|
||||
op: "*",
|
||||
left: read("a"),
|
||||
right: num(2),
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const dense = match([scale]);
|
||||
expect([...dense!.arrays.keys()].sort()).toEqual(["a", "b"]);
|
||||
expect(dense!.range).toEqual({ min: 0, max: 2147483646 });
|
||||
// A binding not definitely assigned before the loop is not read by the guard.
|
||||
expect([...match([scale], new Set())!.arrays.keys()]).toEqual(["a"]);
|
||||
});
|
||||
|
||||
test("loops that may resize, rebind or move storage keep the ordinary copy only", () => {
|
||||
expect(match([{ kind: "exprStmt", expr: method("a", "push", [num(1)]), loc }])).toBeNull();
|
||||
expect(match([write("a", num(0), ref("unknown"))])).toBeNull();
|
||||
expect(
|
||||
match([
|
||||
{ kind: "exprStmt", expr: { kind: "call", callee: "f", args: [], type: F64, loc }, loc },
|
||||
]),
|
||||
).toBeNull();
|
||||
// A nested loop would be duplicated too.
|
||||
expect(match([{ kind: "exprStmt", expr: read("a"), loc }, loop([])])).toBeNull();
|
||||
// No access at the counter: nothing to prove.
|
||||
expect(match([{ kind: "exprStmt", expr: read("a", num(0)), loc }])).toBeNull();
|
||||
// Copying the bounding array into another binding lets it escape.
|
||||
expect(
|
||||
match([
|
||||
{ kind: "exprStmt", expr: read("a"), loc },
|
||||
{ kind: "assign", localId: "b", value: arr("a"), loc },
|
||||
]),
|
||||
).toBeNull();
|
||||
// A rebound array is not covered, but the bounding array still is.
|
||||
const rebound = match([
|
||||
{ kind: "exprStmt", expr: read("b"), loc },
|
||||
{ kind: "exprStmt", expr: read("a"), loc },
|
||||
{ kind: "assign", localId: "b", value: arr("c"), loc },
|
||||
]);
|
||||
expect([...rebound!.arrays.keys()]).toEqual(["a"]);
|
||||
});
|
||||
|
||||
test("counted shapes need a non-negative counter strictly below the length", () => {
|
||||
const ascending = (limit: IrExpr, inclusive: boolean, min = 0) =>
|
||||
countedDenseShape(loop([]), {
|
||||
localId: "i",
|
||||
start: num(min),
|
||||
limit,
|
||||
step: 1,
|
||||
inclusive,
|
||||
range: { min, max: 100 },
|
||||
});
|
||||
const minus = (offset: number): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "-",
|
||||
left: method("a", "length"),
|
||||
right: num(offset),
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
expect(ascending(method("a", "length"), false)?.length.localId).toBe("a");
|
||||
expect(ascending(minus(1), true)?.length.localId).toBe("a");
|
||||
expect(ascending(method("a", "length"), true)).toBeNull();
|
||||
expect(ascending(minus(1), false, -1)).toBeNull();
|
||||
const descending = (offset: number) =>
|
||||
countedDenseShape(loop([]), {
|
||||
localId: "i",
|
||||
start: minus(offset),
|
||||
limit: num(0),
|
||||
step: -1,
|
||||
inclusive: true,
|
||||
range: { min: 0, max: 100 },
|
||||
});
|
||||
expect(descending(1)?.length.localId).toBe("a");
|
||||
expect(descending(0)).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,215 @@
|
||||
/* Dense array loops: a versioned copy of an index loop over an array whose
|
||||
* length the loop never changes.
|
||||
*
|
||||
* `for (let i = 0; i < a.length; i++) b[i] = a[i] * 2` reads its bound from
|
||||
* `a`, and every element access at the counter normally tests the index
|
||||
* against the array's hole-free prefix bound (`filled`, see
|
||||
* dense-array-access.ts) before it touches the slot. When the loop provably
|
||||
* never changes `a.length` (ir/array-length-stability.ts) and never moves or
|
||||
* cuts the storage of the arrays it indexes, one test before the loop covers
|
||||
* every pass: if `a.length` is at most each indexed array's `filled` bound,
|
||||
* every counter value the loop can reach (at least 0, below `a.length`)
|
||||
* names a present value in dense storage. The fast copy then reads and
|
||||
* replaces those slots directly, which leaves LLVM a loop without
|
||||
* per-element branches (vectorizable where the body allows it). The same
|
||||
* test bounds `a.length` by 2^31 - 1, so the fast copy's counter is an int32.
|
||||
* Arrays longer than that are sparse in practice (a dense one would need
|
||||
* 16 GB of slots) and take the ordinary copy, as does every array with a
|
||||
* hole, present undefined or sparse storage below `a.length`. */
|
||||
|
||||
import type { IrExpr, IrLocal, IrStmt } from "../../ir/ir.js";
|
||||
import type { IntegerRange } from "../../ir/integer-ranges.js";
|
||||
import { preservesArrayLength } from "../../ir/array-length-stability.js";
|
||||
import { everyExprChild, everyStmtChild } from "../../ir/traverse.js";
|
||||
import type { LlvmEmitterContext } from "./expr-context.js";
|
||||
import { SCR_ARR_FIELD } from "./common.js";
|
||||
|
||||
export const INT32_MAX = 2147483647;
|
||||
|
||||
export interface DenseArrayLoop {
|
||||
/** The loop's counter binding. */
|
||||
counter: string;
|
||||
/** The array whose length bounds the counter. */
|
||||
length: IrExpr & { kind: "varRef" };
|
||||
/** Arrays whose accesses at the counter are proven to be inside the
|
||||
* hole-free prefix in the fast copy, with a read of each binding. */
|
||||
arrays: ReadonlyMap<string, IrExpr & { kind: "varRef" }>;
|
||||
/** The counter's range on every pass of the fast copy. */
|
||||
range: IntegerRange;
|
||||
}
|
||||
|
||||
/** The fast copy duplicates the loop; keep the duplicated code small. */
|
||||
const MAX_LOOP_NODES = 120;
|
||||
|
||||
/** How a matched integer loop bounds its counter below `length.length`. */
|
||||
export interface DenseLoopShape {
|
||||
counter: string;
|
||||
length: IrExpr & { kind: "varRef" };
|
||||
/** The counter's proven range on every pass of the ordinary loop; the
|
||||
* fast copy keeps its minimum, which must be at least 0. */
|
||||
range: IntegerRange;
|
||||
}
|
||||
|
||||
/** `length.length - c` (c an integer literal, 0 when absent). */
|
||||
function lengthOffset(e: IrExpr): { receiver: IrExpr & { kind: "varRef" }; offset: number } | null {
|
||||
let offset = 0;
|
||||
if (
|
||||
e.kind === "bin" &&
|
||||
e.op === "-" &&
|
||||
e.right.kind === "numLit" &&
|
||||
Number.isSafeInteger(e.right.value)
|
||||
) {
|
||||
offset = e.right.value;
|
||||
e = e.left;
|
||||
}
|
||||
return e.kind === "arrIntrinsic" &&
|
||||
e.method === "length" &&
|
||||
e.args.length === 0 &&
|
||||
e.receiver.kind === "varRef" &&
|
||||
e.receiver.type.kind === "array"
|
||||
? { receiver: e.receiver, offset }
|
||||
: null;
|
||||
}
|
||||
|
||||
/** The array bounding a counted loop's counter below its length: an
|
||||
* ascending loop compared below `a.length - c`, or a descending one starting
|
||||
* at `a.length - c` (c >= 1). */
|
||||
export function countedDenseShape(
|
||||
loop: IrStmt & { kind: "for" },
|
||||
counted: {
|
||||
localId: string;
|
||||
start: IrExpr;
|
||||
limit: IrExpr;
|
||||
step: number;
|
||||
inclusive: boolean;
|
||||
range: IntegerRange;
|
||||
},
|
||||
): DenseLoopShape | null {
|
||||
if (counted.range.min < 0) return null;
|
||||
if (counted.step > 0) {
|
||||
const bound = lengthOffset(counted.limit);
|
||||
if (!bound || bound.offset < (counted.inclusive ? 1 : 0)) return null;
|
||||
return { counter: counted.localId, length: bound.receiver, range: counted.range };
|
||||
}
|
||||
// The start is evaluated once, before the loop; the counter only falls.
|
||||
const start = lengthOffset(counted.start);
|
||||
if (!start || start.offset < 1 || loop.init?.kind !== "varDecl") return null;
|
||||
return { counter: counted.localId, length: start.receiver, range: counted.range };
|
||||
}
|
||||
|
||||
/** Match the fast copy of a loop of the given shape. `initialized` holds the
|
||||
* bindings definitely assigned when the loop starts; `unaliased` the local
|
||||
* arrays no other binding holds. */
|
||||
export function matchDenseArrayLoop(
|
||||
loop: IrStmt & { kind: "for" },
|
||||
shape: DenseLoopShape,
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
unaliased: ReadonlySet<string>,
|
||||
initialized: ReadonlySet<string>,
|
||||
): DenseArrayLoop | null {
|
||||
const { counter, length } = shape;
|
||||
if (shape.range.min < 0 || !loop.cond) return null;
|
||||
let nodes = 0,
|
||||
nested = false;
|
||||
const candidates = new Map<string, IrExpr & { kind: "varRef" }>();
|
||||
// Arrays written, resized, or indexed other than at the counter by an
|
||||
// element write.
|
||||
const disturbed = new Set<string>();
|
||||
const access = (arr: IrExpr, index: IrExpr): void => {
|
||||
if (arr.kind === "varRef" && index.kind === "varRef" && index.localId === counter)
|
||||
candidates.set(arr.localId, arr);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
if (++nodes > MAX_LOOP_NODES) return false;
|
||||
if (e.kind === "assignExpr" || e.kind === "incDec") disturbed.add(e.localId);
|
||||
if (e.kind === "arrayGet") access(e.arr, e.index);
|
||||
if (e.kind === "arrIntrinsic") {
|
||||
if (e.method === "getNumber" && e.args[0]) access(e.receiver, e.args[0]);
|
||||
else if (e.method !== "length" && e.receiver.kind === "varRef")
|
||||
disturbed.add(e.receiver.localId);
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
if (++nodes > MAX_LOOP_NODES) return false;
|
||||
if (s.kind === "for" || s.kind === "while" || s.kind === "doWhile" || s.kind === "forOf")
|
||||
nested = true;
|
||||
if (s.kind === "assign" || s.kind === "varDecl") disturbed.add(s.localId);
|
||||
if (s.kind === "arraySet") {
|
||||
access(s.arr, s.index);
|
||||
if (s.arr.kind === "varRef" && (s.index.kind !== "varRef" || s.index.localId !== counter))
|
||||
disturbed.add(s.arr.localId);
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
if (!loop.body.every(stmt) || !expr(loop.cond) || nested) return null;
|
||||
if (
|
||||
!preservesArrayLength(
|
||||
[loop.cond, ...loop.body],
|
||||
{ receiver: length.localId, inRange: counter },
|
||||
locals,
|
||||
unaliased,
|
||||
)
|
||||
)
|
||||
return null;
|
||||
const arrays = new Map<string, IrExpr & { kind: "varRef" }>();
|
||||
for (const [id, ref] of candidates) {
|
||||
const local = locals.get(id);
|
||||
if (
|
||||
!local ||
|
||||
local.type.kind !== "array" ||
|
||||
local.boxed ||
|
||||
local.tdz ||
|
||||
disturbed.has(id) ||
|
||||
(id !== length.localId && !initialized.has(id))
|
||||
)
|
||||
continue;
|
||||
arrays.set(id, ref);
|
||||
}
|
||||
if (arrays.size === 0) return null;
|
||||
return {
|
||||
counter,
|
||||
length,
|
||||
arrays,
|
||||
range: { min: shape.range.min, max: Math.min(shape.range.max, INT32_MAX - 1) },
|
||||
};
|
||||
}
|
||||
|
||||
/** The fast copy's entry test: the bounding length is at most 2^31 - 1 and
|
||||
* at most every indexed array's hole-free prefix bound. */
|
||||
export function emitDenseLoopGuard(host: LlvmEmitterContext, loop: DenseArrayLoop): string {
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
const header = (array: string, field: number): string => {
|
||||
const ptr = B.tmp();
|
||||
B.line(`${ptr} = getelementptr inbounds %ScrArr, ptr ${array}, i32 0, i32 ${field}`);
|
||||
host.markMemoryPointer(ptr, "array:header");
|
||||
return ptr;
|
||||
};
|
||||
const receiver = host.emitStableReceiver(loop.length, []);
|
||||
const lenPtr = header(receiver.name, 1),
|
||||
len = B.tmp(),
|
||||
small = B.tmp();
|
||||
B.line(
|
||||
`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}${host.arrayLengthRange}`,
|
||||
);
|
||||
B.line(`${small} = icmp ule ${size} ${len}, ${INT32_MAX}`);
|
||||
let valid = small;
|
||||
for (const [id, ref] of loop.arrays) {
|
||||
const array = id === loop.length.localId ? receiver : host.emitStableReceiver(ref, []);
|
||||
const filledPtr = header(array.name, SCR_ARR_FIELD.filled),
|
||||
filled = B.tmp(),
|
||||
covered = B.tmp(),
|
||||
both = B.tmp();
|
||||
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
|
||||
let bound = filled;
|
||||
if (size === "i64") {
|
||||
bound = B.tmp();
|
||||
B.line(`${bound} = zext i32 ${filled} to i64`);
|
||||
}
|
||||
B.line(`${covered} = icmp ule ${size} ${len}, ${bound}`);
|
||||
B.line(`${both} = and i1 ${valid}, ${covered}`);
|
||||
valid = both;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
@@ -55,6 +55,13 @@ import {
|
||||
matchIntegerBytesForLoop,
|
||||
matchIntegerCountedForLoop,
|
||||
} from "../../ir/integer-loops.js";
|
||||
import { findUnaliasedLocalArrays } from "../../ir/array-length-stability.js";
|
||||
import {
|
||||
countedDenseShape,
|
||||
emitDenseLoopGuard,
|
||||
matchDenseArrayLoop,
|
||||
type DenseArrayLoop,
|
||||
} from "./dense-array-loops.js";
|
||||
import { emitCountedLoopLimit } from "./counted-loops.js";
|
||||
import {
|
||||
emitSafeIntegerCheck,
|
||||
@@ -229,7 +236,10 @@ import { findAppendedArrayLiterals } from "./appended-array-literals.js";
|
||||
import { AmbientReceiverReaders, findConstantCallbacks } from "./constant-callbacks.js";
|
||||
import {
|
||||
findScalarStringSlices,
|
||||
findScratchStringSlices,
|
||||
findLentStringParameters,
|
||||
emitStringSliceSnapshot,
|
||||
type LentStringParameters,
|
||||
type StringSliceSnapshot,
|
||||
} from "./string-slices.js";
|
||||
import { LazyCaptures } from "./lazy-captures.js";
|
||||
@@ -623,6 +633,10 @@ export class LlEmitter {
|
||||
private loopArrayBorrows: ReadonlySet<IrStmt> = new Set();
|
||||
private scalarStringSlices = new Map<string, IrExpr & { kind: "strIntrinsic" }>();
|
||||
readonly stringSlices = new Map<string, StringSliceSnapshot>();
|
||||
/** Slices written into stack buffers (findScratchStringSlices). */
|
||||
scratchStringSlices: ReadonlySet<IrExpr> = new Set();
|
||||
/** String parameters a caller may lend scratch slices to. */
|
||||
private lentStringParameters: LentStringParameters = new Map();
|
||||
private localStackUnions = new Map<string, IrExpr & { kind: "unionWrap" }>();
|
||||
private localStackObjects = new Map<string, StackObjectSource>();
|
||||
private reassignedStackObjects = new Set<string>();
|
||||
@@ -674,6 +688,21 @@ export class LlEmitter {
|
||||
return this.sizeType === "i64" ? ", !range !{i64 0, i64 4294967296}" : "";
|
||||
}
|
||||
|
||||
/** Whether an element access at `index` of `arr` lies in the hole-free
|
||||
* prefix: the counter of a dense array loop's fast copy indexing one of
|
||||
* the arrays its entry test covered. */
|
||||
provenDenseIndex(arr: IrExpr, index: IrExpr): boolean {
|
||||
const loop = this.denseLoop;
|
||||
return (
|
||||
loop !== null &&
|
||||
arr.kind === "varRef" &&
|
||||
loop.arrays.has(arr.localId) &&
|
||||
index.kind === "varRef" &&
|
||||
index.localId === loop.counter &&
|
||||
this.integerLoopBindings.has(loop.counter)
|
||||
);
|
||||
}
|
||||
|
||||
fieldAliasAttachment(ptr: string): string {
|
||||
const tag = this.fieldPointerTags.get(ptr);
|
||||
return tag === undefined ? "" : `, !tbaa !${tag}`;
|
||||
@@ -1030,6 +1059,14 @@ export class LlEmitter {
|
||||
* immortal from construction (classes.ts programLifetimeClass). */
|
||||
readonly programLifetime: ReadonlySet<string>;
|
||||
private numericLocals = new Map<string, IrLocal>();
|
||||
/** Local arrays of the current function that no other binding can hold
|
||||
* (ir/array-length-stability.ts). */
|
||||
private unaliasedArrays: ReadonlySet<string> = new Set();
|
||||
/** Dense array loops (dense-array-loops.ts) run in this function. */
|
||||
private denseLoopsEnabled = false;
|
||||
private denseLoopEntries: ReturnType<typeof findInitializedByteLoopBindings> = new Map();
|
||||
/** The fast copy of a dense array loop being emitted. */
|
||||
private denseLoop: DenseArrayLoop | null = null;
|
||||
private captureIds = new Set<string>();
|
||||
/** Active induction bindings retain their width independently of size_t. */
|
||||
integerLoopBindings = new Map<
|
||||
@@ -1325,6 +1362,8 @@ export class LlEmitter {
|
||||
return shape !== undefined && !recordDynKeepsIdentity(shape);
|
||||
},
|
||||
);
|
||||
this.lentStringParameters =
|
||||
this.debug === null ? findLentStringParameters(this.fnByName, this.callLifetimes) : new Map();
|
||||
this.constantCallbacks = findConstantCallbacks(mod, this.callLifetimes);
|
||||
// Integer clones signal a stack overflow through their result, which
|
||||
// needs the single-threaded guard; worker programs also poll for
|
||||
@@ -6675,6 +6714,7 @@ export class LlEmitter {
|
||||
const initializerBindings = this.initializerBindings.get(fn.name) ?? [];
|
||||
const numericFn = withInitializerBindings(fn, initializerBindings);
|
||||
this.numericLocals = new Map(numericFn.locals.map((l) => [l.id, l]));
|
||||
this.unaliasedArrays = findUnaliasedLocalArrays(fn);
|
||||
this.borrowedParameters.clear();
|
||||
this.referenceWrites?.enter(fn);
|
||||
this.reboundOwnerSlots.clear();
|
||||
@@ -6713,6 +6753,12 @@ export class LlEmitter {
|
||||
this.finalFieldBorrows =
|
||||
!fn.async && !fn.generator && constructorClass(fn.name) === null && this.finalFields.size > 0;
|
||||
this.byteWindowsEnabled = this.countedLoopsEnabled;
|
||||
this.denseLoopsEnabled = this.countedLoopsEnabled;
|
||||
this.denseLoop = null;
|
||||
this.denseLoopEntries =
|
||||
this.denseLoopsEnabled && numericFn.locals.some((l) => l.type.kind === "array")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
: new Map();
|
||||
this.byteWindowEntries =
|
||||
this.byteWindowsEnabled && numericFn.locals.some((l) => l.type.kind === "bytes")
|
||||
? findInitializedByteLoopBindings(fn)
|
||||
@@ -6750,6 +6796,15 @@ export class LlEmitter {
|
||||
);
|
||||
this.scalarStringSlices =
|
||||
this.debug === null ? findScalarStringSlices(fn, this.callLifetimes) : new Map();
|
||||
this.scratchStringSlices =
|
||||
this.debug === null
|
||||
? findScratchStringSlices(
|
||||
fn,
|
||||
this.callLifetimes,
|
||||
this.scalarStringSlices,
|
||||
this.lentStringParameters,
|
||||
)
|
||||
: new Set();
|
||||
this.mapReadLifetimes = findMapReadLifetimes(fn, this.boxedUnionsById, this.callLifetimes);
|
||||
this.readNumberMapWrites = findReadNumberMapWrites(fn);
|
||||
this.mapEntryFusion = this.debug === null ? findMapEntryFusions(fn) : null;
|
||||
@@ -8185,7 +8240,16 @@ export class LlEmitter {
|
||||
const acc = elemAccess(s.arr.type.elem);
|
||||
this.emitPublishedArrayGuard(arr.name, s.arr.type, 0, idx.name);
|
||||
if (acc === "ref") this.moveTemp(v);
|
||||
emitDenseArraySet(this, arr.name, idx, s.index, acc, s.arr.type.elem, v.name);
|
||||
emitDenseArraySet(
|
||||
this,
|
||||
arr.name,
|
||||
idx,
|
||||
s.index,
|
||||
acc,
|
||||
s.arr.type.elem,
|
||||
v.name,
|
||||
this.provenDenseIndex(s.arr, s.index),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "arraySetLength": {
|
||||
@@ -8632,7 +8696,12 @@ export class LlEmitter {
|
||||
(this.debug === null ? matchIntegerBytesForLoop(s, this.numericLocals) : null);
|
||||
const countedLoop =
|
||||
!integerLoop && this.countedLoopsEnabled
|
||||
? matchIntegerCountedForLoop(s, this.numericLocals, this.integerRanges)
|
||||
? matchIntegerCountedForLoop(
|
||||
s,
|
||||
this.numericLocals,
|
||||
this.integerRanges,
|
||||
this.unaliasedArrays,
|
||||
)
|
||||
: null;
|
||||
const window =
|
||||
countedLoop && this.byteWindowsEnabled
|
||||
@@ -8645,6 +8714,49 @@ export class LlEmitter {
|
||||
this.byteWindowEntries.get(s) ?? new Set(),
|
||||
)
|
||||
: null;
|
||||
const denseShape = !this.denseLoopsEnabled
|
||||
? null
|
||||
: integerArrayLoop
|
||||
? {
|
||||
counter: integerArrayLoop.localId,
|
||||
length: integerArrayLoop.limitReceiver as IrExpr & { kind: "varRef" },
|
||||
range: { min: 0, max: 4294967294 },
|
||||
}
|
||||
: countedLoop && !countedLoop.guarded
|
||||
? countedDenseShape(s, countedLoop)
|
||||
: null;
|
||||
const dense =
|
||||
denseShape && !window
|
||||
? matchDenseArrayLoop(
|
||||
s,
|
||||
denseShape,
|
||||
this.numericLocals,
|
||||
this.unaliasedArrays,
|
||||
this.denseLoopEntries.get(s) ?? new Set(),
|
||||
)
|
||||
: null;
|
||||
if (dense) {
|
||||
const valid = emitDenseLoopGuard(this, dense);
|
||||
const fast = B.newLabel("dense.loop.fast"),
|
||||
slow = B.newLabel("dense.loop.slow"),
|
||||
done = B.newLabel("dense.loop.done");
|
||||
B.condBr(valid, fast, slow);
|
||||
this.denseLoopsEnabled = false;
|
||||
try {
|
||||
B.startBlock(slow);
|
||||
this.emitStmt(s);
|
||||
B.br(done);
|
||||
B.startBlock(fast);
|
||||
this.denseLoop = dense;
|
||||
this.emitStmt(s);
|
||||
B.br(done);
|
||||
} finally {
|
||||
this.denseLoop = null;
|
||||
this.denseLoopsEnabled = true;
|
||||
}
|
||||
B.startBlock(done);
|
||||
break;
|
||||
}
|
||||
if (window) {
|
||||
const valid = emitByteWindowGuard(this, window);
|
||||
const fast = B.newLabel("bytes.window.fast"),
|
||||
@@ -8747,7 +8859,10 @@ export class LlEmitter {
|
||||
this.integerLoopBindings.set(integerLoop.localId, {
|
||||
slot: integerSlot,
|
||||
type: this.sizeType,
|
||||
range: { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 },
|
||||
range:
|
||||
this.denseLoop?.counter === integerLoop.localId
|
||||
? this.denseLoop.range
|
||||
: { min: 0, max: integerArrayLoop ? 4294967294 : Number.MAX_SAFE_INTEGER - 1 },
|
||||
});
|
||||
} else if (countedLoop) {
|
||||
integerSlot = B.slot();
|
||||
@@ -8762,7 +8877,10 @@ export class LlEmitter {
|
||||
slot: integerSlot,
|
||||
type: "i64",
|
||||
signed: true,
|
||||
range: countedLoop.range,
|
||||
range:
|
||||
this.denseLoop?.counter === countedLoop.localId
|
||||
? this.denseLoop.range
|
||||
: countedLoop.range,
|
||||
});
|
||||
countedLimit = emitCountedLoopLimit(this, countedLoop);
|
||||
} else if (s.init) {
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
} from "./map-number-lookup.js";
|
||||
import { emitBorrowedInput } from "./borrowed-inputs.js";
|
||||
import { emitFusedMapSet, rememberMapEntry } from "./map-entry-fusion.js";
|
||||
import { emitStringSliceRead } from "./string-slices.js";
|
||||
import { emitScratchStringSlice, emitStringSliceRead } from "./string-slices.js";
|
||||
import {
|
||||
STRING_CHAR_CODE_AT_DECL,
|
||||
STRING_CHAR_CODE_AT_INT_MARKER,
|
||||
@@ -395,6 +395,7 @@ export function emitStrIntrinsic(
|
||||
false,
|
||||
);
|
||||
case "slice":
|
||||
if (host.scratchStringSlices.has(e)) return emitScratchStringSlice(host, e, r, args);
|
||||
return call(
|
||||
"scr_str_slice",
|
||||
"ptr (ptr, double, double)",
|
||||
@@ -403,6 +404,7 @@ export function emitStrIntrinsic(
|
||||
true,
|
||||
);
|
||||
case "substring":
|
||||
if (host.scratchStringSlices.has(e)) return emitScratchStringSlice(host, e, r, args);
|
||||
return call(
|
||||
"scr_str_substring",
|
||||
"ptr (ptr, double, double)",
|
||||
@@ -575,7 +577,16 @@ export function emitArrIntrinsic(
|
||||
? host.constantNumericTables.get(e.receiver.localId)
|
||||
: undefined;
|
||||
if (!table && elem.kind === "f64")
|
||||
return { name: emitDenseArrayNumber(host, r.name, index, e.args[0]), type: e.type };
|
||||
return {
|
||||
name: emitDenseArrayNumber(
|
||||
host,
|
||||
r.name,
|
||||
index,
|
||||
e.args[0],
|
||||
host.provenDenseIndex(e.receiver, e.args[0]!),
|
||||
),
|
||||
type: e.type,
|
||||
};
|
||||
host.declare(`declare double @scr_arr_get_number(ptr, double)`);
|
||||
const t = B.tmp();
|
||||
B.line(
|
||||
|
||||
@@ -64,16 +64,25 @@ export function emitLiteralExpr(
|
||||
const integer = B.tmp();
|
||||
const number = B.tmp();
|
||||
B.line(`${integer} = load ${integerSlot.type}, ptr ${integerSlot.slot}`);
|
||||
// A 64-bit induction value is a safe integer, so its sign bit is
|
||||
// clear and the signed conversion is exact: one cvtsi2sd instead of
|
||||
// x86-64's multi-instruction unsigned 64-bit conversion.
|
||||
const signedConversion = integerSlot.signed === true || integerSlot.type === "i64";
|
||||
B.line(
|
||||
`${number} = ${signedConversion ? "sitofp" : "uitofp"} ${integerSlot.type} ${integer} to double`,
|
||||
);
|
||||
const uint32 = integerSlot.type === "i32" ? integer : B.tmp();
|
||||
if (integerSlot.type !== "i32")
|
||||
B.line(`${uint32} = trunc ${integerSlot.type} ${integer} to i32`);
|
||||
// An int32 counter (a dense array loop's) converts from its low half,
|
||||
// so LLVM folds a later `fptosi` of the double (an inlined callee's
|
||||
// index) back to the integer. Otherwise a 64-bit induction value is
|
||||
// a safe integer, so its sign bit is clear and the signed conversion
|
||||
// is exact: one cvtsi2sd instead of x86-64's multi-instruction
|
||||
// unsigned 64-bit conversion. (A uint32 counter would fold through
|
||||
// `uitofp i32` too, but the zero extension it adds on x86-64 cost
|
||||
// more in loops that never convert the double than it saved.)
|
||||
const range = integerSlot.range;
|
||||
const signedConversion = integerSlot.signed === true || integerSlot.type === "i64";
|
||||
if (range && range.min >= -2147483648 && range.max <= 2147483647)
|
||||
B.line(`${number} = sitofp i32 ${uint32} to double`);
|
||||
else
|
||||
B.line(
|
||||
`${number} = ${signedConversion ? "sitofp" : "uitofp"} ${integerSlot.type} ${integer} to double`,
|
||||
);
|
||||
return {
|
||||
name: number,
|
||||
type: e.type,
|
||||
@@ -851,6 +860,7 @@ export function emitContainerExpr(
|
||||
elem,
|
||||
`scr_arr_get_${acc}`,
|
||||
true,
|
||||
host.provenDenseIndex(e.arr, e.index),
|
||||
);
|
||||
return host.own({ name: t, type: e.type });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import { BOOL, F64, STRING, VOID, type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import { analyzeCallLifetimes } from "./call-lifetimes.js";
|
||||
import { findScalarStringSlices } from "./string-slices.js";
|
||||
import {
|
||||
findLentStringParameters,
|
||||
findScalarStringSlices,
|
||||
findScratchStringSlices,
|
||||
} from "./string-slices.js";
|
||||
const loc = { file: "slices.ts", start: 0, end: 0 };
|
||||
const ref = (id: string): IrExpr => ({ kind: "varRef", localId: id, type: STRING, loc });
|
||||
function fixture(): IrFunction {
|
||||
@@ -95,3 +99,146 @@ test.each(["escape", "capture", "write", "other-consumer", "suspend"])(
|
||||
expect(facts(fn).size).toBe(0);
|
||||
},
|
||||
);
|
||||
|
||||
const slice = (receiver: string, start: number, end: number): IrExpr => ({
|
||||
kind: "strIntrinsic",
|
||||
method: "slice",
|
||||
receiver: ref(receiver),
|
||||
args: [
|
||||
{ kind: "numLit", value: start, type: F64, loc },
|
||||
{ kind: "numLit", value: end, type: F64, loc },
|
||||
],
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
const equals = (left: IrExpr): IrExpr => ({
|
||||
kind: "strEq",
|
||||
negated: false,
|
||||
left,
|
||||
right: { kind: "strLit", value: "GET", type: STRING, loc },
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const parse = (value: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "num.fromString",
|
||||
args: [value],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
function lending(body: IrStmt[]): IrFunction {
|
||||
return {
|
||||
name: "scan",
|
||||
params: [{ localId: "source", name: "source", type: STRING }],
|
||||
locals: [
|
||||
{ id: "source", name: "source", type: STRING, mutable: false },
|
||||
{ id: "word", name: "word", type: STRING, mutable: false },
|
||||
],
|
||||
returnType: VOID,
|
||||
loc,
|
||||
body,
|
||||
};
|
||||
}
|
||||
function scratch(fn: IrFunction, extra: IrFunction[] = []): Set<IrExpr> {
|
||||
const functions = new Map([fn, ...extra].map((f) => [f.name, f]));
|
||||
const lifetimes = analyzeCallLifetimes(functions);
|
||||
return findScratchStringSlices(
|
||||
fn,
|
||||
lifetimes,
|
||||
findScalarStringSlices(fn, lifetimes),
|
||||
findLentStringParameters(functions, lifetimes),
|
||||
);
|
||||
}
|
||||
test("slices read only by lending consumers use a stack buffer", () => {
|
||||
const inline = slice("source", 0, 3);
|
||||
const parsed = slice("source", 4, 6);
|
||||
const init = slice("source", 1, 5);
|
||||
const fn = lending([
|
||||
{ kind: "exprStmt", expr: equals(inline), loc },
|
||||
{ kind: "exprStmt", expr: parse(parsed), loc },
|
||||
{ kind: "varDecl", localId: "word", init, loc },
|
||||
{ kind: "exprStmt", expr: equals(ref("word")), loc },
|
||||
{ kind: "exprStmt", expr: parse(ref("word")), loc },
|
||||
]);
|
||||
expect(scratch(fn)).toEqual(new Set([inline, parsed, init]));
|
||||
});
|
||||
test.each(["return", "concat", "slice-of-slice", "capture", "async"])(
|
||||
"a slice local that may be kept (%s) stays owned",
|
||||
(reason) => {
|
||||
const init = slice("source", 1, 5);
|
||||
const fn = lending([
|
||||
{ kind: "varDecl", localId: "word", init, loc },
|
||||
{ kind: "exprStmt", expr: equals(ref("word")), loc },
|
||||
]);
|
||||
if (reason === "return") fn.body.push({ kind: "return", value: ref("word"), loc });
|
||||
if (reason === "concat")
|
||||
fn.body.push({
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "strConcat",
|
||||
left: { kind: "strLit", value: "", type: STRING, loc },
|
||||
right: ref("word"),
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
});
|
||||
if (reason === "slice-of-slice")
|
||||
fn.body.push({ kind: "exprStmt", expr: equals(slice("word", 0, 2)), loc });
|
||||
if (reason === "capture") fn.locals[1]!.boxed = true;
|
||||
if (reason === "async") fn.async = true;
|
||||
expect(scratch(fn).has(init)).toBe(false);
|
||||
},
|
||||
);
|
||||
function helper(name: string, body: IrStmt[]): IrFunction {
|
||||
return {
|
||||
name,
|
||||
params: [{ localId: "text", name: "text", type: STRING }],
|
||||
locals: [{ id: "text", name: "text", type: STRING, mutable: false }],
|
||||
returnType: BOOL,
|
||||
loc,
|
||||
body,
|
||||
};
|
||||
}
|
||||
const callOf = (callee: string, arg: IrExpr): IrExpr => ({
|
||||
kind: "call",
|
||||
callee,
|
||||
args: [arg],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
test("helper parameters read only by lending consumers accept stack slices", () => {
|
||||
const recursive = helper("isGet", [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "logical",
|
||||
op: "||",
|
||||
left: equals(ref("text")),
|
||||
right: callOf("isGet", ref("text")),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
]);
|
||||
const keeps = helper("keeps", [{ kind: "return", value: callOf("stores", ref("text")), loc }]);
|
||||
const stores = helper("stores", [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "arrayLit", elems: [ref("text")], type: { kind: "array", elem: STRING }, loc },
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
|
||||
]);
|
||||
const lent = slice("source", 0, 3);
|
||||
const kept = slice("source", 0, 4);
|
||||
const fn = lending([
|
||||
{ kind: "exprStmt", expr: callOf("isGet", lent), loc },
|
||||
{ kind: "exprStmt", expr: callOf("keeps", kept), loc },
|
||||
]);
|
||||
const functions = [recursive, keeps, stores];
|
||||
expect(scratch(fn, functions)).toEqual(new Set([lent]));
|
||||
const all = new Map([fn, ...functions].map((f) => [f.name, f]));
|
||||
expect([...findLentStringParameters(all, analyzeCallLifetimes(all)).keys()]).toEqual(["isGet"]);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { type IrExpr, type IrFunction, type IrStmt } from "../../ir/ir.js";
|
||||
import {
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrStmt,
|
||||
type IrStrIntrinsicMethod,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "../../ir/traverse.js";
|
||||
import { F64_INF, f64Lit, sizeToDouble } from "./common.js";
|
||||
import type { CallLifetimes } from "./call-lifetimes.js";
|
||||
@@ -6,6 +11,12 @@ import type { LlValue, LlvmEmitterContext } from "./expr-context.js";
|
||||
|
||||
type Slice = IrExpr & { kind: "strIntrinsic" };
|
||||
|
||||
function isSlice(value: IrExpr): value is Slice {
|
||||
return (
|
||||
value.kind === "strIntrinsic" && (value.method === "slice" || value.method === "substring")
|
||||
);
|
||||
}
|
||||
|
||||
/** A local used exclusively for scalar indexing needs no copied bytes.
|
||||
* Snapshot the receiver and normalized bounds at its initializer, including
|
||||
* side effects in the bounds. Escapes, captures, writes, suspension, and all
|
||||
@@ -111,3 +122,234 @@ export function emitStringSliceRead(host: LlvmEmitterContext, value: Slice): LlV
|
||||
);
|
||||
return { name: result, type: value.type };
|
||||
}
|
||||
|
||||
/** Bytes of the stack buffer behind one scratch slice, header included. */
|
||||
export const SCRATCH_SLICE_BYTES = 96;
|
||||
|
||||
/** String methods that read a lent receiver or argument only during their
|
||||
* runtime call and never return it. Methods returning a string can return
|
||||
* their unchanged receiver, so they stay on the owned path. */
|
||||
const LENDING_METHODS: ReadonlySet<IrStrIntrinsicMethod> = new Set([
|
||||
"length",
|
||||
"charCodeAt",
|
||||
"indexOf",
|
||||
"includes",
|
||||
"startsWith",
|
||||
"endsWith",
|
||||
]);
|
||||
|
||||
/** Numeric parsers read their text during the call. */
|
||||
const LENDING_LIB_FNS: ReadonlySet<string> = new Set([
|
||||
"num.fromString",
|
||||
"num.parseInt",
|
||||
"num.parseFloat",
|
||||
]);
|
||||
|
||||
/** Typed string-keyed collection operations: reads probe with the key, and
|
||||
* an insertion stores scr_str_scratch_keep's copy (scr_map_append). */
|
||||
const LENDING_KEY_METHODS: ReadonlySet<string> = new Set(["get", "has", "set", "add", "delete"]);
|
||||
|
||||
function stringKeyed(value: IrExpr & { kind: "mapIntrinsic" | "setIntrinsic" }): boolean {
|
||||
const receiver = value.receiver.type;
|
||||
if (receiver.kind === "map")
|
||||
return receiver.key.kind === "string" && receiver.value.kind !== "dyn";
|
||||
if (receiver.kind === "set") return receiver.elem.kind === "string";
|
||||
return false;
|
||||
}
|
||||
|
||||
/** String parameters, by function and index, that the body reads only in
|
||||
* lending positions (findLentStringParameters). */
|
||||
export type LentStringParameters = ReadonlyMap<string, ReadonlySet<number>>;
|
||||
|
||||
/** The operands `value` reads only while it runs, keeping no reference. */
|
||||
function lentOperands(value: IrExpr, parameters: LentStringParameters): readonly IrExpr[] {
|
||||
switch (value.kind) {
|
||||
case "strEq":
|
||||
case "strCmp":
|
||||
return [value.left, value.right];
|
||||
case "strIntrinsic":
|
||||
return LENDING_METHODS.has(value.method) ? [value.receiver, ...value.args] : [];
|
||||
case "libCall":
|
||||
return LENDING_LIB_FNS.has(value.fn) ? value.args.slice(0, 1) : [];
|
||||
case "mapIntrinsic":
|
||||
case "setIntrinsic":
|
||||
return LENDING_KEY_METHODS.has(value.method) && stringKeyed(value)
|
||||
? value.args.slice(0, 1)
|
||||
: [];
|
||||
case "call": {
|
||||
const lent = parameters.get(value.callee);
|
||||
return lent ? value.args.filter((_, index) => lent.has(index)) : [];
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
interface LendingScan {
|
||||
/** Slice operands of lending consumers, excluding any node also reached
|
||||
* in an owning position. */
|
||||
scratch: Set<IrExpr>;
|
||||
/** Candidates read anywhere other than a lending position. */
|
||||
invalid: Set<string>;
|
||||
/** Direct callees, for the parameter fixpoint. */
|
||||
callees: Set<string>;
|
||||
}
|
||||
|
||||
/** One pass over `fn`: which candidate bindings only lending consumers read
|
||||
* (a slice initializer of a candidate local is not itself a read), and
|
||||
* which slice operands those consumers lend. A switch discriminant is only
|
||||
* compared with the case tests. */
|
||||
function scanLending(
|
||||
fn: IrFunction,
|
||||
candidates: ReadonlySet<string>,
|
||||
initializers: ReadonlyMap<string, Slice>,
|
||||
parameters: LentStringParameters,
|
||||
): LendingScan {
|
||||
const scan: LendingScan = { scratch: new Set(), invalid: new Set(), callees: new Set() };
|
||||
const owned = new Set<IrExpr>();
|
||||
function lent(value: IrExpr): boolean {
|
||||
if (value.kind === "varRef" && candidates.has(value.localId)) return true;
|
||||
if (!isSlice(value)) return expr(value);
|
||||
scan.scratch.add(value);
|
||||
return everyExprChild(value, expr, stmt);
|
||||
}
|
||||
function expr(value: IrExpr): boolean {
|
||||
if (isSlice(value)) owned.add(value);
|
||||
if (value.kind === "varRef" && candidates.has(value.localId)) scan.invalid.add(value.localId);
|
||||
if (value.kind === "closure" || value.kind === "classRef")
|
||||
for (const id of value.captures ?? []) scan.invalid.add(id);
|
||||
if (value.kind === "call") scan.callees.add(value.callee);
|
||||
const operands = lentOperands(value, parameters);
|
||||
if (operands.length === 0) return everyExprChild(value, expr, stmt);
|
||||
return everyExprChild(
|
||||
value,
|
||||
(child) => (operands.includes(child) ? lent(child) : expr(child)),
|
||||
stmt,
|
||||
);
|
||||
}
|
||||
function stmt(value: IrStmt): boolean {
|
||||
// A candidate's single declaration initializes it with its slice.
|
||||
if (value.kind === "varDecl" && value.init !== null && initializers.has(value.localId))
|
||||
return everyExprChild(value.init, expr, stmt);
|
||||
if (value.kind === "switch" && value.disc.type.kind === "string") {
|
||||
lent(value.disc);
|
||||
for (const entry of value.cases) {
|
||||
if (entry.test) expr(entry.test);
|
||||
entry.body.forEach(stmt);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return everyStmtChild(value, expr, stmt);
|
||||
}
|
||||
fn.body.forEach(stmt);
|
||||
for (const node of owned) scan.scratch.delete(node);
|
||||
return scan;
|
||||
}
|
||||
|
||||
/** String parameters that every body reads only in lending positions,
|
||||
* including lent arguments of other such parameters: a caller may pass a
|
||||
* scratch slice to them. Start from every borrowed (unchanged) string
|
||||
* parameter of a synchronous function and drop violations until nothing changes, so
|
||||
* recursion through a lent parameter stays lent. A coroutine can keep its
|
||||
* parameters past the caller's frame, so it never lends. */
|
||||
export function findLentStringParameters(
|
||||
functions: ReadonlyMap<string, IrFunction>,
|
||||
lifetimes: CallLifetimes,
|
||||
): Map<string, Set<number>> {
|
||||
const lent = new Map<string, Set<number>>();
|
||||
for (const fn of functions.values()) {
|
||||
if (fn.async || fn.generator) continue;
|
||||
// Borrowed parameters are the unchanged ones nothing redeclares.
|
||||
const stable = lifetimes.borrowed.get(fn.name);
|
||||
const indexes = new Set<number>();
|
||||
fn.params.forEach((param, index) => {
|
||||
if (param.type.kind === "string" && stable?.has(index)) indexes.add(index);
|
||||
});
|
||||
if (indexes.size > 0) lent.set(fn.name, indexes);
|
||||
}
|
||||
const callers = new Map<string, Set<string>>();
|
||||
const pending = new Set(lent.keys());
|
||||
while (pending.size > 0) {
|
||||
const name = pending.values().next().value as string;
|
||||
pending.delete(name);
|
||||
const indexes = lent.get(name);
|
||||
const fn = functions.get(name);
|
||||
if (!indexes || !fn) continue;
|
||||
const ids = new Set([...indexes].map((index) => fn.params[index]!.localId));
|
||||
const scan = scanLending(fn, ids, new Map(), lent);
|
||||
for (const callee of scan.callees) {
|
||||
let set = callers.get(callee);
|
||||
if (!set) callers.set(callee, (set = new Set()));
|
||||
set.add(name);
|
||||
}
|
||||
let changed = false;
|
||||
for (const index of [...indexes]) {
|
||||
if (!scan.invalid.has(fn.params[index]!.localId)) continue;
|
||||
indexes.delete(index);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) continue;
|
||||
if (indexes.size === 0) lent.delete(name);
|
||||
const affected = callers.get(name);
|
||||
if (affected) for (const caller of affected) if (lent.has(caller)) pending.add(caller);
|
||||
}
|
||||
return lent;
|
||||
}
|
||||
|
||||
/** Slices whose every reader borrows them are written into a stack buffer
|
||||
* instead of a heap allocation (scr_str_slice_scratch): a slice operand of
|
||||
* a lending consumer, and the initializer of a stable local read only by
|
||||
* lending consumers. The copy is ASCII, bounded, and immortal for retain and
|
||||
* release, so it neither pins its source nor needs to be freed; longer or
|
||||
* non-ASCII results keep the ordinary owned string. Each site owns its
|
||||
* buffer: a site runs again only after its previous value is dead, since
|
||||
* nothing can keep it. Coroutine frames do not keep stack buffers across a
|
||||
* suspension, so they are left alone. */
|
||||
export function findScratchStringSlices(
|
||||
fn: IrFunction,
|
||||
lifetimes: CallLifetimes,
|
||||
scalar: ReadonlyMap<string, Slice>,
|
||||
parameters: LentStringParameters = new Map(),
|
||||
): Set<IrExpr> {
|
||||
if (fn.async || fn.generator) return new Set();
|
||||
const bindings = lifetimes.bindings.get(fn.name);
|
||||
const locals = new Map<string, Slice>();
|
||||
everyStmtList(fn.body, {
|
||||
expr: () => true,
|
||||
stmt: (stmt) => {
|
||||
if (
|
||||
stmt.kind === "varDecl" &&
|
||||
bindings?.has(stmt.localId) &&
|
||||
!scalar.has(stmt.localId) &&
|
||||
stmt.init !== null &&
|
||||
isSlice(stmt.init)
|
||||
)
|
||||
locals.set(stmt.localId, stmt.init);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
const scan = scanLending(fn, new Set(locals.keys()), locals, parameters);
|
||||
for (const [id, init] of locals) if (!scan.invalid.has(id)) scan.scratch.add(init);
|
||||
return scan.scratch;
|
||||
}
|
||||
|
||||
/** Write a scratch slice into its own stack buffer. The result is owned:
|
||||
* releasing the scratch copy does nothing, and a heap fallback is freed. */
|
||||
export function emitScratchStringSlice(
|
||||
host: LlvmEmitterContext,
|
||||
slice: Slice,
|
||||
receiver: LlValue,
|
||||
args: readonly LlValue[],
|
||||
): LlValue {
|
||||
const B = host.B;
|
||||
const buffer = B.slot();
|
||||
B.entryAllocas.push(`${buffer} = alloca [${SCRATCH_SLICE_BYTES} x i8], align 8`);
|
||||
host.declare(
|
||||
`declare ptr @scr_str_slice_scratch(ptr, double, double, i1 zeroext, ptr, ${host.sizeType})`,
|
||||
);
|
||||
const result = B.tmp();
|
||||
B.line(
|
||||
`${result} = call ptr @scr_str_slice_scratch(ptr ${receiver.name}, double ${args[0]?.name ?? f64Lit(0)}, double ${args[1]?.name ?? F64_INF}, i1 ${slice.method === "substring" ? "true" : "false"}, ptr ${buffer}, ${host.sizeType} ${SCRATCH_SLICE_BYTES})`,
|
||||
);
|
||||
return host.own({ name: result, type: slice.type });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { expect, test } from "vitest";
|
||||
import { findUnaliasedLocalArrays, preservesArrayLength } from "./array-length-stability.js";
|
||||
import {
|
||||
BOOL,
|
||||
F64,
|
||||
arrayOf,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrLocal,
|
||||
type IrStmt,
|
||||
} from "./ir.js";
|
||||
|
||||
const loc = { file: "array-length-stability.ts", start: 0, end: 0 };
|
||||
const ARR = arrayOf(F64);
|
||||
const num = (value: number): IrExpr => ({ kind: "numLit", value, type: F64, loc });
|
||||
const ref = (localId: string): IrExpr => ({ kind: "varRef", localId, type: F64, loc });
|
||||
const arr = (localId: string): IrExpr => ({ kind: "varRef", localId, type: ARR, loc });
|
||||
const method = (
|
||||
receiver: string,
|
||||
name: Extract<IrExpr, { kind: "arrIntrinsic" }>["method"],
|
||||
args: IrExpr[] = [],
|
||||
): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: name,
|
||||
receiver: arr(receiver),
|
||||
args,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const length = (receiver: string): IrExpr => method(receiver, "length");
|
||||
const read = (receiver: string, index: IrExpr): IrExpr => method(receiver, "getNumber", [index]);
|
||||
const plus = (left: IrExpr, right: IrExpr): IrExpr => ({
|
||||
kind: "bin",
|
||||
op: "+",
|
||||
left,
|
||||
right,
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
const stmt = (expr: IrExpr): IrStmt => ({ kind: "exprStmt", expr, loc });
|
||||
const set = (receiver: string, index: IrExpr, value: IrExpr): IrStmt => ({
|
||||
kind: "arraySet",
|
||||
arr: arr(receiver),
|
||||
index,
|
||||
value,
|
||||
loc,
|
||||
});
|
||||
const cond: IrExpr = { kind: "bin", op: "<", left: ref("i"), right: length("a"), type: BOOL, loc };
|
||||
const sum: IrStmt = {
|
||||
kind: "assign",
|
||||
localId: "s",
|
||||
value: plus(ref("s"), read("a", ref("i"))),
|
||||
loc,
|
||||
};
|
||||
const locals = new Map<string, IrLocal>([
|
||||
["i", { id: "i", name: "i", type: F64, mutable: true }],
|
||||
["s", { id: "s", name: "s", type: F64, mutable: true }],
|
||||
["a", { id: "a", name: "a", type: ARR, mutable: false }],
|
||||
["b", { id: "b", name: "b", type: ARR, mutable: false }],
|
||||
["out", { id: "out", name: "out", type: ARR, mutable: false }],
|
||||
]);
|
||||
const preserves = (
|
||||
body: IrStmt[],
|
||||
inRange: string | null = "i",
|
||||
unaliased: ReadonlySet<string> = new Set(),
|
||||
scope = locals,
|
||||
): boolean => preservesArrayLength([cond, ...body], { receiver: "a", inRange }, scope, unaliased);
|
||||
|
||||
test("reads, local arithmetic and element writes at the in-range counter keep the length", () => {
|
||||
expect(preserves([sum])).toBe(true);
|
||||
expect(preserves([set("a", ref("i"), num(1)), set("b", ref("i"), read("a", ref("i")))])).toBe(
|
||||
true,
|
||||
);
|
||||
expect(preserves([stmt(method("a", "indexOf", [num(3)]))])).toBe(true);
|
||||
});
|
||||
|
||||
test("writes past the counter, without an in-range counter, or to the counter can grow it", () => {
|
||||
expect(preserves([set("a", plus(ref("i"), num(1)), num(1))])).toBe(false);
|
||||
expect(preserves([set("b", ref("s"), num(1))])).toBe(false);
|
||||
expect(preserves([set("a", ref("i"), num(1))], null)).toBe(false);
|
||||
expect(preserves([{ kind: "assign", localId: "i", value: num(0), loc }, sum])).toBe(false);
|
||||
});
|
||||
|
||||
test("resizing methods, length stores, calls and escapes refuse the proof", () => {
|
||||
for (const name of ["push", "pop", "shift", "unshift", "splice", "fill"] as const)
|
||||
expect(preserves([stmt(method("a", name, [num(1)]))])).toBe(false);
|
||||
// An unrelated array may alias the receiver.
|
||||
expect(preserves([stmt(method("b", "push", [num(1)]))])).toBe(false);
|
||||
expect(preserves([{ kind: "arraySetLength", arr: arr("a"), length: num(0), loc }])).toBe(false);
|
||||
expect(preserves([{ kind: "arrayDelete", arr: arr("a"), index: ref("i"), loc }])).toBe(false);
|
||||
expect(preserves([stmt({ kind: "call", callee: "f", args: [], type: F64, loc })])).toBe(false);
|
||||
expect(preserves([stmt({ kind: "call", callee: "f", args: [arr("a")], type: F64, loc })])).toBe(
|
||||
false,
|
||||
);
|
||||
expect(preserves([{ kind: "varDecl", localId: "b", init: arr("a"), loc }])).toBe(false);
|
||||
expect(preserves([{ kind: "assign", localId: "a", value: arr("b"), loc }])).toBe(false);
|
||||
});
|
||||
|
||||
test("an unaliased local array other than the receiver may grow", () => {
|
||||
const push = stmt(method("out", "push", [read("a", ref("i"))]));
|
||||
expect(preserves([push], "i", new Set(["out"]))).toBe(true);
|
||||
expect(preserves([set("out", ref("s"), num(1))], "i", new Set(["out"]))).toBe(true);
|
||||
expect(preserves([push])).toBe(false);
|
||||
// The receiver itself is never resized, even when no alias exists.
|
||||
expect(preserves([stmt(method("a", "push", [num(1)]))], "i", new Set(["a"]))).toBe(false);
|
||||
});
|
||||
|
||||
test("captured receivers have no local proof", () => {
|
||||
const boxed = new Map(locals);
|
||||
boxed.set("a", { ...locals.get("a")!, boxed: true });
|
||||
expect(preserves([sum], "i", new Set(), boxed)).toBe(false);
|
||||
});
|
||||
|
||||
function fn(body: IrStmt[], params: string[] = []): IrFunction {
|
||||
return {
|
||||
loc,
|
||||
name: "f",
|
||||
params: params.map((localId) => ({ localId, type: ARR })),
|
||||
returnType: ARR,
|
||||
locals: [...locals.values()],
|
||||
body,
|
||||
} as IrFunction;
|
||||
}
|
||||
const fresh: IrExpr = { kind: "arrayLit", elems: [], type: ARR, loc };
|
||||
|
||||
test("fresh local arrays read only as receivers have no alias", () => {
|
||||
const body: IrStmt[] = [
|
||||
{ kind: "varDecl", localId: "out", init: fresh, loc },
|
||||
stmt(method("out", "push", [num(1)])),
|
||||
{ kind: "return", value: arr("out"), loc },
|
||||
];
|
||||
expect([...findUnaliasedLocalArrays(fn(body))]).toEqual(["out"]);
|
||||
// Parameters, copies into another binding, and arguments escape.
|
||||
expect(findUnaliasedLocalArrays(fn(body, ["out"])).size).toBe(0);
|
||||
const copied = [...body, { kind: "varDecl", localId: "b", init: arr("out"), loc } as IrStmt];
|
||||
expect(findUnaliasedLocalArrays(fn(copied)).has("out")).toBe(false);
|
||||
const passed = [...body, stmt({ kind: "call", callee: "g", args: [arr("out")], type: F64, loc })];
|
||||
expect(findUnaliasedLocalArrays(fn(passed)).has("out")).toBe(false);
|
||||
const rebound = [...body, { kind: "assign", localId: "out", value: arr("a"), loc } as IrStmt];
|
||||
expect(findUnaliasedLocalArrays(fn(rebound)).has("out")).toBe(false);
|
||||
});
|
||||
@@ -0,0 +1,259 @@
|
||||
import type { IrExpr, IrFunction, IrLocal, IrStmt } from "./ir.js";
|
||||
import { isBorrowSafeMath } from "./analysis.js";
|
||||
import { byteNumberAccess } from "./byte-numbers.js";
|
||||
import { everyExprChild, everyStmtChild } from "./traverse.js";
|
||||
|
||||
/** Array methods that read the receiver without changing its length or its
|
||||
* slot states and run no user code. */
|
||||
const READING_ARRAY_METHODS = new Set<string>([
|
||||
"length",
|
||||
"getNumber",
|
||||
"indexEq",
|
||||
"indexOf",
|
||||
"includes",
|
||||
"nextPresent",
|
||||
]);
|
||||
|
||||
/** String methods that read an immutable string and run no user code. */
|
||||
const READING_STRING_METHODS = new Set<string>(["length", "charCodeAt", "codePointAt"]);
|
||||
|
||||
/** Expressions that compute from their operands and run no user code. Their
|
||||
* children are checked separately. */
|
||||
const PLAIN_EXPRESSIONS = new Set<string>([
|
||||
"numLit",
|
||||
"strLit",
|
||||
"boolLit",
|
||||
"unitLit",
|
||||
"varRef",
|
||||
"bin",
|
||||
"unary",
|
||||
"incDec",
|
||||
"toBool",
|
||||
"logical",
|
||||
"ternary",
|
||||
"strEq",
|
||||
"strCmp",
|
||||
"fieldGet",
|
||||
"recordGet",
|
||||
"unionNarrow",
|
||||
"unionIsTag",
|
||||
"unionDisc",
|
||||
"unionWrap",
|
||||
"upcast",
|
||||
"downcast",
|
||||
"instanceOf",
|
||||
"nullish",
|
||||
"seqExpr",
|
||||
]);
|
||||
|
||||
/** Statements whose own effect is a local write or control flow inside the
|
||||
* loop. Their children are checked separately. */
|
||||
const PLAIN_STATEMENTS = new Set<string>([
|
||||
"varDecl",
|
||||
"assign",
|
||||
"exprStmt",
|
||||
"if",
|
||||
"block",
|
||||
"break",
|
||||
"continue",
|
||||
"for",
|
||||
"while",
|
||||
"doWhile",
|
||||
]);
|
||||
|
||||
/** Array methods whose result is never the receiver itself (unlike
|
||||
* `reverse`, `fill` or `copyWithin`, which return it). */
|
||||
const CONTAINED_RECEIVER_METHODS = new Set<string>([
|
||||
...READING_ARRAY_METHODS,
|
||||
"push",
|
||||
"pop",
|
||||
"shift",
|
||||
"unshift",
|
||||
"splice",
|
||||
"join",
|
||||
"slice",
|
||||
]);
|
||||
|
||||
/** Local arrays that no other binding can hold: every write of the local
|
||||
* stores a fresh array literal and every read of it is the receiver of an
|
||||
* element access or array method (or the value of a `return`, which ends the
|
||||
* activation). No alias of such an array exists, so mutating it cannot change
|
||||
* the length of an array held by any other binding. */
|
||||
export function findUnaliasedLocalArrays(fn: IrFunction): ReadonlySet<string> {
|
||||
const candidates = new Set<string>();
|
||||
const captured = new Set(
|
||||
[...(fn.captures ?? []), ...(fn.classCaptures ?? [])].map((c) => c.localId),
|
||||
);
|
||||
for (const local of fn.locals)
|
||||
if (local.type.kind === "array" && !local.boxed && !local.tdz && !captured.has(local.id))
|
||||
candidates.add(local.id);
|
||||
for (const param of fn.params) candidates.delete(param.localId);
|
||||
const written = new Set<string>();
|
||||
const fresh = (value: IrExpr | null): boolean =>
|
||||
value !== null && (value.kind === "arrayLit" || value.kind === "arrayNewLen");
|
||||
const receiver = (e: IrExpr): void => {
|
||||
if (e.kind !== "varRef") expr(e);
|
||||
};
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
candidates.delete(e.localId);
|
||||
return true;
|
||||
case "assignExpr":
|
||||
candidates.delete(e.localId);
|
||||
break;
|
||||
case "arrIntrinsic":
|
||||
if (!CONTAINED_RECEIVER_METHODS.has(e.method)) break;
|
||||
receiver(e.receiver);
|
||||
e.args.forEach(expr);
|
||||
return true;
|
||||
case "arrayGet":
|
||||
case "arrayHas":
|
||||
case "arrayState":
|
||||
receiver(e.arr);
|
||||
expr(e.index);
|
||||
return true;
|
||||
}
|
||||
return everyExprChild(e, expr, stmt);
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
case "assign": {
|
||||
const value = s.kind === "varDecl" ? s.init : s.value;
|
||||
if (fresh(value)) written.add(s.localId);
|
||||
else if (value) candidates.delete(s.localId);
|
||||
if (value) expr(value);
|
||||
return true;
|
||||
}
|
||||
case "arraySet":
|
||||
receiver(s.arr);
|
||||
expr(s.index);
|
||||
expr(s.value);
|
||||
return true;
|
||||
case "return":
|
||||
if (s.value) receiver(s.value);
|
||||
return true;
|
||||
case "forOf":
|
||||
case "tryCatch":
|
||||
if (s.kind === "forOf") candidates.delete(s.localId);
|
||||
if (s.kind === "tryCatch" && s.catchLocalId) candidates.delete(s.catchLocalId);
|
||||
break;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
fn.body.every(stmt);
|
||||
// A binding with no visible write takes its value some other way.
|
||||
for (const id of candidates) if (!written.has(id)) candidates.delete(id);
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export interface ArrayLengthLoop {
|
||||
/** The local holding the array whose length must not change. */
|
||||
receiver: string;
|
||||
/** A binding that holds a value below the array's length wherever the
|
||||
* loop's condition, body or update reads it (the induction variable of
|
||||
* `for (let i = 0; i < a.length; i++)` when the body never writes it, as
|
||||
* long as the length itself has not changed). An element write at this
|
||||
* index replaces an existing element or sets a non-index property. */
|
||||
inRange: string | null;
|
||||
}
|
||||
|
||||
/** Whether running `nodes` (a loop's condition, body and update) can never
|
||||
* change the length of the array held by local `loop.receiver`. The proof is
|
||||
* syntactic and conservative: every expression and statement must be one
|
||||
* that runs no user code (no calls, callbacks, closures, iterators,
|
||||
* suspension or exception regions); the binding is never written and only
|
||||
* ever read as the receiver of an element access or a reading array method,
|
||||
* so the array cannot escape to code that resizes it; and nothing in the
|
||||
* nodes resizes any array that may be the same one. Element writes are
|
||||
* accepted at the in-range binding (no array grows by them: an alias of the
|
||||
* receiver has the same length) and anywhere in an unaliased local array.
|
||||
* Array methods that resize an array are accepted only on unaliased local
|
||||
* arrays other than the receiver. */
|
||||
export function preservesArrayLength(
|
||||
nodes: readonly (IrExpr | IrStmt)[],
|
||||
loop: ArrayLengthLoop,
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
unaliased: ReadonlySet<string>,
|
||||
): boolean {
|
||||
const local = locals.get(loop.receiver);
|
||||
if (!local || local.type.kind !== "array" || local.boxed || local.tdz) return false;
|
||||
const separate = (e: IrExpr): boolean =>
|
||||
e.kind === "varRef" && e.localId !== loop.receiver && unaliased.has(e.localId);
|
||||
// A receiver position: the receiver binding itself is read there without
|
||||
// escaping.
|
||||
const receiver = (e: IrExpr): boolean =>
|
||||
(e.kind === "varRef" && e.localId === loop.receiver) || expr(e);
|
||||
const written = (id: string): boolean => id === loop.receiver || id === loop.inRange;
|
||||
const expr = (e: IrExpr): boolean => {
|
||||
switch (e.kind) {
|
||||
case "varRef":
|
||||
return e.localId !== loop.receiver;
|
||||
case "assignExpr":
|
||||
case "incDec":
|
||||
return !written(e.localId) && everyExprChild(e, expr, stmt);
|
||||
case "arrayGet":
|
||||
case "arrayHas":
|
||||
case "arrayState":
|
||||
return receiver(e.arr) && expr(e.index);
|
||||
case "arrIntrinsic":
|
||||
if (READING_ARRAY_METHODS.has(e.method)) return receiver(e.receiver) && e.args.every(expr);
|
||||
return separate(e.receiver) && e.args.every(expr);
|
||||
case "strIntrinsic":
|
||||
return (
|
||||
READING_STRING_METHODS.has(e.method) &&
|
||||
e.receiver.type.kind === "string" &&
|
||||
expr(e.receiver) &&
|
||||
e.args.every(expr)
|
||||
);
|
||||
case "bytesIntrinsic": {
|
||||
const numeric = byteNumberAccess(e);
|
||||
return (
|
||||
(numeric ? !numeric.write : e.method === "get" || e.method === "length") &&
|
||||
expr(e.receiver) &&
|
||||
e.args.every(expr)
|
||||
);
|
||||
}
|
||||
case "libCall":
|
||||
return (
|
||||
isBorrowSafeMath(e.fn) &&
|
||||
e.args.every((arg) => arg.type.kind === "f64") &&
|
||||
e.args.every(expr)
|
||||
);
|
||||
case "strConcat":
|
||||
return (
|
||||
e.left.type.kind === "string" &&
|
||||
e.right.type.kind === "string" &&
|
||||
expr(e.left) &&
|
||||
expr(e.right)
|
||||
);
|
||||
default:
|
||||
return PLAIN_EXPRESSIONS.has(e.kind) && everyExprChild(e, expr, stmt);
|
||||
}
|
||||
};
|
||||
const stmt = (s: IrStmt): boolean => {
|
||||
switch (s.kind) {
|
||||
case "varDecl":
|
||||
case "assign":
|
||||
if (written(s.localId)) return false;
|
||||
break;
|
||||
case "arraySet":
|
||||
return (
|
||||
(separate(s.arr) ||
|
||||
(s.index.kind === "varRef" &&
|
||||
loop.inRange !== null &&
|
||||
s.index.localId === loop.inRange)) &&
|
||||
receiver(s.arr) &&
|
||||
expr(s.index) &&
|
||||
expr(s.value)
|
||||
);
|
||||
case "return":
|
||||
return s.value === null || expr(s.value);
|
||||
default:
|
||||
if (!PLAIN_STATEMENTS.has(s.kind)) return false;
|
||||
}
|
||||
return everyStmtChild(s, expr, stmt);
|
||||
};
|
||||
return nodes.every((node) => ("type" in node ? expr(node as IrExpr) : stmt(node as IrStmt)));
|
||||
}
|
||||
@@ -118,6 +118,9 @@ export function boundedIntegerLoopFacts(
|
||||
stable(e.receiver.localId)
|
||||
)
|
||||
return { min: 0, max: Number.MAX_SAFE_INTEGER };
|
||||
// Every evaluation of an array length lies in its range, so the
|
||||
// condition bounds each pass even when the body resizes the array.
|
||||
if (isArrayLength(e)) return ARRAY_LENGTH_RANGE;
|
||||
return null;
|
||||
}
|
||||
let step: IntegerRange;
|
||||
@@ -134,12 +137,15 @@ export function boundedIntegerLoopFacts(
|
||||
if (step.min <= 0 || step.max > 2147483647) return result;
|
||||
}
|
||||
const limit = bound(loop.cond.right);
|
||||
if (!limit || limit.max < start.min) return result;
|
||||
const distance = limit.max - start.min;
|
||||
// The counter is an integer, so a strict bound admits passes up to one
|
||||
// below the limit.
|
||||
const last = limit && (loop.cond.op === "<" ? limit.max - 1 : limit.max);
|
||||
if (!limit || last === null || last < start.min) return result;
|
||||
const distance = last - start.min;
|
||||
const iterations = Math.floor(distance / step.min) + 1;
|
||||
if (!Number.isSafeInteger(distance) || !Number.isSafeInteger(iterations)) return result;
|
||||
// Header facts also cover the failing final condition and zero iterations.
|
||||
const maximum = Math.max(start.max, limit.max + step.max);
|
||||
const maximum = Math.max(start.max, last + step.max);
|
||||
if (Number.isSafeInteger(maximum)) result.set(counter, { min: start.min, max: maximum });
|
||||
for (const [id, hi] of upper) {
|
||||
const range = initial.get(id);
|
||||
@@ -154,6 +160,20 @@ export function boundedIntegerLoopFacts(
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Every length an array can have: SCR_ARR_MAX_LENGTH (scr_array.c). */
|
||||
export const ARRAY_LENGTH_RANGE: IntegerRange = { min: 0, max: 4294967295 };
|
||||
|
||||
/** `a.length` of a typed array value (an ordinary JS array). */
|
||||
function isArrayLength(e: IrExpr): boolean {
|
||||
return (
|
||||
e.kind === "arrIntrinsic" &&
|
||||
e.method === "length" &&
|
||||
e.args.length === 0 &&
|
||||
e.type.kind === "f64" &&
|
||||
e.receiver.type.kind === "array"
|
||||
);
|
||||
}
|
||||
|
||||
/** The most UTF-16 code units a runtime string holds: SCR_STR_MAX_CAP
|
||||
* (scr_runtime.h) caps a string's UTF-8 bytes, and a UTF-16 length never
|
||||
* exceeds the UTF-8 byte length. */
|
||||
@@ -241,6 +261,9 @@ export function guardedLoopCounterFacts(
|
||||
stable(e.receiver.localId)
|
||||
)
|
||||
return { min: 0, max: STRING_MAX_UNITS };
|
||||
// An array length can change on any pass, but never leaves its range,
|
||||
// which is all a ceiling needs.
|
||||
if (isArrayLength(e)) return ARRAY_LENGTH_RANGE;
|
||||
return null;
|
||||
};
|
||||
// The bounds a condition's conjuncts put on counters while it holds.
|
||||
|
||||
@@ -208,3 +208,111 @@ test("a string read at the counter is an integer consumer", () => {
|
||||
s.body = [read(ref("limit"))];
|
||||
expect(matchIntegerCountedForLoop(s, withText)).toBeNull();
|
||||
});
|
||||
|
||||
test("a stable array length bounds a counted loop; a resizing body keeps it a condition", () => {
|
||||
const ARR = arrayOf(F64);
|
||||
const array = (localId: string): IrExpr => ({ kind: "varRef", localId, type: ARR, loc });
|
||||
const length: IrExpr = {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: array("a"),
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const read = (index: IrExpr): IrExpr => ({
|
||||
kind: "arrIntrinsic",
|
||||
method: "getNumber",
|
||||
receiver: array("a"),
|
||||
args: [index],
|
||||
type: F64,
|
||||
loc,
|
||||
});
|
||||
// for (let i = 0; i < a.length; i += 2) a[i];
|
||||
const strided = (body: IrStmt[]): IrStmt & { kind: "for" } => {
|
||||
const s = counted(length);
|
||||
s.update = {
|
||||
kind: "assign",
|
||||
localId: "i",
|
||||
value: { kind: "bin", op: "+", left: ref("i"), right: number(2), type: F64, loc },
|
||||
loc,
|
||||
};
|
||||
s.body = [{ kind: "exprStmt", expr: read(ref("i")), loc }, ...body];
|
||||
return s;
|
||||
};
|
||||
const ranges = new Map([[length, { min: 0, max: 4294967295 }]]);
|
||||
expect(matchIntegerCountedForLoop(strided([]), locals, ranges, new Set())).toMatchObject({
|
||||
localId: "i",
|
||||
guarded: false,
|
||||
range: { min: 0, max: 4294967294 },
|
||||
});
|
||||
// Without the alias facts there is no stability proof.
|
||||
expect(matchIntegerCountedForLoop(strided([]), locals, ranges)).toBeNull();
|
||||
const push: IrStmt = {
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: array("a"),
|
||||
args: [number(1)],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
};
|
||||
expect(matchIntegerCountedForLoop(strided([push]), locals, ranges, new Set())).toBeNull();
|
||||
// An element write at the counter stays below the length; one past it can grow the array.
|
||||
const write = (index: IrExpr): IrStmt => ({
|
||||
kind: "arraySet",
|
||||
arr: array("a"),
|
||||
index,
|
||||
value: number(0),
|
||||
loc,
|
||||
});
|
||||
expect(
|
||||
matchIntegerCountedForLoop(strided([write(ref("i"))]), locals, ranges, new Set()),
|
||||
).not.toBeNull();
|
||||
const past = { kind: "bin", op: "+", left: ref("i"), right: number(1), type: F64, loc } as const;
|
||||
expect(matchIntegerCountedForLoop(strided([write(past)]), locals, ranges, new Set())).toBeNull();
|
||||
});
|
||||
|
||||
test("a descending loop from an array's last index takes an integer counter for its reads", () => {
|
||||
const ARR = arrayOf(F64);
|
||||
const length: IrExpr = {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: { kind: "varRef", localId: "a", type: ARR, loc },
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const start: IrExpr = { kind: "bin", op: "-", left: length, right: number(1), type: F64, loc };
|
||||
const s = loop(start);
|
||||
s.cond = { kind: "bin", op: ">=", left: ref("i"), right: number(0), type: BOOL, loc };
|
||||
s.update = {
|
||||
kind: "assign",
|
||||
localId: "i",
|
||||
value: { kind: "bin", op: "-", left: ref("i"), right: number(1), type: F64, loc },
|
||||
loc,
|
||||
};
|
||||
s.body = [
|
||||
{
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
kind: "arrIntrinsic",
|
||||
method: "getNumber",
|
||||
receiver: { kind: "varRef", localId: "a", type: ARR, loc },
|
||||
args: [ref("i")],
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
];
|
||||
const ranges = new Map([[start, { min: -1, max: 4294967294 }]]);
|
||||
expect(matchIntegerCountedForLoop(s, locals, ranges)).toMatchObject({
|
||||
step: -1,
|
||||
guarded: false,
|
||||
range: { min: 0, max: 4294967294 },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { IntegerRange, IntegerRanges } from "./integer-ranges.js";
|
||||
|
||||
import { everyExprChild, everyStmtChild, everyStmtList } from "./traverse.js";
|
||||
import { byteNumberAccess } from "./byte-numbers.js";
|
||||
import { preservesArrayLength } from "./array-length-stability.js";
|
||||
|
||||
/** A canonical byte loop whose induction variable is mathematically an
|
||||
* unsigned integer for every body entry. Backends may keep this binding in
|
||||
@@ -47,15 +48,17 @@ function constantStep(update: IrStmt | null, id: string): number | null {
|
||||
}
|
||||
|
||||
/** Pure initial values may be evaluated again on a fallback path. Bounds
|
||||
* additionally have to remain invariant throughout the loop. Array length
|
||||
* is allowed only in the initializer; its mutable value is never hoisted
|
||||
* from a condition. Typed-array lengths are fixed for a given receiver. */
|
||||
* additionally have to remain invariant throughout the loop. An array length
|
||||
* is hoisted from a condition only when `stableLength` proves the loop never
|
||||
* changes it; otherwise it is allowed only in the initializer. Typed-array
|
||||
* lengths are fixed for a given receiver. */
|
||||
function stableLoopValue(
|
||||
value: IrExpr,
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
body: IrStmt[],
|
||||
invariant: boolean,
|
||||
counter: string,
|
||||
stableLength: (receiver: string) => boolean = () => false,
|
||||
): boolean {
|
||||
switch (value.kind) {
|
||||
case "numLit":
|
||||
@@ -77,16 +80,16 @@ function stableLoopValue(
|
||||
value.op === "&" ||
|
||||
value.op === ">>>" ||
|
||||
value.op === "|") &&
|
||||
stableLoopValue(value.left, locals, body, invariant, counter) &&
|
||||
stableLoopValue(value.right, locals, body, invariant, counter)
|
||||
stableLoopValue(value.left, locals, body, invariant, counter, stableLength) &&
|
||||
stableLoopValue(value.right, locals, body, invariant, counter, stableLength)
|
||||
);
|
||||
case "bytesIntrinsic":
|
||||
case "arrIntrinsic":
|
||||
return (
|
||||
(value.method === "length" ||
|
||||
(value.kind === "bytesIntrinsic" && value.method === "byteLength")) &&
|
||||
(value.kind === "bytesIntrinsic" || !invariant) &&
|
||||
value.receiver.kind === "varRef" &&
|
||||
(value.kind === "bytesIntrinsic" || !invariant || stableLength(value.receiver.localId)) &&
|
||||
stableLoopValue(value.receiver, locals, body, invariant, counter)
|
||||
);
|
||||
default:
|
||||
@@ -103,6 +106,7 @@ export function matchIntegerCountedForLoop(
|
||||
stmt: IrStmt & { kind: "for" },
|
||||
locals: ReadonlyMap<string, IrLocal>,
|
||||
ranges: IntegerRanges = new Map(),
|
||||
unaliased: ReadonlySet<string> | null = null,
|
||||
): IntegerCountedForLoop | null {
|
||||
const init = stmt.init;
|
||||
if (init?.kind !== "varDecl" || !init.init) return null;
|
||||
@@ -135,7 +139,38 @@ export function matchIntegerCountedForLoop(
|
||||
op = ({ "<": ">", "<=": ">=", ">": "<", ">=": "<=" } as Record<string, string>)[op] ?? "";
|
||||
}
|
||||
if (step > 0 ? op !== "<" && op !== "<=" : op !== ">" && op !== ">=") return null;
|
||||
if (!stableLoopValue(limit, locals, stmt.body, true, local.id)) return null;
|
||||
// An ascending counter compared below `a.length - c` holds an index below
|
||||
// the length on every pass while that length is unchanged.
|
||||
const belowLength = (receiver: string): boolean => {
|
||||
let offset = 0,
|
||||
length = limit;
|
||||
if (
|
||||
length.kind === "bin" &&
|
||||
length.op === "-" &&
|
||||
length.right.kind === "numLit" &&
|
||||
Number.isFinite(length.right.value)
|
||||
) {
|
||||
offset = length.right.value;
|
||||
length = length.left;
|
||||
}
|
||||
return (
|
||||
step > 0 &&
|
||||
length.kind === "arrIntrinsic" &&
|
||||
length.method === "length" &&
|
||||
length.receiver.kind === "varRef" &&
|
||||
length.receiver.localId === receiver &&
|
||||
(op === "<" ? offset >= 0 : offset >= 1)
|
||||
);
|
||||
};
|
||||
const stableLength = (receiver: string): boolean =>
|
||||
unaliased !== null &&
|
||||
preservesArrayLength(
|
||||
[cond, ...stmt.body],
|
||||
{ receiver, inRange: belowLength(receiver) ? local.id : null },
|
||||
locals,
|
||||
unaliased,
|
||||
);
|
||||
if (!stableLoopValue(limit, locals, stmt.body, true, local.id, stableLength)) return null;
|
||||
const inclusive = op === "<=" || op === ">=";
|
||||
const guardLimit = step > 0 ? 2 ** 53 - (inclusive ? 1 : 0) : -(2 ** 53) + (inclusive ? 1 : 0);
|
||||
const boundRange =
|
||||
@@ -178,10 +213,19 @@ export function matchIntegerCountedForLoop(
|
||||
// A string read at the counter indexes with the integer directly.
|
||||
if (e.kind === "strIntrinsic" && e.method === "charCodeAt" && e.args[0] && counter(e.args[0]))
|
||||
useful = true;
|
||||
// So does an array element read; only a loop that needs no versioned
|
||||
// copy takes the integer counter for it.
|
||||
if (
|
||||
!guarded &&
|
||||
((e.kind === "arrayGet" && counter(e.index)) ||
|
||||
(e.kind === "arrIntrinsic" && e.method === "getNumber" && e.args[0] && counter(e.args[0])))
|
||||
)
|
||||
useful = true;
|
||||
return (!guarded || ++cost <= 80) && everyExprChild(e, expr, bodyStmt);
|
||||
};
|
||||
const bodyStmt = (s: IrStmt): boolean => {
|
||||
if (s.kind === "bytesSet" && counter(s.index)) useful = true;
|
||||
if (!guarded && s.kind === "arraySet" && counter(s.index)) useful = true;
|
||||
if (
|
||||
guarded &&
|
||||
(s.kind === "for" || s.kind === "forOf" || s.kind === "while" || s.kind === "doWhile")
|
||||
|
||||
@@ -334,7 +334,8 @@ test("filtered strided loops retain decoded values and bound secondary cursors",
|
||||
const ranges = analyzeIntegerRanges(f);
|
||||
expect(ranges.get(cursor)).toEqual({ min: 0, max: 80 });
|
||||
expect(ranges.get(decoded)).toEqual({ min: 0, max: 4294967295 });
|
||||
expect(ranges.get(conditionRead)).toEqual({ min: 0, max: 124 });
|
||||
// The last pass enters at most at 99, so the failing test reads at most 123.
|
||||
expect(ranges.get(conditionRead)).toEqual({ min: 0, max: 123 });
|
||||
for (const mutation of ["fraction", "repeat", "overflow", "counter"] as const) {
|
||||
const changed = structuredClone(f);
|
||||
const body = (changed.body[1] as IrStmt & { kind: "for" }).body;
|
||||
@@ -795,6 +796,77 @@ test("a while condition on a string's length bounds a counter, and guarded incre
|
||||
expect(run((inside) => [observe(inside), bump, rebound]).after).toBeNull();
|
||||
});
|
||||
|
||||
test("an array length bounds while and for counters even when the body resizes the array", () => {
|
||||
// let i = 0; while (i < items.length) { observe(i); items.push(i); i++; } observe(i);
|
||||
const ARR = { kind: "array", elem: F64 } as const;
|
||||
const items: IrExpr = { loc, kind: "varRef", localId: "items", type: ARR };
|
||||
const length: IrExpr = {
|
||||
loc,
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: items,
|
||||
args: [],
|
||||
type: F64,
|
||||
};
|
||||
const push: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: {
|
||||
loc,
|
||||
kind: "arrIntrinsic",
|
||||
method: "push",
|
||||
receiver: items,
|
||||
args: [ref("i")],
|
||||
type: F64,
|
||||
},
|
||||
};
|
||||
const bump: IrStmt = {
|
||||
loc,
|
||||
kind: "exprStmt",
|
||||
expr: { loc, kind: "incDec", op: "+", prefix: false, localId: "i", type: F64 },
|
||||
};
|
||||
const inside = ref("i"),
|
||||
after = ref("i");
|
||||
const f = fn([
|
||||
{ loc, kind: "assign", localId: "i", value: num(0) },
|
||||
{
|
||||
loc,
|
||||
kind: "while",
|
||||
cond: condition("<", ref("i"), length),
|
||||
body: [observe(inside), push, bump],
|
||||
},
|
||||
observe(after),
|
||||
]);
|
||||
f.locals.push(
|
||||
{ id: "i", name: "i", type: F64, mutable: true },
|
||||
{ id: "items", name: "items", type: ARR, mutable: false },
|
||||
);
|
||||
const ranges = analyzeIntegerRanges(f);
|
||||
expect(ranges.get(inside)).toEqual({ min: 0, max: 4294967294 });
|
||||
expect(ranges.get(after)).toEqual({ min: 0, max: 4294967295 });
|
||||
// A secondary cursor of `for (let j = 0; j < items.length; j++) k++` stays a uint32.
|
||||
const cursor = ref("k");
|
||||
const loop: IrStmt = {
|
||||
loc,
|
||||
kind: "for",
|
||||
init: { loc, kind: "varDecl", localId: "j", init: num(0) },
|
||||
cond: condition("<", ref("j"), length),
|
||||
update: { loc, kind: "assign", localId: "j", value: bin("+", ref("j"), num(1)) },
|
||||
body: [
|
||||
push,
|
||||
observe(cursor),
|
||||
{ loc, kind: "assign", localId: "k", value: bin("+", ref("k"), num(1)) },
|
||||
],
|
||||
};
|
||||
const g = fn([{ loc, kind: "assign", localId: "k", value: num(0) }, loop]);
|
||||
g.locals.push(
|
||||
{ id: "j", name: "j", type: F64, mutable: true },
|
||||
{ id: "k", name: "k", type: F64, mutable: true },
|
||||
{ id: "items", name: "items", type: ARR, mutable: false },
|
||||
);
|
||||
expect(analyzeIntegerRanges(g).get(cursor)).toEqual({ min: 0, max: 4294967295 });
|
||||
});
|
||||
|
||||
test("whole-program slot facts seed parameters, field reads and call results", () => {
|
||||
const field: IrExpr = {
|
||||
loc,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5993-array-length-loop-resizing.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5994-array-length-sparse-loops.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5995-array-dense-loop-boundaries.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/scratch-string-slices.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -77,7 +77,8 @@ static inline uint64_t scr_map_hash_str(const ScrStr *k) {
|
||||
uint32_t h = k->hash;
|
||||
if (__builtin_expect(h != 0, 1)) return scr_map_hash_widen(h);
|
||||
h = scr_map_hash_fold(k->data, k->len);
|
||||
if (k->rc != SIZE_MAX) ((ScrStr *)k)->hash = h;
|
||||
/* A scratch slice is a writable stack copy (scr_str_is_scratch). */
|
||||
if (k->rc != SIZE_MAX || scr_str_is_scratch(k)) ((ScrStr *)k)->hash = h;
|
||||
return scr_map_hash_widen(h);
|
||||
#else
|
||||
return scr_map_hash_bytes(k->data, k->len);
|
||||
@@ -931,7 +932,11 @@ static void scr_map_append(ScrMap *m, uint64_t hash, uint64_t key, uint64_t val,
|
||||
m->filter |= scr_map_filter_bits(hash);
|
||||
m->nlive++;
|
||||
if (m->key_kind == SCR_MAP_KEY_STR) {
|
||||
scr_str_retain((ScrStr *)scr_map_slot_to_ptr(key)); /* key is borrowed */
|
||||
ScrStr *k = scr_map_slot_to_ptr(key); /* key is borrowed */
|
||||
if (__builtin_expect(scr_str_is_scratch(k), 0))
|
||||
m->entries[idx].key = scr_map_slot_from_ptr(scr_str_scratch_keep(k));
|
||||
else
|
||||
scr_str_retain(k);
|
||||
} else if (m->key_kind == SCR_MAP_KEY_DYN) {
|
||||
const ScrDyn *d = (ScrDyn *)scr_map_slot_to_ptr(key);
|
||||
if (d->kind == SCR_DYN_NUM && d->v.num == 0) {
|
||||
|
||||
@@ -1999,20 +1999,34 @@ ScrStr *scr_str_slice(ScrStr *s, double start, double end) {
|
||||
return scr_str_slice_units(s, e, from, to);
|
||||
}
|
||||
|
||||
/* One slice() or substring() boundary in [0, length]: ToIntegerOrInfinity,
|
||||
* then slice's end-relative negative or substring's clamp. Conversion to an
|
||||
* integer truncates toward zero exactly like ToIntegerOrInfinity, so no
|
||||
* libm call is needed; NaN fails both comparisons and lands on 0, and a
|
||||
* negative fraction above -1 is -0, which slice treats as 0. */
|
||||
static inline size_t scr_slice_unit(double v, size_t length, bool substring) {
|
||||
if (v >= 0) return v >= (double)length ? length : (size_t)v;
|
||||
if (substring || !(v > -(double)length)) return 0;
|
||||
long long unit = (long long)v; /* in (-length, 0] */
|
||||
return unit == 0 ? 0 : (size_t)((long long)length + unit);
|
||||
}
|
||||
|
||||
/* slice()/substring() boundaries in UTF-16 units over the receiver's length. */
|
||||
static void scr_str_slice_bounds(ScrStr *s, ScrSidx *e, double start, double end, bool substring,
|
||||
size_t *from_out, size_t *to_out) {
|
||||
size_t length = scr_sidx_len(s, e);
|
||||
size_t from = scr_slice_unit(start, length, substring);
|
||||
size_t to = scr_slice_unit(end, length, substring);
|
||||
if (substring && from > to) { size_t swap = from; from = to; to = swap; }
|
||||
*from_out = from;
|
||||
*to_out = to;
|
||||
}
|
||||
|
||||
void scr_str_slice_range(ScrStr *s, double start, double end, bool substring,
|
||||
ScrStringSlice *out) {
|
||||
ScrSidx *e = scr_sidx_for(s);
|
||||
size_t length = scr_sidx_len(s, e);
|
||||
double a = scr_to_integer_or_infinity(start), b = scr_to_integer_or_infinity(end);
|
||||
size_t from, to;
|
||||
if (substring) {
|
||||
from = a <= 0 ? 0 : a >= (double)length ? length : (size_t)a;
|
||||
to = b <= 0 ? 0 : b >= (double)length ? length : (size_t)b;
|
||||
if (from > to) { size_t swap = from; from = to; to = swap; }
|
||||
} else {
|
||||
from = scr_slice_boundary(a, length);
|
||||
to = scr_slice_boundary(b, length);
|
||||
}
|
||||
scr_str_slice_bounds(s, e, start, end, substring, &from, &to);
|
||||
out->start = from;
|
||||
out->length = to > from ? to - from : 0;
|
||||
out->split = 0;
|
||||
@@ -2024,6 +2038,56 @@ void scr_str_slice_range(ScrStr *s, double start, double end, bool substring,
|
||||
}
|
||||
}
|
||||
|
||||
/* Fill a scratch buffer with `units` ASCII bytes (scr_str_is_scratch). */
|
||||
static inline ScrStr *scr_str_scratch_fill(ScrStr *scratch, const char *data, size_t units) {
|
||||
scratch->rc = SIZE_MAX;
|
||||
scratch->len = units;
|
||||
scratch->cap = 0; /* below len: the scratch marker */
|
||||
scratch->ascii = 1;
|
||||
#if SCR_STR_HASH_CACHE
|
||||
scratch->hash = 0;
|
||||
#endif
|
||||
scr_copy_bytes(scratch->data, data, units);
|
||||
scratch->data[units] = '\0';
|
||||
return scratch;
|
||||
}
|
||||
|
||||
ScrStr *scr_str_slice_scratch(ScrStr *s, double start, double end, bool substring,
|
||||
ScrStr *scratch, size_t bytes) {
|
||||
size_t capacity = bytes - sizeof(ScrStr) - 1;
|
||||
if (__builtin_expect(s->ascii, 1)) {
|
||||
/* Units are bytes: no index entry, and the bounds are byte offsets. */
|
||||
size_t length = s->len;
|
||||
size_t from = scr_slice_unit(start, length, substring);
|
||||
size_t to = scr_slice_unit(end, length, substring);
|
||||
if (substring && from > to) { size_t swap = from; from = to; to = swap; }
|
||||
size_t units = to > from ? to - from : 0;
|
||||
/* Shorter results are interned statics. */
|
||||
if (units >= 2 && units <= capacity) return scr_str_scratch_fill(scratch, s->data + from, units);
|
||||
return scr_str_slice_units(s, NULL, from, to);
|
||||
}
|
||||
ScrSidx *e = scr_sidx_for(s);
|
||||
size_t from, to;
|
||||
scr_str_slice_bounds(s, e, start, end, substring, &from, &to);
|
||||
size_t units = to > from ? to - from : 0;
|
||||
if (units >= 2 && units <= capacity) {
|
||||
/* Equal unit and byte counts between two character boundaries prove
|
||||
* the span is ASCII: every other character takes more bytes than
|
||||
* UTF-16 units. Non-ASCII results keep the owned copy and its index. */
|
||||
bool first, last;
|
||||
size_t from_b = scr_u16_to_byte_c(s, e, from, &first);
|
||||
size_t to_b = scr_u16_to_byte_c(s, e, to, &last);
|
||||
if (!first && !last && to_b - from_b == units)
|
||||
return scr_str_scratch_fill(scratch, s->data + from_b, units);
|
||||
}
|
||||
return scr_str_slice_units(s, e, from, to);
|
||||
}
|
||||
|
||||
ScrStr *scr_str_scratch_keep(ScrStr *s) {
|
||||
if (!scr_str_is_scratch(s)) return scr_str_retain(s);
|
||||
return scr_str_from_span_ascii(s->data, s->len);
|
||||
}
|
||||
|
||||
double scr_str_slice_char_code_at(ScrStr *s, const ScrStringSlice *range, double index) {
|
||||
double unit = scr_to_integer_or_infinity(index);
|
||||
if (!(unit >= 0) || unit >= (double)range->length) return NAN;
|
||||
|
||||
@@ -184,6 +184,14 @@ static inline ScrStr *scr_str_retain(ScrStr *s) {
|
||||
|
||||
void scr_str_release(ScrStr *s); /* NULL-tolerant (uninitialized locals) */
|
||||
|
||||
/* A transient stack copy of a slice (scr_str_slice_scratch): immortal for
|
||||
* retain and release, and valid only while its borrowing consumer runs. No
|
||||
* other string has a capacity below its length. Containers that keep a
|
||||
* string they were lent store scr_str_scratch_keep's copy instead. */
|
||||
static inline bool scr_str_is_scratch(const ScrStr *s) {
|
||||
return s->rc == SIZE_MAX && s->cap < s->len;
|
||||
}
|
||||
|
||||
/* ── lone surrogates: canonical WTF-8 storage ─────────────────────────
|
||||
* String storage is UTF-8 extended to lone UTF-16 surrogates: an unpaired
|
||||
* surrogate is its generalized 3-byte encoding ED A0..BF 80..BF, while a
|
||||
|
||||
@@ -83,6 +83,16 @@ typedef struct ScrStringSlice {
|
||||
void scr_str_slice_range(ScrStr *s, double start, double end, bool substring,
|
||||
ScrStringSlice *out);
|
||||
double scr_str_slice_char_code_at(ScrStr *s, const ScrStringSlice *range, double index);
|
||||
/* A slice that only borrowing consumers read (string-slices.ts) is written
|
||||
* into the caller's `bytes`-sized stack buffer as an immortal ASCII string,
|
||||
* whose header marks it as a scratch copy (scr_str_is_scratch). Results
|
||||
* that do not fit, are not ASCII, or are interned statics are returned as
|
||||
* the ordinary +1 slice. Releasing either result is correct. */
|
||||
ScrStr *scr_str_slice_scratch(ScrStr *s, double start, double end, bool substring,
|
||||
ScrStr *scratch, size_t bytes);
|
||||
/* The owning reference a container stores for `s`: a heap copy of a scratch
|
||||
* string, else `s` retained. */
|
||||
ScrStr *scr_str_scratch_keep(ScrStr *s);
|
||||
|
||||
/* repeat(count): count < 0 or Infinity is a JS RangeError; scriptc has no
|
||||
* exceptions, so it prints "scriptc: RangeError: Invalid count value" to
|
||||
|
||||
@@ -284,6 +284,36 @@ static void test_span_keys(void) {
|
||||
scr_map_release(numbers);
|
||||
}
|
||||
|
||||
/* A scratch slice probes like any key; an insertion stores a heap copy, so
|
||||
* the stored key survives the buffer's next contents. */
|
||||
static void test_scratch_keys(void) {
|
||||
ScrMap *m = scr_map_new(SCR_MAP_KEY_STR, SCR_MAP_VAL_F64, NULL);
|
||||
ScrStr *line = S("alpha beta alpha");
|
||||
_Alignas(16) unsigned char buffer[96];
|
||||
ScrStr *alpha = scr_str_slice_scratch(line, 0, 5, false, (ScrStr *)buffer, sizeof buffer);
|
||||
check(alpha == (ScrStr *)buffer && scr_str_is_scratch(alpha), "short ASCII slice uses the buffer");
|
||||
scr_map_set_str_f64(m, alpha, 1);
|
||||
double out = 0;
|
||||
check(scr_map_get_str_f64(m, alpha, &out) && out == 1, "scratch key probes its entry");
|
||||
ScrStr *beta = scr_str_slice_scratch(line, 6, 10, false, (ScrStr *)buffer, sizeof buffer);
|
||||
check(!scr_map_has_str(m, beta), "refilled buffer probes its new text");
|
||||
scr_map_set_str_f64(m, beta, 2);
|
||||
ScrStr *again = scr_str_slice_scratch(line, 11, 16, false, (ScrStr *)buffer, sizeof buffer);
|
||||
scr_map_set_str_f64(m, again, 3);
|
||||
check(scr_map_size(m) == 2, "scratch overwrite keeps one entry");
|
||||
ScrStr *first = scr_map_iter_key_str(m, 0), *second = scr_map_iter_key_str(m, 1);
|
||||
ScrStr *a = S("alpha"), *b = S("beta");
|
||||
check(!scr_str_is_scratch(first) && scr_str_eq(first, a), "stored key is an owned copy");
|
||||
check(!scr_str_is_scratch(second) && scr_str_eq(second, b), "second stored key is a copy");
|
||||
check(scr_map_get_str_f64(m, a, &out) && out == 3, "overwrite through scratch key");
|
||||
scr_str_release(a);
|
||||
scr_str_release(b);
|
||||
scr_str_release(first);
|
||||
scr_str_release(second);
|
||||
scr_str_release(line);
|
||||
scr_map_release(m);
|
||||
}
|
||||
|
||||
static void test_same_value_zero(void) {
|
||||
ScrMap *m = scr_map_new(SCR_MAP_KEY_F64, SCR_MAP_VAL_BOOL, NULL);
|
||||
scr_map_set_f64_bool(m, 0.0 / 0.0, true); /* NaN key */
|
||||
@@ -793,6 +823,7 @@ int main(void) {
|
||||
test_string_entries();
|
||||
test_same_value_zero();
|
||||
test_span_keys();
|
||||
test_scratch_keys();
|
||||
test_string_hash_cache();
|
||||
#ifdef SCR_RC_AUDIT
|
||||
test_rc_accounting();
|
||||
|
||||
@@ -205,6 +205,38 @@ static void surrogate_asserts(void) {
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Scratch slices hold the materialized text. Only ASCII results of at
|
||||
* least two units use the caller's buffer, marked as scratch. */
|
||||
{
|
||||
ScrStr *sources[] = {two, s, scr_str_new("GET /api/users 200", 18)};
|
||||
const double marks[] = {-INFINITY, -7.5, -3, -0.5, -0.0, 0, 0.5, 1,
|
||||
2, 3.9, 5, 9, 40, INFINITY, NAN};
|
||||
const size_t nmarks = sizeof marks / sizeof marks[0];
|
||||
_Alignas(16) unsigned char buffer[96];
|
||||
for (size_t k = 0; k < 3; k++) {
|
||||
for (size_t a = 0; a < nmarks; a++) {
|
||||
for (size_t b = 0; b < nmarks; b++) {
|
||||
for (unsigned substring = 0; substring < 2; substring++) {
|
||||
ScrStr *src = sources[k];
|
||||
ScrStr *scratch = scr_str_slice_scratch(src, marks[a], marks[b], substring != 0,
|
||||
(ScrStr *)buffer, sizeof buffer);
|
||||
ScrStr *copy = substring ? scr_str_substring(src, marks[a], marks[b])
|
||||
: scr_str_slice(src, marks[a], marks[b]);
|
||||
bool used = scratch == (ScrStr *)buffer;
|
||||
bool expected = copy->len >= 2 && scr_bytes_all_ascii(copy->data, copy->len);
|
||||
total++;
|
||||
if (scratch->len != copy->len || memcmp(scratch->data, copy->data, copy->len) != 0 ||
|
||||
used != expected || used != scr_str_is_scratch(scratch) ||
|
||||
scr_str_utf16_len(scratch) != scr_str_utf16_len(copy))
|
||||
failed++;
|
||||
scr_str_release(scratch);
|
||||
scr_str_release(copy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
scr_str_release(sources[2]);
|
||||
}
|
||||
scr_str_release(two);
|
||||
scr_str_release(s);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
// Index loops bounded by an array's length whose bodies resize that array,
|
||||
// directly, through an alias, a helper or a closure: the bound is re-read on
|
||||
// every pass, exactly like Node.
|
||||
|
||||
function pushInBody(): string {
|
||||
const a = [1, 2, 3];
|
||||
const seen: number[] = [];
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
seen.push(a[i]);
|
||||
if (a.length < 6) a.push(a[i] * 10);
|
||||
}
|
||||
return `${seen.join(",")} | ${a.length}`;
|
||||
}
|
||||
|
||||
function stridedPush(): string {
|
||||
const a = [0, 1, 2, 3];
|
||||
let visits = 0;
|
||||
for (let i = 0; i < a.length; i += 2) {
|
||||
visits++;
|
||||
if (a.length < 9) a.push(i);
|
||||
}
|
||||
return `${visits} ${a.join(",")}`;
|
||||
}
|
||||
|
||||
function truncateInBody(): string {
|
||||
const a = [5, 6, 7, 8, 9, 10];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
sum += a[i];
|
||||
if (i === 2) a.length = 4;
|
||||
}
|
||||
return `${sum} ${a.length}`;
|
||||
}
|
||||
|
||||
function growLengthInBody(): string {
|
||||
const a = [1, 2];
|
||||
let holes = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] === undefined) holes++;
|
||||
if (i === 0) a.length = 5;
|
||||
}
|
||||
return `${holes} ${a.length}`;
|
||||
}
|
||||
|
||||
function helperPush(xs: number[], v: number): void {
|
||||
if (xs.length < 7) xs.push(v);
|
||||
}
|
||||
|
||||
function pushThroughHelper(): string {
|
||||
const a = [1, 2, 3];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
sum += a[i];
|
||||
helperPush(a, i + 100);
|
||||
}
|
||||
return `${sum} ${a.length}`;
|
||||
}
|
||||
|
||||
function pushThroughClosure(): string {
|
||||
const a = [1, 2, 3];
|
||||
const grow = (v: number): void => {
|
||||
if (a.length < 6) a.push(v);
|
||||
};
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
sum += a[i];
|
||||
grow(i);
|
||||
}
|
||||
return `${sum} ${a.length}`;
|
||||
}
|
||||
|
||||
function pushThroughAlias(): string {
|
||||
const a = [1, 2, 3];
|
||||
const alias = a;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
sum += a[i];
|
||||
if (alias.length < 8) alias.push(i * 2);
|
||||
}
|
||||
return `${sum} ${a.length}`;
|
||||
}
|
||||
|
||||
function aliasedParameters(a: number[], out: number[]): string {
|
||||
// `out` may be `a` itself.
|
||||
let n = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
n++;
|
||||
if (out.length < 10) out.push(a[i]);
|
||||
}
|
||||
return `${n} ${a.length} ${out.length}`;
|
||||
}
|
||||
|
||||
function writePastEnd(): string {
|
||||
const a = [1, 2, 3];
|
||||
let passes = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
passes++;
|
||||
if (i + 1 === a.length && a.length < 6) a[i + 1] = i;
|
||||
}
|
||||
return `${passes} ${a.join(",")}`;
|
||||
}
|
||||
|
||||
function shiftInBody(): string {
|
||||
const a = [1, 2, 3, 4, 5, 6];
|
||||
const seen: number[] = [];
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
seen.push(a[i]);
|
||||
a.shift();
|
||||
}
|
||||
return `${seen.join(",")} | ${a.join(",")}`;
|
||||
}
|
||||
|
||||
function spliceInDescendingLoop(): string {
|
||||
const a = [1, 2, 3, 4, 5, 6, 7];
|
||||
for (let i = a.length - 1; i >= 0; i--) if (a[i] % 2 === 0) a.splice(i, 1);
|
||||
return a.join(",");
|
||||
}
|
||||
|
||||
function whilePush(): string {
|
||||
const a = [3, 1];
|
||||
let i = 0;
|
||||
let sum = 0;
|
||||
while (i < a.length) {
|
||||
sum += a[i];
|
||||
if (a.length < 5) a.push(sum);
|
||||
i++;
|
||||
}
|
||||
return `${sum} ${i} ${a.join(",")}`;
|
||||
}
|
||||
|
||||
function nestedResize(): string {
|
||||
const rows = [[1], [2, 3], [4, 5, 6]];
|
||||
let total = 0;
|
||||
for (let r = 0; r < rows.length; r++) {
|
||||
const row = rows[r];
|
||||
for (let c = 0; c < row.length; c++) {
|
||||
total += row[c];
|
||||
if (row.length < 3) row.push(c);
|
||||
}
|
||||
}
|
||||
return `${total} ${rows.map((row) => row.length).join(",")}`;
|
||||
}
|
||||
|
||||
function pushIntoOther(): string {
|
||||
const a = [2, 4, 6];
|
||||
const out: number[] = [];
|
||||
for (let i = 0; i < a.length; i++) out.push(a[i] + 1);
|
||||
for (let i = 0; i < out.length; i += 2) out.push(-1);
|
||||
return `${out.join(",")}`;
|
||||
}
|
||||
|
||||
console.log(pushInBody());
|
||||
console.log(stridedPush());
|
||||
console.log(truncateInBody());
|
||||
console.log(growLengthInBody());
|
||||
console.log(pushThroughHelper());
|
||||
console.log(pushThroughClosure());
|
||||
console.log(pushThroughAlias());
|
||||
const shared = [7, 8, 9];
|
||||
console.log(aliasedParameters(shared, shared));
|
||||
console.log(aliasedParameters([7, 8, 9], []));
|
||||
console.log(writePastEnd());
|
||||
console.log(shiftInBody());
|
||||
console.log(spliceInDescendingLoop());
|
||||
console.log(whilePush());
|
||||
console.log(nestedResize());
|
||||
console.log(pushIntoOther());
|
||||
@@ -0,0 +1,133 @@
|
||||
// Index loops over sparse arrays with lengths beyond 2^31, holes, and arrays
|
||||
// that become sparse while a loop runs. Dense-loop fast paths must leave all
|
||||
// of these on the general path.
|
||||
|
||||
function partialAscending(a: number[], limit: number): string {
|
||||
let sum = 0;
|
||||
let passes = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (passes++ === limit) break;
|
||||
sum += a[i] === undefined ? -1 : a[i];
|
||||
}
|
||||
return `${passes} ${sum}`;
|
||||
}
|
||||
|
||||
function partialDescending(a: number[], limit: number): string {
|
||||
let last = -1;
|
||||
let passes = 0;
|
||||
for (let i = a.length - 1; i >= 0; i--) {
|
||||
if (passes++ === limit) break;
|
||||
last = i;
|
||||
}
|
||||
return `${passes} ${last}`;
|
||||
}
|
||||
|
||||
function stridedFromEnd(a: number[]): string {
|
||||
const hits: number[] = [];
|
||||
for (let i = a.length - 1; i >= 0 && hits.length < 3; i -= 1000000000) hits.push(i);
|
||||
return hits.join(",");
|
||||
}
|
||||
|
||||
function whileWithLimit(a: number[], limit: number): number {
|
||||
let i = 0;
|
||||
let present = 0;
|
||||
while (i < a.length && i < limit) {
|
||||
if (a[i] !== undefined) present++;
|
||||
i++;
|
||||
}
|
||||
return present * 1000 + i;
|
||||
}
|
||||
|
||||
function writeAtCounter(a: number[], limit: number): string {
|
||||
for (let i = 0; i < a.length && i < limit; i++) a[i] = i * 2;
|
||||
return `${a.length} ${a[0]} ${a[limit - 1]} ${a[limit]}`;
|
||||
}
|
||||
|
||||
function holeSum(a: number[]): string {
|
||||
let sum = 0;
|
||||
let nan = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
const v = a[i];
|
||||
if (Number.isNaN(v + 0)) nan++;
|
||||
else sum += v;
|
||||
}
|
||||
return `${sum} ${nan}`;
|
||||
}
|
||||
|
||||
function firstValues(a: number[]): string {
|
||||
let s = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (i === 4) break;
|
||||
s += a[i];
|
||||
}
|
||||
let t = 0;
|
||||
for (let i = a.length - 1; i >= 0; i--) {
|
||||
if (i < a.length - 2) break;
|
||||
t += a[i];
|
||||
}
|
||||
let u = 0;
|
||||
for (let i = 0; i < a.length; i += 2) {
|
||||
if (i > 2) break;
|
||||
u += a[i];
|
||||
}
|
||||
return `${s} ${t} ${u}`;
|
||||
}
|
||||
|
||||
const huge: number[] = [1, 2, 3];
|
||||
huge.length = 3e9;
|
||||
console.log(huge.length);
|
||||
console.log(partialAscending(huge, 5));
|
||||
console.log(firstValues(huge), firstValues([1, 2, 3, 4, 5]));
|
||||
console.log(partialDescending(huge, 4));
|
||||
console.log(stridedFromEnd(huge));
|
||||
console.log(whileWithLimit(huge, 6));
|
||||
console.log(writeAtCounter(huge, 4));
|
||||
console.log(huge.length);
|
||||
|
||||
const edge: number[] = [];
|
||||
edge[4294967294] = 7;
|
||||
console.log(edge.length, partialDescending(edge, 2), partialAscending(edge, 3));
|
||||
|
||||
const holey = [1, 2];
|
||||
holey[3] = 4;
|
||||
holey[5] = 6;
|
||||
console.log(holeSum(holey));
|
||||
const sparse: number[] = [1, 2];
|
||||
sparse[10] = 5;
|
||||
console.log(holeSum(sparse));
|
||||
|
||||
// Becomes sparse mid-loop: a write far past the end through another binding.
|
||||
function sparseMidLoop(): string {
|
||||
const a = [1, 2, 3, 4];
|
||||
const b = a;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (i === 1) b[3000000000] = 9;
|
||||
if (i > 6) break;
|
||||
sum += a[i] === undefined ? 100 : a[i];
|
||||
}
|
||||
return `${sum} ${a.length}`;
|
||||
}
|
||||
console.log(sparseMidLoop());
|
||||
|
||||
function holeMidLoop(): string {
|
||||
const a = [1, 2, 3, 4, 5];
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (i === 1) a.length = 3;
|
||||
if (i === 2) a.length = 5;
|
||||
sum += a[i] === undefined ? 100 : a[i];
|
||||
}
|
||||
return `${sum}`;
|
||||
}
|
||||
console.log(holeMidLoop());
|
||||
|
||||
const objects: { v: number }[] = [{ v: 1 }, { v: 2 }, { v: 3 }];
|
||||
objects.length = 5;
|
||||
try {
|
||||
let total = 0;
|
||||
for (let i = 0; i < objects.length; i++) total += objects[i].v;
|
||||
console.log(total);
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError ? "TypeError" : "other");
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Index loops whose array length the loop never changes, run over dense,
|
||||
// holey, partially filled, aliased and short arrays, with fractional,
|
||||
// negative and offset bounds, ascending, strided and descending counters.
|
||||
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
function sum(a: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = 0; i < a.length; i++) s += a[i];
|
||||
return s;
|
||||
}
|
||||
|
||||
function scale(a: number[], b: number[]): string {
|
||||
for (let i = 0; i < a.length; i++) b[i] = a[i] * 2;
|
||||
return `${b.length}:${b.join(",")}`;
|
||||
}
|
||||
|
||||
function inPlace(a: number[]): string {
|
||||
for (let i = 0; i < a.length; i++) a[i] = a[i] + 1;
|
||||
return a.join(",");
|
||||
}
|
||||
|
||||
function strided(a: number[], step: number): string {
|
||||
let s = 0;
|
||||
if (step === 2) for (let i = 0; i < a.length; i += 2) s += a[i];
|
||||
else for (let i = 1; i < a.length; i += 3) s += a[i];
|
||||
return `${s}`;
|
||||
}
|
||||
|
||||
function descending(a: number[]): string {
|
||||
const out: number[] = [];
|
||||
for (let i = a.length - 1; i >= 0; i--) out.push(a[i]);
|
||||
return out.join(",");
|
||||
}
|
||||
|
||||
function descendingPairs(a: number[]): number {
|
||||
let s = 0;
|
||||
for (let i = a.length - 2; i > 0; i -= 2) s += a[i] - a[i + 1];
|
||||
return s;
|
||||
}
|
||||
|
||||
function offsetBounds(a: number[]): string {
|
||||
let inner = 0;
|
||||
for (let i = 1; i < a.length - 1; i++) inner += a[i];
|
||||
let inclusive = 0;
|
||||
for (let i = 0; i <= a.length - 1; i++) inclusive += a[i];
|
||||
let diffs = 0;
|
||||
for (let i = 0; i < a.length - 1; i++) diffs += a[i + 1] - a[i];
|
||||
return `${inner} ${inclusive} ${diffs}`;
|
||||
}
|
||||
|
||||
function fractionalBounds(a: number[]): string {
|
||||
const seen: number[] = [];
|
||||
for (let i = 0.5; i < a.length; i++) seen.push(a[i] === undefined ? -1 : 1);
|
||||
for (let i = -2; i < a.length - 3; i++) seen.push(a[i] === undefined ? -2 : a[i]);
|
||||
for (let i = 0; i < a.length - 0.5; i++) seen.push(i);
|
||||
return seen.join(",");
|
||||
}
|
||||
|
||||
function whileLoop(a: number[]): number {
|
||||
let i = 0;
|
||||
let s = 0;
|
||||
while (i < a.length) {
|
||||
s += a[i] * i;
|
||||
i++;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function constLength(a: number[]): number {
|
||||
const n = a.length;
|
||||
let s = 0;
|
||||
for (let i = 0; i < n; i++) s += a[i];
|
||||
return s;
|
||||
}
|
||||
|
||||
function points(ps: Point[]): string {
|
||||
let s = 0;
|
||||
for (let i = 0; i < ps.length; i++) s += ps[i].x * ps[i].y;
|
||||
return `${s}`;
|
||||
}
|
||||
|
||||
function flags(bs: boolean[]): number {
|
||||
let n = 0;
|
||||
for (let i = 0; i < bs.length; i++) if (bs[i]) n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
function names(ss: string[]): string {
|
||||
let out = "";
|
||||
for (let i = ss.length - 1; i >= 0; i--) out += ss[i];
|
||||
return out;
|
||||
}
|
||||
|
||||
function maybe(xs: (number | undefined)[]): string {
|
||||
let s = 0;
|
||||
let missing = 0;
|
||||
for (let i = 0; i < xs.length; i++) {
|
||||
const v = xs[i];
|
||||
if (v === undefined) missing++;
|
||||
else s += v;
|
||||
}
|
||||
return `${s} ${missing}`;
|
||||
}
|
||||
|
||||
function nested(g: number[][]): number {
|
||||
let s = 0;
|
||||
for (let r = 0; r < g.length; r++) {
|
||||
const row = g[r];
|
||||
for (let c = 0; c < row.length; c++) s += row[c] * (r + 1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function counterAsNumber(a: number[]): string {
|
||||
let mixed = 0;
|
||||
for (let i = 0; i < a.length; i++) mixed += i / 2 + (i | 0) + a[i] * -0;
|
||||
return `${mixed} ${Object.is(mixed, -0)}`;
|
||||
}
|
||||
|
||||
function earlyExit(a: number[], target: number): number {
|
||||
for (let i = 0; i < a.length; i++) if (a[i] === target) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
const dense = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
console.log(sum(dense), sum([]), sum([0.5, -0.25]));
|
||||
console.log(scale(dense, new Array<number>(8).fill(0)));
|
||||
console.log(scale(dense, [9, 9]));
|
||||
console.log(scale([1, 2, 3], []));
|
||||
const self = [1, 2, 3, 4];
|
||||
console.log(scale(self, self));
|
||||
console.log(inPlace([10, 20, 30]));
|
||||
console.log(strided(dense, 2), strided(dense, 3));
|
||||
console.log(descending(dense), descending([]));
|
||||
console.log(descendingPairs(dense), descendingPairs([1]));
|
||||
console.log(offsetBounds(dense), offsetBounds([4]), offsetBounds([]));
|
||||
console.log(fractionalBounds([1, 2, 3, 4, 5]));
|
||||
console.log(whileLoop(dense));
|
||||
console.log(constLength(dense));
|
||||
console.log(points([{ x: 1, y: 2 }, { x: 3, y: 4 }]));
|
||||
console.log(flags([true, false, true, true]));
|
||||
console.log(names(["a", "b", "c"]));
|
||||
console.log(maybe([1, undefined, 3]));
|
||||
console.log(nested([[1, 2], [3], [], [4, 5, 6]]));
|
||||
console.log(counterAsNumber([1, 2, 3]));
|
||||
console.log(earlyExit(dense, 5), earlyExit(dense, 50));
|
||||
|
||||
// Holes and partially filled storage take the checked path.
|
||||
const holey = [1];
|
||||
holey[2] = 3;
|
||||
holey[4] = 5;
|
||||
console.log(sum(holey), descending(holey), whileLoop(holey));
|
||||
const target = [0, 0, 0];
|
||||
target[4] = 0;
|
||||
console.log(scale(dense.slice(0, 5), target));
|
||||
const gap: number[] = [];
|
||||
gap[3] = 1;
|
||||
console.log(sum(gap), scale([1, 2, 3, 4], gap));
|
||||
const shortened = [1, 2, 3, 4, 5, 6];
|
||||
shortened.length = 3;
|
||||
shortened.length = 6;
|
||||
console.log(sum(shortened), constLength(shortened));
|
||||
const holeyPoints: Point[] = [{ x: 1, y: 1 }];
|
||||
holeyPoints[2] = { x: 2, y: 2 };
|
||||
try {
|
||||
console.log(points(holeyPoints));
|
||||
} catch (e) {
|
||||
console.log(e instanceof TypeError ? "TypeError" : "other");
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Slices read only by comparisons, numeric parsers, collection probes,
|
||||
// switch discriminants and lent helper parameters, including the keys a
|
||||
// map or set keeps after the slice that produced them is gone.
|
||||
const ascii = "GET /api/users?id=42 200 17.5ms";
|
||||
const mixed = "Aé水x😀yz-12.5e1 ok";
|
||||
const bounds = [-Infinity, -40, -8, -2.5, -1, -0.5, -0, 0, 0.5, 1, 2, 3, 4.9, 5, 7, 10, 40, Infinity, NaN];
|
||||
|
||||
function compare(source: string, start: number, end: number): string {
|
||||
const s = source.slice(start, end);
|
||||
const t = source.substring(start, end);
|
||||
const parts = [
|
||||
String(s === "GET"),
|
||||
String(t === "GET"),
|
||||
String(source.slice(start, end) === source.substring(start, end)),
|
||||
String(source.slice(start, end) < "M"),
|
||||
String(source.substring(start, end) >= "a"),
|
||||
String(s.length),
|
||||
String(t.length),
|
||||
String(s.charCodeAt(0)),
|
||||
String(t.charCodeAt(t.length - 1)),
|
||||
String(s.indexOf("a")),
|
||||
String(t.includes("😀")),
|
||||
String(s.startsWith("A")),
|
||||
String(t.endsWith("z")),
|
||||
];
|
||||
return parts.join(",");
|
||||
}
|
||||
for (const source of [ascii, mixed]) {
|
||||
for (const start of bounds) {
|
||||
const row: string[] = [];
|
||||
for (const end of bounds) row.push(compare(source, start, end));
|
||||
console.log(row.join(" | "));
|
||||
}
|
||||
}
|
||||
|
||||
// Numeric parsing of slices.
|
||||
const numbers = " 42|-0|0x1F|1e3|.5|Infinity|-Infinity|12px|abc|+7.25| 9 |0b11|1_000|";
|
||||
let offset = 0;
|
||||
while (offset < numbers.length) {
|
||||
const bar = numbers.indexOf("|", offset);
|
||||
const field = numbers.slice(offset, bar);
|
||||
console.log(
|
||||
JSON.stringify(field),
|
||||
Number(field),
|
||||
+numbers.substring(offset, bar),
|
||||
parseInt(field),
|
||||
parseInt(numbers.slice(offset, bar), 16),
|
||||
parseFloat(field),
|
||||
Object.is(Number(numbers.slice(offset, bar)), -0),
|
||||
);
|
||||
offset = bar + 1;
|
||||
}
|
||||
console.log(Number(mixed.slice(9, 16)), parseFloat(mixed.slice(9)), parseInt(mixed.slice(10, 12)));
|
||||
|
||||
// Collection probes and insertions keyed by slices. The stored keys must
|
||||
// outlive every later slice written to the same site.
|
||||
const counts = new Map<string, number>();
|
||||
const seen = new Set<string>();
|
||||
const words = "alpha beta gamma alpha delta beta alpha epsilon-with-a-much-longer-key-than-the-stack-buffer-holds-alpha zeta é-key é-key x x";
|
||||
for (let round = 0; round < 3; round++) {
|
||||
let start = 0;
|
||||
while (start < words.length) {
|
||||
let end = words.indexOf(" ", start);
|
||||
if (end < 0) end = words.length;
|
||||
const word = words.slice(start, end);
|
||||
counts.set(word, (counts.get(word) ?? 0) + 1);
|
||||
if (!seen.has(word)) seen.add(word);
|
||||
if (counts.get(words.substring(start, end)) === 2) seen.delete(word);
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
console.log([...counts.entries()].map(([key, value]) => `${key}=${value}`).join(","));
|
||||
console.log([...seen].join(","));
|
||||
console.log(counts.has(ascii.slice(0, 3)), counts.has(words.slice(0, 5)), seen.has(words.slice(6, 10)));
|
||||
const index = new Map<string, number>();
|
||||
for (let i = 0; i + 3 <= ascii.length; i++) index.set(ascii.slice(i, i + 3), i);
|
||||
console.log(index.size, index.get("api"), index.get(ascii.substring(5, 8)), [...index.keys()].slice(0, 6).join("/"));
|
||||
const reads = new Map<string, string>([["api", "first"], ["users", "second"]]);
|
||||
console.log(reads.get(ascii.slice(5, 8)), reads.get(ascii.slice(9, 14)) ?? "none", reads.get(ascii.slice(0, 1)) ?? "none");
|
||||
|
||||
// Switch discriminants.
|
||||
function method(line: string): number {
|
||||
switch (line.slice(0, line.indexOf(" "))) {
|
||||
case "GET":
|
||||
return 1;
|
||||
case "POST":
|
||||
return 2;
|
||||
case "é":
|
||||
return 3;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
console.log(method(ascii), method("POST /x"), method("é y"), method("PATCH /x"), method("nospace"));
|
||||
|
||||
// Lent helper parameters, including recursion through a lent parameter.
|
||||
function isKeyword(text: string): boolean {
|
||||
return text === "let" || text === "const" || text === "return";
|
||||
}
|
||||
function depth(text: string, n: number): number {
|
||||
if (n === 0) return text.length;
|
||||
return depth(text, n - 1) + (text === "abc" ? 1 : 0);
|
||||
}
|
||||
function keep(text: string): string[] {
|
||||
return [text];
|
||||
}
|
||||
const program = "let x = 1; const y = 2; return x + y; letter";
|
||||
let keywords = 0;
|
||||
const kept: string[][] = [];
|
||||
for (let i = 0; i < program.length; i++) {
|
||||
for (let j = i + 2; j <= Math.min(program.length, i + 6); j++) {
|
||||
if (isKeyword(program.slice(i, j))) keywords++;
|
||||
}
|
||||
if (i % 9 === 0) kept.push(keep(program.slice(i, i + 4)));
|
||||
}
|
||||
console.log(keywords, depth("abcdef".slice(0, 3), 4), kept.map((k) => k[0]).join("|"));
|
||||
+5392
-5386
File diff suppressed because it is too large
Load Diff
@@ -375,7 +375,8 @@ export function integerRangeCases(): RangeCase[] {
|
||||
],
|
||||
},
|
||||
],
|
||||
[proof(cursor, 0, 80), proof(decoded, 0, 4294967295), proof(head, 0, 124)],
|
||||
// Passes enter at 0, 24, 48, 72 and 96 (at most 99), so the failing test reads at most 123.
|
||||
[proof(cursor, 0, 80), proof(decoded, 0, 4294967295), proof(head, 0, 123)],
|
||||
(func) => {
|
||||
for (const id of ["cursor", "decoded"])
|
||||
func.locals.push({ id, name: id, type: F64, mutable: true });
|
||||
|
||||
Reference in New Issue
Block a user