Compile classes that extend Array (#2059)

This commit is contained in:
Chris Tate
2026-10-26 03:57:16 -05:00
committed by GitHub
77 changed files with 2435 additions and 44 deletions
+2
View File
@@ -52,6 +52,8 @@ title: "Limitations"
<include>../limitations/types-and-class-definitions/static-field-updates.mdx</include>
<include>../limitations/types-and-class-definitions/array-subclasses.mdx</include>
<include>../limitations/types-and-class-definitions/local-class-expressions.mdx</include>
<include>../limitations/types-and-class-definitions/constructor-function-inheritance.mdx</include>
@@ -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<T> extends Array<T>`), 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.
@@ -146,6 +146,10 @@ function analyzeCycles(
...(c.localCaptures !== undefined
? [{ name: "<class>", type: { kind: "classval" as const, className: c.name } }]
: []),
// An instance of a class that extends Array holds its elements.
...(c.arrayElem !== undefined
? [{ name: "<elements>", type: { kind: "array" as const, elem: c.arrayElem } }]
: []),
],
})),
...(mod.records ?? []).map((r) => ({
+122 -11
View File
@@ -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
@@ -733,7 +761,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) {
@@ -908,13 +941,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(", ") : ""} } ` +
@@ -1001,6 +1036,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);
@@ -1017,6 +1053,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);
@@ -1098,6 +1140,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 &&
@@ -1113,7 +1156,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)}`,
@@ -1244,6 +1292,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);
@@ -1331,6 +1418,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
@@ -1367,7 +1461,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);
@@ -1390,6 +1489,12 @@ export function emitClassShapes(
tracedFields.forEach((f, i) => {
tr.push(...traceFieldVisitLines(struct, f.index, i, llvmCommentText(f.name)));
});
// An Array subclass instance's elements, when its current element
// storage is cycle-capable (a species result can retype it).
if (cls.arrayElem !== undefined) {
host.declare(`declare void @scr_arr_subclass_trace(ptr, ptr, ptr)`);
tr.push(` call void @scr_arr_subclass_trace(ptr %o, ptr %visit, ptr %ctx) ; the elements`);
}
tr.push(` ret void`, `}`, ``);
defs.push(...tr);
@@ -1407,6 +1512,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 (the same test as the trace's).
host.declare(`declare void @scr_arr_subclass_gc_drop(ptr)`);
gf.push(` call void @scr_arr_subclass_gc_drop(ptr %o)`);
}
gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``);
defs.push(...gf);
}
@@ -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 {
@@ -1235,6 +1236,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")
@@ -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";
@@ -278,6 +279,82 @@ 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.speciesCreate") {
// The receiver is borrowed; the call answers a new reference.
const receiver = emitBorrowedInput(host, e.args[0]!);
const length = host.emitExpr(e.args[1]!);
host.declare(`declare ptr @scr_arr_species_create(ptr, double)`);
const out = B.tmp();
B.line(
`${out} = call ptr @scr_arr_species_create(ptr ${receiver.name}, double ${length.name})`,
);
const value = host.own({ name: out, type: e.type });
host.emitPendingCheck();
return value;
}
if (e.fn === "array.speciesFinish") {
// The instance is borrowed; the method's result moves into the call,
// which answers it or the instance that holds its elements.
const instance = emitBorrowedInput(host, e.args[0]!);
const result = host.emitExpr(e.args[1]!);
host.toHeap(result);
const setLength = host.emitExpr(e.args[2]!);
host.declare(`declare ptr @scr_arr_species_finish(ptr, ptr, i1 zeroext)`);
const out = B.tmp();
B.line(
`${out} = call ptr @scr_arr_species_finish(ptr ${instance.name}, ptr ${result.name}, i1 zeroext ${setLength.name})`,
);
const value = host.own({ name: out, type: e.type });
host.emitPendingCheck();
return value;
}
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)
@@ -1457,6 +1457,13 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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.speciesCreate": "scr_arr_species_create",
"array.speciesFinish": "scr_arr_species_finish",
"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",
+29 -16
View File
@@ -1198,7 +1198,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" ||
@@ -1215,21 +1241,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
@@ -0,0 +1,757 @@
/* 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 { boundedArrayHofHelper } from "../containers/array-callbacks.js";
import { storedClassValueType, type ClassInfo } from "../lower-classes.js";
import {
BOOL,
DYN,
F64,
STRING,
UNDEFINED_T,
arrayOf,
typeEquals,
type IrExpr,
type IrFunction,
type IrLocal,
type IrStmt,
type IrType,
type SrcLoc,
} from "../../../ir/ir.js";
/** The element type of `class X extends Array<T>` (`extends Array` in
* JavaScript reads `any[]`, whose elements are checked-dynamic). */
export function arrayClassBase(
lowerer: Lowerer,
heritage: ts.ExpressionWithTypeArguments,
): { elem: IrType } {
const decl = heritage.parent?.parent;
const self =
decl && ts.isClassLikeDeclaration(decl)
? lowerer.checker.getSymbolAtLocation(decl.name ?? decl)
: undefined;
const element = heritage.typeArguments?.nodes[0];
if (
self &&
element &&
mentionsClass(lowerer, lowerer.checker.getTypeAtLocation(element), self, new Set())
)
lowerer.unsupported("SC1090", heritage, "classes that extend an Array of their own instances");
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 };
}
/** Whether `type` is the class `self`'s instance type, or holds it through
* unions, intersections or type arguments (`Group | null`, `Group[]`,
* `Map<string, Group>`): the class's array type would contain itself.
* Another class's members do not count, its instances are references. */
function mentionsClass(
lowerer: Lowerer,
type: ts.Type,
self: ts.Symbol,
seen: Set<ts.Type>,
): boolean {
if (seen.has(type)) return false;
seen.add(type);
if (type.getSymbol() === self) return true;
if (type.isUnionType() || type.isIntersectionType())
return ts.constituentTypes(type).some((part) => mentionsClass(lowerer, part, self, seen));
return lowerer.checker
.getTypeArguments(type as ts.TypeReference)
.some((argument) => mentionsClass(lowerer, argument, self, seen));
}
/** 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 };
}
/** The receiver of a member that `info` declares, read on `value` (the
* lowered `node`): an array value is viewed as the class object. An array
* whose elements are not the class's (a species result of another element
* type, narrowed back to the class by `instanceof`) is refused. */
export function arrayClassReceiver(
lowerer: Lowerer,
info: ClassInfo,
value: IrExpr,
node: ts.Node,
): IrExpr {
if (value.type.kind !== "array") return value;
const root = arrayRootOf(info);
if (root?.extendsArray === undefined || !typeEquals(value.type.elem, root.extendsArray.elem))
lowerer.unsupported(
"SC1090",
node,
"members of an Array subclass on an array whose elements are another type (a species result narrowed back to the class)",
);
return arrayClassView(info, value, locOf(node));
}
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). */
/** A constructor parameter the species length argument can fill: a
* number, a checked-dynamic value, or a union with a number arm
* (`length?: number`). */
function takesLength(lowerer: Lowerer, type: IrType): boolean {
if (type.kind === "f64" || type.kind === "dyn") return true;
if (type.kind !== "union") return false;
return lowerer.unions.get(type.unionId)?.arms.some((arm) => arm.kind === "f64") === true;
}
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]!.mode === "omittable") &&
takesLength(lowerer, params[0]!.type) &&
params.slice(1).every((p) => p.mode === "omittable")
) {
// An omitted argument is what a call without it passes: the
// parameter's default, or undefined.
const blame = info.decl ?? null;
const omitted = (p: (typeof params)[number]): IrExpr =>
p.callDefault ??
(blame
? lowerer.undefinedArgFor(p.type, loc, blame)
: lowerer.coerceToExpected(
{ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } as IrExpr,
p.type,
));
const rest = params.slice(1).map(omitted);
// A dyn first parameter (a JavaScript `constructor(size)`) and an
// optional or defaulted one take the length as a number, and their
// omitted value in the no-argument form.
if (params[0]!.type.kind === "dyn" || params[0]!.mode === "omittable")
without = [
params[0]!.type.kind === "dyn" ? dynUndefinedExpr(loc) : omitted(params[0]!),
...rest,
];
withLength = [
params[0]!.type.kind === "dyn"
? { kind: "dynFrom", value: n(), type: DYN, loc }
: lowerer.coerceToExpected(n(), params[0]!.type),
...params.slice(1).map(omitted),
];
}
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), whether the length is set after the
* elements are defined (slice, splice, concat), and whether the method
* calls back into the program (map, filter, flatMap), which it does after
* the species constructor ran. */
const SPECIES_METHODS: ReadonlyMap<
string,
{ resultLength: boolean; setLength: boolean; callback: boolean }
> = new Map([
["map", { resultLength: true, setLength: false, callback: true }],
["filter", { resultLength: false, setLength: false, callback: true }],
["slice", { resultLength: true, setLength: true, callback: false }],
["splice", { resultLength: true, setLength: true, callback: false }],
["concat", { resultLength: false, setLength: true, callback: false }],
["flat", { resultLength: false, setLength: false, callback: false }],
["flatMap", { resultLength: false, setLength: false, callback: true }],
]);
/** 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;
const receiverRef: IrExpr = { kind: "varRef", localId: local.id, type: receiver.type, loc };
if (mode.callback) {
// The species constructor runs before the first callback: the
// arguments are evaluated in order, the receiver's length is read, the
// instance is created (with that length for map, 0 otherwise), then
// the method runs to that length and its elements move into the
// instance.
const bounded =
result.kind === "call" &&
result.args[0]?.kind === "varRef" &&
result.args[0].localId === local.id
? boundedArrayHofHelper(lowerer, result.callee)
: null;
if (bounded === null || result.kind !== "call")
lowerer.unsupported(
"SC1090",
call,
`'${access.name.text}' in this form when the program declares a class that extends Array`,
);
const stmts: IrStmt[] = [{ kind: "varDecl", localId: local.id, init: receiver, loc }];
const args: IrExpr[] = [receiverRef];
for (const arg of result.args.slice(1)) {
const held = lowerer.declareHiddenLocal("%speciesArgument", arg.type);
stmts.push({ kind: "varDecl", localId: held.id, init: arg, loc });
args.push({ kind: "varRef", localId: held.id, type: arg.type, loc });
}
const bound = lowerer.declareHiddenLocal("%speciesLength", F64);
stmts.push({
kind: "varDecl",
localId: bound.id,
init: {
kind: "arrIntrinsic",
method: "length",
receiver: receiverRef,
args: [],
type: F64,
loc,
},
loc,
});
const boundRef: IrExpr = { kind: "varRef", localId: bound.id, type: F64, loc };
args.push(boundRef);
const instance = lowerer.declareHiddenLocal("%speciesInstance", receiver.type);
const length: IrExpr = mode.resultLength
? boundRef
: { kind: "numLit", value: 0, type: F64, loc };
stmts.push({
kind: "varDecl",
localId: instance.id,
init: {
kind: "libCall",
fn: "array.speciesCreate",
args: [receiverRef, length],
type: receiver.type,
loc,
},
loc,
});
return {
kind: "seqExpr",
stmts,
result: {
kind: "libCall",
fn: "array.speciesFinish",
args: [
{ kind: "varRef", localId: instance.id, type: receiver.type, loc },
{ ...result, callee: bounded, args },
{ kind: "boolLit", value: mode.setLength, type: BOOL, loc },
],
type: result.type,
loc,
},
type: result.type,
loc,
};
}
return {
kind: "seqExpr",
stmts: [{ kind: "varDecl", localId: local.id, init: receiver, loc }],
result: {
kind: "libCall",
fn: "array.speciesAdopt",
args: [
receiverRef,
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)",
);
}
}
@@ -3376,3 +3376,33 @@ export function lowerTuplePredicateCall(
loc,
};
}
/** The `%arr.<method>` helper `name` (map, flatMap or filter) taking its
* iteration bound as a last parameter: the loop runs to the bound instead
* of the receiver's length when it starts. ArraySpeciesCreate reads the
* receiver's length before the species constructor runs, so a constructor
* that appends to the receiver does not extend the loop
* (lowerWithArraySpecies). Null when `name` is no such helper. */
export function boundedArrayHofHelper(lowerer: Lowerer, name: string): string | null {
const info = lowerer.arrHofInfo.get(name);
if (!info || info.method === "forEach") return null;
const bounded = `${name}.bounded`;
if (lowerer.liftedFnNamed(bounded)) return bounded;
const fn = lowerer.liftedFnNamed(name);
if (!fn) return null;
let replaced = false;
const body = fn.body.map((stmt): IrStmt => {
if (replaced || stmt.kind !== "varDecl" || stmt.localId !== "n.0") return stmt;
replaced = true;
return { ...stmt, init: varRef("bound.0", F64, stmt.loc) };
});
if (!replaced) return null;
lowerer.liftedFns.push({
...fn,
name: bounded,
params: [...fn.params, { localId: "bound.0", name: "bound", type: F64 }],
locals: [...fn.locals, { id: "bound.0", name: "bound", type: F64, mutable: false }],
body,
});
return bounded;
}
@@ -560,12 +560,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));
@@ -932,9 +935,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;
@@ -774,6 +774,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" ||
@@ -1,6 +1,11 @@
import { widensReducedRecords } from "../function-returns.js";
import { widthNarrowHelper } from "../coercions/unions.js";
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
import {
arrayClassMemberOwner,
arrayClassReceiver,
lowerArrayClassConstructorRead,
} from "../builtins/array-classes.js";
import { indexedUnionArms, lowerIndexedUnionMember } from "../indexed-unions.js";
import { keyedHelperName } from "../stable-names.js";
import { builtinNamespaceValue } from "../builtin-namespaces.js";
@@ -322,6 +327,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)) {
@@ -3839,6 +3846,32 @@ 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 receiver = arrayClassReceiver(
lowerer,
owner,
lowerer.lowerExpr(access.expression),
access.expression,
);
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;
@@ -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";
@@ -109,6 +110,8 @@ const PRIMITIVE_WRAPPERS = ["Boolean", "Number", "String", "BigInt", "Symbol"] a
* 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);
@@ -1,3 +1,8 @@
import {
arrayClassMemberOwner,
arrayClassReceiver,
lowerArrayClassStaticCall,
} from "./builtins/array-classes.js";
import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js";
import { assertFunctionAliasOf } from "./lower-assert.js";
import { keyedHelperName } from "./stable-names.js";
@@ -1737,6 +1742,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) ??
@@ -10181,6 +10187,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) ??
@@ -10214,6 +10226,13 @@ export function lowerObjectMethodCall(
: null;
if (receiverIr === null) return null;
const lowerReceiver = (): IrExpr => {
if (arrayClassOwner !== null)
return arrayClassReceiver(
lowerer,
arrayClassOwner,
lowerer.lowerExpr(access.expression),
access.expression,
);
const receiver = specializedReceiver ?? lowerer.lowerExpr(access.expression);
const optional = lowerer.runtimeOptionalPropertyReceiver(
access.expression,
@@ -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";
@@ -607,6 +615,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. */
@@ -1995,6 +2007,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;
@@ -2195,6 +2208,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;
@@ -2295,6 +2315,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;
@@ -4416,7 +4442,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 &&
@@ -4482,6 +4508,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,
@@ -4498,6 +4531,7 @@ export function collectClassShapeInner(
...(genericBase ? { genericBase } : {}),
...(callableBase ? { callableBase } : {}),
...(extendsDate ? { extendsDate: true as const } : {}),
...(extendsArray ? { extendsArray } : {}),
...(factoryBaseExpression ? { factoryBaseExpression } : {}),
subclasses: [],
throwingSetters,
@@ -7995,6 +8029,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))) {
@@ -8043,6 +8083,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));
@@ -8110,7 +8154,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
@@ -8122,7 +8166,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
@@ -9169,7 +9213,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",
@@ -9362,6 +9408,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",
@@ -1,3 +1,8 @@
import {
arrayClassMemberOwner,
arrayOfClassInstance,
arrayRootOf,
} from "./builtins/array-classes.js";
import { keyedHelperName } from "./stable-names.js";
import {
isNewTarget,
@@ -509,7 +514,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).
@@ -585,7 +606,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
@@ -966,6 +966,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)) {
@@ -1,4 +1,11 @@
import { unionNarrowFailureMessage } from "./union-narrow.js";
import {
arrayClassFieldsFn,
arrayClassSpeciesFn,
arrayOfClassInstance,
arrayRootOf,
lowerWithArraySpecies,
} from "./builtins/array-classes.js";
import {
compactDeclOrigins,
inPackageFile,
@@ -8954,6 +8961,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++,
@@ -9082,6 +9103,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
@@ -10770,6 +10795,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) {
@@ -14768,7 +14805,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 {
@@ -1810,6 +1810,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
@@ -2615,6 +2645,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++;
@@ -4040,6 +4040,32 @@ function buildLibFnSignatures() {
// 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 },
/** ArraySpeciesCreate ahead of a method that calls back into the program
* (map, filter, flatMap): the receiver (borrowed) and the species
* constructor's length argument. The result has the receiver's type: the
* instance, or the receiver itself when it is a plain array. */
"array.speciesCreate": { argTypes: [null, F64], result: VOID },
/** The method's plain result (moved) defined on the instance
* array.speciesCreate answered (borrowed), and whether to set the length.
* The result has the second argument's type. */
"array.speciesFinish": { argTypes: [null, null, 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 },
// The value of the foreign element (SCR_ARR_FOREIGN) at args[1] of an
// array (args[0], any element type); callers test the element state
// first. Borrowed; never throws.
@@ -157,6 +157,8 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
"classes[].errorNameAfter": "other",
"classes[].prototypeDataHelper": "reference",
"classes[].instancePrototypeHelper": "reference",
"classes[].arrayFields": "reference",
"classes[].arraySpecies": "reference",
"classes[].stringTagHelper": "reference",
"classes[].localCaptures[].name": "other",
"classes[].localCaptures[].localId": "other",
@@ -285,6 +287,14 @@ const REFERENCE_FIXTURES: Record<string, () => 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 }],
}),
"classes[].stringTagHelper": () => ({
...module(),
classes: [{ name: "%m0.Cell", fields: [], stringTagHelper: TARGET, loc }],
@@ -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);
if (cls.stringTagHelper !== undefined) names.push(cls.stringTagHelper);
}
return names;
+11
View File
@@ -1537,6 +1537,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
+1
View File
@@ -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;
+56
View File
@@ -3026,6 +3026,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;
@@ -3133,6 +3145,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);
}
@@ -4223,6 +4244,41 @@ 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.speciesCreate") {
if (e.args[0]?.type.kind !== "array" || !typeEquals(e.type, e.args[0].type))
err("libCall array.speciesCreate must have its receiver's array type", e.loc);
return true;
}
if (e.fn === "array.speciesFinish") {
const plain = e.args[1];
if (plain?.type.kind !== "array" || !typeEquals(e.type, plain.type))
err("libCall array.speciesFinish must have its array argument's type", e.loc);
if (e.args[0]?.type.kind !== "array")
err("libCall array.speciesFinish instance must be an array", e.loc);
return true;
}
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;
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7720-array-subclass-basics.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7721-array-subclass-species.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7722-array-subclass-zero-array.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7723-array-subclass-as-array.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7724-array-subclass-cycles.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7725-array-subclass-operations.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7726-array-subclass-expressions.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7728-array-subclass-species-order.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7730-array-subclass-retyped-trace.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7731-array-subclass-deep-release.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7732-array-subclass-species-bound.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7733-array-subclass-optional-length.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8333-array-subclass-view-writes.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8334-array-subclass-inspect-depth.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/array-subclass-fences.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/diagnostics/array-subclass-retyped-members.ts"
],
"diags": []
}
+292 -3
View File
@@ -1669,8 +1669,8 @@ static void scr_arr_release_foreign(ScrArr *a) {
x->foreign = false;
}
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;
@@ -1737,10 +1737,255 @@ 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);
static ScrArr *scr_arr_species_move(ScrArr *species, ScrArr *result, 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_species_create(ScrArr *recv, double ctor_length) {
const ScrArrClass *cls = scr_arr_class_of(recv);
if (cls == NULL) return scr_arr_retain(recv);
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 scr_arr_retain(recv);
}
ScrArr *species = cls->species(ctor_length, true);
if (scr_exc_pending()) {
scr_arr_release(species);
return scr_arr_retain(recv);
}
return species;
}
ScrArr *scr_arr_species_finish(ScrArr *species, ScrArr *result, bool set_length) {
if (scr_arr_class_of(species) == NULL) return result;
return scr_arr_species_move(scr_arr_retain(species), result, 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;
}
return scr_arr_species_move(species, result, set_length);
}
/* Defines the present elements of `result` (owned) on the species instance
* `species` (owned) and answers the instance, or the result with an error
* pending when the instance cannot take them. */
static ScrArr *scr_arr_species_move(ScrArr *species, ScrArr *result, bool set_length) {
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);
/* A value written through a dynamic view (scr_arr_set_foreign retains
* it); the instance has the result's element type. */
else if (state == SCR_ARR_FOREIGN) scr_arr_set_foreign(species, (double)i, scr_slot_to_ptr(slot));
}
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;
}
/* Whether an Array subclass instance's elements are the collector's: its
* current element ops trace them. A species result can retype an empty
* instance's storage (scr_arr_species_move), so this follows the storage,
* not the element type the class declares. */
static bool scr_arr_subclass_elements_traced(const ScrArr *a) {
const ScrArrOps *ops = scr_arr_ops(a);
return ops != NULL && ops->trace != NULL;
}
void scr_arr_subclass_trace(void *a, ScrTraceVisit visit, void *ctx) {
if (scr_arr_subclass_elements_traced((ScrArr *)a)) scr_arr_trace_v(a, visit, ctx);
}
void scr_arr_subclass_gc_drop(ScrArr *a) {
if (!scr_arr_subclass_elements_traced(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
}
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). */
@@ -1754,6 +1999,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_rc_destroy(a, scr_arr_ext_of(a)->cls->destroy);
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)) {
@@ -5711,10 +5962,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))
@@ -5733,6 +6006,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"};
@@ -5765,6 +6047,13 @@ bool scr_arr_has_named_key(ScrArr *a, ScrStr *key) {
void scr_arr_adopt_view_properties(ScrArr *a, ScrDyn *view, ScrArr *target) {
ScrArrExt *from = scr_arr_ext_of(target);
/* An Array subclass instance's class is in its out-of-line storage, which
* the commit gives to `a`: the class moves to `a`'s, and so stays with the
* instance, while `a` is released as a plain array. */
if (from && from->cls) {
scr_arr_ext(a)->cls = from->cls;
from->cls = NULL;
}
if (from && from->prop_len) {
/* Both arrays have the same element type, so the slots keep their owner. */
ScrArrExt *x = scr_arr_ext(a);
+74 -2
View File
@@ -175,10 +175,34 @@ 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;
/* Some slot may hold SCR_ARR_FOREIGN; release and copies walk the states
* for their payloads only then. */
bool foreign;
} 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)(void *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
@@ -482,11 +506,59 @@ 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);
/* The collector trace of an Array subclass instance's elements: visits
* them when its current element ops trace them. */
void scr_arr_subclass_trace(void *a, ScrTraceVisit visit, void *ctx);
/* 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);
/* ArraySpeciesCreate ahead of an Array method that calls back into the
* program (map, filter, flatMap), so the species constructor runs before
* the first callback: a subclass instance `recv` (borrowed) answers
* `new C(ctor_length)`, a plain receiver itself. Result owned. */
ScrArr *scr_arr_species_create(ScrArr *recv, double ctor_length);
/* The present elements of the method's result `result` (owned) defined on
* the instance scr_arr_species_create answered (`species`, borrowed), and
* its length set to the result's when `set_length`; a plain receiver's
* result as it is. Result owned. */
ScrArr *scr_arr_species_finish(ScrArr *species, ScrArr *result, 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
* (non-index) properties as its metadata and the noncanonical numeric
* properties of `target`, so neither is lost when the storage is swapped. */
* (non-index) properties as its metadata, the noncanonical numeric
* properties of `target` and the class of an Array subclass instance, so
* none is lost when the storage is swapped. */
void scr_arr_adopt_view_properties(ScrArr *a, struct ScrDyn *view, ScrArr *target);
/* True when `key` is the string spelling of a number that is not an array
* index ("-1", "1.5", "NaN", "Infinity"), or "undefined", with the number in
+24 -3
View File
@@ -2499,8 +2499,26 @@ 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 += insp_obj_shows(&props->v.obj.entries[i]);
if (len == 0 && nprops == 0 && !g_show_hidden) return scr_str_new("[]", 2);
if (recurse > depth) return insp_styled_lit("[Array]", 7, INSP_SPECIAL);
/* 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 && !g_show_hidden) {
if (!cls) return scr_str_new("[]", 2);
InspBuf eb = {0};
ib_cstr(&eb, prefix);
ib_cstr(&eb, "[]");
return ib_take(&eb);
}
if (recurse > depth) {
if (!cls) return insp_styled_lit("[Array]", 7, INSP_SPECIAL);
InspBuf eb = {0};
ib_char(&eb, '[');
ib_cstr(&eb, cls);
ib_char(&eb, ']');
return insp_styled(ib_take(&eb), INSP_SPECIAL);
}
scr_insp_begin(recurse + 1);
/* Node's maxArrayLength (100) counts output entries; a run of holes
* is one `<n empty items>` entry (formatSpecialArray). */
@@ -2570,7 +2588,10 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
* `[length]` under showHidden, or the last property. */
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);
@@ -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<number> {
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));
@@ -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<number> {
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());
@@ -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));
@@ -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<string> {
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));
@@ -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<Member> {
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);
@@ -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<string> {
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);
@@ -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));
@@ -0,0 +1,52 @@
// map, filter and flatMap on a class that extends Array construct their
// result through the class before the first callback runs, with the
// receiver's length for map and 0 for filter and flatMap.
class Logged extends Array<number> {
constructor(...args: number[]) {
super(...args);
console.log("construct", args.length === 1 ? `length ${args[0]}` : `${args.length} items`);
}
}
const source = Logged.from([1, 2, 3]);
console.log("map");
const mapped = source.map((value) => {
console.log(" callback", value);
return value * 10;
});
console.log(mapped, mapped instanceof Logged);
console.log("filter");
const kept = source.filter((value) => {
console.log(" callback", value);
return value !== 2;
});
console.log(kept, kept instanceof Logged);
console.log("flatMap");
const spread = source.flatMap((value) => {
console.log(" callback", value);
return [value, -value];
});
console.log(spread.length, spread instanceof Logged);
console.log("map to strings");
const labels = source.map((value) => {
console.log(" callback", value);
return `#${value}`;
});
console.log(labels, labels instanceof Logged);
console.log("throwing callback");
try {
source.map((value) => {
if (value === 2) throw new Error(`stopped at ${value}`);
return value;
});
} catch (error) {
console.log((error as Error).message);
}
console.log("plain receiver");
const plain: number[] = [4, 5];
console.log(plain.map((value) => value + 1), plain.filter((value) => value > 4));
@@ -0,0 +1,33 @@
// A cycle-capable class that extends Array (its elements and its own field
// can close a cycle) whose map results take other element types: the
// collector traces and tears down each instance by the elements it holds.
// Garbage cycles make the collector run while the results are alive.
class Holder {
group: Group | null = null;
}
class Group extends Array<Holder | null> {
owner: Holder | null = null;
}
function churn(rounds: number): number {
let made = 0;
for (let i = 0; i < rounds; i++) {
const holder = new Holder();
const group = new Group();
group.owner = holder;
holder.group = group;
group.push(holder);
made++;
}
return made;
}
const groups = new Group();
groups.push(new Holder(), null, new Holder());
const numbers = groups.map((_, i) => i * 1.5 + 1e6);
const labels = groups.map((_, i) => `label-${i}`);
console.log(churn(20000));
console.log(numbers instanceof Group, numbers.length, numbers.join(","));
console.log(labels instanceof Group, labels.join(","));
console.log(churn(20000), numbers[2], labels[1]);
@@ -0,0 +1,17 @@
// Releasing a long chain of instances of a class that extends Array, each
// holding the next one, tears them down without growing the native stack.
class Link extends Array<number> {
next: Link | null = null;
}
let head: Link | null = new Link();
let tail = head;
for (let i = 0; i < 300000; i++) {
const link = new Link();
link.push(i);
tail.next = link;
tail = link;
}
console.log(tail.length, tail[0]);
head = null;
console.log("released", head);
@@ -0,0 +1,32 @@
// map, filter and flatMap read the receiver's length before the species
// constructor runs: elements the constructor appends to the receiver are
// not visited.
let watched: Growing | null = null;
class Growing extends Array<number> {
constructor(...args: number[]) {
super(...args);
if (watched !== null) watched.push(watched.length * 100);
}
}
const source = Growing.from([1, 2, 3]);
watched = source;
const visits: number[] = [];
const doubled = source.map((value) => {
visits.push(value);
return value * 2;
});
console.log(doubled.join(","), visits.join(","), source.join(","));
visits.length = 0;
const kept = source.filter((value) => {
visits.push(value);
return value > 1;
});
console.log(kept.join(","), visits.join(","), source.length);
visits.length = 0;
const spread = source.flatMap((value) => {
visits.push(value);
return [value];
});
console.log(spread.length, visits.length, source.length);
@@ -0,0 +1,26 @@
// A class that extends Array whose constructor's length parameter is
// optional or has a default takes the species length argument: map,
// filter and slice construct their results through it.
class Optional extends Array<number> {
constructor(length?: number) {
super(length ?? 0);
}
}
class Defaulted extends Array<string> {
tag: string;
constructor(length = 0, tag = "none") {
super(length);
this.tag = tag;
}
}
const optional = Optional.from([1, 2, 3]);
const mapped = optional.map((value) => value * 10);
console.log(mapped instanceof Optional, mapped.join(","), optional.slice(1) instanceof Optional);
console.log(optional.filter((value) => value !== 2).join(","));
const defaulted = new Defaulted(2).fill("x");
const sliced = defaulted.slice(0, 1);
if (sliced instanceof Defaulted) console.log(sliced.length, sliced.tag);
console.log(defaulted.map((s) => s + "!").join(","));
@@ -0,0 +1,27 @@
// An Array subclass written through an unknown[] view: a number pushed
// through the view, and values its element type cannot hold, which the
// array keeps as written. The instance keeps its class and its fields, and
// the species results of slice and concat keep the written values.
class Scores extends Array<number> {
label = "scores";
}
const plain = new Scores();
plain.push(1, 2);
const plainView: unknown[] = plain;
plainView.push(3);
console.log(plain instanceof Scores, plain.label, plain.length, JSON.stringify(plain));
const scores = new Scores();
scores.push(1, 2);
const view: unknown[] = scores;
view.push("x", { a: 1 });
console.log(scores instanceof Scores, scores.label, scores.length, JSON.stringify(scores), scores.join("|"));
const tail = scores.slice(1);
console.log(tail instanceof Scores, tail.length, JSON.stringify(tail));
const joined = scores.concat([9]);
console.log(joined instanceof Scores, joined.length, JSON.stringify(joined));
scores[2] = 7;
console.log(JSON.stringify(scores), view[3]);
scores.length = 1;
console.log(scores instanceof Scores, scores.label, JSON.stringify(scores), JSON.stringify(tail));
@@ -0,0 +1,13 @@
// util.inspect of Array subclass instances: the `Name(length)` prefix, and
// the `[Name]` placeholder past the depth limit, plain and with colors.
import { inspect } from "node:util";
class Scores extends Array<number> {}
const s = new Scores();
s.push(1, 2);
console.log(inspect(new Scores()), inspect(s));
console.log(inspect([s], { depth: 0 }), inspect([[1]], { depth: 0 }));
console.log(JSON.stringify(inspect([s], { depth: 0, colors: true })));
console.log(JSON.stringify(inspect([[1]], { depth: 0, colors: true })));
console.log(JSON.stringify(inspect(s, { colors: true })));
@@ -0,0 +1,25 @@
// Array subclasses that do not compile yet, each refused where it is
// declared: a generic subclass, a subclass of a class that extends Array,
// a class that declares its own Symbol.species, and a class whose elements
// are its own instances.
class Bag<T> extends Array<T> {
firstOr(fallback: T): T {
return this.length > 0 ? this[0]! : fallback;
}
}
class Base extends Array<number> {}
class Derived extends Base {}
class Speciated extends Array<number> {
static get [Symbol.species](): ArrayConstructor {
return Array;
}
}
class Nested extends Array<Nested | null> {}
console.log(new Bag<number>().firstOr(1));
console.log(new Derived().length);
console.log(new Speciated().length);
console.log(new Nested().length);
@@ -0,0 +1,24 @@
// A species result of another element type is still an instance of the
// class, so `instanceof` narrows it back. Its elements are not the class's,
// so the class's fields and methods on it are refused where they are read.
class Holder {
tag = "h";
}
class Group extends Array<Holder | null> {
owner: Holder | null = null;
firstTag(): string {
const first = this[0];
return first === null || first === undefined ? "none" : first.tag;
}
}
const groups = new Group();
groups.push(new Holder(), null);
const numbers = groups.map((_, i) => i + 1);
if (numbers instanceof Group) {
numbers.owner = new Holder();
console.log(numbers.owner.tag);
console.log(numbers.firstTag());
}
@@ -0,0 +1,55 @@
array-subclass-fences.ts:5:22 - error SC1090: a generic class that extends Array is not supported yet
4 | // are its own instances.
5 | class Bag<T> extends Array<T> {
| ^~~~~~~~
6 | firstOr(fallback: T): T {
array-subclass-fences.ts:12:23 - error SC1090: extending 'Base', a class that extends Array (one level of Array subclass compiles) is not supported yet
11 | class Base extends Array<number> {}
12 | class Derived extends Base {}
| ^~~~
13 |
array-subclass-fences.ts:15:3 - error SC1090: Symbol.species on a class that extends Array (its methods' results construct the class itself) is not supported yet
14 | class Speciated extends Array<number> {
15 | static get [Symbol.species](): ArrayConstructor {
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 | return Array;
array-subclass-fences.ts:20:22 - error SC1090: classes that extend an Array of their own instances are not supported yet
19 |
20 | class Nested extends Array<Nested | null> {}
| ^~~~~~~~~~~~~~~~~~~~
21 |
array-subclass-fences.ts:22:13 - error SC1090: constructing values other than classes declared in the program is not supported yet
21 |
22 | console.log(new Bag<number>().firstOr(1));
| ^~~~~~~~~~~~~~~~~
23 | console.log(new Derived().length);
array-subclass-fences.ts:23: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
22 | console.log(new Bag<number>().firstOr(1));
23 | console.log(new Derived().length);
| ^~~~~~~~~~~~~
24 | console.log(new Speciated().length);
array-subclass-fences.ts:24: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
23 | console.log(new Derived().length);
24 | console.log(new Speciated().length);
| ^~~~~~~~~~~~~~~
25 | console.log(new Nested().length);
array-subclass-fences.ts:25: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
24 | console.log(new Speciated().length);
25 | console.log(new Nested().length);
| ^~~~~~~~~~~~
26 |
@@ -0,0 +1,20 @@
array-subclass-retyped-members.ts:21:3 - error SC1090: members of an Array subclass on an array whose elements are another type (a species result narrowed back to the class) is not supported yet
20 | if (numbers instanceof Group) {
21 | numbers.owner = new Holder();
| ^~~~~~~
22 | console.log(numbers.owner.tag);
array-subclass-retyped-members.ts:22:15 - error SC1090: members of an Array subclass on an array whose elements are another type (a species result narrowed back to the class) is not supported yet
21 | numbers.owner = new Holder();
22 | console.log(numbers.owner.tag);
| ^~~~~~~
23 | console.log(numbers.firstTag());
array-subclass-retyped-members.ts:23:15 - error SC1090: members of an Array subclass on an array whose elements are another type (a species result narrowed back to the class) is not supported yet
22 | console.log(numbers.owner.tag);
23 | console.log(numbers.firstTag());
| ^~~~~~~
24 | }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 2527, "san": 3409 }
@@ -0,0 +1 @@
{ "plain": 2468, "san": 3577 }