diff --git a/docs/content/docs/limitations.mdx b/docs/content/docs/limitations.mdx
index a86ed217e..862e49b12 100644
--- a/docs/content/docs/limitations.mdx
+++ b/docs/content/docs/limitations.mdx
@@ -52,6 +52,8 @@ title: "Limitations"
../limitations/types-and-class-definitions/static-field-updates.mdx
+../limitations/types-and-class-definitions/array-subclasses.mdx
+
../limitations/types-and-class-definitions/local-class-expressions.mdx
../limitations/types-and-class-definitions/constructor-function-inheritance.mdx
diff --git a/docs/content/limitations/types-and-class-definitions/array-subclasses.mdx b/docs/content/limitations/types-and-class-definitions/array-subclasses.mdx
new file mode 100644
index 000000000..ea76e7313
--- /dev/null
+++ b/docs/content/limitations/types-and-class-definitions/array-subclasses.mdx
@@ -0,0 +1,3 @@
+- **Array subclasses:** A class that extends `Array` compiles: its instances are arrays, and `map`, `filter`, `slice` and the other methods that create arrays return instances of the subclass, as in Node.js.
+ A generic subclass (`class Bag extends Array`), a class that extends such a subclass, and a subclass that declares `Symbol.species` are unsupported.
+ A method whose result the subclass's constructor cannot construct throws a `TypeError`: the constructor's parameters must take the one length argument, and a `map` to another element type needs a constructor that adds no elements.
diff --git a/packages/compiler/src/backend/cycle-analysis.ts b/packages/compiler/src/backend/cycle-analysis.ts
index fbd96e854..b03979bd9 100644
--- a/packages/compiler/src/backend/cycle-analysis.ts
+++ b/packages/compiler/src/backend/cycle-analysis.ts
@@ -146,6 +146,10 @@ function analyzeCycles(
...(c.localCaptures !== undefined
? [{ name: "", type: { kind: "classval" as const, className: c.name } }]
: []),
+ // An instance of a class that extends Array holds its elements.
+ ...(c.arrayElem !== undefined
+ ? [{ name: "", type: { kind: "array" as const, elem: c.arrayElem } }]
+ : []),
],
})),
...(mod.records ?? []).map((r) => ({
diff --git a/packages/compiler/src/backend/llvm/classes.ts b/packages/compiler/src/backend/llvm/classes.ts
index 82e34de90..d848b86fc 100644
--- a/packages/compiler/src/backend/llvm/classes.ts
+++ b/packages/compiler/src/backend/llvm/classes.ts
@@ -58,6 +58,7 @@ import {
traceAdapter,
type ShapeHost,
youngEntryLines,
+ arrElemInit,
} from "./shapes.js";
/** One virtual method slot of a hierarchy: the ROOT-MOST declaring class
@@ -371,11 +372,34 @@ function runtimePrefixOf(meta: LlClassMeta): RuntimePrefix | null {
* on classes rooted at a runtime base, then the stream-state slot on
* stream-rooted ones. */
function headerEnd(meta: LlClassMeta): number {
+ if (meta.def.arrayElem !== undefined) return ARRAY_PREFIX.length + 1;
if (!meta.hierarchy || meta.idInRc) return 1;
if (streamRooted(meta)) return 5;
return runtimePrefixOf(meta) ? 4 : 2;
}
+/** The array header a class declared `extends Array` begins with, after
+ * the size_t reference count: %ScrArr's len, cap, elem, cyc, spare, filled
+ * and data (scr_array.h). */
+const ARRAY_PREFIX = ["i32", "i32", "i8", "i8", "i16", "i32", "ptr"];
+
+/** LLVM `c"..."` text of a string's UTF-8 bytes and a terminating NUL. */
+function nulTerminatedBytes(text: string): string {
+ let out = "";
+ for (const byte of Buffer.from(text, "utf8"))
+ out +=
+ byte >= 0x20 && byte < 0x7f && byte !== 0x22 && byte !== 0x5c
+ ? String.fromCharCode(byte)
+ : `\\${byte.toString(16).padStart(2, "0").toUpperCase()}`;
+ return out + "\\00";
+}
+
+/** The class descriptor (ScrArrClass: name, destroy) of a class declared
+ * `extends Array`. */
+export function arrayClassDescriptor(className: string): string {
+ return `@${mangleClassStruct(className)}.arrcls`;
+}
+
/** The GEP index of the ThreadSlots slot word (an i32) in a class whose
* hierarchy holds ThreadSlots keys: right after the header words, so every
* member of the hierarchy has it at the same offset. */
@@ -473,7 +497,11 @@ export interface ClassHost extends ShapeHost {
* hierarchy folds away. The cycle analysis leaves the hierarchy untraced, so
* no trace adapter ever visits an instance (as for every untraced class). */
export function programLifetimeClass(host: ClassHost, meta: LlClassMeta): boolean {
- return host.programLifetime?.has(meta.root.def.name) === true && meta.root.def.runtime !== true;
+ return (
+ host.programLifetime?.has(meta.root.def.name) === true &&
+ meta.root.def.runtime !== true &&
+ meta.root.def.arrayElem === undefined
+ );
}
/** The reference count a program-lifetime instance is created with: the
@@ -725,7 +753,12 @@ export function assignCompactLayouts(
): void {
const isCompact = (className: string): boolean => {
const target = metaMap.get(className);
- return target !== undefined && target.def.runtime !== true && compact(target.root);
+ return (
+ target !== undefined &&
+ target.def.runtime !== true &&
+ target.root.def.arrayElem === undefined &&
+ compact(target.root)
+ );
};
const byPre = [...metaMap.values()].sort((a, b) => a.pre - b.pre);
for (const meta of byPre) {
@@ -900,13 +933,15 @@ export function emitClassShapes(
// usual pointer reinterprets.
const runtimePrefix = runtimePrefixOf(meta);
const prefix =
- meta.hierarchy && !meta.idInRc
- ? streamRooted(meta)
- ? ["ptr", "ptr", "ptr", "ptr"]
- : runtimePrefix
- ? ["ptr", "ptr", "ptr"]
- : ["ptr"]
- : [];
+ cls.arrayElem !== undefined
+ ? ARRAY_PREFIX
+ : meta.hierarchy && !meta.idInRc
+ ? streamRooted(meta)
+ ? ["ptr", "ptr", "ptr", "ptr"]
+ : runtimePrefix
+ ? ["ptr", "ptr", "ptr"]
+ : ["ptr"]
+ : [];
const members = [...prefix, ...(meta.threadSlot ? ["i32"] : []), ...fieldTys];
typeDefs.push(
`%${mangleClassStruct(cls.name)} = type { ${host.sizeType}${members.length ? ", " + members.join(", ") : ""} } ` +
@@ -993,6 +1028,7 @@ export function emitClassShapes(
const types = ownedFields(cls).map(({ field }) => storedFieldType(cls, field));
for (const t of types) fieldTypes.push(t);
if (programLifetimeClass(host, meta)) continue;
+ if (cls.arrayElem !== undefined) continue;
if (meta.hierarchy && (meta.children.length !== 0 || meta.root.def.runtime === true)) continue;
if (!types.some((t) => isRefCounted(t) && needsBoundedRelease(t))) continue;
const release = mangleClassRelease(cls.name);
@@ -1009,6 +1045,12 @@ export function emitClassShapes(
}
const directTargets = usedDirectTargets(host, candidates, fieldTypes);
+ // The descriptor entries of a class declared `extends Array`: a helper
+ // the module defines, or null (a class that is never constructed has
+ // neither).
+ const defined = new Set(mod.functions.map((fn) => fn.name));
+ const arrayClassFn = (name: string | undefined): string =>
+ name !== undefined && defined.has(name) ? `@${mangleFunction(name)}` : "null";
for (const cls of emitted) {
const meta = metaMap.get(cls.name)!;
const struct = mangleClassStruct(cls.name);
@@ -1090,6 +1132,7 @@ export function emitClassShapes(
const destroy = `${meta.hierarchy ? mangleClassReleaseDirect(cls.name) : mangleClassRelease(cls.name)}_destroy`;
const segmented =
chained &&
+ cls.arrayElem === undefined &&
!meta.compact &&
runtimePrefix === null &&
!isStreamRooted &&
@@ -1105,7 +1148,12 @@ export function emitClassShapes(
// members). Runs at rc == 0.
const teardown = (lines: string[]): void => {
const release = (prefix: string, f: (typeof refFields)[number], t: number): string[] => {
- const direct = bounded ? directDestroyTarget(host, f.type, directTargets) : null;
+ // An Array subclass instance's destroy releases its fields plainly
+ // (it takes no chain length).
+ const direct =
+ bounded && cls.arrayElem === undefined
+ ? directDestroyTarget(host, f.type, directTargets)
+ : null;
if (direct === null)
return [
` call void ${releaseSym(host, f.type)}(ptr %v${t}) ; ${llvmCommentText(f.name)}`,
@@ -1236,6 +1284,45 @@ export function emitClassShapes(
reld.push(`done:`, ` ret void`, `}`, ``);
defs.push(...reld);
}
+ } else if (cls.arrayElem !== undefined) {
+ // An instance is an array: its release is the array's, which reaches
+ // the class's destroy (the descriptor's) when the count reaches zero.
+ host.declare(`declare void @scr_arr_release(ptr)`);
+ host.declare(`declare void @scr_arr_subclass_drop(ptr)`);
+ const destroy = `${mangleClassRelease(cls.name)}.arrdestroy`;
+ const destroyBody: string[] = [
+ `define internal void @${destroy}(ptr %o) ${FN_ATTRS} { ; destroy ${cls.name} (an Array subclass instance)`,
+ `entry:`,
+ ];
+ const fieldsTeardown: string[] = [];
+ teardown(fieldsTeardown);
+ const free = emitObjectFree(host, traced);
+ destroyBody.push(
+ ...fieldsTeardown.slice(0, fieldsTeardown.length - free.length),
+ ` call void @scr_arr_subclass_drop(ptr %o)`,
+ ...free,
+ ` ret void`,
+ `}`,
+ ``,
+ );
+ const displayName = cls.name.includes(".")
+ ? cls.name.slice(cls.name.lastIndexOf(".") + 1)
+ : cls.name;
+ const jsName = cls.jsName ?? displayName;
+ defs.push(
+ ...destroyBody,
+ // The name is the class's own copy: its address identifies the
+ // class in dyn snapshots of an instance (scr_arr_copy_metadata).
+ `${arrayClassDescriptor(cls.name)}.name = private constant [${Buffer.byteLength(jsName, "utf8") + 1} x i8] c"${nulTerminatedBytes(jsName)}"`,
+ `${arrayClassDescriptor(cls.name)} = internal constant { ptr, ptr, ptr, ptr } { ptr ${arrayClassDescriptor(cls.name)}.name, ptr @${destroy}, ptr ${arrayClassFn(cls.arrayFields)}, ptr ${arrayClassFn(cls.arraySpecies)} }`,
+ ``,
+ `define internal void @${mangleClassRelease(cls.name)}(ptr %o) ${FN_ATTRS} { ; release ${cls.name} (the array's)`,
+ `entry:`,
+ ` call void @scr_arr_release(ptr %o)`,
+ ` ret void`,
+ `}`,
+ ``,
+ );
} else {
const freeBody: string[] = [];
teardown(freeBody);
@@ -1323,6 +1410,13 @@ export function emitClassShapes(
nw.push(
` store ${host.sizeType} ${lifetime ? programLifetimeRc(host.sizeType) : 1}, ptr %o`,
);
+ if (cls.arrayElem !== undefined) {
+ const init = arrElemInit(host, cls.arrayElem);
+ host.declare(`declare void @scr_arr_subclass_init(ptr, i8, ptr, ptr, i1 zeroext)`);
+ nw.push(
+ ` call void @scr_arr_subclass_init(ptr %o, i8 ${init.kind}, ptr ${init.ops}, ptr ${arrayClassDescriptor(cls.name)}, i1 ${traced})`,
+ );
+ }
if (lifetime && audit) {
// --sanitize builds: the RC audit skips its leak report when the
// program keeps program-lifetime objects (scr_console.c), and
@@ -1359,7 +1453,12 @@ export function emitClassShapes(
// Young instances: program-owned traced classes. Classes rooted at a
// runtime base (errors, emitters, events, streams) carry runtime state
// and stay with trial deletion, like every runtime family.
- if (traced && !lifetime && (!meta.hierarchy || meta.root.def.runtime !== true))
+ if (
+ traced &&
+ !lifetime &&
+ (!meta.hierarchy || meta.root.def.runtime !== true) &&
+ cls.arrayElem === undefined
+ )
nw.push(...youngEntryLines(host));
nw.push(` ret ptr ${meta.hierarchy ? "%o.vt" : "%o"}`, `}`, ``);
defs.push(...nw);
@@ -1382,6 +1481,14 @@ export function emitClassShapes(
tracedFields.forEach((f, i) => {
tr.push(...traceFieldVisitLines(struct, f.index, i, llvmCommentText(f.name)));
});
+ // An Array subclass instance's elements, when they are cycle-capable.
+ const tracedElements =
+ cls.arrayElem !== undefined &&
+ traceAdapter(host, { kind: "array", elem: cls.arrayElem }) !== null;
+ if (tracedElements) {
+ host.declare(`declare void @scr_arr_trace_v(ptr, ptr, ptr)`);
+ tr.push(` call void @scr_arr_trace_v(ptr %o, ptr %visit, ptr %ctx) ; the elements`);
+ }
tr.push(` ret void`, `}`, ``);
defs.push(...tr);
@@ -1399,6 +1506,12 @@ export function emitClassShapes(
);
});
if (meta.threadSlot) gf.push(...slotForgetLines(host, struct, meta, "gfw"));
+ if (cls.arrayElem !== undefined) {
+ // The elements: traced ones are the collector's, the others are
+ // released with the storage.
+ host.declare(`declare void @scr_arr_subclass_gc_drop(ptr, i1 zeroext)`);
+ gf.push(` call void @scr_arr_subclass_gc_drop(ptr %o, i1 ${tracedElements})`);
+ }
gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``);
defs.push(...gf);
}
diff --git a/packages/compiler/src/backend/llvm/expr-calls.ts b/packages/compiler/src/backend/llvm/expr-calls.ts
index 6ff52619e..5a16dabf3 100644
--- a/packages/compiler/src/backend/llvm/expr-calls.ts
+++ b/packages/compiler/src/backend/llvm/expr-calls.ts
@@ -1,4 +1,5 @@
/* Focused LLVM expression emission extracted from emitter.ts. */
+import { arrayClassDescriptor } from "./classes.js";
import { InternalCompilerError } from "../../errors.js";
import { newValueMayThrow } from "../../ir/analysis.js";
import {
@@ -1154,6 +1155,17 @@ export function emitCallExpr(
return host.own({ name: v.name, type: e.type });
}
case "instanceOf": {
+ // An array is an instance of a class declared `extends Array` when
+ // its ext names that class (scr_arr_class_of).
+ if (e.value.type.kind === "array") {
+ const v = emitBorrowedInputs(host, [e.value])[0]!;
+ host.declare(`declare ptr @scr_arr_class_of(ptr)`);
+ const cls = host.B.tmp();
+ const result = host.B.tmp();
+ host.B.line(`${cls} = call ptr @scr_arr_class_of(ptr ${v.name})`);
+ host.B.line(`${result} = icmp eq ptr ${cls}, ${arrayClassDescriptor(e.className)}`);
+ return { name: result, type: e.type };
+ }
// O(1) preorder-interval test against the vtable the object
// carries; the target's interval is a compile-time constant.
if (e.value.type.kind !== "object")
diff --git a/packages/compiler/src/backend/llvm/lib-dispatch.ts b/packages/compiler/src/backend/llvm/lib-dispatch.ts
index 9cd11feb1..06ac8aa0e 100644
--- a/packages/compiler/src/backend/llvm/lib-dispatch.ts
+++ b/packages/compiler/src/backend/llvm/lib-dispatch.ts
@@ -1,3 +1,4 @@
+import { arrayClassDescriptor } from "./classes.js";
import { emitBigIntWrap } from "./bigint-wrap.js";
import { emitBorrowedInput, emitBorrowedInputs } from "./borrowed-inputs.js";
import { borrowsIteratorInputs, borrowsJsonInputs } from "./checked-value-lifetimes.js";
@@ -276,6 +277,53 @@ export function emitGenericLibCall(host: LlvmEmitterContext, e: LibCallExpr): Ll
host.dyn.emitRecordSourceSpread(B, built.name, value.name, type.shapeId);
return { name: "", type: e.type };
}
+ if (e.fn === "array.speciesAdopt") {
+ // The receiver is borrowed; the method's result moves into the call,
+ // which answers it or the species instance that replaces it.
+ const receiver = emitBorrowedInput(host, e.args[0]!);
+ const result = host.emitExpr(e.args[1]!);
+ host.toHeap(result);
+ const length = host.emitExpr(e.args[2]!);
+ const setLength = host.emitExpr(e.args[3]!);
+ host.declare(`declare ptr @scr_arr_species_adopt(ptr, ptr, double, i1 zeroext)`);
+ const out = B.tmp();
+ B.line(
+ `${out} = call ptr @scr_arr_species_adopt(ptr ${receiver.name}, ptr ${result.name}, double ${length.name}, i1 zeroext ${setLength.name})`,
+ );
+ const value = host.own({ name: out, type: e.type });
+ host.emitPendingCheck();
+ return value;
+ }
+ if (e.fn === "array.dynIsClass") {
+ const className = e.args[1]!.kind === "strLit" ? e.args[1]!.value : null;
+ if (className === null)
+ throw new InternalCompilerError("llvm emitter bug: array.dynIsClass needs a class name");
+ const value = emitBorrowedInput(host, e.args[0]!);
+ host.declare(`declare zeroext i1 @scr_dyn_is_array_class(ptr, ptr)`);
+ const out = B.tmp();
+ B.line(
+ `${out} = call zeroext i1 @scr_dyn_is_array_class(ptr ${value.name}, ptr ${arrayClassDescriptor(className)})`,
+ );
+ return { name: out, type: e.type };
+ }
+ if (e.fn === "array.classConstructAdopt") {
+ const className = e.args[0]!.kind === "strLit" ? e.args[0]!.value : null;
+ if (className === null)
+ throw new InternalCompilerError(
+ "llvm emitter bug: array.classConstructAdopt needs a class name",
+ );
+ const result = host.emitExpr(e.args[1]!);
+ host.toHeap(result);
+ const length = host.emitExpr(e.args[2]!);
+ host.declare(`declare ptr @scr_arr_class_construct_adopt(ptr, ptr, double)`);
+ const out = B.tmp();
+ B.line(
+ `${out} = call ptr @scr_arr_class_construct_adopt(ptr ${arrayClassDescriptor(className)}, ptr ${result.name}, double ${length.name})`,
+ );
+ const value = host.own({ name: out, type: e.type });
+ host.emitPendingCheck();
+ return value;
+ }
if (e.fn === "dyn.typedRefIs" || e.fn === "dyn.typedRefKeyIs") {
const literal = e.args[1]!.kind === "strLit" ? e.args[1]!.value : null;
if (e.fn === "dyn.typedRefKeyIs" && literal === null)
diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts
index c9b125cd7..c706fb187 100644
--- a/packages/compiler/src/backend/llvm/lib-shared.ts
+++ b/packages/compiler/src/backend/llvm/lib-shared.ts
@@ -1348,6 +1348,11 @@ export const LIB_FN_SYMS: Record = {
"http.reqPipeSock": "scr_http_req_pipe_sock",
"http.reqResume": "scr_http_req_resume",
"http.reqPause": "scr_http_req_pause",
+ "array.subclassConstruct": "scr_arr_subclass_construct",
+ "array.subclassConstructDyn": "scr_arr_subclass_construct_dyn",
+ "array.speciesAdopt": "scr_arr_species_adopt",
+ "array.classConstructAdopt": "scr_arr_class_construct_adopt",
+ "array.dynIsClass": "scr_dyn_is_array_class",
"http.reqDestroy": "scr_http_req_destroy",
"http.reqSetEncoding": "scr_http_req_set_encoding",
"http.resSetHeader": "scr_http_res_set_header",
diff --git a/packages/compiler/src/backend/llvm/shapes.ts b/packages/compiler/src/backend/llvm/shapes.ts
index b3f5084d3..08d0db9b4 100644
--- a/packages/compiler/src/backend/llvm/shapes.ts
+++ b/packages/compiler/src/backend/llvm/shapes.ts
@@ -1084,7 +1084,33 @@ export function arrNewCall(
* live in its own block): capText must be "0". */
small = false,
): string {
- const useRef =
+ const useRef = arrUsesRef(host, elem);
+ if (!useRef) {
+ if (small) {
+ host.declare(`declare ptr @scr_arr_new_small(i32)`);
+ return `call ptr @scr_arr_new_small(i32 ${elemKindNum(elem)})`;
+ }
+ host.declare(`declare ptr @scr_arr_new(i32, ${host.sizeType})`);
+ return `call ptr @scr_arr_new(i32 ${elemKindNum(elem)}, ${host.sizeType} ${capText})`;
+ }
+ if (small) {
+ host.declare(`declare ptr @scr_arr_new_ops_small(ptr)`);
+ return `call ptr @scr_arr_new_ops_small(ptr ${arrOps(host, elem)})`;
+ }
+ host.declare(`declare ptr @scr_arr_new_ops(ptr, ${host.sizeType})`);
+ return `call ptr @scr_arr_new_ops(ptr ${arrOps(host, elem)}, ${host.sizeType} ${capText})`;
+}
+
+/** The ScrElemKind and ops descriptor (or `null`) an array of `elem`
+ * starts with: the arrays arrNewCall constructs, for the instances of a
+ * class that extends Array (scr_arr_subclass_init). */
+export function arrElemInit(host: ShapeHost, elem: IrType): { kind: number; ops: string } {
+ if (!arrUsesRef(host, elem)) return { kind: elemKindNum(elem), ops: "null" };
+ return { kind: host.compactArrayElem?.(elem) === true ? 6 : 5, ops: arrOps(host, elem) };
+}
+
+function arrUsesRef(host: ShapeHost, elem: IrType): boolean {
+ return (
elem.kind === "record" ||
elem.kind === "object" ||
elem.kind === "union" ||
@@ -1101,21 +1127,8 @@ export function arrNewCall(
elem.kind === "dyn" || // native collection seeds/drains: traced REF
elem.kind === "regex" || // RegExp values: scr_regex_* adapters, no trace (no refs inside)
elem.kind === "date" || // Date objects: scr_date_* adapters, no trace (no refs inside)
- (elem.kind === "array" && traceAdapter(host, elem) !== null);
- if (!useRef) {
- if (small) {
- host.declare(`declare ptr @scr_arr_new_small(i32)`);
- return `call ptr @scr_arr_new_small(i32 ${elemKindNum(elem)})`;
- }
- host.declare(`declare ptr @scr_arr_new(i32, ${host.sizeType})`);
- return `call ptr @scr_arr_new(i32 ${elemKindNum(elem)}, ${host.sizeType} ${capText})`;
- }
- if (small) {
- host.declare(`declare ptr @scr_arr_new_ops_small(ptr)`);
- return `call ptr @scr_arr_new_ops_small(ptr ${arrOps(host, elem)})`;
- }
- host.declare(`declare ptr @scr_arr_new_ops(ptr, ${host.sizeType})`);
- return `call ptr @scr_arr_new_ops(ptr ${arrOps(host, elem)}, ${host.sizeType} ${capText})`;
+ (elem.kind === "array" && traceAdapter(host, elem) !== null)
+ );
}
/** The %ScrArrOps descriptor (retain, release, trace, registry id) of a REF
diff --git a/packages/compiler/src/frontend/lowering/builtins/array-classes.ts b/packages/compiler/src/frontend/lowering/builtins/array-classes.ts
new file mode 100644
index 000000000..0136eca08
--- /dev/null
+++ b/packages/compiler/src/frontend/lowering/builtins/array-classes.ts
@@ -0,0 +1,610 @@
+/* Classes declared `extends Array`. An instance is an array of the class's
+ * element type: its struct begins with the runtime's array header (the
+ * reference count is the array's) and the class's own fields follow it, so
+ * every array operation applies to the instance unchanged and passing it
+ * where a plain array is expected passes the same object. The class's
+ * TypeScript type maps to its element array type; the class's own members,
+ * construction and `instanceof` view the array as the class object
+ * (arrayClassView). */
+import * as ts from "../../ts7/adapter.js";
+import { type Lowerer } from "../lowerer.js";
+import { isJsSourceFile, locOf } from "../../program.js";
+import { stripTypeWrappers } from "../typed-arguments.js";
+import { dynUndefinedExpr, nodeThrowExpr } from "../../../ir/build.js";
+import { lowerArrayFromCall, lowerArrayOfCall } from "../containers/array-construction.js";
+import { storedClassValueType, type ClassInfo } from "../lower-classes.js";
+import {
+ BOOL,
+ DYN,
+ F64,
+ STRING,
+ UNDEFINED_T,
+ arrayOf,
+ type IrExpr,
+ type IrFunction,
+ type IrLocal,
+ type IrStmt,
+ type IrType,
+ type SrcLoc,
+} from "../../../ir/ir.js";
+
+/** The element type of `class X extends Array` (`extends Array` in
+ * JavaScript reads `any[]`, whose elements are checked-dynamic). */
+export function arrayClassBase(
+ lowerer: Lowerer,
+ heritage: ts.ExpressionWithTypeArguments,
+): { elem: IrType } {
+ const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(heritage));
+ // `extends Array` in JavaScript reads `any[]`: dyn elements.
+ if (isJsSourceFile(ts.getSourceFileOfNode(heritage)) && mapped?.kind !== "array")
+ return { elem: DYN };
+ if (mapped?.kind !== "array")
+ lowerer.unsupported(
+ "SC1090",
+ heritage,
+ "extending Array with an element type that does not compile",
+ );
+ return { elem: mapped!.kind === "array" ? mapped!.elem : DYN };
+}
+
+/** The class declared `extends Array` at the root of `info`'s chain, or null. */
+export function arrayRootOf(info: ClassInfo | null | undefined): ClassInfo | null {
+ for (let c = info ?? null; c; c = c.base) if (c.extendsArray) return c;
+ return null;
+}
+
+/** The array a class instance is (a reinterpret: the same object). */
+export function arrayOfClassInstance(info: ClassInfo, value: IrExpr, loc: SrcLoc): IrExpr {
+ const root = arrayRootOf(info)!;
+ return { kind: "upcast", value, type: arrayOf(root.extendsArray!.elem), loc };
+}
+
+/** The class object an instance of `info` is, viewed from its array type
+ * (a reinterpret; the value is known to be an instance of the class). */
+export function arrayClassView(info: ClassInfo, value: IrExpr, loc: SrcLoc): IrExpr {
+ return { kind: "downcast", value, type: { kind: "object", className: info.def.name }, loc };
+}
+
+function thisArray(info: ClassInfo, thisLocal: IrLocal, loc: SrcLoc): IrExpr {
+ return arrayOfClassInstance(
+ info,
+ {
+ kind: "varRef",
+ localId: thisLocal.id,
+ type: { kind: "object", className: info.def.name },
+ loc,
+ },
+ loc,
+ );
+}
+
+/** The implicit constructor's super(...args): `pack` is its dyn argument
+ * array, applied with the Array constructor's length-or-elements rule. */
+export function arraySuperCall(
+ info: ClassInfo,
+ thisRef: IrExpr,
+ pack: IrExpr,
+ loc: SrcLoc,
+): IrStmt {
+ const elem = arrayRootOf(info)!.extendsArray!.elem;
+ return {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "array.subclassConstructDyn",
+ args: [
+ arrayOfClassInstance(info, thisRef, loc),
+ pack,
+ { kind: "boolLit", value: elem.kind === "dyn", type: BOOL, loc },
+ ],
+ type: { kind: "void" },
+ loc,
+ },
+ loc,
+ };
+}
+
+/** An explicit super(...) in a class declared `extends Array`: one number
+ * argument sets the length; otherwise the arguments (spreads included)
+ * become the elements, with the same one-number rule at run time. */
+export function lowerArraySuperCall(
+ lowerer: Lowerer,
+ info: ClassInfo,
+ thisLocal: IrLocal,
+ superCall: ts.CallExpression,
+): IrStmt[] {
+ const loc = locOf(superCall);
+ const elem = arrayRootOf(info)!.extendsArray!.elem;
+ const args = superCall.arguments.nodes;
+ if (args.length === 0) return [];
+ const only = args.length === 1 && !ts.isSpreadElement(args[0]!) ? args[0]! : null;
+ if (only !== null && (lowerer.typeOf(only).flags & ts.TypeFlags.NumberLike) !== 0) {
+ return [
+ {
+ kind: "arraySetLength",
+ arr: thisArray(info, thisLocal, loc),
+ length: lowerer.lowerExprExpecting(only, { kind: "f64" }),
+ loc,
+ },
+ ];
+ }
+ const arrT = arrayOf(elem);
+ const elements: IrExpr[] = [];
+ const spreads: number[] = [];
+ for (const arg of args) {
+ if (ts.isSpreadElement(arg)) {
+ spreads.push(elements.length);
+ elements.push(lowerer.lowerExprExpecting(arg.expression, arrT));
+ } else elements.push(lowerer.lowerExprExpecting(arg, elem));
+ }
+ const pack: IrExpr = {
+ kind: "arrayLit",
+ elems: elements,
+ ...(spreads.length > 0 ? { spreads } : {}),
+ type: arrT,
+ loc,
+ } as IrExpr;
+ return [
+ {
+ kind: "exprStmt",
+ expr: {
+ kind: "libCall",
+ fn: "array.subclassConstruct",
+ args: [
+ thisArray(info, thisLocal, loc),
+ pack,
+ { kind: "boolLit", value: elem.kind === "dyn", type: BOOL, loc },
+ ],
+ type: { kind: "void" },
+ loc,
+ },
+ loc,
+ },
+ ];
+}
+
+/** The ClassInfo of a class declared `extends Array`, by its declaration. */
+export function arrayClassOfDecl(
+ lowerer: Lowerer,
+ decl: ts.ClassLikeDeclaration,
+): ClassInfo | null {
+ const symbol = decl.name ? lowerer.checker.getSymbolAtLocation(decl.name) : undefined;
+ let info = symbol ? lowerer.classBySymbol.get(symbol) : undefined;
+ if (!info)
+ for (const candidate of lowerer.classes.values()) if (candidate.decl === decl) info = candidate;
+ return info && arrayRootOf(info) ? info : null;
+}
+
+/** The class that declares the member `access` names when that class is
+ * declared `extends Array` (its own fields, methods and accessors), or null
+ * for the Array members it inherits and every other access. */
+export function arrayClassMemberOwner(
+ lowerer: Lowerer,
+ access: ts.PropertyAccessExpression,
+): ClassInfo | null {
+ const symbol = lowerer.checker.getSymbolAtLocation(access.name);
+ const decl = symbol?.declarations?.[0];
+ const owner = decl?.parent;
+ if (owner === undefined || !ts.isClassLikeDeclaration(owner)) return null;
+ const declarer = arrayClassOfDecl(lowerer, owner);
+ if (declarer === null) return null;
+ // The receiver's own class (a subclass member reached through a base
+ // spelling is the same instance).
+ const receiver = lowerer.typeOf(access.expression).getSymbol()?.valueDeclaration;
+ if (receiver !== undefined && ts.isClassLikeDeclaration(receiver))
+ return arrayClassOfDecl(lowerer, receiver) ?? declarer;
+ return declarer;
+}
+
+/** `x instanceof Array` on a static array (always true: every array value,
+ * subclass instances included, is an Array) and `x instanceof X` for a
+ * class X declared `extends Array`: the array's class, read from its ext
+ * at run time. Null for every other instanceof. */
+export function lowerArrayClassInstanceOf(
+ lowerer: Lowerer,
+ expr: ts.BinaryExpression,
+ loc: SrcLoc,
+): IrExpr | null {
+ if (!ts.isIdentifier(expr.right)) return null;
+ const leftType = lowerer.mapTypeOf(lowerer.typeOf(expr.left));
+ if (lowerer.isStdlibGlobal(expr.right, "Array")) {
+ // A checked-dynamic value: the Array.isArray test (one realm).
+ if (leftType?.kind === "dyn")
+ return {
+ kind: "dynTest",
+ test: "array",
+ value: lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN),
+ type: BOOL,
+ loc,
+ } as IrExpr;
+ if (leftType?.kind !== "array") return null;
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "exprStmt", expr: lowerer.lowerExpr(expr.left), loc }],
+ result: { kind: "boolLit", value: true, type: BOOL, loc },
+ type: BOOL,
+ loc,
+ };
+ }
+ const symbol = lowerer.resolveValueSymbol(expr.right);
+ const decl = symbol?.valueDeclaration;
+ let info: ClassInfo | null = null;
+ if (decl !== undefined && ts.isClassLikeDeclaration(decl)) {
+ info = arrayClassOfDecl(lowerer, decl);
+ } else {
+ // A binding that holds a class value (`const C = class extends Array
+ // {}`): its static class, the only class it can hold while nothing
+ // extends that class.
+ const stored = storedClassValueType(lowerer, expr.right);
+ const held = stored?.kind === "classval" ? lowerer.classes.get(stored.className) : undefined;
+ if (held === undefined || arrayRootOf(held) === null) return null;
+ if (held.subclasses.length > 0)
+ lowerer.unsupported(
+ "SC1090",
+ expr,
+ "'instanceof' against a binding holding a class that extends Array and has subclasses",
+ );
+ info = held;
+ }
+ if (info === null) return null;
+ const value = lowerer.lowerExpr(expr.left);
+ if (value.type.kind === "array")
+ return { kind: "instanceOf", value, className: info.def.name, type: BOOL, loc };
+ if (value.type.kind === "dyn")
+ return {
+ kind: "libCall",
+ fn: "array.dynIsClass",
+ args: [value, { kind: "strLit", value: info.def.name, type: STRING, loc }],
+ type: BOOL,
+ loc,
+ };
+ if (value.type.kind === "object" && value.type.className === info.def.name)
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "exprStmt", expr: value, loc }],
+ result: { kind: "boolLit", value: true, type: BOOL, loc },
+ type: BOOL,
+ loc,
+ };
+ return null;
+}
+
+/** The class declared `extends Array` that a value of `type` is an instance
+ * of, or null. */
+export function arrayClassOfType(lowerer: Lowerer, type: ts.Type): ClassInfo | null {
+ const decl = type.getSymbol()?.valueDeclaration;
+ return decl !== undefined && ts.isClassLikeDeclaration(decl)
+ ? arrayClassOfDecl(lowerer, decl)
+ : null;
+}
+
+/** `x.constructor` on an instance of a class declared `extends Array`: the
+ * class (the class has no subclasses, so the static type names it). */
+export function lowerArrayClassConstructorRead(
+ lowerer: Lowerer,
+ access: ts.PropertyAccessExpression,
+ loc: SrcLoc,
+): IrExpr | null {
+ if (access.name.text !== "constructor") return null;
+ const info = arrayClassOfType(lowerer, lowerer.typeOf(access.expression));
+ if (info === null) return null;
+ const value = lowerer.lowerExpr(access.expression);
+ const cls = lowerer.classValueRef(info, access);
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "exprStmt", expr: value, loc }],
+ result: cls,
+ type: cls.type,
+ loc,
+ };
+}
+
+/** `%C.arrayFields(a)`: the own fields of an instance of a class declared
+ * `extends Array` as a dyn object, in declaration order (the descriptor's
+ * `fields`, which util.inspect and Object.keys read). Fields whose type
+ * has no dyn form are left out. Null when the class has no such field. */
+export function arrayClassFieldsFn(lowerer: Lowerer, info: ClassInfo): IrFunction | null {
+ const elem = arrayRootOf(info)!.extendsArray!.elem;
+ const arrT = arrayOf(elem);
+ const loc = info.def.loc;
+ const fields = info.def.fields.filter(
+ (f) => !f.name.startsWith("%") && !f.name.startsWith("#") && lowerer.dynConvertible(f.type),
+ );
+ if (fields.length === 0) return null;
+ const name = `%${info.def.name}.arrayFields`;
+ const self = (): IrExpr =>
+ arrayClassView(info, { kind: "varRef", localId: "a.0", type: arrT, loc }, loc);
+ const value: IrExpr = {
+ kind: "dynObjLit",
+ fields: fields.map((f) => ({
+ key: { kind: "strLit", value: f.name, type: STRING, loc },
+ value: lowerer.coerceToExpected(
+ {
+ kind: "fieldGet",
+ obj: self(),
+ className: info.def.name,
+ field: f.name,
+ type: f.type,
+ loc,
+ },
+ DYN,
+ ),
+ })),
+ type: DYN,
+ loc,
+ };
+ return {
+ name,
+ params: [{ localId: "a.0", name: "a", type: arrT }],
+ returnType: DYN,
+ locals: [{ id: "a.0", name: "a", type: arrT, mutable: false }],
+ body: [{ kind: "return", value, loc }],
+ loc,
+ };
+}
+
+/** `%C.species(n, withLength)`: `new C(n)`, or `new C()` without
+ * `withLength`, as ArraySpeciesCreate, Array.of and Array.from call it (the
+ * descriptor's `species`). Null when the constructor's parameters cannot
+ * take those arguments: they must be none, a forwarding rest (dyn or
+ * number), or a number first parameter whose others may be omitted (whose
+ * no-argument form throws). */
+export function arrayClassSpeciesFn(lowerer: Lowerer, info: ClassInfo): IrFunction | null {
+ const elem = arrayRootOf(info)!.extendsArray!.elem;
+ const arrT = arrayOf(elem);
+ const loc = info.def.loc;
+ const n = (): IrExpr => ({ kind: "varRef", localId: "n.0", type: F64, loc });
+ const params = info.ctorParams;
+ // The constructor arguments with the length and without it (null: that
+ // form throws a TypeError).
+ let withLength: IrExpr[] | null = null;
+ let without: IrExpr[] | null = null;
+ if (params.length === 0) {
+ withLength = [];
+ without = [];
+ } else if (params.length === 1 && params[0]!.mode === "dynRest") {
+ withLength = [
+ {
+ kind: "dynArrLit",
+ elems: [{ kind: "dynFrom", value: n(), type: DYN, loc }],
+ type: DYN,
+ loc,
+ },
+ ];
+ without = [{ kind: "dynArrLit", elems: [], type: DYN, loc }];
+ } else if (
+ params.length === 1 &&
+ params[0]!.mode === "rest" &&
+ params[0]!.type.kind === "array" &&
+ params[0]!.type.elem.kind === "f64"
+ ) {
+ withLength = [{ kind: "arrayLit", elems: [n()], type: params[0]!.type, loc } as IrExpr];
+ without = [{ kind: "arrayLit", elems: [], type: params[0]!.type, loc } as IrExpr];
+ } else if (
+ params[0]!.mode === "required" &&
+ (params[0]!.type.kind === "f64" || params[0]!.type.kind === "dyn") &&
+ params.slice(1).every((p) => p.mode === "omittable")
+ ) {
+ const rest = params
+ .slice(1)
+ .map((p) =>
+ lowerer.coerceToExpected(
+ { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } as IrExpr,
+ p.type,
+ ),
+ );
+ // A dyn first parameter (a JavaScript `constructor(size)`) takes the
+ // length as a number, and undefined in the no-argument form.
+ if (params[0]!.type.kind === "dyn") without = [dynUndefinedExpr(loc), ...rest.map((e) => e)];
+ withLength = [
+ params[0]!.type.kind === "dyn" ? { kind: "dynFrom", value: n(), type: DYN, loc } : n(),
+ ...params
+ .slice(1)
+ .map((p) =>
+ lowerer.coerceToExpected(
+ { kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } as IrExpr,
+ p.type,
+ ),
+ ),
+ ];
+ }
+ if (withLength === null) return null;
+ const construct = (args: IrExpr[]): IrStmt => ({
+ kind: "return",
+ value: arrayOfClassInstance(
+ info,
+ {
+ kind: "new",
+ className: info.def.name,
+ args,
+ type: { kind: "object", className: info.def.name },
+ loc,
+ },
+ loc,
+ ),
+ loc,
+ });
+ const unsupported: IrStmt = {
+ kind: "exprStmt",
+ expr: nodeThrowExpr(
+ 1,
+ "",
+ `constructing ${info.def.jsName ?? info.def.name} without arguments is not supported yet`,
+ DYN,
+ loc,
+ ),
+ loc,
+ };
+ return {
+ name: `%${info.def.name}.species`,
+ params: [
+ { localId: "n.0", name: "n", type: F64 },
+ { localId: "w.0", name: "withLength", type: BOOL },
+ ],
+ returnType: arrT,
+ locals: [
+ { id: "n.0", name: "n", type: F64, mutable: false },
+ { id: "w.0", name: "withLength", type: BOOL, mutable: false },
+ ],
+ body: [
+ {
+ kind: "if",
+ cond: { kind: "varRef", localId: "w.0", type: BOOL, loc },
+ then: [construct(withLength)],
+ else_: [without !== null ? construct(without) : unsupported],
+ loc,
+ },
+ ],
+ loc,
+ };
+}
+
+/** Whether the program declares a class that extends Array. Array methods
+ * consult species only then. */
+export function programDeclaresArrayClass(lowerer: Lowerer): boolean {
+ for (const info of lowerer.classes.values()) if (info.extendsArray) return true;
+ return false;
+}
+
+/** The Array methods that create their result through ArraySpeciesCreate:
+ * whether the species constructor receives the result's length (map,
+ * slice, splice; the others pass 0) and whether the length is set after
+ * the elements are defined (slice, splice, concat). */
+const SPECIES_METHODS: ReadonlyMap = new Map(
+ [
+ ["map", { resultLength: true, setLength: false }],
+ ["filter", { resultLength: false, setLength: false }],
+ ["slice", { resultLength: true, setLength: true }],
+ ["splice", { resultLength: true, setLength: true }],
+ ["concat", { resultLength: false, setLength: true }],
+ ["flat", { resultLength: false, setLength: false }],
+ ["flatMap", { resultLength: false, setLength: false }],
+ ],
+);
+
+/** An Array method call whose result an Array subclass instance creates
+ * through its species (scr_arr_species_adopt): the receiver is evaluated
+ * once into a hidden local that the method's lowering reads, and the
+ * method's plain result passes through the species constructor when the
+ * receiver is a subclass instance. Programs that declare no Array subclass
+ * keep the plain lowering. */
+export function lowerWithArraySpecies(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+ lower: () => IrExpr | null,
+): IrExpr | null {
+ const mode = SPECIES_METHODS.get(access.name.text);
+ if (mode === undefined || !programDeclaresArrayClass(lowerer)) return lower();
+ const receiverType = lowerer.mapTypeOf(lowerer.typeOf(access.expression));
+ if (receiverType?.kind !== "array") return lower();
+ const loc = locOf(call);
+ const receiver = lowerer.lowerExpr(access.expression);
+ if (receiver.type.kind !== "array") return lower();
+ const local = lowerer.declareHiddenLocal("%speciesReceiver", receiver.type);
+ const key = stripTypeWrappers(access.expression);
+ lowerer.stagedArguments.set(key, { localId: local.id, type: receiver.type });
+ let result: IrExpr | null;
+ try {
+ result = lower();
+ } finally {
+ lowerer.stagedArguments.delete(key);
+ }
+ if (result === null || result.type.kind !== "array") return result;
+ return {
+ kind: "seqExpr",
+ stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }],
+ result: {
+ kind: "libCall",
+ fn: "array.speciesAdopt",
+ args: [
+ { kind: "varRef", localId: local.id, type: receiver.type, loc },
+ result,
+ { kind: "numLit", value: mode.resultLength ? -1 : 0, type: F64, loc },
+ { kind: "boolLit", value: mode.setLength, type: BOOL, loc },
+ ],
+ type: result.type,
+ loc,
+ },
+ type: result.type,
+ loc,
+ };
+}
+
+/** `C.of(...items)` and `C.from(iterable)` on a class declared `extends
+ * Array` that declares neither: Array.of / Array.from's elements, in an
+ * instance the class constructs (`new C(items.length)` for of, `new C()`
+ * for an iterable's from), as Node's inherited statics do. */
+export function lowerArrayClassStaticCall(
+ lowerer: Lowerer,
+ call: ts.CallExpression,
+ access: ts.PropertyAccessExpression,
+): IrExpr | null {
+ const name = access.name.text;
+ if (name !== "of" && name !== "from") return null;
+ if (!ts.isIdentifier(access.expression)) return null;
+ const symbol = lowerer.resolveValueSymbol(access.expression);
+ const decl = symbol?.valueDeclaration;
+ if (decl === undefined || !ts.isClassLikeDeclaration(decl)) return null;
+ const info = arrayClassOfDecl(lowerer, decl);
+ if (info === null || lowerer.findStaticOn(info, name)) return null;
+ const loc = locOf(call);
+ const args = call.arguments.nodes;
+ let ctorLength: number;
+ if (name === "of") {
+ if (args.some(ts.isSpreadElement))
+ lowerer.unsupported("SC1090", call, "spreading arguments into Array.of on an Array subclass");
+ ctorLength = args.length;
+ } else {
+ const source = args[0] ? lowerer.mapTypeOf(lowerer.typeOf(args[0])) : null;
+ if (
+ source?.kind !== "array" &&
+ source?.kind !== "string" &&
+ source?.kind !== "set" &&
+ source?.kind !== "map"
+ )
+ lowerer.unsupported(
+ "SC1090",
+ call,
+ "Array.from on an Array subclass over a value that is not an array, string, Set or Map",
+ );
+ ctorLength = -1;
+ }
+ const plain =
+ name === "of"
+ ? lowerArrayOfCall(lowerer, call, access, true)
+ : lowerArrayFromCall(lowerer, call, access, true);
+ if (plain === null || plain.type.kind !== "array") return plain;
+ return {
+ kind: "libCall",
+ fn: "array.classConstructAdopt",
+ args: [
+ { kind: "strLit", value: info.def.name, type: STRING, loc },
+ plain,
+ { kind: "numLit", value: ctorLength, type: F64, loc },
+ ],
+ type: plain.type,
+ loc,
+ };
+}
+
+/** A class declared `extends Array` that defines `static [Symbol.species]`
+ * (Node would construct method results through that constructor). */
+export function refuseArrayClassSpecies(lowerer: Lowerer, decl: ts.ClassLikeDeclaration): void {
+ for (const member of decl.members.nodes) {
+ const name = member.name;
+ if (
+ name !== undefined &&
+ ts.isComputedPropertyName(name) &&
+ ts.isPropertyAccessExpression(name.expression) &&
+ name.expression.name.text === "species" &&
+ ts.isIdentifier(name.expression.expression) &&
+ name.expression.expression.text === "Symbol"
+ )
+ lowerer.unsupported(
+ "SC1090",
+ member,
+ "Symbol.species on a class that extends Array (its methods' results construct the class itself)",
+ );
+ }
+}
diff --git a/packages/compiler/src/frontend/lowering/containers/array-construction.ts b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
index 1f10640c0..284f87104 100644
--- a/packages/compiler/src/frontend/lowering/containers/array-construction.ts
+++ b/packages/compiler/src/frontend/lowering/containers/array-construction.ts
@@ -498,12 +498,15 @@ export function lowerArrayOfCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
+ /** The receiver is a class declared `extends Array` (array-classes.ts
+ * constructs the result through it). */
+ arrayClassReceiver = false,
): IrExpr | null {
if (
call.questionDotToken ||
access.questionDotToken ||
access.name.text !== "of" ||
- !lowerer.isStdlibGlobal(access.expression, "Array")
+ (!arrayClassReceiver && !lowerer.isStdlibGlobal(access.expression, "Array"))
)
return null;
let result = lowerer.mapTypeOf(lowerer.typeOf(call));
@@ -856,9 +859,11 @@ export function lowerArrayFromCall(
lowerer: Lowerer,
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
+ /** The receiver is a class declared `extends Array` (array-classes.ts). */
+ arrayClassReceiver = false,
): IrExpr | null {
if (call.questionDotToken || access.questionDotToken) return null;
- if (!lowerer.isStdlibGlobal(access.expression, "Array")) return null;
+ if (!arrayClassReceiver && !lowerer.isStdlibGlobal(access.expression, "Array")) return null;
if (access.name.text !== "from") return null;
const loc = locOf(call);
const args = call.arguments.nodes;
diff --git a/packages/compiler/src/frontend/lowering/expressions/binary.ts b/packages/compiler/src/frontend/lowering/expressions/binary.ts
index 00a23caf5..dbe89544e 100644
--- a/packages/compiler/src/frontend/lowering/expressions/binary.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/binary.ts
@@ -773,6 +773,12 @@ export function ensureString(lowerer: Lowerer, e: IrExpr, node: ts.Node): IrExpr
isUnitType(a),
) ??
false);
+ // Checked-dynamic elements: the runtime's String() over the array's
+ // dyn form (its join, element by element).
+ if (elem.kind === "dyn") {
+ const boxed: IrExpr = { kind: "dynFrom", value: e, type: DYN, loc: e.loc };
+ return { kind: "toString", operand: boxed, type: STRING, loc: e.loc };
+ }
if (
elem.kind === "f64" ||
elem.kind === "string" ||
diff --git a/packages/compiler/src/frontend/lowering/expressions/property-access.ts b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
index 8148e85f5..9d76a0f17 100644
--- a/packages/compiler/src/frontend/lowering/expressions/property-access.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/property-access.ts
@@ -1,4 +1,9 @@
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
+import {
+ arrayClassMemberOwner,
+ arrayClassView,
+ lowerArrayClassConstructorRead,
+} from "../builtins/array-classes.js";
import { keyedHelperName } from "../stable-names.js";
import { staticallySelected } from "../static-typeof.js";
import * as ts from "../../ts7/adapter.js";
@@ -290,6 +295,8 @@ export function lowerPropertyAccessExpr(
expr: ts.PropertyAccessExpression,
loc: SrcLoc,
): IrExpr {
+ const arrayClassConstructor = lowerArrayClassConstructorRead(lowerer, expr, loc);
+ if (arrayClassConstructor !== null) return arrayClassConstructor;
// `value.#x` on a checked-dynamic value, or on a checked extraction of
// one: the field of the brand-checked receiver (checkedPrivateValue).
if (ts.isPrivateIdentifier(expr.name)) {
@@ -3717,6 +3724,31 @@ export function fieldTarget(
);
}
}
+ // A field declared by a class that extends Array, on an instance (whose
+ // type maps to its array): the class object's field.
+ {
+ const owner = arrayClassMemberOwner(lowerer, access);
+ const name = access.name.text;
+ if (
+ owner !== null &&
+ (owner.fields.has(name) ||
+ lowerer.findMethodOn(owner, `get:${name}`) ||
+ lowerer.findMethodOn(owner, `set:${name}`))
+ ) {
+ const value = lowerer.lowerExpr(access.expression);
+ const receiver =
+ value.type.kind === "array"
+ ? arrayClassView(owner, value, locOf(access.expression))
+ : value;
+ return classFieldTarget(
+ lowerer,
+ access.expression,
+ { kind: "object", className: owner.def.name },
+ access.name.text,
+ receiver,
+ );
+ }
+ }
const stored = ts.isIdentifier(access.expression)
? (lowerer.peekLocal(access.expression)?.type ?? lowerer.globalOf(access.expression)?.type)
: undefined;
diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts
index 1651bf0d9..08987e538 100644
--- a/packages/compiler/src/frontend/lowering/expressions/relational.ts
+++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts
@@ -1,4 +1,5 @@
/* instanceof and in, including private-name and runtime-key in. */
+import { lowerArrayClassInstanceOf } from "../builtins/array-classes.js";
import { dateRootOf } from "../builtins/date-classes.js";
import { keyedHelperName } from "../stable-names.js";
import * as ts from "../../ts7/adapter.js";
@@ -106,6 +107,8 @@ const NO_CLASS_INSTANCE_KINDS: ReadonlySet = new Set([
* Folding is limited to side-effect-free operands — dropping a computed
* operand would skip its effects, so those are rejected instead. */
export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc: SrcLoc): IrExpr {
+ const arrayClassTest = lowerArrayClassInstanceOf(lowerer, expr, loc);
+ if (arrayClassTest !== null) return arrayClassTest;
const webAssembly = lowerWebAssemblyInstanceof(lowerer, expr);
if (webAssembly !== null) return webAssembly;
refuseWebAssemblyUse(lowerer, expr.right);
diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts
index 8ac97fd32..2be259226 100644
--- a/packages/compiler/src/frontend/lowering/lower-calls.ts
+++ b/packages/compiler/src/frontend/lowering/lower-calls.ts
@@ -1,3 +1,8 @@
+import {
+ arrayClassMemberOwner,
+ arrayClassView,
+ lowerArrayClassStaticCall,
+} from "./builtins/array-classes.js";
import { keyedHelperName } from "./stable-names.js";
import { perKeyOrder } from "./key-order-witness.js";
import { isForwardedTupleSpread, lowerForwardingApply } from "./typed-arguments.js";
@@ -1669,6 +1674,7 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
lowerIteratorStaticFence(lowerer, expr, expr.expression) ??
lowerObjectStaticCall(lowerer, expr, expr.expression) ??
lowerObjectFromEntriesCall(lowerer, expr, expr.expression) ??
+ lowerArrayClassStaticCall(lowerer, expr, expr.expression) ??
lowerArrayFromCall(lowerer, expr, expr.expression) ??
lowerArrayFromAsyncCall(lowerer, expr, expr.expression) ??
lowerArrayOfCall(lowerer, expr, expr.expression) ??
@@ -10075,6 +10081,12 @@ export function lowerObjectMethodCall(
? probe
: null;
if (specializedReceiver) mappedReceiver = specializedReceiver.type;
+ // A member declared by a class that extends Array, called on an instance
+ // (whose type maps to its array): the class's own method.
+ const arrayClassOwner =
+ mappedReceiver?.kind === "array" ? arrayClassMemberOwner(lowerer, access) : null;
+ if (arrayClassOwner !== null)
+ mappedReceiver = { kind: "object", className: arrayClassOwner.def.name };
if (mappedReceiver?.kind === "union") {
const dispatched =
lowerUnionObjectMethodCall(lowerer, call, access, mappedReceiver) ??
@@ -10108,6 +10120,12 @@ export function lowerObjectMethodCall(
: null;
if (receiverIr === null) return null;
const lowerReceiver = (): IrExpr => {
+ if (arrayClassOwner !== null) {
+ const value = lowerer.lowerExpr(access.expression);
+ return value.type.kind === "array"
+ ? arrayClassView(arrayClassOwner, value, locOf(access.expression))
+ : value;
+ }
const receiver = specializedReceiver ?? lowerer.lowerExpr(access.expression);
const optional = lowerer.runtimeOptionalPropertyReceiver(
access.expression,
diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts
index 3e49304d2..dfc27dd86 100644
--- a/packages/compiler/src/frontend/lowering/lower-classes.ts
+++ b/packages/compiler/src/frontend/lowering/lower-classes.ts
@@ -7,6 +7,14 @@ import {
lowerDateSuperMethodCall,
} from "./builtins/date-classes.js";
import { predeclareForwardClass } from "./lower-stmts.js";
+import {
+ arrayClassBase,
+ arrayClassFieldsFn,
+ arrayClassSpeciesFn,
+ arraySuperCall,
+ refuseArrayClassSpecies,
+ lowerArraySuperCall,
+} from "./builtins/array-classes.js";
import { lowerDateStaticCall } from "./builtins/dates.js";
import { keyedHelperName } from "./stable-names.js";
import { lowerValueReturningConstructorNew } from "./js-dynamic-objects.js";
@@ -590,6 +598,10 @@ export interface ClassInfo {
* constructor's rules, and the Date methods it does not declare lower
* over that value. Subclasses inherit the field. */
extendsDate?: true;
+ /** A class declared `extends Array` (builtins/array-classes.ts): its
+ * instances are arrays of `elem` whose own fields follow the array
+ * header, and its TypeScript type maps to `elem[]`. */
+ extendsArray?: { elem: IrType };
/** The synthetic LifetimeRegion class (@scriptc/experimental,
* lifetime-regions.ts): its constructor is synthesized with the class,
* and its methods lower at their call sites. */
@@ -1978,6 +1990,7 @@ export function collectClassShapeInner(
let genericBase: ts.ExpressionWithTypeArguments | undefined;
let callableBase: ClassInfo["callableBase"];
let extendsDate = false;
+ let extendsArray: { elem: IrType } | undefined;
let factoryBaseExpression: ts.Expression | undefined;
const adoptCallableBase = (expression: ts.Expression): boolean => {
if (familyMode || inst || mixin) return false;
@@ -2178,6 +2191,13 @@ export function collectClassShapeInner(
if (t && adoptCallableBase(t.expression)) continue;
lowerer.unsupported("SC1090", clause, "extending computed expressions");
}
+ if ((familyMode || inst) && lowerer.isStdlibGlobal(t.expression, "Array"))
+ lowerer.unsupported("SC1090", t, "a generic class that extends Array");
+ if (!familyMode && !inst && !mixin && lowerer.isStdlibGlobal(t.expression, "Array")) {
+ extendsArray = arrayClassBase(lowerer, t);
+ refuseArrayClassSpecies(lowerer, decl);
+ continue;
+ }
if (!familyMode && !inst && !mixin && lowerer.isStdlibGlobal(t.expression, "Date")) {
if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes");
extendsDate = true;
@@ -2278,6 +2298,12 @@ export function collectClassShapeInner(
continue;
}
if (t.typeArguments?.nodes) lowerer.unsupported("SC1090", t, "extending generic classes");
+ if (named?.extendsArray)
+ lowerer.unsupported(
+ "SC1090",
+ t,
+ `extending '${t.expression.text}', a class that extends Array (one level of Array subclass compiles)`,
+ );
base = named;
if (!base) {
if (adoptCallableBase(t.expression)) continue;
@@ -4384,7 +4410,7 @@ export function collectClassShapeInner(
ctor && !mixinForwarding
? lowerer.paramShapes(ctor.parameters.nodes)
: (base?.ctorParams ??
- (callableBase || extendsDate ? [{ type: DYN, mode: "dynRest" }] : []));
+ (callableBase || extendsDate || extendsArray ? [{ type: DYN, mode: "dynRest" }] : []));
if (
ctor &&
!mixinForwarding &&
@@ -4439,6 +4465,13 @@ export function collectClassShapeInner(
return am.length > 0 ? { abstractMethods: am } : {};
})(),
...(inst ? { genericOf: inst.family.def.name } : {}),
+ ...(extendsArray
+ ? {
+ arrayElem: extendsArray.elem,
+ arrayFields: `%${className}.arrayFields`,
+ arraySpecies: `%${className}.species`,
+ }
+ : {}),
loc: locOf(decl),
},
fields,
@@ -4455,6 +4488,7 @@ export function collectClassShapeInner(
...(genericBase ? { genericBase } : {}),
...(callableBase ? { callableBase } : {}),
...(extendsDate ? { extendsDate: true as const } : {}),
+ ...(extendsArray ? { extendsArray } : {}),
...(factoryBaseExpression ? { factoryBaseExpression } : {}),
subclasses: [],
throwingSetters,
@@ -7926,6 +7960,12 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
const diagsBefore = lowerer.diags.length;
try {
out.push(lowerer.lowerClassCtor(info));
+ if (info.extendsArray) {
+ const fields = arrayClassFieldsFn(lowerer, info);
+ if (fields) out.push(fields);
+ const species = arrayClassSpeciesFn(lowerer, info);
+ if (species) out.push(species);
+ }
} catch (e) {
if (!(e instanceof PoisonError)) throw e;
if (info.decl && isJsSourceFile(ts.getSourceFileOfNode(info.decl))) {
@@ -7974,6 +8014,10 @@ export function lowerClassMembers(lowerer: Lowerer, info: ClassInfo): IrFunction
* the base's signature — forward every param to super(), then run own
* field initializers. */
export function lowerClassCtor(lowerer: Lowerer, info: ClassInfo): IrFunction {
+ if (info.extendsArray) {
+ lowerer.noteEdge(`%${info.def.name}.arrayFields`);
+ lowerer.noteEdge(`%${info.def.name}.species`);
+ }
const cached = info.localClass?.bodies.get(`%${info.def.name}.constructor`);
if (cached) return cached;
return withInstanceBindings(lowerer, info, () => lowerClassCtorInner(lowerer, info));
@@ -8041,7 +8085,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction {
lowerer.ctx.argumentsLocal = local;
}
body.push(...declared.prologue);
- if (!ctorBase && !info.callableBase && !info.extendsDate) {
+ if (!ctorBase && !info.callableBase && !info.extendsDate && !info.extendsArray) {
// Node's base-class order: field initializers run at the start
// of construction, the parameter-property assignments open the
// constructor body (probed — a field initializer reading a
@@ -8053,7 +8097,7 @@ function lowerClassCtorInner(lowerer: Lowerer, info: ClassInfo): IrFunction {
body.push(...lowerer.lowerDerivedCtorBody(info, thisLocal));
}
} else {
- if (ctorBase || info.callableBase || info.extendsDate) {
+ if (ctorBase || info.callableBase || info.extendsDate || info.extendsArray) {
// Synthetic forwarding params (the inherited ABI signature).
// Nothing references them by symbol — only the super call below,
// which forwards the already-completed values UNCHANGED (defaults
@@ -8972,7 +9016,9 @@ export function lowerDerivedCtorBody(
loc,
},
];
- if (info.callableBase || info.extendsDate) {
+ if (info.extendsArray) {
+ statements.push(...lowerArraySuperCall(lowerer, info, thisLocal, superCall));
+ } else if (info.callableBase || info.extendsDate) {
if (superCall.arguments.nodes.some(ts.isSpreadElement))
lowerer.unsupported(
"SC1090",
@@ -9165,6 +9211,7 @@ export function superCallStmt(
loc,
};
if (info.extendsDate) return dateSuperCall(thisRef, info, args[0]!, loc);
+ if (info.extendsArray) return arraySuperCall(info, thisRef, args[0]!, loc);
if (info.callableBase)
return {
kind: "exprStmt",
diff --git a/packages/compiler/src/frontend/lowering/lower-exprs.ts b/packages/compiler/src/frontend/lowering/lower-exprs.ts
index c790581d8..e178de2eb 100644
--- a/packages/compiler/src/frontend/lowering/lower-exprs.ts
+++ b/packages/compiler/src/frontend/lowering/lower-exprs.ts
@@ -1,3 +1,8 @@
+import {
+ arrayClassMemberOwner,
+ arrayOfClassInstance,
+ arrayRootOf,
+} from "./builtins/array-classes.js";
import { keyedHelperName } from "./stable-names.js";
import {
isNewTarget,
@@ -499,7 +504,23 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// noImplicitThis, so the generic scope walk here is preflight-safe.
const local = lowerer.resolveThis(expr);
if (local) {
- const value: IrExpr = { kind: "varRef", localId: local.id, type: local.type, loc };
+ let value: IrExpr = { kind: "varRef", localId: local.id, type: local.type, loc };
+ // `this` in a class declared `extends Array` is the array, except as
+ // the receiver of a member the class declares.
+ if (local.type.kind === "object") {
+ const arrayClass = lowerer.classes.get(local.type.className);
+ const parent = expr.parent;
+ if (
+ arrayClass &&
+ arrayRootOf(arrayClass) &&
+ !(
+ ts.isPropertyAccessExpression(parent) &&
+ parent.expression === expr &&
+ arrayClassMemberOwner(lowerer, parent) !== null
+ )
+ )
+ value = arrayOfClassInstance(arrayClass, value, loc);
+ }
// `this.#x` in a function-local class's prototype method, whose `this`
// is the call-site value: the field lives on the native instance of
// this evaluation (checkedPrivateReceiver).
@@ -575,7 +596,24 @@ function lowerExprInner(lowerer: Lowerer, expr: ts.Expression): IrExpr {
// any other super position (field reads, bare references) stays out.
lowerer.unsupported("SC1090", expr, "'super' outside super() and super.method() calls");
}
- if (ts.isNewExpression(expr)) return lowerer.lowerNew(expr);
+ if (ts.isNewExpression(expr)) {
+ const value = lowerer.lowerNew(expr);
+ // `new X()` for a class declared `extends Array` is the array, except
+ // as the receiver of a member the class declares (as `this` above).
+ if (value.type.kind !== "object") return value;
+ const arrayClass = lowerer.classes.get(value.type.className);
+ if (!arrayClass || !arrayRootOf(arrayClass)) return value;
+ let operand: ts.Expression = expr;
+ while (ts.isParenthesizedExpression(operand.parent)) operand = operand.parent;
+ const parent = operand.parent;
+ if (
+ ts.isPropertyAccessExpression(parent) &&
+ parent.expression === operand &&
+ arrayClassMemberOwner(lowerer, parent) !== null
+ )
+ return value;
+ return arrayOfClassInstance(arrayClass, value, loc);
+ }
if (ts.isAwaitExpression(expr)) {
if (!lowerer.ctx.isAsync) {
// A tsc-clean occurrence here is outside both an async function
diff --git a/packages/compiler/src/frontend/lowering/lower-modules.ts b/packages/compiler/src/frontend/lowering/lower-modules.ts
index b0139aba9..b69805adf 100644
--- a/packages/compiler/src/frontend/lowering/lower-modules.ts
+++ b/packages/compiler/src/frontend/lowering/lower-modules.ts
@@ -949,6 +949,12 @@ export function moduleArtifacts(
visitClass("%Readable");
visitClass("%Writable");
}
+ // A class declared `extends Array` named by its instanceof test
+ // or by X.of / X.from.
+ if (node.fn === "array.dynIsClass" && node.args[1]?.kind === "strLit")
+ visitClass(node.args[1].value);
+ if (node.fn === "array.classConstructAdopt" && node.args[0]?.kind === "strLit")
+ visitClass(node.args[0].value);
break;
case "dynFrom":
if (boxedChild(node.value.type)) {
diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts
index 14af97de7..0845826d2 100644
--- a/packages/compiler/src/frontend/lowering/lowerer.ts
+++ b/packages/compiler/src/frontend/lowering/lowerer.ts
@@ -1,3 +1,10 @@
+import {
+ arrayClassFieldsFn,
+ arrayClassSpeciesFn,
+ arrayOfClassInstance,
+ arrayRootOf,
+ lowerWithArraySpecies,
+} from "./builtins/array-classes.js";
import { isDeclBoundaryFile } from "../decl-projection.js";
import { relieveHeapPressure } from "../../heap-pressure.js";
import { moduleNamespaceObject } from "./module-namespace-elements.js";
@@ -8129,6 +8136,20 @@ export class Lowerer {
lower: () =>
info.localClass && !info.localClass.ready ? null : this.lowerClassCtor(info),
});
+ // The descriptor helpers of a class that extends Array, reached
+ // from its constructor.
+ if (info.extendsArray) {
+ units.set(`%${cName}.arrayFields`, {
+ order: unitOrder++,
+ roots: [],
+ lower: () => arrayClassFieldsFn(this, info),
+ });
+ units.set(`%${cName}.species`, {
+ order: unitOrder++,
+ roots: [],
+ lower: () => arrayClassSpeciesFn(this, info),
+ });
+ }
for (const { mName, member } of this.classMethodMembers(info)) {
units.set(`%${cName}.${mName}`, {
order: unitOrder++,
@@ -8244,6 +8265,10 @@ export class Lowerer {
register(`%${cName}.constructor`, classCtorRoots(info), () =>
info.localClass && !info.localClass.ready ? null : this.lowerClassCtor(info),
);
+ if (info.extendsArray) {
+ register(`%${cName}.arrayFields`, [], () => arrayClassFieldsFn(this, info));
+ register(`%${cName}.species`, [], () => arrayClassSpeciesFn(this, info));
+ }
for (const { mName, member } of this.classMethodMembers(info)) {
register(`%${cName}.${mName}`, functionRoots(member), () =>
info.localClass && !info.localClass.ready
@@ -9608,6 +9633,18 @@ export class Lowerer {
const result = this.coerceToExpected(expr.result, expected);
return { ...expr, result, type: result.type };
}
+ // An instance of a class declared `extends Array` is its array (the
+ // same object); an array slot of another element type converts it as
+ // an array.
+ if (expr.type.kind === "object" && expected.kind !== "object") {
+ const arrayClass = this.classes.get(expr.type.className);
+ if (arrayClass && arrayRootOf(arrayClass)) {
+ const asArray = arrayOfClassInstance(arrayClass, expr, expr.loc);
+ return typeEquals(asArray.type, expected)
+ ? asArray
+ : this.coerceToExpected(asArray, expected);
+ }
+ }
// A destination that holds undefined takes an unassigned deferred-init
// field's slot as is (Node passes the undefined along).
if (expected.kind === "union" && this.armTag(expected.unionId, UNDEFINED_T) >= 0) {
@@ -13315,7 +13352,9 @@ export class Lowerer {
call: ts.CallExpression,
access: ts.PropertyAccessExpression,
): IrExpr | null {
- return lowerArrayMethodCall(this, call, access);
+ return lowerWithArraySpecies(this, call, access, () =>
+ lowerArrayMethodCall(this, call, access),
+ );
}
lowerMapMethodCall(call: ts.CallExpression, access: ts.PropertyAccessExpression): IrExpr | null {
diff --git a/packages/compiler/src/frontend/type-mapper.ts b/packages/compiler/src/frontend/type-mapper.ts
index fc159116f..4273eace0 100644
--- a/packages/compiler/src/frontend/type-mapper.ts
+++ b/packages/compiler/src/frontend/type-mapper.ts
@@ -1779,6 +1779,36 @@ export function isFetchHandleType(
return handle;
}
+/** The array type a class declared `extends Array` maps to (its base type,
+ * `T[]`), or null for every other class. */
+function arrayClassBaseType(
+ classDecl: ts.ClassLikeDeclaration,
+ instance: ts.Type,
+ ctx: TypeMapperCtx,
+): IrType | null {
+ const heritage = classDecl.heritageClauses?.nodes.find(
+ (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
+ )?.types?.nodes[0];
+ if (heritage === undefined || !ts.isIdentifier(heritage.expression)) return null;
+ if (heritage.expression.text !== "Array") return null;
+ // The polymorphic `this` type of the class names the class's own type.
+ const symbol = instance.getSymbol();
+ const declared =
+ (instance.flags & ts.TypeFlags.TypeParameter) !== 0 && symbol !== undefined
+ ? ctx.checker.getDeclaredTypeOfSymbol(symbol)
+ : instance;
+ if ((declared.flags & ts.TypeFlags.Object) === 0) return null;
+ const target = declared.isTypeReference() ? (declared.getTarget() ?? declared) : declared;
+ const bases = ctx.checker.getBaseTypes(target as ts.InterfaceType);
+ const base = bases[0];
+ if (base === undefined || !ctx.checker.isArrayType(base)) return null;
+ const mapped = mapType(base, ctx);
+ if (mapped?.kind === "array") return mapped;
+ // `extends Array` in JavaScript reads `any[]`: dyn elements (the class's
+ // arrayClassBase).
+ return isJsSourceFile(ts.getSourceFileOfNode(classDecl)) ? { kind: "array", elem: DYN } : null;
+}
+
export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
if (mapTypeDepth >= MAP_TYPE_MAX_DEPTH) return null;
// These intrinsic domains cannot depend on generic bindings, declaration
@@ -2546,6 +2576,12 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
return viaMixin;
}
}
+ // A class declared `extends Array` (frontend/lowering/builtins/
+ // array-classes.ts): its instances are arrays of the base's elements.
+ {
+ const arrayBase = arrayClassBaseType(classDecl, widened, ctx);
+ if (arrayBase !== null) return arrayBase;
+ }
if (classExprNeedsContext(classDecl)) {
const local = ctx.localClassInstance?.(classDecl, widened);
if (local) contextResolutions++;
diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts
index ac7413c6a..db7dc81c3 100644
--- a/packages/compiler/src/ir/builtin-signatures.ts
+++ b/packages/compiler/src/ir/builtin-signatures.ts
@@ -3891,6 +3891,23 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
// The string keys `for...in` visits on an array (args[0], any element
// type) after its indices. Borrowed; never throws.
"array.namedKeys": { argTypes: [null], result: arrayOf(STRING) },
+ /** super(...) in a class declared `extends Array`: the instance, the
+ * arguments (an array of its element type, or the implicit constructor's
+ * dyn argument array) and whether the elements are dyn values. */
+ "array.subclassConstruct": { argTypes: [null, null, BOOL], result: VOID },
+ "array.subclassConstructDyn": { argTypes: [null, DYN, BOOL], result: VOID },
+ /** ArraySpeciesCreate over an Array method's plain result: the receiver
+ * (borrowed), the result (moved), the species constructor's length
+ * argument (-1: the result's length) and whether to set the length. The
+ * result has the second argument's type. */
+ "array.speciesAdopt": { argTypes: [null, null, F64, BOOL], result: VOID },
+ /** Array.of / Array.from on a class declared `extends Array`: the class
+ * (its name, a literal), the plain result (moved) and the constructor's
+ * length argument (negative: no argument). */
+ "array.classConstructAdopt": { argTypes: [STRING, null, F64], result: VOID },
+ /** `value instanceof C` for a dyn value and a class that extends Array
+ * (its name, a literal). */
+ "array.dynIsClass": { argTypes: [DYN, STRING], result: BOOL },
// Whether the array (args[0]) still has the key (args[1]) from its
// namedKeys list. Borrowed; never throws.
"array.hasNamedKey": { argTypes: [null, STRING], result: BOOL },
diff --git a/packages/compiler/src/ir/function-references.test.ts b/packages/compiler/src/ir/function-references.test.ts
index 91e1d5f05..33d68c621 100644
--- a/packages/compiler/src/ir/function-references.test.ts
+++ b/packages/compiler/src/ir/function-references.test.ts
@@ -157,6 +157,8 @@ const MODULE_FIELDS: Record>, Role> = {
"classes[].errorNameAfter": "other",
"classes[].prototypeDataHelper": "reference",
"classes[].instancePrototypeHelper": "reference",
+ "classes[].arrayFields": "reference",
+ "classes[].arraySpecies": "reference",
"classes[].localCaptures[].name": "other",
"classes[].localCaptures[].localId": "other",
"classes[].symbolFields[].field": "other",
@@ -281,6 +283,14 @@ const REFERENCE_FIXTURES: Record IrModule> = {
...module(),
classes: [{ name: "%m0.Cell", fields: [], instancePrototypeHelper: TARGET, loc }],
}),
+ "classes[].arrayFields": () => ({
+ ...module(),
+ classes: [{ name: "%m0.Row", fields: [], arrayElem: F64, arrayFields: TARGET, loc }],
+ }),
+ "classes[].arraySpecies": () => ({
+ ...module(),
+ classes: [{ name: "%m0.Row", fields: [], arrayElem: F64, arraySpecies: TARGET, loc }],
+ }),
"lib.exports[].fnName": () => ({
...module(),
lib: {
diff --git a/packages/compiler/src/ir/function-references.ts b/packages/compiler/src/ir/function-references.ts
index db474e9bb..da05e854c 100644
--- a/packages/compiler/src/ir/function-references.ts
+++ b/packages/compiler/src/ir/function-references.ts
@@ -24,6 +24,8 @@ export function moduleFunctionReferences(mod: IrModule): string[] {
for (const cls of mod.classes ?? []) {
if (cls.prototypeDataHelper !== undefined) names.push(cls.prototypeDataHelper);
if (cls.instancePrototypeHelper !== undefined) names.push(cls.instancePrototypeHelper);
+ if (cls.arrayFields !== undefined) names.push(cls.arrayFields);
+ if (cls.arraySpecies !== undefined) names.push(cls.arraySpecies);
}
return names;
}
diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts
index 550333edd..b9849152c 100644
--- a/packages/compiler/src/ir/ir.ts
+++ b/packages/compiler/src/ir/ir.ts
@@ -1508,6 +1508,17 @@ export interface IrClassDef {
* RUNTIME_ERROR_CLASSES). User subclasses are ordinary emitted classes
* whose layout prefix embeds ScrError's fields. */
runtime?: true;
+ /** A class declared `extends Array`: its instances are arrays of this
+ * element type. The struct begins with the runtime's array header (the
+ * reference count is the array's, field 0) and the class's fields follow
+ * it; the runtime finds the class through the array's ext. */
+ arrayElem?: IrType;
+ /** The function (array) => dyn listing such a class's own fields
+ * (ScrArrClass.fields); absent when it declares none. */
+ arrayFields?: string;
+ /** The function (length) => array constructing such a class for
+ * ArraySpeciesCreate (ScrArrClass.species). */
+ arraySpecies?: string;
/** Base class name (single inheritance, `extends`). A class is IN A
* HIERARCHY when it has a base or is some class's base; hierarchy classes
* carry a vtable word after `rc` (backends), standalone classes are laid
diff --git a/packages/compiler/src/ir/traverse.ts b/packages/compiler/src/ir/traverse.ts
index 80425a056..e83bf5c42 100644
--- a/packages/compiler/src/ir/traverse.ts
+++ b/packages/compiler/src/ir/traverse.ts
@@ -140,6 +140,7 @@ export function everyModuleNode(mod: IrModule, visitor: IrModuleVisitor): boolea
for (const cls of mod.classes ?? []) {
for (const capture of cls.localCaptures ?? []) if (!type(capture.type, cls.loc)) return false;
for (const field of cls.fields) if (!type(field.type, cls.loc)) return false;
+ if (cls.arrayElem !== undefined && !type(cls.arrayElem, cls.loc)) return false;
}
for (const global of mod.globals ?? [])
if (!type(global.type, global.source?.loc ?? entryLoc)) return false;
diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts
index d7eee18bf..0dea09d1e 100644
--- a/packages/compiler/src/ir/validate.ts
+++ b/packages/compiler/src/ir/validate.ts
@@ -2993,6 +2993,18 @@ function validateFunction(
}
break;
}
+ // An instance of a class declared `extends Array` and its array
+ // are the same object: upcast to the array, downcast back.
+ {
+ const instance = e.kind === "upcast" ? e.value.type : e.type;
+ const array = e.kind === "upcast" ? e.type : e.value.type;
+ if (instance.kind === "object" && array.kind === "array") {
+ const elem = classes.get(instance.className)?.arrayElem;
+ if (elem === undefined || !typeEquals(elem, array.elem))
+ err(`${e.kind}: "${instance.className}" is not an array of that element type`, e.loc);
+ break;
+ }
+ }
if (e.type.kind !== "object" || e.value.type.kind !== "object") {
err(`${e.kind} between non-class types`, e.loc);
break;
@@ -3100,6 +3112,15 @@ function validateFunction(
case "instanceOf": {
checkExpr(e.value);
if (e.type.kind !== "bool") err("instanceOf must be bool", e.loc);
+ // An array against a class declared `extends Array` (its ext).
+ if (e.value.type.kind === "array") {
+ if (classes.get(e.className)?.arrayElem === undefined)
+ err(
+ `instanceOf of an array against "${e.className}", which does not extend Array`,
+ e.loc,
+ );
+ break;
+ }
if (!hierarchy.has(e.className)) {
err(`instanceOf against non-hierarchy class "${e.className}"`, e.loc);
}
@@ -4190,6 +4211,28 @@ function validateFunction(
}
function checkLibValueCall(e: IrExpr & { kind: "libCall" }): boolean {
+ // ArraySpeciesCreate over an Array method's result, and Array.of /
+ // Array.from on a class that extends Array: the result has the plain
+ // result's array type.
+ if (e.fn === "array.dynIsClass") {
+ const cls = e.args[1]?.kind === "strLit" ? classes.get(e.args[1].value) : undefined;
+ if (cls?.arrayElem === undefined)
+ err("libCall array.dynIsClass must name a class that extends Array", e.loc);
+ return false;
+ }
+ if (e.fn === "array.speciesAdopt" || e.fn === "array.classConstructAdopt") {
+ const plain = e.args[1];
+ if (plain?.type.kind !== "array" || !typeEquals(e.type, plain.type))
+ err(`libCall ${e.fn} must have its array argument's type`, e.loc);
+ if (e.fn === "array.speciesAdopt" && e.args[0]?.type.kind !== "array")
+ err("libCall array.speciesAdopt receiver must be an array", e.loc);
+ if (e.fn === "array.classConstructAdopt") {
+ const cls = e.args[0]?.kind === "strLit" ? classes.get(e.args[0].value) : undefined;
+ if (cls?.arrayElem === undefined)
+ err("libCall array.classConstructAdopt must name a class that extends Array", e.loc);
+ }
+ return true;
+ }
const cryptoPromise = CRYPTO_PROMISE_VALUES[e.fn];
if (cryptoPromise) {
const inner = e.type.kind === "promise" ? e.type.inner : undefined;
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7720-array-subclass-basics.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7720-array-subclass-basics.ts.json
new file mode 100644
index 000000000..11b1d8d51
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7720-array-subclass-basics.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7720-array-subclass-basics.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7721-array-subclass-species.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7721-array-subclass-species.ts.json
new file mode 100644
index 000000000..41bd59eb2
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7721-array-subclass-species.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7721-array-subclass-species.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7722-array-subclass-zero-array.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7722-array-subclass-zero-array.js.json
new file mode 100644
index 000000000..6cbc69c47
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7722-array-subclass-zero-array.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7722-array-subclass-zero-array.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7723-array-subclass-as-array.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7723-array-subclass-as-array.ts.json
new file mode 100644
index 000000000..4671e20ea
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7723-array-subclass-as-array.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7723-array-subclass-as-array.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7724-array-subclass-cycles.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7724-array-subclass-cycles.ts.json
new file mode 100644
index 000000000..7956c64c5
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7724-array-subclass-cycles.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7724-array-subclass-cycles.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7725-array-subclass-operations.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7725-array-subclass-operations.ts.json
new file mode 100644
index 000000000..5d9c82a12
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7725-array-subclass-operations.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7725-array-subclass-operations.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7726-array-subclass-expressions.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7726-array-subclass-expressions.js.json
new file mode 100644
index 000000000..7d49ce0ac
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7726-array-subclass-expressions.js.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/corpus/7726-array-subclass-expressions.js"
+ ],
+ "diags": []
+}
diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/array-subclass-fences.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/array-subclass-fences.ts.json
new file mode 100644
index 000000000..659372fdb
--- /dev/null
+++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/array-subclass-fences.ts.json
@@ -0,0 +1,6 @@
+{
+ "order": [
+ "/tests/diagnostics/array-subclass-fences.ts"
+ ],
+ "diags": []
+}
diff --git a/packages/runtime/src/scr_array.c b/packages/runtime/src/scr_array.c
index 4c7b84d98..aed1b5c0c 100644
--- a/packages/runtime/src/scr_array.c
+++ b/packages/runtime/src/scr_array.c
@@ -1590,8 +1590,8 @@ static inline __attribute__((always_inline)) void scr_arr_free(ScrArr *a) {
scr_mem_free(a);
}
-static void scr_arr_destroy(void *object) {
- ScrArr *a = object;
+/* Releases an array's elements and ext contents (destroy's first half). */
+static void scr_arr_release_contents(ScrArr *a) {
if (scr_arr_ext_of(a)) {
scr_dyn_release(scr_arr_ext_of(a)->metadata);
scr_arr_ext_of(a)->metadata = NULL;
@@ -1657,10 +1657,209 @@ static void scr_arr_destroy(void *object) {
}
}
}
+}
+
+static void scr_arr_destroy(void *object) {
+ ScrArr *a = object;
+ scr_arr_release_contents(a);
if (a->cyc) scr_arr_gc_free(a);
else scr_arr_free(a);
}
+void scr_arr_subclass_init(ScrArr *a, uint8_t elem, const ScrArrOps *ops, const ScrArrClass *cls,
+ bool cyc) {
+ a->len = 0;
+ a->cap = 0;
+ a->elem = elem;
+ a->cyc = cyc ? 1 : 0;
+ uint32_t id = ops ? scr_arr_ops_id(ops) : 0;
+ a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT);
+ a->filled = 0;
+ a->data = NULL;
+ ScrArrExt *x = scr_arr_ext(a);
+ if (id == SCR_ARR_OPS_OVERFLOW) x->ops = ops;
+ x->cls = cls;
+#ifdef SCR_RC_AUDIT
+ scr_live_arrays++;
+#endif
+}
+
+/* super(...args) in a class that extends Array, as the Array constructor:
+ * one number argument sets the length (a RangeError for an invalid length),
+ * otherwise the arguments become the elements. `args` has the instance's
+ * element type and is borrowed; `dyn_elems` says its elements are dyn
+ * values, whose number test reads the value. */
+void scr_arr_subclass_construct(ScrArr *a, ScrArr *args, bool dyn_elems) {
+ if (args->len == 1) {
+ if (args->elem == SCR_ELEM_F64 || args->elem == SCR_ELEM_I32) {
+ scr_arr_set_len(a, scr_arr_get_number(args, 0));
+ return;
+ }
+ if (dyn_elems) {
+ const ScrDyn *d = scr_arr_borrow_ref(args, 0);
+ if (d != NULL && d->kind == SCR_DYN_NUM) {
+ scr_arr_set_len(a, d->v.num);
+ return;
+ }
+ }
+ }
+ if (args->len > 0) (void)scr_arr_push_spread(a, args);
+}
+
+/* The implicit constructor of a class that extends Array forwards its
+ * arguments (a dyn array) to super: the same rule, each argument converted
+ * to the element type. Element types other than numbers, booleans, strings
+ * and dyn values take only the length form. */
+void scr_arr_subclass_construct_dyn(ScrArr *a, const ScrDyn *args, bool dyn_elems) {
+ if (args == NULL || args->kind != SCR_DYN_ARR) return;
+ size_t n = args->v.arr.len;
+ ScrDyn *const *items = args->v.arr.items;
+ if (n == 1 && items[0] != NULL && items[0]->kind == SCR_DYN_NUM) {
+ scr_arr_set_len(a, items[0]->v.num);
+ return;
+ }
+ for (size_t i = 0; i < n; i++) {
+ ScrDyn *d = items[i];
+ if (dyn_elems) {
+ (void)scr_arr_push_ref(a, scr_dyn_retain(d));
+ continue;
+ }
+ if ((a->elem == SCR_ELEM_F64 || a->elem == SCR_ELEM_I32) && d && d->kind == SCR_DYN_NUM) {
+ (void)scr_arr_push_f64(a, d->v.num);
+ } else if (a->elem == SCR_ELEM_BOOL && d && d->kind == SCR_DYN_BOOL) {
+ (void)scr_arr_push_bool(a, d->v.b);
+ } else if (a->elem == SCR_ELEM_STR && d && d->kind == SCR_DYN_STR) {
+ (void)scr_arr_push_ref(a, scr_str_retain(d->v.str));
+ } else {
+ static const char msg[] =
+ "constructing this Array subclass with element arguments is not supported yet";
+ scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
+ return;
+ }
+ }
+}
+
+static void scr_arr_set_slot(ScrArr *a, double i, uint64_t slot);
+
+static ScrArr *scr_arr_species_fill(const ScrArrClass *cls, ScrArr *result, double ctor_length,
+ bool with_length, bool set_length);
+
+/* Gives an Array subclass instance holding no elements (only holes) another
+ * element type: its storage is dropped and its length kept. */
+static void scr_arr_retype_empty(ScrArr *a, uint8_t elem, const ScrArrOps *ops) {
+ uint32_t len = a->len;
+ ScrArrExt *x = scr_arr_ext_of(a);
+ const ScrArrClass *cls = x->cls;
+ scr_arr_free_parts(a);
+ a->data = NULL;
+ a->len = a->cap = a->filled = 0;
+ a->elem = elem;
+ uint32_t id = ops ? scr_arr_ops_id(ops) : 0;
+ a->spare = (uint16_t)(id << SCR_ARR_OPS_SHIFT);
+ x = scr_arr_ext(a);
+ if (id == SCR_ARR_OPS_OVERFLOW) x->ops = ops;
+ x->cls = cls;
+ scr_arr_set_len(a, (double)len);
+}
+
+ScrArr *scr_arr_species_adopt(ScrArr *recv, ScrArr *result, double ctor_length, bool set_length) {
+ const ScrArrClass *cls = scr_arr_class_of(recv);
+ if (cls == NULL) return result;
+ if (ctor_length < 0) ctor_length = (double)result->len;
+ return scr_arr_species_fill(cls, result, ctor_length, true, set_length);
+}
+
+ScrArr *scr_arr_class_construct_adopt(const ScrArrClass *cls, ScrArr *result, double ctor_length) {
+ return scr_arr_species_fill(cls, result, ctor_length < 0 ? 0 : ctor_length, ctor_length >= 0, true);
+}
+
+static ScrArr *scr_arr_species_fill(const ScrArrClass *cls, ScrArr *result, double ctor_length,
+ bool with_length, bool set_length) {
+ if (cls->species == NULL) {
+ static const char msg[] =
+ "this Array subclass's constructor cannot construct a species result (Symbol.species)";
+ scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
+ return result;
+ }
+ ScrArr *species = cls->species(ctor_length, with_length);
+ if (scr_exc_pending()) {
+ scr_arr_release(species);
+ return result;
+ }
+ if (species->elem != result->elem || scr_arr_ops(species) != scr_arr_ops(result)) {
+ /* A result of another element type (map, flatMap): an instance the
+ * constructor left without elements takes the result's element type;
+ * its length stays. An instance whose collector header the elements
+ * would need, or that already holds elements, cannot. */
+ const ScrArrOps *ops = scr_arr_ops(result);
+ bool traced = ops != NULL && ops->trace != NULL;
+ bool empty = true;
+ for (size_t i = 0; empty && i < species->len; i++) {
+ uint64_t slot;
+ empty = scr_arr_state_at(species, i, &slot) == SCR_ARR_HOLE;
+ }
+ if (!empty || (traced && !species->cyc)) {
+ static const char msg[] =
+ "an Array subclass's species result of another element type is not supported yet";
+ scr_throw_error_msg(SCR_ERR_TYPE, msg, sizeof msg - 1);
+ scr_arr_release(species);
+ return result;
+ }
+ scr_arr_retype_empty(species, result->elem, ops);
+ }
+ for (size_t i = 0; i < result->len; i++) {
+ uint64_t slot;
+ uint8_t state = scr_arr_state_at(result, i, &slot);
+ if (state == SCR_ARR_VALUE) scr_arr_set_slot(species, (double)i, scr_elem_retain_slot(result, slot));
+ else if (state == SCR_ARR_UNDEFINED) scr_arr_set_undefined(species, (double)i);
+ }
+ if (set_length) scr_arr_set_len(species, (double)result->len);
+ scr_arr_release(result);
+ return species;
+}
+
+bool scr_dyn_is_array_class(const ScrDyn *d, const ScrArrClass *cls) {
+ if (d == NULL) return false;
+ /* A live view of the native instance. */
+ if (d->kind == SCR_DYN_TYPED_REF && d->v.typed_ref.array != NULL && !d->v.typed_ref.record &&
+ !d->v.typed_ref.klass && !d->v.typed_ref.bag_class)
+ return scr_arr_class_of(d->v.typed_ref.ptr) == cls;
+ /* A snapshot: scr_arr_copy_metadata names the class by its name's
+ * address, which each class owns. */
+ if (d->kind == SCR_DYN_ARR) {
+ const ScrDyn *props = d->v.arr.properties;
+ return props != NULL && props->v.obj.class_name == cls->name;
+ }
+ return false;
+}
+
+const ScrArrClass *scr_arr_class_of(const ScrArr *a) {
+ if (!(a->spare & SCR_ARR_HAS_EXT)) return NULL;
+ return scr_arr_ext_of((ScrArr *)a)->cls;
+}
+
+void scr_arr_subclass_gc_drop(ScrArr *a, bool traced) {
+ if (!traced) scr_arr_release_contents(a);
+ scr_arr_free_parts(a);
+ a->data = NULL;
+ a->len = a->cap = a->filled = 0;
+ a->spare &= (uint16_t)~SCR_ARR_HAS_EXT;
+#ifdef SCR_RC_AUDIT
+ scr_live_arrays--;
+#endif
+}
+
+void scr_arr_subclass_drop(ScrArr *a) {
+ scr_arr_release_contents(a);
+ scr_arr_free_parts(a);
+ a->data = NULL;
+ a->len = a->cap = a->filled = 0;
+ a->spare &= (uint16_t)~SCR_ARR_HAS_EXT;
+#ifdef SCR_RC_AUDIT
+ scr_live_arrays--;
+#endif
+}
+
/* Whether destroying `a`, an acyclic array, releases nothing: it has no ext
* and its elements are numbers, booleans or compact references, or
* references whose release does nothing (SCR_ARR_OPS_RELEASE_NOTHING). */
@@ -1674,6 +1873,12 @@ static inline bool scr_arr_releases_nothing(const ScrArr *a) {
void scr_arr_release(ScrArr *a) {
if (!a || a->rc == SIZE_MAX) return; /* NULL: an uninitialized `let` local */
if (--a->rc == 0) {
+ /* An Array subclass instance: its class tears it down. */
+ if ((a->spare & SCR_ARR_HAS_EXT) && scr_arr_ext_of(a)->cls) {
+ if (a->cyc) scr_cyc_dead(a);
+ scr_arr_ext_of(a)->cls->destroy(a);
+ return;
+ }
/* Nothing to tear down: free the storage without scr_rc_destroy's
* queue (no release can recurse) or the element loop. */
if (!a->cyc && scr_arr_releases_nothing(a)) {
@@ -5350,10 +5555,32 @@ bool scr_arr_numeric_key(const char *key, size_t len, double *out) {
bool scr_arr_has_properties(const ScrArr *a) {
const ScrArrExt *x = scr_arr_ext_of(a);
- return x && (x->prop_len || x->metadata);
+ return x && (x->prop_len || x->metadata || x->cls);
+}
+
+/* The own fields of an Array subclass instance (ScrArrClass.fields), a
+ * fresh dyn object, or NULL. */
+static ScrDyn *scr_arr_class_fields(ScrArr *a) {
+ const ScrArrClass *cls = scr_arr_class_of(a);
+ if (!cls || !cls->fields) return NULL;
+ return cls->fields(scr_arr_retain(a));
}
void scr_arr_copy_metadata(ScrArr *a, ScrDyn *target) {
+ const ScrArrClass *cls = scr_arr_class_of(a);
+ if (cls) {
+ /* An Array subclass instance: the class name util.inspect prints before
+ * the elements, then the class's own fields as properties. */
+ if (!target->v.arr.properties) target->v.arr.properties = scr_dyn_new_obj();
+ target->v.arr.properties->v.obj.class_name = cls->name;
+ ScrDyn *fields = scr_arr_class_fields(a);
+ if (fields) {
+ for (size_t i = 0; i < fields->v.obj.len; i++)
+ scr_dyn_copy_entry(target, fields, &fields->v.obj.entries[i]);
+ scr_dyn_release(fields);
+ }
+ return;
+ }
const ScrDyn *metadata = SCR_AX(a)->metadata;
if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE &&
scr_arr_match_copy(a, target))
@@ -5372,6 +5599,15 @@ ScrArr *scr_arr_named_keys(ScrArr *a) {
ScrArr *keys = scr_arr_new(SCR_ELEM_STR, 0);
for (size_t i = 0; i < x->prop_len; i++)
scr_arr_push_ref(keys, scr_str_new(x->props[i].key, x->props[i].key_len));
+ ScrDyn *fields = scr_arr_class_fields(a);
+ if (fields) {
+ for (size_t i = 0; i < fields->v.obj.len; i++) {
+ const ScrDynEntry *entry = &fields->v.obj.entries[i];
+ if (entry->enumerable) scr_arr_push_ref(keys, scr_str_new(entry->key, entry->key_len));
+ }
+ scr_dyn_release(fields);
+ return keys;
+ }
const ScrDyn *metadata = x->metadata;
if (metadata && metadata->kind == SCR_DYN_HANDLE && metadata->v.handle.tag == SCR_DYNH_MATCH_SOURCE) {
static const char *const names[] = {"index", "input", "groups"};
diff --git a/packages/runtime/src/scr_array.h b/packages/runtime/src/scr_array.h
index 23390a183..de51e2938 100644
--- a/packages/runtime/src/scr_array.h
+++ b/packages/runtime/src/scr_array.h
@@ -171,7 +171,31 @@ typedef struct ScrArrExt {
/* The element ops of an array whose ops have no registry id
* (SCR_ARR_OPS_OVERFLOW). */
const ScrArrOps *ops;
+ /* The class of an instance of a program class that extends Array
+ * (scr_arr_subclass_init); NULL for every other array. */
+ const struct ScrArrClass *cls;
} ScrArrExt;
+
+/* A program class that extends Array. Its instances are emitted structs
+ * that begin with a ScrArr (the reference count is the array's) and keep
+ * the class's own fields after it; they always have an ext, whose `cls`
+ * names the class. Every array entry point accepts them as arrays. */
+typedef struct ScrArrClass {
+ /* The class's name, as constructor.name reads it. */
+ const char *name;
+ /* Destroys an instance whose count reached zero: releases the class's own
+ * fields, then scr_arr_subclass_drop, then frees the instance. */
+ void (*destroy)(ScrArr *a);
+ /* The class's own fields as a dyn object in declaration order (the array
+ * is passed owned, +1), or NULL when the class declares none. util.inspect
+ * and Object.keys list them after the elements. */
+ struct ScrDyn *(*fields)(ScrArr *a);
+ /* `new C(length)` (`new C()` without `with_length`): the instance
+ * ArraySpeciesCreate makes for map, filter, slice, splice, concat, flat
+ * and flatMap, and Array.of / Array.from called on the class. NULL when
+ * the class's constructor cannot take that argument. */
+ ScrArr *(*species)(double length, bool with_length);
+} ScrArrClass;
/* The LLVM backend mirrors ScrArr as %ScrArr: its inline retain/release read
* `cyc` (field 4, i8) to decide mark-live / candidate buffering exactly like
* scr_arr_retain and scr_arr_release; its inline element access uses len and
@@ -471,6 +495,40 @@ static inline ScrArr *scr_arr_retain(ScrArr *a) {
* NULL-tolerant (uninitialized locals). Cycle-capable arrays (`cyc`) feed
* the candidate-root buffer like every headered type. */
void scr_arr_release(ScrArr *a);
+/* Initializes the ScrArr prefix of a zeroed, freshly allocated instance of
+ * an Array subclass (count already 1): no elements, the element kind and
+ * ops, and an ext naming the class. `cyc` says the instance carries the
+ * collector header (its class is cycle-capable). */
+void scr_arr_subclass_init(ScrArr *a, uint8_t elem, const ScrArrOps *ops, const ScrArrClass *cls,
+ bool cyc);
+/* The collector's teardown of an Array subclass instance's storage: the
+ * elements are released unless they are the collector's (`traced`). */
+void scr_arr_subclass_gc_drop(ScrArr *a, bool traced);
+/* super(...args) in a class that extends Array (the Array constructor's
+ * length-or-elements rule); `args` is borrowed. */
+void scr_arr_subclass_construct(ScrArr *a, ScrArr *args, bool dyn_elems);
+/* The implicit constructor's super(...args) over its dyn argument array. */
+void scr_arr_subclass_construct_dyn(ScrArr *a, const struct ScrDyn *args, bool dyn_elems);
+/* ArraySpeciesCreate over the result `result` (owned) of an Array method
+ * called on `recv` (borrowed): a plain receiver keeps the result; a
+ * subclass instance's result is `new C(ctor_length)`, whose indices then
+ * receive the result's present elements, and whose length is set to the
+ * result's when `set_length` (slice, splice and concat set it; map,
+ * filter, flat and flatMap do not). A negative `ctor_length` passes the
+ * result's length. Result owned. */
+ScrArr *scr_arr_species_adopt(ScrArr *recv, ScrArr *result, double ctor_length, bool set_length);
+/* Array.of / Array.from called on an Array subclass: `new C(ctor_length)`
+ * (`new C()` when ctor_length is negative), then the elements of `result`
+ * (owned) and its length, as scr_arr_species_adopt. */
+ScrArr *scr_arr_class_construct_adopt(const ScrArrClass *cls, ScrArr *result, double ctor_length);
+/* `value instanceof C` for a dyn value and a class that extends Array: a
+ * live view of an instance of C, or a snapshot of one. */
+bool scr_dyn_is_array_class(const struct ScrDyn *d, const ScrArrClass *cls);
+/* The class of an Array subclass instance, or NULL for a plain array. */
+const ScrArrClass *scr_arr_class_of(const ScrArr *a);
+/* Releases the elements of an Array subclass instance and frees its
+ * storage and ext, leaving the instance itself to its class's destroy. */
+void scr_arr_subclass_drop(ScrArr *a);
void scr_arr_copy_metadata(ScrArr *a, struct ScrDyn *target);
/* The commit of a live array view calls this on `a`, the array converted
* from `view`, before `target` takes its storage: `a` takes the view's named
diff --git a/packages/runtime/src/scr_inspect.c b/packages/runtime/src/scr_inspect.c
index 79b2564ff..3ad34cb95 100644
--- a/packages/runtime/src/scr_inspect.c
+++ b/packages/runtime/src/scr_inspect.c
@@ -1513,8 +1513,25 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
const ScrDyn *props = d->v.arr.properties;
size_t nprops = 0;
for (size_t i = 0; props && i < props->v.obj.len; i++) nprops += props->v.obj.entries[i].enumerable;
- if (len == 0 && nprops == 0) return scr_str_new("[]", 2);
- if (recurse > depth) return scr_str_new("[Array]", 7);
+ /* An instance of a class that extends Array: Node's `Name(length) `
+ * prefix (scr_arr_copy_metadata names the class). */
+ const char *cls = props ? props->v.obj.class_name : NULL;
+ char prefix[160] = "";
+ if (cls) snprintf(prefix, sizeof prefix, "%.120s(%zu) ", cls, len);
+ if (len == 0 && nprops == 0) {
+ InspBuf eb = {0};
+ ib_cstr(&eb, prefix);
+ ib_cstr(&eb, "[]");
+ return ib_take(&eb);
+ }
+ if (recurse > depth) {
+ if (!cls) return scr_str_new("[Array]", 7);
+ InspBuf eb = {0};
+ ib_char(&eb, '[');
+ ib_cstr(&eb, cls);
+ ib_char(&eb, ']');
+ return ib_take(&eb);
+ }
scr_insp_begin(recurse + 1);
/* Node's maxArrayLength (100) counts output entries; a run of holes
* is one `` entry (formatSpecialArray). */
@@ -1568,7 +1585,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
* array has no properties). */
bool last_out = entries > 100;
ScrStr *base = scr_str_new("", 0);
- ScrStr *b0 = scr_str_new("[", 1);
+ InspBuf ob = {0};
+ ib_cstr(&ob, prefix);
+ ib_char(&ob, '[');
+ ScrStr *b0 = ib_take(&ob);
ScrStr *b1 = scr_str_new("]", 1);
ScrStr *out = scr_insp_end(base, b0, b1, recurse + 1, true, last_out);
scr_str_release(base);
diff --git a/tests/corpus/7720-array-subclass-basics.ts b/tests/corpus/7720-array-subclass-basics.ts
new file mode 100644
index 000000000..1e5130f9d
--- /dev/null
+++ b/tests/corpus/7720-array-subclass-basics.ts
@@ -0,0 +1,44 @@
+// A class that extends Array: construction (one number argument sets the
+// length, other arguments become the elements), length and index writes,
+// Array.isArray, instanceof, constructor, inherited methods, the subclass's
+// own fields and methods, and how console.log and JSON show the instance.
+class Stack extends Array {
+ pushes = 0;
+
+ top(): number | undefined {
+ return this[this.length - 1];
+ }
+
+ add(value: number): this {
+ this.push(value);
+ this.pushes++;
+ return this;
+ }
+}
+
+const s = new Stack();
+s.add(1).add(2).add(3);
+console.log(s.length, s.top(), s.pushes);
+console.log(Array.isArray(s), s instanceof Stack, s instanceof Array, s.constructor === Stack);
+console.log(s);
+console.log(JSON.stringify(s));
+s[5] = 9;
+console.log(s.length, s[4], s);
+s.length = 2;
+console.log(s, s.indexOf(2), s.includes(3), s.join("-"));
+s.reverse();
+console.log(s.top(), s);
+
+const sized = new Stack(3);
+console.log(sized.length, sized, 0 in sized);
+const listed = new Stack(4, 5);
+console.log(listed.length, listed);
+
+try {
+ new Stack(-1);
+} catch (e) {
+ console.log((e as Error).name, (e as Error).message);
+}
+
+for (const v of listed) console.log("item", v);
+console.log([...listed, 6], Object.keys(listed));
diff --git a/tests/corpus/7721-array-subclass-species.ts b/tests/corpus/7721-array-subclass-species.ts
new file mode 100644
index 000000000..03e0d19ab
--- /dev/null
+++ b/tests/corpus/7721-array-subclass-species.ts
@@ -0,0 +1,37 @@
+// Array.prototype methods that create a new array construct it through the
+// subclass (ArraySpeciesCreate): map, filter, slice, splice, concat, flat
+// and flatMap return an instance of the subclass, built by calling its
+// constructor with the length the specification passes. Methods that
+// return the receiver keep it, and Array.of / Array.from inherited as
+// statics construct the subclass too.
+class Logged extends Array {
+ constructor(...args: number[]) {
+ super(...args);
+ console.log("construct", args.length === 1 ? `length ${args[0]}` : `items ${args.join(",")}`);
+ }
+
+ sum(): number {
+ let total = 0;
+ for (const v of this) total += v;
+ return total;
+ }
+}
+
+const base = Logged.of(1, 2, 3, 4);
+console.log(base instanceof Logged, base);
+const doubled = base.map((v) => v * 2);
+console.log(doubled instanceof Logged, (doubled as Logged).sum(), doubled);
+const even = base.filter((v) => v % 2 === 0);
+console.log(even instanceof Logged, even);
+const part = base.slice(1, 3);
+console.log(part instanceof Logged, part);
+const removed = base.splice(0, 1);
+console.log(removed instanceof Logged, removed, base);
+const joined = base.concat([7, 8]);
+console.log(joined instanceof Logged, joined.length, joined);
+const flat = base.flatMap((v) => [v, v]);
+console.log(flat instanceof Logged, flat.length);
+const filled = base.fill(0, 0, 1);
+console.log(filled === base, base);
+const from = Logged.from([5, 6]);
+console.log(from instanceof Logged, (from as Logged).sum());
diff --git a/tests/corpus/7722-array-subclass-zero-array.js b/tests/corpus/7722-array-subclass-zero-array.js
new file mode 100644
index 000000000..1ed52d386
--- /dev/null
+++ b/tests/corpus/7722-array-subclass-zero-array.js
@@ -0,0 +1,16 @@
+// lru-cache's ZeroArray: a JavaScript class extending Array whose
+// constructor fills the new array with zeros, constructed directly and
+// through map (species construction runs the constructor again).
+class ZeroArray extends Array {
+ constructor(size) {
+ super(size);
+ this.fill(0);
+ }
+}
+
+const z = new ZeroArray(4);
+z[1] = 7;
+console.log(z, z.length, z instanceof ZeroArray, Array.isArray(z));
+const m = z.map((v) => v + 1);
+console.log(m, m instanceof ZeroArray);
+console.log(z.slice(1) instanceof ZeroArray, String(z));
diff --git a/tests/corpus/7723-array-subclass-as-array.ts b/tests/corpus/7723-array-subclass-as-array.ts
new file mode 100644
index 000000000..5faf5b13f
--- /dev/null
+++ b/tests/corpus/7723-array-subclass-as-array.ts
@@ -0,0 +1,21 @@
+// A subclass instance passed where a plain array is expected is the same
+// object: writes through either view are seen through the other, and
+// species construction still applies in code typed with the plain array.
+class Path extends Array {
+ label = "path";
+}
+
+function addSegment(parts: string[], segment: string): string[] {
+ parts.push(segment);
+ return parts.filter((part) => part.length > 0);
+}
+
+const p = new Path();
+p.push("a", "", "b");
+const filtered = addSegment(p, "c");
+console.log(p.length, filtered.length, filtered instanceof Path, (filtered as Path).label);
+const view: string[] = p;
+view[0] = "z";
+console.log(p[0], view === p, p.label);
+console.log(p.map((s) => s.toUpperCase()));
+console.log(Object.keys(p));
diff --git a/tests/corpus/7724-array-subclass-cycles.ts b/tests/corpus/7724-array-subclass-cycles.ts
new file mode 100644
index 000000000..9a1a7b813
--- /dev/null
+++ b/tests/corpus/7724-array-subclass-cycles.ts
@@ -0,0 +1,36 @@
+// An Array subclass whose elements and fields refer back to the instance:
+// the cycles are collected once nothing outside them is reachable, and
+// the elements and fields read back as Node reads them.
+class Member {
+ name: string;
+ group: Group | undefined;
+ constructor(name: string, group: Group | undefined) {
+ this.name = name;
+ this.group = group;
+ }
+}
+
+class Group extends Array {
+ owner: Member | undefined = undefined;
+
+ add(name: string): Member {
+ const member = new Member(name, this);
+ this.push(member);
+ return member;
+ }
+}
+
+function build(round: number): number {
+ const g = new Group();
+ const lead = g.add(`lead${round}`);
+ g.add(`member${round}`);
+ g.owner = lead;
+ return g.length + (g[0]!.group === g ? 1 : 0) + (g.owner === lead ? 1 : 0);
+}
+
+let total = 0;
+for (let i = 0; i < 200; i++) total += build(i);
+console.log(total);
+const kept = new Group();
+kept.add("a").group?.add("b");
+console.log(kept.length, kept.map((m) => m.name).join(","), kept[1]!.group === kept);
diff --git a/tests/corpus/7725-array-subclass-operations.ts b/tests/corpus/7725-array-subclass-operations.ts
new file mode 100644
index 000000000..94cfa8b04
--- /dev/null
+++ b/tests/corpus/7725-array-subclass-operations.ts
@@ -0,0 +1,46 @@
+// Array operations on an Array subclass instance: iteration, spread,
+// destructuring, the mutators that return the instance itself, searches,
+// length writes, Array.isArray through unknown, instanceof through unknown,
+// and JSON.
+class Row extends Array {
+ tag = "row";
+ #reads = 0;
+
+ static of2(a: string, b: string): Row {
+ const row = new Row();
+ row.push(a, b);
+ return row;
+ }
+
+ get size(): number {
+ this.#reads++;
+ return this.length;
+ }
+
+ get reads(): number {
+ return this.#reads;
+ }
+
+ label(): string {
+ return `${this.tag}:${this.join("|")}`;
+ }
+}
+
+const r = new Row();
+console.log(r.push("b", "a", "c"), r.length, r.label());
+const [first, ...rest] = r;
+console.log(first, rest, [...r].length);
+for (const [i, v] of r.entries()) console.log(i, v);
+console.log(r.sort() === r, r.label(), r.reverse() === r, r.label());
+console.log(r.fill("x", 1) === r, r.label(), r.indexOf("x"), r.lastIndexOf("x"), r.at(-1));
+console.log(r.pop(), r.shift(), r.length, r.unshift("z"), r.label());
+r.length = 0;
+console.log(r.length, r.label(), JSON.stringify({ r }));
+const u: unknown = r;
+console.log(Array.isArray(u), u instanceof Row, u instanceof Array);
+const rows: Row[] = [new Row(), new Row()];
+rows[1]!.push("q");
+console.log(rows.map((row) => row.label()).join(" "));
+console.log({ nested: rows[1] });
+const pair = Row.of2("m", "n");
+console.log(pair.size, pair.size, pair.reads, pair.label(), pair instanceof Row);
diff --git a/tests/corpus/7726-array-subclass-expressions.js b/tests/corpus/7726-array-subclass-expressions.js
new file mode 100644
index 000000000..d2e5d2a04
--- /dev/null
+++ b/tests/corpus/7726-array-subclass-expressions.js
@@ -0,0 +1,21 @@
+// Classes extending Array reached through expressions rather than
+// declarations and locals: a construction used in place, a class
+// expression held by a binding, and an instanceof test whose class the
+// program never constructs.
+class Sized extends Array {
+ describe() {
+ return `sized ${this.length}`;
+ }
+}
+
+console.log(new Sized(3).length, new Sized(2).describe(), new Sized(1, 2).join("-"));
+console.log(new Sized(4).fill(1), (new Sized(2)).length);
+
+const Holder = class extends Array {};
+const held = new Holder();
+held.push("a", "b");
+console.log(held instanceof Holder, held instanceof Array, [] instanceof Holder, held.length);
+
+class Unbuilt extends Array {}
+const values = [[1, 2], "text", new Sized(1)];
+for (const value of values) console.log(value instanceof Unbuilt, Array.isArray(value));
diff --git a/tests/diagnostics/array-subclass-fences.ts b/tests/diagnostics/array-subclass-fences.ts
new file mode 100644
index 000000000..cc2966201
--- /dev/null
+++ b/tests/diagnostics/array-subclass-fences.ts
@@ -0,0 +1,21 @@
+// Array subclasses that do not compile yet, each refused where it is
+// declared: a generic subclass, a subclass of a class that extends Array,
+// and a class that declares its own Symbol.species.
+class Bag extends Array {
+ firstOr(fallback: T): T {
+ return this.length > 0 ? this[0]! : fallback;
+ }
+}
+
+class Base extends Array {}
+class Derived extends Base {}
+
+class Speciated extends Array {
+ static get [Symbol.species](): ArrayConstructor {
+ return Array;
+ }
+}
+
+console.log(new Bag().firstOr(1));
+console.log(new Derived().length);
+console.log(new Speciated().length);
diff --git a/tests/harness/__snapshots__/array-subclass-fences.ts.txt b/tests/harness/__snapshots__/array-subclass-fences.ts.txt
new file mode 100644
index 000000000..3d81a58a9
--- /dev/null
+++ b/tests/harness/__snapshots__/array-subclass-fences.ts.txt
@@ -0,0 +1,41 @@
+array-subclass-fences.ts:4:22 - error SC1090: a generic class that extends Array is not supported yet
+
+ 3 | // and a class that declares its own Symbol.species.
+ 4 | class Bag extends Array {
+ | ^~~~~~~~
+ 5 | firstOr(fallback: T): T {
+
+array-subclass-fences.ts:11:23 - error SC1090: extending 'Base', a class that extends Array (one level of Array subclass compiles) is not supported yet
+
+ 10 | class Base extends Array {}
+ 11 | class Derived extends Base {}
+ | ^~~~
+ 12 |
+
+array-subclass-fences.ts:14:3 - error SC1090: Symbol.species on a class that extends Array (its methods' results construct the class itself) is not supported yet
+
+ 13 | class Speciated extends Array {
+ 14 | static get [Symbol.species](): ArrayConstructor {
+ | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ 15 | return Array;
+
+array-subclass-fences.ts:19:13 - error SC1090: constructing values other than classes declared in the program is not supported yet
+
+ 18 |
+ 19 | console.log(new Bag().firstOr(1));
+ | ^~~~~~~~~~~~~~~~~
+ 20 | console.log(new Derived().length);
+
+array-subclass-fences.ts:20:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
+
+ 19 | console.log(new Bag().firstOr(1));
+ 20 | console.log(new Derived().length);
+ | ^~~~~~~~~~~~~
+ 21 | console.log(new Speciated().length);
+
+array-subclass-fences.ts:21:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
+
+ 20 | console.log(new Derived().length);
+ 21 | console.log(new Speciated().length);
+ | ^~~~~~~~~~~~~~~
+ 22 |
\ No newline at end of file
diff --git a/tests/harness/corpus-costs/7720-array-subclass-basics.ts.json b/tests/harness/corpus-costs/7720-array-subclass-basics.ts.json
new file mode 100644
index 000000000..a663de213
--- /dev/null
+++ b/tests/harness/corpus-costs/7720-array-subclass-basics.ts.json
@@ -0,0 +1 @@
+{ "plain": 11000, "san": 9000 }
diff --git a/tests/harness/corpus-costs/7721-array-subclass-species.ts.json b/tests/harness/corpus-costs/7721-array-subclass-species.ts.json
new file mode 100644
index 000000000..a663de213
--- /dev/null
+++ b/tests/harness/corpus-costs/7721-array-subclass-species.ts.json
@@ -0,0 +1 @@
+{ "plain": 11000, "san": 9000 }
diff --git a/tests/harness/corpus-costs/7722-array-subclass-zero-array.js.json b/tests/harness/corpus-costs/7722-array-subclass-zero-array.js.json
new file mode 100644
index 000000000..a663de213
--- /dev/null
+++ b/tests/harness/corpus-costs/7722-array-subclass-zero-array.js.json
@@ -0,0 +1 @@
+{ "plain": 11000, "san": 9000 }
diff --git a/tests/harness/corpus-costs/7723-array-subclass-as-array.ts.json b/tests/harness/corpus-costs/7723-array-subclass-as-array.ts.json
new file mode 100644
index 000000000..a663de213
--- /dev/null
+++ b/tests/harness/corpus-costs/7723-array-subclass-as-array.ts.json
@@ -0,0 +1 @@
+{ "plain": 11000, "san": 9000 }
diff --git a/tests/harness/corpus-costs/7724-array-subclass-cycles.ts.json b/tests/harness/corpus-costs/7724-array-subclass-cycles.ts.json
new file mode 100644
index 000000000..a663de213
--- /dev/null
+++ b/tests/harness/corpus-costs/7724-array-subclass-cycles.ts.json
@@ -0,0 +1 @@
+{ "plain": 11000, "san": 9000 }
diff --git a/tests/harness/corpus-costs/7725-array-subclass-operations.ts.json b/tests/harness/corpus-costs/7725-array-subclass-operations.ts.json
new file mode 100644
index 000000000..a663de213
--- /dev/null
+++ b/tests/harness/corpus-costs/7725-array-subclass-operations.ts.json
@@ -0,0 +1 @@
+{ "plain": 11000, "san": 9000 }
diff --git a/tests/harness/corpus-costs/7726-array-subclass-expressions.js.json b/tests/harness/corpus-costs/7726-array-subclass-expressions.js.json
new file mode 100644
index 000000000..a663de213
--- /dev/null
+++ b/tests/harness/corpus-costs/7726-array-subclass-expressions.js.json
@@ -0,0 +1 @@
+{ "plain": 11000, "san": 9000 }