mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
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.
This commit is contained in:
@@ -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]!);
|
||||
|
||||
@@ -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]!);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -263,7 +263,9 @@ export const READLINE_DOCUMENTED_OPTIONS: ReadonlySet<string> = new Set([
|
||||
* this set hit the SC2020 fence. */
|
||||
export const ARRAY_METHODS = new Set([
|
||||
"push",
|
||||
"unshift",
|
||||
"pop",
|
||||
"reverse",
|
||||
"concat",
|
||||
"map",
|
||||
"filter",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<string, readonly string[] | undefined> = {
|
||||
push: ["push", "pushSpread"],
|
||||
unshift: ["unshift", "unshiftSpread"],
|
||||
pop: ["pop"],
|
||||
reverse: ["reverse"],
|
||||
indexOf: ["indexOf"],
|
||||
includes: ["includes"],
|
||||
join: ["join"],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<number>([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(","));
|
||||
@@ -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<string, number>();\nm.set("a", 1);\nconsole.log(m.has("a"));\n' },
|
||||
{ id: "stdlib.date.now", source: "console.log(Date.now() > 0);\n" },
|
||||
|
||||
Reference in New Issue
Block a user