mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-26 06:34:20 +08:00
Remove the last element of a packed array inline on pop (#1282)
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
BOOL,
|
||||
F64,
|
||||
STRING,
|
||||
UNDEFINED_T,
|
||||
VOID,
|
||||
arrayOf,
|
||||
type IrExpr,
|
||||
@@ -23,11 +24,17 @@ const ref = (localId: string, type: IrExpr["type"]): IrExpr => ({
|
||||
});
|
||||
const index = ref("i", F64);
|
||||
|
||||
function module(name: string, returnType: IrExpr["type"], body: IrStmt[]): IrModule {
|
||||
function module(
|
||||
name: string,
|
||||
returnType: IrExpr["type"],
|
||||
body: IrStmt[],
|
||||
unions: IrModule["unions"] = [],
|
||||
): IrModule {
|
||||
return {
|
||||
irVersion: 16,
|
||||
sourceFile: loc.file,
|
||||
entry: "__main",
|
||||
unions,
|
||||
functions: [
|
||||
{ name: "__main", params: [], returnType: VOID, locals: [], body: [], loc },
|
||||
{
|
||||
@@ -174,6 +181,76 @@ test.each([32, 64] as const)(
|
||||
},
|
||||
);
|
||||
|
||||
test.each([32, 64] as const)(
|
||||
"pop removes the last element of a packed array inline and keeps a cold runtime pop (%i-bit)",
|
||||
(bits) => {
|
||||
const maybe = { kind: "union", unionId: "maybe" } as const;
|
||||
const pop: IrExpr = {
|
||||
kind: "arrIntrinsic",
|
||||
method: "pop",
|
||||
receiver: ref("a", nums),
|
||||
args: [],
|
||||
type: maybe,
|
||||
loc,
|
||||
};
|
||||
const llvm = emitLlvmModule(
|
||||
module(
|
||||
"take",
|
||||
maybe,
|
||||
[{ kind: "return", value: pop, loc }],
|
||||
[{ id: "maybe", arms: [F64, UNDEFINED_T] }],
|
||||
),
|
||||
{ pointerBits: bits },
|
||||
);
|
||||
const fn = body(llvm, "take");
|
||||
const size = bits === 32 ? "i32" : "i64";
|
||||
// An empty array answers undefined without a call.
|
||||
expect(fn).toMatch(new RegExp(`icmp eq ${size} %t\\d+, 0\\n br i1 %t\\d+, label %remove\\.a`));
|
||||
expect(fn).toMatch(new RegExp(`sub nuw ${size} %t\\d+, 1`));
|
||||
expect(fn).toMatch(/store i64 0, ptr /);
|
||||
expect(fn).toMatch(/store i8 0, ptr /);
|
||||
expect(fn).toMatch(/phi i64 /);
|
||||
expect(fn).toMatch(/call zeroext i8 @scr_arr_pop_state\(.*\) cold$/m);
|
||||
},
|
||||
);
|
||||
|
||||
test("a discarded pop releases the removed element inline", () => {
|
||||
const words = ref("s", arrayOf(STRING));
|
||||
const length: IrExpr = {
|
||||
kind: "arrIntrinsic",
|
||||
method: "length",
|
||||
receiver: words,
|
||||
args: [],
|
||||
type: F64,
|
||||
loc,
|
||||
};
|
||||
const llvm = emitLlvmModule(
|
||||
module("drop", VOID, [
|
||||
{
|
||||
kind: "arraySetLength",
|
||||
arr: words,
|
||||
length: {
|
||||
kind: "bin",
|
||||
op: "-",
|
||||
left: length,
|
||||
right: { kind: "numLit", value: 1, type: F64, loc },
|
||||
type: F64,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
pop: true,
|
||||
},
|
||||
]),
|
||||
);
|
||||
const fn = body(llvm, "drop");
|
||||
expect(fn).toMatch(/fsub double %t\d+, 1\.0/);
|
||||
expect(fn).toMatch(/load i64, ptr .*\n(?:.*\n)*? store i64 0, ptr /);
|
||||
expect(fn).toMatch(/inttoptr i64 %t\d+ to ptr/);
|
||||
expect(fn).toContain("call void @scr_str_release(");
|
||||
// Holes, sparse storage, and every other length keep the runtime call.
|
||||
expect(fn).toContain("call void @scr_arr_set_len(");
|
||||
});
|
||||
|
||||
test.each([32, 64] as const)(
|
||||
"indices below the hole-free prefix bound skip the state bytes (%i-bit)",
|
||||
(bits) => {
|
||||
|
||||
@@ -508,6 +508,75 @@ export function emitDenseArraySet(
|
||||
B.startBlock(join);
|
||||
}
|
||||
|
||||
/** The dense half of `pop`. An empty array branches to `empty` (the
|
||||
* runtime's pop returns a hole state there and changes nothing). When the
|
||||
* hole-free prefix bound equals the length, the last element is a present
|
||||
* value in dense storage, and the runtime's pop is exactly: clear its slot
|
||||
* and state, lower the bound and the length to its index, and move the
|
||||
* slot out. That block is left open and its slot bits are returned as an
|
||||
* i64 (a boolean's 0 or 1, zero-extended); every other array branches to
|
||||
* `slow`, which keeps the runtime call. */
|
||||
export function emitDenseArrayPop(
|
||||
host: LlvmEmitterContext,
|
||||
array: string,
|
||||
acc: ArrayAccess,
|
||||
empty: string,
|
||||
slow: string,
|
||||
): string {
|
||||
const B = host.B;
|
||||
const size = host.sizeType;
|
||||
const header = (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 lenPtr = header(SCR_ARR_FIELD.len),
|
||||
len = B.tmp(),
|
||||
none = B.tmp();
|
||||
B.line(`${len} = load ${size}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
|
||||
B.line(`${none} = icmp eq ${size} ${len}, 0`);
|
||||
const check = B.newLabel("arr.pop.check"),
|
||||
dense = B.newLabel("arr.pop.dense");
|
||||
B.condBr(none, empty, check);
|
||||
B.startBlock(check);
|
||||
const filledPtr = header(SCR_ARR_FIELD.filled),
|
||||
filled = B.tmp(),
|
||||
packed = B.tmp();
|
||||
B.line(`${filled} = load i32, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
|
||||
B.line(`${packed} = icmp eq ${size} ${sizedFilled(host, filled)}, ${len}`);
|
||||
B.condBr(packed, dense, slow);
|
||||
B.startBlock(dense);
|
||||
const last = B.tmp();
|
||||
B.line(`${last} = sub nuw ${size} ${len}, 1`);
|
||||
const dataPtr = header(SCR_ARR_FIELD.data),
|
||||
data = B.tmp(),
|
||||
statesPtr = header(SCR_ARR_FIELD.present),
|
||||
states = B.tmp();
|
||||
B.line(`${data} = load ptr, ptr ${dataPtr}${host.fieldAliasAttachment(dataPtr)}`);
|
||||
B.line(`${states} = load ptr, ptr ${statesPtr}${host.fieldAliasAttachment(statesPtr)}`);
|
||||
const valuePtr = valuePointer(host, { offset: last, data }, acc);
|
||||
const statePtr = statePointer(host, { offset: last, states });
|
||||
const slotTy = slotType(acc);
|
||||
let raw = B.tmp();
|
||||
B.line(`${raw} = load ${slotTy}, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
if (slotTy === "i8") {
|
||||
const wide = B.tmp();
|
||||
B.line(`${wide} = zext i8 ${raw} to i64`);
|
||||
raw = wide;
|
||||
}
|
||||
B.line(`store ${slotTy} 0, ptr ${valuePtr}${host.fieldAliasAttachment(valuePtr)}`);
|
||||
B.line(`store i8 0, ptr ${statePtr}${host.fieldAliasAttachment(statePtr)}`);
|
||||
let last32 = last;
|
||||
if (size === "i64") {
|
||||
last32 = B.tmp();
|
||||
B.line(`${last32} = trunc i64 ${last} to i32`);
|
||||
}
|
||||
B.line(`store i32 ${last32}, ptr ${filledPtr}${host.fieldAliasAttachment(filledPtr)}`);
|
||||
B.line(`store ${size} ${last}, ptr ${lenPtr}${host.fieldAliasAttachment(lenPtr)}`);
|
||||
return raw;
|
||||
}
|
||||
|
||||
/** `scr_arr_push_*` (the array takes the value's reference) returning the
|
||||
* new length as a double. When `len < cap` the runtime's append is exactly
|
||||
* a dense store at `len` plus `len + 1`: no sparse entry can exist past the
|
||||
|
||||
@@ -199,7 +199,12 @@ import {
|
||||
type LocalArrayRead,
|
||||
} from "./local-array-reads.js";
|
||||
import { CheckedNarrows, emitCheckedNarrowReceiver } from "./checked-narrows.js";
|
||||
import { emitDenseArrayGet, emitDenseArraySet, emitDenseSlotUnlink } from "./dense-array-access.js";
|
||||
import {
|
||||
emitDenseArrayGet,
|
||||
emitDenseArrayPop,
|
||||
emitDenseArraySet,
|
||||
emitDenseSlotUnlink,
|
||||
} from "./dense-array-access.js";
|
||||
import {
|
||||
emitStackMapRead,
|
||||
findMapReadLifetimes,
|
||||
@@ -8130,6 +8135,28 @@ export class LlEmitter {
|
||||
B.line(`${same} = fcmp oeq double ${length.name}, ${lenNumber}`);
|
||||
B.condBr(same, done, resize);
|
||||
B.startBlock(resize);
|
||||
if (s.pop === true) {
|
||||
// A discarded pop's `length - 1` on a packed array removes the
|
||||
// last element inline, then releases it.
|
||||
const call = B.newLabel("arr.len.call"),
|
||||
pop = B.newLabel("arr.len.pop");
|
||||
const lastNumber = B.tmp(),
|
||||
dropsLast = B.tmp();
|
||||
B.line(`${lastNumber} = fsub double ${lenNumber}, 1.0`);
|
||||
B.line(`${dropsLast} = fcmp oeq double ${length.name}, ${lastNumber}`);
|
||||
B.condBr(dropsLast, pop, call);
|
||||
B.startBlock(pop);
|
||||
const elem = s.arr.type.elem;
|
||||
const acc = elemAccess(elem);
|
||||
const raw = emitDenseArrayPop(this, arr.name, acc, call, call);
|
||||
if (acc === "ref" && isRefCounted(elem)) {
|
||||
const old = B.tmp();
|
||||
B.line(`${old} = inttoptr i64 ${raw} to ptr`);
|
||||
this.releaseValue(old, elem);
|
||||
}
|
||||
B.br(done);
|
||||
B.startBlock(call);
|
||||
}
|
||||
B.line(`call void @scr_arr_set_len(ptr ${arr.name}, double ${length.name})`);
|
||||
this.emitPendingCheck();
|
||||
B.br(done);
|
||||
|
||||
@@ -49,7 +49,11 @@ import {
|
||||
STRING_UTF16_LEN_DECL,
|
||||
} from "./string-reads.js";
|
||||
import { exactInteger, widenInteger } from "./integer-values.js";
|
||||
import { emitDenseArrayNumber, emitDenseArrayPush } from "./dense-array-access.js";
|
||||
import {
|
||||
emitDenseArrayNumber,
|
||||
emitDenseArrayPop,
|
||||
emitDenseArrayPush,
|
||||
} from "./dense-array-access.js";
|
||||
import { emitUnionWiden } from "./expr-records.js";
|
||||
|
||||
export function resolveThunkFor(host: LlvmEmitterContext, inner: IrType): string {
|
||||
@@ -667,19 +671,38 @@ export function emitArrIntrinsic(
|
||||
const rawSlot = B.slot();
|
||||
const resultSlot = B.slot();
|
||||
B.entryAllocas.push(`${rawSlot} = alloca i64`, `${resultSlot} = alloca ptr`);
|
||||
B.line(`store i64 0, ptr ${rawSlot}`);
|
||||
const state = B.tmp();
|
||||
const has = B.tmp();
|
||||
B.line(`${state} = call zeroext i8 @scr_arr_${method}_state(ptr ${r.name}, ptr ${rawSlot})`);
|
||||
B.line(`${has} = icmp eq i8 ${state}, 1`);
|
||||
const lp = B.newLabel("remove.p");
|
||||
const la = B.newLabel("remove.a");
|
||||
const lj = B.newLabel("remove.j");
|
||||
// pop answers an empty or packed array inline; shift and every other
|
||||
// pop take the runtime call.
|
||||
let denseRaw: string | null = null;
|
||||
let denseLabel = "";
|
||||
let slowLabel = "";
|
||||
if (method === "pop") {
|
||||
slowLabel = B.newLabel("remove.slow");
|
||||
denseRaw = emitDenseArrayPop(host, r.name, acc, la, slowLabel);
|
||||
denseLabel = B.currentLabel();
|
||||
B.br(lp);
|
||||
B.startBlock(slowLabel);
|
||||
}
|
||||
B.line(`store i64 0, ptr ${rawSlot}`);
|
||||
const state = B.tmp();
|
||||
const has = B.tmp();
|
||||
const slowRaw = B.tmp();
|
||||
B.line(
|
||||
`${state} = call zeroext i8 @scr_arr_${method}_state(ptr ${r.name}, ptr ${rawSlot})${denseRaw === null ? "" : " cold"}`,
|
||||
);
|
||||
B.line(`${slowRaw} = load i64, ptr ${rawSlot}`);
|
||||
B.line(`${has} = icmp eq i8 ${state}, 1`);
|
||||
B.condBr(has, lp, la);
|
||||
B.startBlock(lp);
|
||||
const raw = B.tmp();
|
||||
let raw = slowRaw;
|
||||
const value = B.tmp();
|
||||
B.line(`${raw} = load i64, ptr ${rawSlot}`);
|
||||
if (denseRaw !== null) {
|
||||
raw = B.tmp();
|
||||
B.line(`${raw} = phi i64 [ ${denseRaw}, %${denseLabel} ], [ ${slowRaw}, %${slowLabel} ]`);
|
||||
}
|
||||
if (into) {
|
||||
// Slot bits for f64; a canonical 0/1 for bool, like scr_union_new_bool.
|
||||
let bits = raw;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/5681-array-pop-and-length.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Removing the last element with a statement-level pop() or `length = n`,
|
||||
// for every element kind, on dense arrays, arrays with holes and present
|
||||
// undefined values, sparse arrays and arrays shared between bindings.
|
||||
// Removed references are released; the remaining elements, the length and
|
||||
// later appends match Node.
|
||||
|
||||
class Item {
|
||||
readonly id: number;
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
function show(label: string, values: unknown[]): void {
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
if (!(i in values)) parts.push("<hole>");
|
||||
else {
|
||||
const v = values[i];
|
||||
parts.push(v instanceof Item ? `I${v.id}` : (JSON.stringify(v) ?? "undefined"));
|
||||
}
|
||||
}
|
||||
console.log(label, values.length, parts.join(" "));
|
||||
}
|
||||
|
||||
const numbers = [1.5, 2.5, 3.5, 4.5];
|
||||
numbers.pop();
|
||||
numbers.length = numbers.length - 1;
|
||||
numbers.push(9);
|
||||
show("numbers", numbers);
|
||||
|
||||
const flags = [true, false, true];
|
||||
flags.pop();
|
||||
flags.length = 1;
|
||||
flags.push(false);
|
||||
show("flags", flags);
|
||||
|
||||
const words = ["a", "b", "c", "d"];
|
||||
const alias = words;
|
||||
words.pop();
|
||||
alias.length = alias.length - 1;
|
||||
words.push("z");
|
||||
show("words", words);
|
||||
show("alias", alias);
|
||||
|
||||
const items = [new Item(1), new Item(2), new Item(3)];
|
||||
const kept = items[2]!;
|
||||
items.pop();
|
||||
items.length = 1;
|
||||
items.push(new Item(4));
|
||||
show("items", items);
|
||||
console.log("kept", kept.id);
|
||||
|
||||
// Holes and present undefined at the end.
|
||||
const holes: (number | undefined)[] = [1, 2];
|
||||
holes[4] = 5;
|
||||
holes.length = 4;
|
||||
show("holes trimmed", holes);
|
||||
holes.length = 3;
|
||||
show("holes trimmed again", holes);
|
||||
const undef: (string | undefined)[] = ["x", undefined, undefined];
|
||||
undef.pop();
|
||||
undef.length = undef.length - 1;
|
||||
show("undefined", undef);
|
||||
|
||||
// A sparse array: a far index then shrinking past it and back by one.
|
||||
const sparse: number[] = [1, 2, 3];
|
||||
sparse[2000000] = 4;
|
||||
sparse.length = 2000000;
|
||||
show("sparse cut", sparse.slice(0, 4));
|
||||
console.log("sparse length", sparse.length, 1999999 in sparse);
|
||||
sparse.length = sparse.length - 1;
|
||||
console.log("sparse length", sparse.length);
|
||||
|
||||
// A stack used the way a checker pushes and pops contextual entries.
|
||||
const stack: Item[] = [];
|
||||
const top: number[] = [];
|
||||
let checksum = 0;
|
||||
for (let i = 0; i < 20000; i++) {
|
||||
stack.push(new Item(i));
|
||||
top.push(i);
|
||||
if (i % 3 === 2) {
|
||||
stack.pop();
|
||||
top.length = top.length - 1;
|
||||
}
|
||||
if (stack.length > 50) {
|
||||
stack.length = stack.length - 1;
|
||||
top.pop();
|
||||
}
|
||||
checksum = (checksum + stack[stack.length - 1]!.id + top[top.length - 1]!) % 1000003;
|
||||
}
|
||||
console.log("stack", stack.length, top.length, checksum);
|
||||
|
||||
// Lengths that are not one less: unchanged, longer, invalid.
|
||||
const other = [1, 2, 3];
|
||||
other.length = 3;
|
||||
other.length = 5;
|
||||
show("longer", other);
|
||||
other.length = 1;
|
||||
show("shorter by many", other);
|
||||
try {
|
||||
other.length = -1;
|
||||
} catch (e) {
|
||||
console.log("invalid", (e as Error).name, other.length);
|
||||
}
|
||||
try {
|
||||
other.length = 0.5;
|
||||
} catch (e) {
|
||||
console.log("fraction", (e as Error).name, other.length);
|
||||
}
|
||||
const empty: number[] = [];
|
||||
empty.pop();
|
||||
empty.length = 0;
|
||||
show("empty", empty);
|
||||
|
||||
// pop() results: the last element, undefined for an empty array, a hole or a
|
||||
// present undefined, and values taken from sparse storage.
|
||||
const nums = [0.5, -0, 7];
|
||||
console.log("pop number", nums.pop(), Object.is(nums.pop(), -0), nums.pop(), nums.pop(), nums.length);
|
||||
const bools = [false, true];
|
||||
console.log("pop bool", bools.pop(), bools.pop(), bools.pop(), bools.length);
|
||||
const strs = ["p", "q"];
|
||||
const strAlias = strs;
|
||||
console.log("pop string", strs.pop(), strAlias.pop(), strs.pop(), strAlias.length);
|
||||
const objs = [new Item(7), new Item(8)];
|
||||
const popped = objs.pop();
|
||||
console.log("pop item", popped?.id, objs.pop()?.id, objs.pop(), objs.length);
|
||||
const gaps: number[] = [1];
|
||||
gaps[3] = 4;
|
||||
console.log("pop gaps", gaps.pop(), gaps.pop(), gaps.pop(), gaps.pop(), gaps.pop(), gaps.length);
|
||||
const maybe: (number | undefined)[] = [1, undefined];
|
||||
console.log("pop present undefined", maybe.pop(), maybe.length, maybe.pop(), maybe.length);
|
||||
const far: string[] = ["a"];
|
||||
far[100000] = "b";
|
||||
console.log("pop sparse", far.pop(), far.length, far.pop(), far.length);
|
||||
let popTotal = 0;
|
||||
const work: number[] = [];
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
work.push(i, i + 1);
|
||||
popTotal += work.pop()!;
|
||||
if (i % 5 === 0) popTotal += work.pop() ?? -1;
|
||||
}
|
||||
console.log("pop loop", work.length, popTotal);
|
||||
@@ -1841,6 +1841,7 @@
|
||||
"5648-dns-lookup-forms.ts": 2992,
|
||||
"5649-dns-service-servers.ts": 2712,
|
||||
"5650-dns-lookup-errors-untyped.cjs": 2822,
|
||||
"5681-array-pop-and-length.ts": 3754,
|
||||
"5800-broadcast-channel.ts": 2899,
|
||||
"5801-broadcast-channel.cjs": 2727,
|
||||
"5802-broadcast-channel-workers.ts": 2809,
|
||||
@@ -4538,6 +4539,7 @@
|
||||
"5648-dns-lookup-forms.ts": 3416,
|
||||
"5649-dns-service-servers.ts": 4916,
|
||||
"5650-dns-lookup-errors-untyped.cjs": 3889,
|
||||
"5681-array-pop-and-length.ts": 3216,
|
||||
"5800-broadcast-channel.ts": 28374,
|
||||
"5801-broadcast-channel.cjs": 27251,
|
||||
"5802-broadcast-channel-workers.ts": 27251,
|
||||
|
||||
Reference in New Issue
Block a user