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:
Chris Tate
2026-08-09 13:57:57 -05:00
parent 23a2d3309e
commit 9062f3c04e
13 changed files with 321 additions and 22 deletions
@@ -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",
+10 -2
View File
@@ -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;
+4 -2
View File
@@ -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"
+4 -3
View File
@@ -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"],
+12
View File
@@ -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",
+65
View File
@@ -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");
+9
View File
@@ -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);
+64
View File
@@ -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(","));
+2
View File
@@ -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" },