Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/src/generated/node-v24-compatibility-meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.22.0",
"nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93",
"artifactVersion": "56f660f5e2eb7391a048",
"artifactVersion": "cade1b8e6e0c5c42fcc9",
"rowCount": 3880
}
10 changes: 5 additions & 5 deletions docs/src/generated/node-v24-compatibility.json
Original file line number Diff line number Diff line change
Expand Up @@ -569,9 +569,9 @@
"apiEntries": 120,
"static": {
"supported": 0,
"partial": 50,
"partial": 51,
"refused": 0,
"not-implemented": 70,
"not-implemented": 69,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
Expand Down Expand Up @@ -57593,9 +57593,9 @@
"anchor": "",
"nodeStability": null,
"static": {
"status": "not-implemented",
"detail": "No static compiler lowering is registered for this API yet.",
"verification": "registry-gap"
"status": "partial",
"detail": "Implemented for the call shapes accepted by the compiler lowering.",
"verification": "test-backed"
}
},
{
Expand Down
19 changes: 11 additions & 8 deletions internal/compatibility/generated/node-v24-backlog.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"tierItemCount": 3456,
"tiers": {
"static": {
"audit-partial": 1142,
"verify-gap": 2187,
"audit-partial": 1143,
"verify-gap": 2186,
"replace-refusal": 40,
"classify": 2,
"implement": 85
Expand Down Expand Up @@ -57507,12 +57507,15 @@
"priority": "normal",
"tiers": {
"static": {
"status": "not-implemented",
"action": "verify-gap",
"verification": "registry-gap",
"confidence": "low",
"source": "compiler-unmatched:process",
"tests": []
"status": "partial",
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "surface-manifest:node-builtin.process.execArgv",
"tests": [
"tests/corpus/990-process-basics.ts",
"tests/corpus/2314-process-introspection.ts"
]
}
}
},
Expand Down
12 changes: 8 additions & 4 deletions internal/compatibility/generated/node-v24-internal.json
Original file line number Diff line number Diff line change
Expand Up @@ -596,9 +596,9 @@
"apiEntries": 120,
"static": {
"supported": 0,
"partial": 50,
"partial": 51,
"refused": 0,
"not-implemented": 70,
"not-implemented": 69,
"by-design": 0,
"unreviewed": 0,
"not-applicable": 0
Expand Down Expand Up @@ -80949,8 +80949,12 @@
"anchor": "",
"nodeStability": null,
"static": {
"status": "not-implemented",
"evidence": "compiler-unmatched:process"
"status": "partial",
"evidence": "surface-manifest:node-builtin.process.execArgv",
"tests": [
"tests/corpus/990-process-basics.ts",
"tests/corpus/2314-process-introspection.ts"
]
},
"anchorSource": "chapter"
},
Expand Down
79 changes: 78 additions & 1 deletion packages/compiler/src/backend/llvm/dense-array-access.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
BOOL,
F64,
STRING,
UNDEFINED_T,
VOID,
arrayOf,
type IrExpr,
Expand All @@ -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 },
{
Expand Down Expand Up @@ -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) => {
Expand Down
69 changes: 69 additions & 0 deletions packages/compiler/src/backend/llvm/dense-array-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion packages/compiler/src/backend/llvm/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
37 changes: 30 additions & 7 deletions packages/compiler/src/backend/llvm/expr-containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -667,19 +671,38 @@ export function emitArrIntrinsic(
const rawSlot = B.slot();
const resultSlot = B.slot();
B.entryAllocas.push(`${rawSlot} = alloca i64`, `${resultSlot} = alloca ptr`);
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();
B.line(`${state} = call zeroext i8 @scr_arr_${method}_state(ptr ${r.name}, ptr ${rawSlot})`);
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`);
const lp = B.newLabel("remove.p");
const la = B.newLabel("remove.a");
const lj = B.newLabel("remove.j");
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/5681-array-pop-and-length.ts"
],
"diags": []
}
Loading
Loading