Compile classes that extend Array

A class declared `extends Array` (or `extends Array<T>`) was refused with
"extending classes not declared in the program ('Array')", which blocked
lru-cache at module load. An instance is now an array of the class's
element type: its struct begins with the runtime's array header, whose
reference count it shares, and the class's own fields follow it. The
class's TypeScript type maps to its element array type, so every array
operation applies to an instance, and passing it where an array is
expected passes the same object. The class's own fields, methods,
accessors and statics read the array as the class object.

super() follows the Array constructor: one number argument sets the
length (a RangeError for an invalid one), other arguments become the
elements, and the implicit constructor forwards its arguments the same
way. The runtime finds the class through the array's ext (ScrArrClass:
name, destroy, the own fields, the species constructor), so instanceof
the class (or a binding that holds it) works on static, array-typed and
dyn values, util.inspect
prints `Name(length) [ ..., field: value ]`, Object.keys lists the own
fields, and a release through any array path reaches the class's
teardown. Instances whose elements or fields can form cycles carry the
collector header and are traced.

map, filter, slice, splice, concat, flat and flatMap construct their
result through the class as ArraySpeciesCreate does: `new C(length)` with
the specification's argument, then the elements, then the length where
the specification sets it. A map to another element type retypes the
new instance when its constructor added no elements. Array.of and
Array.from called on the class construct through it. Programs that
declare no Array subclass compile as before.

A generic subclass, a subclass of such a class and `static
[Symbol.species]` are refused at compile time; a species result the
class's constructor cannot construct throws a TypeError.

String() of an array with checked-dynamic elements now uses the
runtime's String() instead of being refused.

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