diff --git a/docs/src/generated/node-v24-compatibility-meta.json b/docs/src/generated/node-v24-compatibility-meta.json index 500876593..db36cdb52 100644 --- a/docs/src/generated/node-v24-compatibility-meta.json +++ b/docs/src/generated/node-v24-compatibility-meta.json @@ -2,6 +2,6 @@ "schemaVersion": 3, "nodeVersion": "24.22.0", "nodeCommit": "f14cd3aa7bf9ca8df2cc5b8bb14beb0f335b1d93", - "artifactVersion": "56f660f5e2eb7391a048", + "artifactVersion": "cade1b8e6e0c5c42fcc9", "rowCount": 3880 } diff --git a/docs/src/generated/node-v24-compatibility.json b/docs/src/generated/node-v24-compatibility.json index 568cdb4c9..dadc9e836 100644 --- a/docs/src/generated/node-v24-compatibility.json +++ b/docs/src/generated/node-v24-compatibility.json @@ -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 @@ -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" } }, { diff --git a/internal/compatibility/generated/node-v24-backlog.json b/internal/compatibility/generated/node-v24-backlog.json index 683d477a5..12b12d63b 100644 --- a/internal/compatibility/generated/node-v24-backlog.json +++ b/internal/compatibility/generated/node-v24-backlog.json @@ -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 @@ -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" + ] } } }, diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 393955bc0..2b97f675e 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -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 @@ -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" }, diff --git a/packages/compiler/src/backend/llvm/dense-array-access.test.ts b/packages/compiler/src/backend/llvm/dense-array-access.test.ts index dc23ceae2..31680a518 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.test.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.test.ts @@ -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) => { diff --git a/packages/compiler/src/backend/llvm/dense-array-access.ts b/packages/compiler/src/backend/llvm/dense-array-access.ts index aeaf3e70f..183bb8239 100644 --- a/packages/compiler/src/backend/llvm/dense-array-access.ts +++ b/packages/compiler/src/backend/llvm/dense-array-access.ts @@ -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 diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index 8bae1abaf..79ad75d02 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -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); diff --git a/packages/compiler/src/backend/llvm/expr-containers.ts b/packages/compiler/src/backend/llvm/expr-containers.ts index a2d5fd35a..9c77690f8 100644 --- a/packages/compiler/src/backend/llvm/expr-containers.ts +++ b/packages/compiler/src/backend/llvm/expr-containers.ts @@ -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`); + 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; diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5681-array-pop-and-length.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5681-array-pop-and-length.ts.json new file mode 100644 index 000000000..d20701cae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/5681-array-pop-and-length.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/5681-array-pop-and-length.ts" + ], + "diags": [] +} diff --git a/tests/corpus/5681-array-pop-and-length.ts b/tests/corpus/5681-array-pop-and-length.ts new file mode 100644 index 000000000..451b32576 --- /dev/null +++ b/tests/corpus/5681-array-pop-and-length.ts @@ -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(""); + 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); diff --git a/tests/harness/corpus-costs.json b/tests/harness/corpus-costs.json index 25f00b1f7..ebaa91179 100644 --- a/tests/harness/corpus-costs.json +++ b/tests/harness/corpus-costs.json @@ -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, @@ -4537,6 +4538,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,