From 9062f3c04ee55f4b2f82eef5e5eb1699acebe4e1 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Sun, 9 Aug 2026 13:57:57 -0500 Subject: [PATCH] Add static array unshift and reverse - Lower unshift and reverse through the C and LLVM backends. - Preserve spread, mutation, evaluation-order, and reference-counting semantics. - Add runtime, differential, and surface-manifest coverage. --- .../src/backend/emission/emit-exprs.ts | 22 +++++++ packages/compiler/src/backend/llvm/emitter.ts | 36 ++++++++++ .../src/frontend/lowering/lower-containers.ts | 51 ++++++++++----- .../src/frontend/lowering/surfaces.ts | 2 + packages/compiler/src/ir/nodes.ts | 12 +++- packages/compiler/src/ir/validate.ts | 6 +- packages/compiler/src/library/fence-eval.ts | 7 +- packages/compiler/surface-manifest.json | 12 ++++ packages/runtime/src/scr_array.c | 65 +++++++++++++++++++ packages/runtime/src/scr_runtime.h | 9 +++ packages/runtime/test/test_array.c | 64 ++++++++++++++++++ tests/corpus/2685-array-unshift-reverse.ts | 55 ++++++++++++++++ tests/harness/surface-manifest.test.ts | 2 + 13 files changed, 321 insertions(+), 22 deletions(-) create mode 100644 tests/corpus/2685-array-unshift-reverse.ts diff --git a/packages/compiler/src/backend/emission/emit-exprs.ts b/packages/compiler/src/backend/emission/emit-exprs.ts index a977366fd..aebc6ec60 100644 --- a/packages/compiler/src/backend/emission/emit-exprs.ts +++ b/packages/compiler/src/backend/emission/emit-exprs.ts @@ -1404,6 +1404,24 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp { E.line(`}`); return E.newTemp(e.type, `scr_arr_len(${r.name})`); } + case "unshift": { + // Like push, every argument evaluates before mutation. Apply + // them from right to left so their final front order remains + // the source order. Ref ownership moves into the array. + const vs = e.args.map((a) => E.emitExpr(a)); + if (acc === "ref") vs.forEach((v) => E.moveTemp(v)); + let last: Temp | undefined; + for (let i = vs.length - 1; i >= 0; i--) { + last = E.newTemp(e.type, `scr_arr_unshift_${acc}(${r.name}, ${vs[i]!.name})`); + } + return last ?? E.newTemp(e.type, `scr_arr_len(${r.name})`); + } + case "unshiftSpread": { + // The runtime snapshots and retains the borrowed source, + // including the aliasing `a.unshift(...a)` case. + const src = E.emitExpr(e.args[0]!); + return E.newTemp(e.type, `scr_arr_unshift_spread(${r.name}, ${src.name})`); + } case "pop": // Ownership of a refcounted element moves OUT of the array to // this temp (+1 to us, the runtime does not release it). @@ -1443,6 +1461,10 @@ export function emitExpr(E: CEmitter, e: IrExpr): Temp { } case "toReversed": return E.newTemp(e.type, `scr_arr_to_reversed(${r.name})`); + case "reverse": + // Mutates in place and returns a retained reference to the + // receiver, preserving Array.prototype.reverse identity. + return E.newTemp(e.type, `scr_arr_reverse(${r.name})`); case "toSpliced": { const start = E.emitExpr(e.args[0]!); const count = E.emitExpr(e.args[1]!); diff --git a/packages/compiler/src/backend/llvm/emitter.ts b/packages/compiler/src/backend/llvm/emitter.ts index a62ce1faf..c5db43c70 100644 --- a/packages/compiler/src/backend/llvm/emitter.ts +++ b/packages/compiler/src/backend/llvm/emitter.ts @@ -8954,6 +8954,35 @@ class LlEmitter { B.line(`${t} = call double @scr_arr_len(ptr ${r.name})`); return { name: t, type: e.type }; } + case "unshift": { + // Evaluate every argument before the first mutation, then insert + // from right to left so the final front order is source order. + const vs = e.args.map((a) => this.emitExpr(a)); + if (acc === "ref") vs.forEach((v) => this.moveTemp(v)); + this.declare( + `declare double @scr_arr_unshift_${acc}(ptr, ${accArg})`, + ); + let last = ""; + for (let i = vs.length - 1; i >= 0; i--) { + last = B.tmp(); + B.line( + `${last} = call double @scr_arr_unshift_${acc}(ptr ${r.name}, ${accTy} ${vs[i]!.name})`, + ); + } + if (last !== "") return { name: last, type: e.type }; + this.declare(`declare double @scr_arr_len(ptr)`); + const t = B.tmp(); + B.line(`${t} = call double @scr_arr_len(ptr ${r.name})`); + return { name: t, type: e.type }; + } + case "unshiftSpread": { + // The runtime snapshots the borrowed source and handles self-spread. + const src = this.emitExpr(e.args[0]!); + this.declare(`declare double @scr_arr_unshift_spread(ptr, ptr)`); + const t = B.tmp(); + B.line(`${t} = call double @scr_arr_unshift_spread(ptr ${r.name}, ptr ${src.name})`); + return { name: t, type: e.type }; + } case "pop": { // Ownership of a refcounted element moves OUT of the array to // this temp (+1 to us, the runtime does not release it). @@ -9013,6 +9042,13 @@ class LlEmitter { B.line(`${t} = call ptr @scr_arr_to_reversed(ptr ${r.name})`); return this.own({ name: t, type: e.type }); } + case "reverse": { + // Mutates in place and returns the same receiver as a fresh +1. + this.declare(`declare ptr @scr_arr_reverse(ptr)`); + const t = B.tmp(); + B.line(`${t} = call ptr @scr_arr_reverse(ptr ${r.name})`); + return this.own({ name: t, type: e.type }); + } case "toSpliced": { const start = this.emitExpr(e.args[0]!); const count = this.emitExpr(e.args[1]!); diff --git a/packages/compiler/src/frontend/lowering/lower-containers.ts b/packages/compiler/src/frontend/lowering/lower-containers.ts index da557647b..9ff624bad 100644 --- a/packages/compiler/src/frontend/lowering/lower-containers.ts +++ b/packages/compiler/src/frontend/lowering/lower-containers.ts @@ -86,7 +86,8 @@ function lowerOptionalDefaultArg( : L.lowerExprExpecting(node, expected); } -/** Ambient array method calls. `push`/`pop`/`indexOf`/`includes`/`join` +/** Ambient array method calls. `push`/`unshift`/`pop`/`reverse`/ + * `indexOf`/`includes`/`join` * lower to arrIntrinsic; the HOF family — `map`/`filter`/`forEach`/ * `find`/`some`/`every`/`flatMap`/`reduce`/`reduceRight` — desugars to a * direct call of a synthetic loop function (see lowerArrayHofCall and @@ -199,6 +200,19 @@ function lowerOptionalDefaultArg( loc, }; } + if (name === "reverse") { + if (call.arguments.length !== 0) { + L.noLowering(`.reverse with ${call.arguments.length} arguments`, call); + } + return { + kind: "arrIntrinsic", + method: "reverse", + receiver: L.lowerExpr(access.expression), + args: [], + type: receiverIr, + loc, + }; + } if (name === "with") { if (call.arguments.length !== 2 || call.arguments.some(ts.isSpreadElement)) { L.noLowering(`.with with ${call.arguments.length} arguments`, call); @@ -265,11 +279,11 @@ function lowerOptionalDefaultArg( // indexOf/includes take a fromIndex, join's separator is optional, the // predicate/mapping HOFs take a thisArg. Each unlowered form is fenced // per site (SC2020), never silently truncated to the supported - // arguments. push lowers every declared form (variadic, 0 args + // arguments. push/unshift lower every declared form (variadic, 0 args // included — Node returns the unchanged length); reduce/reduceRight // lower both declared forms (with and without an initial value). const arity = { - push: [0, Number.MAX_SAFE_INTEGER], pop: [0, 0], indexOf: [1, 1], includes: [1, 1], join: [1, 1], + push: [0, Number.MAX_SAFE_INTEGER], unshift: [0, Number.MAX_SAFE_INTEGER], pop: [0, 0], indexOf: [1, 1], includes: [1, 1], join: [1, 1], concat: [0, Number.MAX_SAFE_INTEGER], slice: [0, 2], shift: [0, 0], splice: [1, 2], at: [1, 1], map: [1, 1], filter: [1, 1], forEach: [1, 1], find: [1, 1], findIndex: [1, 1], some: [1, 1], @@ -277,7 +291,7 @@ function lowerOptionalDefaultArg( every: [1, 1], flatMap: [1, 1], reduce: [1, 2], reduceRight: [1, 2], }[ name as - | "push" | "pop" | "concat" | "indexOf" | "includes" | "join" | "slice" | "shift" | "splice" | "at" | "map" | "filter" + | "push" | "unshift" | "pop" | "concat" | "indexOf" | "includes" | "join" | "slice" | "shift" | "splice" | "at" | "map" | "filter" | "forEach" | "find" | "findIndex" | "findLast" | "findLastIndex" | "some" | "every" | "flatMap" | "reduce" | "reduceRight" ]; if (call.arguments.length < arity[0]! || call.arguments.length > arity[1]!) { @@ -298,17 +312,17 @@ function lowerOptionalDefaultArg( hint, ); } - if (name === "push" || name === "pop") { + if (name === "push" || name === "unshift" || name === "pop") { const receiver = L.lowerExpr(access.expression); - // `a.push(...src)`: append src's elements in order (count - // snapshotted first, so `a.push(...a)` duplicates like JS) and - // return the new length. + // `a.push(...src)` / `a.unshift(...src)`: copy src's elements in + // order (count snapshotted first, so the self-spread forms duplicate + // like JS) and return the new length. const spreadArg = call.arguments.length === 1 && ts.isSpreadElement(call.arguments[0]!) ? call.arguments[0]! : null; - if (name === "push" && spreadArg && ts.isSpreadElement(spreadArg)) { + if ((name === "push" || name === "unshift") && spreadArg && ts.isSpreadElement(spreadArg)) { let src = L.lowerExpr(spreadArg.expression); - // `a.push(...someSet)`: a same-element Set drains first + // `a.push(...someSet)` / `a.unshift(...someSet)`: a same-element Set drains first // (setIntrinsic toArray — insertion order), then appends. if (src.type.kind === "set" && typeEquals(src.type.elem, elem)) { src = { kind: "setIntrinsic", method: "toArray", receiver: src, args: [], type: arrayOf(src.type.elem), loc }; @@ -317,23 +331,30 @@ function lowerOptionalDefaultArg( L.unsupported( "SC1090", spreadArg, - `pushing a spread of '${L.fmt(src.type)}' onto a '${L.fmt(receiverIr)}' array (only a same-element-type array spreads)`, + `${name === "push" ? "pushing" : "unshifting"} a spread of '${L.fmt(src.type)}' onto a '${L.fmt(receiverIr)}' array (only a same-element-type array spreads)`, ); } - return { kind: "arrIntrinsic", method: "pushSpread", receiver, args: [src], type: F64, loc }; + return { + kind: "arrIntrinsic", + method: name === "push" ? "pushSpread" : "unshiftSpread", + receiver, + args: [src], + type: F64, + loc, + }; } - // Pushed values flow into the element slot like an assignment would: + // Inserted values flow into the element slot like an assignment would: // union-element arrays wrap plain arm values (coerceInto is inert // when the types already agree). const args = call.arguments.map((a) => - name === "push" ? L.lowerExprExpecting(a, elem) : L.lowerExpr(a), + name === "push" || name === "unshift" ? L.lowerExprExpecting(a, elem) : L.lowerExpr(a), ); return { kind: "arrIntrinsic", method: name, receiver, args, - type: name === "push" ? F64 : elem, + type: name === "push" || name === "unshift" ? F64 : elem, loc, }; } diff --git a/packages/compiler/src/frontend/lowering/surfaces.ts b/packages/compiler/src/frontend/lowering/surfaces.ts index cad31512f..8cdf8274c 100644 --- a/packages/compiler/src/frontend/lowering/surfaces.ts +++ b/packages/compiler/src/frontend/lowering/surfaces.ts @@ -263,7 +263,9 @@ export const READLINE_DOCUMENTED_OPTIONS: ReadonlySet = new Set([ * this set hit the SC2020 fence. */ export const ARRAY_METHODS = new Set([ "push", + "unshift", "pop", + "reverse", "concat", "map", "filter", diff --git a/packages/compiler/src/ir/nodes.ts b/packages/compiler/src/ir/nodes.ts index 73324589b..8bda21a12 100644 --- a/packages/compiler/src/ir/nodes.ts +++ b/packages/compiler/src/ir/nodes.ts @@ -1293,6 +1293,8 @@ export type IrArrIntrinsicMethod = | "length" | "push" | "pushSpread" + | "unshift" + | "unshiftSpread" | "pop" | "indexOf" | "includes" @@ -1300,6 +1302,7 @@ export type IrArrIntrinsicMethod = | "slice" | "shift" | "splice" + | "reverse" /** ES2023 copying methods. `toSpliced` receives [start, deleteCount, * itemsArray], with omitted arguments completed by the frontend; * `with` receives [index, value] and throws Node's catchable RangeError @@ -4192,7 +4195,10 @@ export type IrExpr = * ownership of refcounted args MOVES into the array), `pushSpread` (`a.push(...src)`: * one arg of the RECEIVER's own array type, BORROWED — its elements * append in order, count snapshotted first so `a.push(...a)` duplicates - * exactly like JS; returns the new length), `pop` (returns elem — traps on an + * exactly like JS; returns the new length), `unshift` (the matching + * variadic front insertion; all arguments evaluate before mutation and + * ref ownership moves in), `unshiftSpread` (one borrowed same-typed array, + * with self-spread snapshot semantics), `pop` (returns elem — traps on an * empty array; ownership moves OUT to the caller), `indexOf` (one * elem-typed arg, BORROWED; strict equality — NaN never matches; → f64), * `includes` (one elem-typed arg, borrowed; SameValueZero — NaN DOES @@ -4206,7 +4212,9 @@ export type IrExpr = * relative/clamped start and clamped count, an omitted count removes to * the end [backends fill +Infinity, the slice convention]; the result is * a fresh +1 array of the removed elements IN ORDER, their ownership - * MOVED from the receiver; insertion forms are frontend-fenced). */ + * MOVED from the receiver; insertion forms are frontend-fenced), and + * `reverse` (zero args; swaps the receiver's slots in place and returns + * the same array identity as an owned reference). */ | { kind: "arrIntrinsic"; method: IrArrIntrinsicMethod; diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index c15b72b91..4b29d2133 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -2372,9 +2372,9 @@ function validateFunction( } const elem = e.receiver.type.elem; const sig = - e.method === "push" + e.method === "push" || e.method === "unshift" ? { argTypes: e.args.map(() => elem), result: F64 } - : e.method === "pushSpread" + : e.method === "pushSpread" || e.method === "unshiftSpread" ? { argTypes: [e.receiver.type], result: F64 } : e.method === "pop" ? { argTypes: [], result: elem } @@ -2388,6 +2388,8 @@ function validateFunction( ? { argTypes: [F64, F64], result: e.receiver.type } : e.method === "toReversed" ? { argTypes: [], result: e.receiver.type } + : e.method === "reverse" + ? { argTypes: [], result: e.receiver.type } : e.method === "toSpliced" ? { argTypes: [F64, F64, e.receiver.type], result: e.receiver.type } : e.method === "with" diff --git a/packages/compiler/src/library/fence-eval.ts b/packages/compiler/src/library/fence-eval.ts index d2ff7720d..02b0a8a8c 100644 --- a/packages/compiler/src/library/fence-eval.ts +++ b/packages/compiler/src/library/fence-eval.ts @@ -167,12 +167,13 @@ function fenceTaxonomy(): FenceTaxonomy { /* ── classification: what policing a static entry means ───────────────── */ -/** Array methods with a distinct IR trace (arrIntrinsic methods). `push` - * includes the spread spelling — `a.push(...xs)` lowers to pushSpread and - * is the same surface. The rest of ARRAY_METHODS desugars to plain loops. */ +/** Array methods whose distinct arrIntrinsic traces can be policed. The + * variadic inserters include their spread spellings under the same surface. */ const ARR_DETECTABLE: Record = { push: ["push", "pushSpread"], + unshift: ["unshift", "unshiftSpread"], pop: ["pop"], + reverse: ["reverse"], indexOf: ["indexOf"], includes: ["includes"], join: ["join"], diff --git a/packages/compiler/surface-manifest.json b/packages/compiler/surface-manifest.json index 33703baab..1354c26b9 100644 --- a/packages/compiler/surface-manifest.json +++ b/packages/compiler/surface-manifest.json @@ -2097,6 +2097,12 @@ "name": "Array.prototype.reduceRight", "status": "static" }, + { + "id": "stdlib.array.reverse", + "kind": "stdlib", + "name": "Array.prototype.reverse", + "status": "static" + }, { "id": "stdlib.array.slice", "kind": "stdlib", @@ -2127,6 +2133,12 @@ "name": "Array.prototype.toSpliced", "status": "static" }, + { + "id": "stdlib.array.unshift", + "kind": "stdlib", + "name": "Array.prototype.unshift", + "status": "static" + }, { "id": "stdlib.array.with", "kind": "stdlib", diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c index c0d99be52..42568d634 100644 --- a/packages/runtime/src/scr_array.c +++ b/packages/runtime/src/scr_array.c @@ -68,6 +68,16 @@ static void scr_elem_release(const ScrArr *a, uint64_t slot) { else if (a->elem == SCR_ELEM_REF) a->elem_release(p); } +static uint64_t scr_elem_retain_slot(const ScrArr *a, uint64_t slot) { + if (!scr_elem_is_ref(a->elem)) return slot; + void *p = scr_slot_to_ptr(slot); + if (a->elem == SCR_ELEM_STR) p = scr_str_retain((ScrStr *)p); + else if (a->elem == SCR_ELEM_ARR) p = scr_arr_retain((ScrArr *)p); + else if (a->elem == SCR_ELEM_BYTES) p = scr_bytes_retain((ScrBytes *)p); + else p = a->elem_retain(p); + return scr_slot_from_ptr(p); +} + /* ── lifecycle ─────────────────────────────────────────────────────────── */ static void scr_arr_grow(ScrArr *a, size_t need) { @@ -259,6 +269,61 @@ double scr_arr_push_ref(ScrArr *a, void *v) { return scr_arr_push_slot(a, scr_slot_from_ptr(v)); } +/* ── unshift / reverse ────────────────────────────────────────────────── + * Single-element unshift takes ownership, matching push. The emitter calls + * it from right to left after evaluating every variadic argument, preserving + * JS argument order without a temporary array. The spread form borrows and + * retains its source, snapshots the count, and handles self-spread after the + * tail move by reading the relocated original block. */ +static double scr_arr_unshift_slot(ScrArr *a, uint64_t slot) { + scr_arr_grow(a, a->len + 1); + memmove(a->data + 1, a->data, a->len * sizeof(uint64_t)); + a->data[0] = slot; + a->len++; + return (double)a->len; +} + +double scr_arr_unshift_f64(ScrArr *a, double v) { + return scr_arr_unshift_slot(a, scr_slot_from_f64(v)); +} + +double scr_arr_unshift_bool(ScrArr *a, bool v) { + return scr_arr_unshift_slot(a, (uint64_t)(v ? 1 : 0)); +} + +double scr_arr_unshift_ref(ScrArr *a, void *v) { + return scr_arr_unshift_slot(a, scr_slot_from_ptr(v)); +} + +double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src) { + size_t old_len = a->len; + size_t add = src->len; + if (add == 0) return (double)old_len; + if (add > SIZE_MAX - old_len) scr_arr_oom(); + scr_arr_grow(a, old_len + add); + memmove(a->data + add, a->data, old_len * sizeof(uint64_t)); + if (src == a) { + for (size_t i = 0; i < add; i++) { + a->data[i] = scr_elem_retain_slot(a, a->data[add + i]); + } + } else { + for (size_t i = 0; i < add; i++) { + a->data[i] = scr_elem_retain_slot(src, src->data[i]); + } + } + a->len = old_len + add; + return (double)a->len; +} + +ScrArr *scr_arr_reverse(ScrArr *a) { + for (size_t i = 0; i < a->len / 2; i++) { + uint64_t tmp = a->data[i]; + a->data[i] = a->data[a->len - 1 - i]; + a->data[a->len - 1 - i] = tmp; + } + return scr_arr_retain(a); +} + static uint64_t scr_arr_pop_slot(ScrArr *a) { if (a->len == 0) { scr_trap("scriptc: RangeError: pop() on an empty array\n"); diff --git a/packages/runtime/src/scr_runtime.h b/packages/runtime/src/scr_runtime.h index cdb0ff003..94faab623 100644 --- a/packages/runtime/src/scr_runtime.h +++ b/packages/runtime/src/scr_runtime.h @@ -886,6 +886,15 @@ double scr_arr_push_f64(ScrArr *a, double v); double scr_arr_push_bool(ScrArr *a, bool v); double scr_arr_push_ref(ScrArr *a, void *v); +/* unshift returns the new length and _ref takes ownership. The spread form + * borrows a same-element-kind source, retains copied refs, and snapshots + * self-spread. reverse mutates in place and returns the receiver at +1. */ +double scr_arr_unshift_f64(ScrArr *a, double v); +double scr_arr_unshift_bool(ScrArr *a, bool v); +double scr_arr_unshift_ref(ScrArr *a, void *v); +double scr_arr_unshift_spread(ScrArr *a, const ScrArr *src); +ScrArr *scr_arr_reverse(ScrArr *a); + /* pop traps on an empty array; _ref transfers ownership out (+1 to the * caller, no release). */ double scr_arr_pop_f64(ScrArr *a); diff --git a/packages/runtime/test/test_array.c b/packages/runtime/test/test_array.c index 5238f27e3..be8c48326 100644 --- a/packages/runtime/test/test_array.c +++ b/packages/runtime/test/test_array.c @@ -76,6 +76,69 @@ static void test_bool(void) { scr_arr_release(a); } +static void test_unshift_reverse(void) { +#ifdef SCR_RC_AUDIT + long strings0 = scr_str_live_count(); + long arrays0 = scr_arr_live_count(); +#endif + + /* The emitter applies variadic unshift arguments right-to-left. */ + ScrArr *a = scr_arr_new(SCR_ELEM_F64, 0); + scr_arr_push_f64(a, 3); + scr_arr_push_f64(a, 4); + check_f64(scr_arr_unshift_f64(a, 2), 3, "unshift f64 grows"); + check_f64(scr_arr_unshift_f64(a, 1), 4, "variadic-style unshift length"); + check_f64(scr_arr_get_f64(a, 0), 1, "unshift preserves arg order [0]"); + check_f64(scr_arr_get_f64(a, 1), 2, "unshift preserves arg order [1]"); + + ScrArr *same = scr_arr_reverse(a); + check(same == a, "reverse returns receiver identity"); + check_f64(scr_arr_get_f64(a, 0), 4, "reverse mutates first slot"); + check_f64(scr_arr_get_f64(a, 3), 1, "reverse mutates last slot"); + scr_arr_release(same); /* reverse's returned +1 */ + + ScrArr *front = scr_arr_new(SCR_ELEM_F64, 0); + scr_arr_push_f64(front, 8); + scr_arr_push_f64(front, 9); + check_f64(scr_arr_unshift_spread(a, front), 6, "unshift spread length"); + check_f64(scr_arr_get_f64(a, 0), 8, "unshift spread first"); + check_f64(scr_arr_get_f64(a, 1), 9, "unshift spread second"); + scr_arr_release(front); + scr_arr_release(a); + + /* Self-spread snapshots the original block before front insertion. */ + ScrArr *self = scr_arr_new(SCR_ELEM_BOOL, 0); + scr_arr_push_bool(self, true); + scr_arr_push_bool(self, false); + check_f64(scr_arr_unshift_spread(self, self), 4, "unshift self-spread length"); + check(scr_arr_get_bool(self, 0) && !scr_arr_get_bool(self, 1), + "unshift self-spread copied prefix"); + check(scr_arr_get_bool(self, 2) && !scr_arr_get_bool(self, 3), + "unshift self-spread kept original tail"); + scr_arr_release(self); + + /* Spread retains ref elements; either array may then die first. */ + ScrArr *src = scr_arr_new(SCR_ELEM_STR, 0); + scr_arr_push_ref(src, scr_str_new("front", 5)); + ScrArr *dst = scr_arr_new(SCR_ELEM_STR, 0); + scr_arr_push_ref(dst, scr_str_new("back", 4)); + scr_arr_unshift_spread(dst, src); + ScrStr *copied = (ScrStr *)scr_arr_get_ref(dst, 0); + check(copied->rc == 3, "unshift spread retained ref element"); + scr_str_release(copied); + scr_arr_release(src); + ScrStr *still = (ScrStr *)scr_arr_get_ref(dst, 0); + check(strcmp(still->data, "front") == 0, + "unshift spread ref survives source release"); + scr_str_release(still); + scr_arr_release(dst); + +#ifdef SCR_RC_AUDIT + check(scr_str_live_count() == strings0, "unshift/reverse: no strings leaked"); + check(scr_arr_live_count() == arrays0, "unshift/reverse: no arrays leaked"); +#endif +} + static void test_str_rc(void) { #ifdef SCR_RC_AUDIT long strings0 = scr_str_live_count(); @@ -401,6 +464,7 @@ int main(int argc, char **argv) { test_f64_basics(); test_bool(); + test_unshift_reverse(); test_str_rc(); test_nested_rc(); test_index_of_includes(); diff --git a/tests/corpus/2685-array-unshift-reverse.ts b/tests/corpus/2685-array-unshift-reverse.ts new file mode 100644 index 000000000..b375e7536 --- /dev/null +++ b/tests/corpus/2685-array-unshift-reverse.ts @@ -0,0 +1,55 @@ +// Static Array.prototype.unshift and mutating reverse: variadic insertion +// preserves source order, returns the new length, and evaluates every +// argument before the first mutation. reverse mutates in place and returns +// the receiver identity. +const nums: number[] = [3, 4]; +console.log(nums.unshift(1, 2), nums.join(",")); +console.log(nums.unshift(), nums.join(",")); + +const observed: number[] = [10]; +console.log(observed.unshift(observed.length, observed.length)); +console.log(observed.join(",")); + +// A single same-element array or Set spread keeps iteration order. The +// self-spread form snapshots the original values before front insertion. +const fromArray: number[] = [7, 8]; +console.log(nums.unshift(...fromArray), nums.join(",")); +const fromSet = new Set([5, 6]); +console.log(nums.unshift(...fromSet), nums.join(",")); +const self: number[] = [1, 2, 3]; +console.log(self.unshift(...self), self.join(",")); + +// reverse returns THE receiver: mutating the result mutates the original. +const reversed = nums.reverse(); +console.log(nums.join(",")); +reversed.push(99); +console.log(nums.join(",")); +console.log(reversed.reverse().unshift(0), nums.join(",")); + +// Scalar and refcounted element representations all use the same slot +// mutation without copying or releasing the elements. +const bits: boolean[] = [false, true]; +bits.unshift(true, false); +console.log(bits.reverse().join(",")); + +interface Row { + id: number; + label: string; +} +const rows: Row[] = [{ id: 2, label: "b" }]; +const first: Row = { id: 1, label: "a" }; +rows.unshift(first); +const sameRows = rows.reverse(); +sameRows[1]!.label = "A"; +console.log(rows[0]!.id, rows[1]!.label, rows.length); + +const optional: (string | undefined)[] = ["tail"]; +optional.unshift(undefined, "head"); +optional.reverse(); +console.log(optional.length, optional[0] ?? "-", optional[1] ?? "-", optional[2] === undefined); + +console.log([1, 2, 3].reverse().join("-")); +const empty: string[] = []; +const emptyResult = empty.reverse(); +emptyResult.unshift("x"); +console.log(empty.join(",")); diff --git a/tests/harness/surface-manifest.test.ts b/tests/harness/surface-manifest.test.ts index cdbf6dfa3..ac3cc2d05 100644 --- a/tests/harness/surface-manifest.test.ts +++ b/tests/harness/surface-manifest.test.ts @@ -111,6 +111,8 @@ const PROBES: Probe[] = [ { id: "syntax.compound-assignment.plus", source: 'let x = 1;\nx += 2;\nconsole.log(x);\n' }, { id: "stdlib.string.charCodeAt", source: 'console.log("abc".charCodeAt(0));\n' }, { id: "stdlib.array.push", source: "const xs: number[] = [1];\nxs.push(2);\nconsole.log(xs.length);\n" }, + { id: "stdlib.array.unshift", source: "const xs: number[] = [2];\nconsole.log(xs.unshift(1), xs[0]);\n" }, + { id: "stdlib.array.reverse", source: "const xs: number[] = [1, 2];\nconsole.log(xs.reverse()[0]);\n" }, { id: "stdlib.math.floor", source: "console.log(Math.floor(1.5));\n" }, { id: "stdlib.map.has", source: 'const m = new Map();\nm.set("a", 1);\nconsole.log(m.has("a"));\n' }, { id: "stdlib.date.now", source: "console.log(Date.now() > 0);\n" },