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