Fix the species trace, deep release, iteration bound and optional length findings

This commit is contained in:
Chris Tate
2026-10-23 23:31:58 -05:00
parent 0e733f22cf
commit 82ebbd4df7
19 changed files with 334 additions and 71 deletions
+8 -10
View File
@@ -1481,13 +1481,11 @@ 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`);
// An Array subclass instance's elements, when its current element
// storage is cycle-capable (a species result can retype it).
if (cls.arrayElem !== undefined) {
host.declare(`declare void @scr_arr_subclass_trace(ptr, ptr, ptr)`);
tr.push(` call void @scr_arr_subclass_trace(ptr %o, ptr %visit, ptr %ctx) ; the elements`);
}
tr.push(` ret void`, `}`, ``);
defs.push(...tr);
@@ -1508,9 +1506,9 @@ 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})`);
// released with the storage (the same test as the trace's).
host.declare(`declare void @scr_arr_subclass_gc_drop(ptr)`);
gf.push(` call void @scr_arr_subclass_gc_drop(ptr %o)`);
}
gf.push(...emitObjectFree(host, true), ` ret void`, `}`, ``);
defs.push(...gf);
@@ -12,6 +12,7 @@ import { isJsSourceFile, locOf } from "../../program.js";
import { stripTypeWrappers } from "../typed-arguments.js";
import { dynUndefinedExpr, nodeThrowExpr } from "../../../ir/build.js";
import { lowerArrayFromCall, lowerArrayOfCall } from "../containers/array-construction.js";
import { boundedArrayHofHelper } from "../containers/array-callbacks.js";
import { storedClassValueType, type ClassInfo } from "../lower-classes.js";
import {
BOOL,
@@ -34,6 +35,18 @@ export function arrayClassBase(
lowerer: Lowerer,
heritage: ts.ExpressionWithTypeArguments,
): { elem: IrType } {
const decl = heritage.parent?.parent;
const self =
decl && ts.isClassLikeDeclaration(decl)
? lowerer.checker.getSymbolAtLocation(decl.name ?? decl)
: undefined;
const element = heritage.typeArguments?.nodes[0];
if (
self &&
element &&
mentionsClass(lowerer, lowerer.checker.getTypeAtLocation(element), self, new Set())
)
lowerer.unsupported("SC1090", heritage, "classes that extend an Array of their own instances");
const mapped = lowerer.mapTypeOf(lowerer.checker.getTypeAtLocation(heritage));
// `extends Array` in JavaScript reads `any[]`: dyn elements.
if (isJsSourceFile(ts.getSourceFileOfNode(heritage)) && mapped?.kind !== "array")
@@ -47,6 +60,26 @@ export function arrayClassBase(
return { elem: mapped!.kind === "array" ? mapped!.elem : DYN };
}
/** Whether `type` is the class `self`'s instance type, or holds it through
* unions, intersections or type arguments (`Group | null`, `Group[]`,
* `Map<string, Group>`): the class's array type would contain itself.
* Another class's members do not count, its instances are references. */
function mentionsClass(
lowerer: Lowerer,
type: ts.Type,
self: ts.Symbol,
seen: Set<ts.Type>,
): boolean {
if (seen.has(type)) return false;
seen.add(type);
if (type.getSymbol() === self) return true;
if (type.isUnionType() || type.isIntersectionType())
return ts.constituentTypes(type).some((part) => mentionsClass(lowerer, part, self, seen));
return lowerer.checker
.getTypeArguments(type as ts.TypeReference)
.some((argument) => mentionsClass(lowerer, argument, self, seen));
}
/** The class declared `extends Array` at the root of `info`'s chain, or null. */
export function arrayRootOf(info: ClassInfo | null | undefined): ClassInfo | null {
for (let c = info ?? null; c; c = c.base) if (c.extendsArray) return c;
@@ -349,6 +382,15 @@ export function arrayClassFieldsFn(lowerer: Lowerer, info: ClassInfo): IrFunctio
* take those arguments: they must be none, a forwarding rest (dyn or
* number), or a number first parameter whose others may be omitted (whose
* no-argument form throws). */
/** A constructor parameter the species length argument can fill: a
* number, a checked-dynamic value, or a union with a number arm
* (`length?: number`). */
function takesLength(lowerer: Lowerer, type: IrType): boolean {
if (type.kind === "f64" || type.kind === "dyn") return true;
if (type.kind !== "union") return false;
return lowerer.unions.get(type.unionId)?.arms.some((arm) => arm.kind === "f64") === true;
}
export function arrayClassSpeciesFn(lowerer: Lowerer, info: ClassInfo): IrFunction | null {
const elem = arrayRootOf(info)!.extendsArray!.elem;
const arrT = arrayOf(elem);
@@ -381,31 +423,35 @@ export function arrayClassSpeciesFn(lowerer: Lowerer, info: ClassInfo): IrFuncti
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[0]!.mode === "required" || params[0]!.mode === "omittable") &&
takesLength(lowerer, params[0]!.type) &&
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(
// An omitted argument is what a call without it passes: the
// parameter's default, or undefined.
const blame = info.decl ?? null;
const omitted = (p: (typeof params)[number]): IrExpr =>
p.callDefault ??
(blame
? lowerer.undefinedArgFor(p.type, loc, blame)
: lowerer.coerceToExpected(
{ kind: "unitLit", unit: "undefined", type: UNDEFINED_T, loc } as IrExpr,
p.type,
),
),
));
const rest = params.slice(1).map(omitted);
// A dyn first parameter (a JavaScript `constructor(size)`) and an
// optional or defaulted one take the length as a number, and their
// omitted value in the no-argument form.
if (params[0]!.type.kind === "dyn" || params[0]!.mode === "omittable")
without = [
params[0]!.type.kind === "dyn" ? dynUndefinedExpr(loc) : omitted(params[0]!),
...rest,
];
withLength = [
params[0]!.type.kind === "dyn"
? { kind: "dynFrom", value: n(), type: DYN, loc }
: lowerer.coerceToExpected(n(), params[0]!.type),
...params.slice(1).map(omitted),
];
}
if (withLength === null) return null;
@@ -517,14 +563,17 @@ export function lowerWithArraySpecies(
const receiverRef: IrExpr = { kind: "varRef", localId: local.id, type: receiver.type, loc };
if (mode.callback) {
// The species constructor runs before the first callback: the
// arguments are evaluated in order, then the instance is created (with
// the receiver's length for map, 0 otherwise), then the method runs and
// its elements move into the instance.
if (
result.kind !== "call" ||
result.args[0]?.kind !== "varRef" ||
result.args[0].localId !== local.id
)
// arguments are evaluated in order, the receiver's length is read, the
// instance is created (with that length for map, 0 otherwise), then
// the method runs to that length and its elements move into the
// instance.
const bounded =
result.kind === "call" &&
result.args[0]?.kind === "varRef" &&
result.args[0].localId === local.id
? boundedArrayHofHelper(lowerer, result.callee)
: null;
if (bounded === null || result.kind !== "call")
lowerer.unsupported(
"SC1090",
call,
@@ -537,9 +586,25 @@ export function lowerWithArraySpecies(
stmts.push({ kind: "varDecl", localId: held.id, init: arg, loc });
args.push({ kind: "varRef", localId: held.id, type: arg.type, loc });
}
const bound = lowerer.declareHiddenLocal("%speciesLength", F64);
stmts.push({
kind: "varDecl",
localId: bound.id,
init: {
kind: "arrIntrinsic",
method: "length",
receiver: receiverRef,
args: [],
type: F64,
loc,
},
loc,
});
const boundRef: IrExpr = { kind: "varRef", localId: bound.id, type: F64, loc };
args.push(boundRef);
const instance = lowerer.declareHiddenLocal("%speciesInstance", receiver.type);
const length: IrExpr = mode.resultLength
? { kind: "arrIntrinsic", method: "length", receiver: receiverRef, args: [], type: F64, loc }
? boundRef
: { kind: "numLit", value: 0, type: F64, loc };
stmts.push({
kind: "varDecl",
@@ -561,7 +626,7 @@ export function lowerWithArraySpecies(
fn: "array.speciesFinish",
args: [
{ kind: "varRef", localId: instance.id, type: receiver.type, loc },
{ ...result, args },
{ ...result, callee: bounded, args },
{ kind: "boolLit", value: mode.setLength, type: BOOL, loc },
],
type: result.type,
@@ -3171,3 +3171,33 @@ export function lowerTuplePredicateCall(
loc,
};
}
/** The `%arr.<method>` helper `name` (map, flatMap or filter) taking its
* iteration bound as a last parameter: the loop runs to the bound instead
* of the receiver's length when it starts. ArraySpeciesCreate reads the
* receiver's length before the species constructor runs, so a constructor
* that appends to the receiver does not extend the loop
* (lowerWithArraySpecies). Null when `name` is no such helper. */
export function boundedArrayHofHelper(lowerer: Lowerer, name: string): string | null {
const info = lowerer.arrHofInfo.get(name);
if (!info || info.method === "forEach") return null;
const bounded = `${name}.bounded`;
if (lowerer.liftedFnNamed(bounded)) return bounded;
const fn = lowerer.liftedFnNamed(name);
if (!fn) return null;
let replaced = false;
const body = fn.body.map((stmt): IrStmt => {
if (replaced || stmt.kind !== "varDecl" || stmt.localId !== "n.0") return stmt;
replaced = true;
return { ...stmt, init: varRef("bound.0", F64, stmt.loc) };
});
if (!replaced) return null;
lowerer.liftedFns.push({
...fn,
name: bounded,
params: [...fn.params, { localId: "bound.0", name: "bound", type: F64 }],
locals: [...fn.locals, { id: "bound.0", name: "bound", type: F64, mutable: false }],
body,
});
return bounded;
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7730-array-subclass-retyped-trace.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7731-array-subclass-deep-release.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7732-array-subclass-species-bound.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7733-array-subclass-optional-length.ts"
],
"diags": []
}
+16 -3
View File
@@ -1868,8 +1868,21 @@ const ScrArrClass *scr_arr_class_of(const ScrArr *a) {
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);
/* Whether an Array subclass instance's elements are the collector's: its
* current element ops trace them. A species result can retype an empty
* instance's storage (scr_arr_species_move), so this follows the storage,
* not the element type the class declares. */
static bool scr_arr_subclass_elements_traced(const ScrArr *a) {
const ScrArrOps *ops = scr_arr_ops(a);
return ops != NULL && ops->trace != NULL;
}
void scr_arr_subclass_trace(void *a, ScrTraceVisit visit, void *ctx) {
if (scr_arr_subclass_elements_traced((ScrArr *)a)) scr_arr_trace_v(a, visit, ctx);
}
void scr_arr_subclass_gc_drop(ScrArr *a) {
if (!scr_arr_subclass_elements_traced(a)) scr_arr_release_contents(a);
scr_arr_free_parts(a);
a->data = NULL;
a->len = a->cap = a->filled = 0;
@@ -1906,7 +1919,7 @@ void scr_arr_release(ScrArr *a) {
/* 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);
scr_rc_destroy(a, scr_arr_ext_of(a)->cls->destroy);
return;
}
/* Nothing to tear down: free the storage without scr_rc_destroy's
+5 -2
View File
@@ -185,7 +185,7 @@ typedef struct ScrArrClass {
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);
void (*destroy)(void *a);
/* The class's own fields as a dyn object in declaration order (the array
* is passed owned, +1), or NULL when the class declares none. util.inspect
* and Object.keys list them after the elements. */
@@ -503,7 +503,10 @@ void scr_arr_subclass_init(ScrArr *a, uint8_t elem, const ScrArrOps *ops, const
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);
void scr_arr_subclass_gc_drop(ScrArr *a);
/* The collector trace of an Array subclass instance's elements: visits
* them when its current element ops trace them. */
void scr_arr_subclass_trace(void *a, ScrTraceVisit visit, void *ctx);
/* super(...args) in a class that extends Array (the Array constructor's
* length-or-elements rule); `args` is borrowed. */
void scr_arr_subclass_construct(ScrArr *a, ScrArr *args, bool dyn_elems);
@@ -0,0 +1,33 @@
// A cycle-capable class that extends Array (its elements and its own field
// can close a cycle) whose map results take other element types: the
// collector traces and tears down each instance by the elements it holds.
// Garbage cycles make the collector run while the results are alive.
class Holder {
group: Group | null = null;
}
class Group extends Array<Holder | null> {
owner: Holder | null = null;
}
function churn(rounds: number): number {
let made = 0;
for (let i = 0; i < rounds; i++) {
const holder = new Holder();
const group = new Group();
group.owner = holder;
holder.group = group;
group.push(holder);
made++;
}
return made;
}
const groups = new Group();
groups.push(new Holder(), null, new Holder());
const numbers = groups.map((_, i) => i * 1.5 + 1e6);
const labels = groups.map((_, i) => `label-${i}`);
console.log(churn(20000));
console.log(numbers instanceof Group, numbers.length, numbers.join(","));
console.log(labels instanceof Group, labels.join(","));
console.log(churn(20000), numbers[2], labels[1]);
@@ -0,0 +1,17 @@
// Releasing a long chain of instances of a class that extends Array, each
// holding the next one, tears them down without growing the native stack.
class Link extends Array<number> {
next: Link | null = null;
}
let head: Link | null = new Link();
let tail = head;
for (let i = 0; i < 300000; i++) {
const link = new Link();
link.push(i);
tail.next = link;
tail = link;
}
console.log(tail.length, tail[0]);
head = null;
console.log("released", head);
@@ -0,0 +1,32 @@
// map, filter and flatMap read the receiver's length before the species
// constructor runs: elements the constructor appends to the receiver are
// not visited.
let watched: Growing | null = null;
class Growing extends Array<number> {
constructor(...args: number[]) {
super(...args);
if (watched !== null) watched.push(watched.length * 100);
}
}
const source = Growing.from([1, 2, 3]);
watched = source;
const visits: number[] = [];
const doubled = source.map((value) => {
visits.push(value);
return value * 2;
});
console.log(doubled.join(","), visits.join(","), source.join(","));
visits.length = 0;
const kept = source.filter((value) => {
visits.push(value);
return value > 1;
});
console.log(kept.join(","), visits.join(","), source.length);
visits.length = 0;
const spread = source.flatMap((value) => {
visits.push(value);
return [value];
});
console.log(spread.length, visits.length, source.length);
@@ -0,0 +1,26 @@
// A class that extends Array whose constructor's length parameter is
// optional or has a default takes the species length argument: map,
// filter and slice construct their results through it.
class Optional extends Array<number> {
constructor(length?: number) {
super(length ?? 0);
}
}
class Defaulted extends Array<string> {
tag: string;
constructor(length = 0, tag = "none") {
super(length);
this.tag = tag;
}
}
const optional = Optional.from([1, 2, 3]);
const mapped = optional.map((value) => value * 10);
console.log(mapped instanceof Optional, mapped.join(","), optional.slice(1) instanceof Optional);
console.log(optional.filter((value) => value !== 2).join(","));
const defaulted = new Defaulted(2).fill("x");
const sliced = defaulted.slice(0, 1);
if (sliced instanceof Defaulted) console.log(sliced.length, sliced.tag);
console.log(defaulted.map((s) => s + "!").join(","));
+5 -1
View File
@@ -1,6 +1,7 @@
// 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.
// a class that declares its own Symbol.species, and a class whose elements
// are its own instances.
class Bag<T> extends Array<T> {
firstOr(fallback: T): T {
return this.length > 0 ? this[0]! : fallback;
@@ -16,6 +17,9 @@ class Speciated extends Array<number> {
}
}
class Nested extends Array<Nested | null> {}
console.log(new Bag<number>().firstOr(1));
console.log(new Derived().length);
console.log(new Speciated().length);
console.log(new Nested().length);
@@ -1,41 +1,55 @@
array-subclass-fences.ts:4:22 - error SC1090: a generic class that extends Array is not supported yet
array-subclass-fences.ts:5:22 - error SC1090: a generic class that extends Array is not supported yet
3 | // and a class that declares its own Symbol.species.
4 | class Bag<T> extends Array<T> {
4 | // are its own instances.
5 | class Bag<T> extends Array<T> {
| ^~~~~~~~
5 | firstOr(fallback: T): T {
6 | 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
array-subclass-fences.ts:12:23 - error SC1090: extending 'Base', a class that extends Array (one level of Array subclass compiles) is not supported yet
10 | class Base extends Array<number> {}
11 | class Derived extends Base {}
11 | class Base extends Array<number> {}
12 | class Derived extends Base {}
| ^~~~
12 |
13 |
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
array-subclass-fences.ts:15:3 - error SC1090: Symbol.species on a class that extends Array (its methods' results construct the class itself) is not supported yet
13 | class Speciated extends Array<number> {
14 | static get [Symbol.species](): ArrayConstructor {
14 | class Speciated extends Array<number> {
15 | static get [Symbol.species](): ArrayConstructor {
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15 | return Array;
16 | return Array;
array-subclass-fences.ts:19:13 - error SC1090: constructing values other than classes declared in the program is not supported yet
array-subclass-fences.ts:20:22 - error SC1090: classes that extend an Array of their own instances are not supported yet
18 |
19 | console.log(new Bag<number>().firstOr(1));
19 |
20 | class Nested extends Array<Nested | null> {}
| ^~~~~~~~~~~~~~~~~~~~
21 |
array-subclass-fences.ts:22:13 - error SC1090: constructing values other than classes declared in the program is not supported yet
21 |
22 | console.log(new Bag<number>().firstOr(1));
| ^~~~~~~~~~~~~~~~~
20 | console.log(new Derived().length);
23 | 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
array-subclass-fences.ts:23:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
19 | console.log(new Bag<number>().firstOr(1));
20 | console.log(new Derived().length);
22 | console.log(new Bag<number>().firstOr(1));
23 | console.log(new Derived().length);
| ^~~~~~~~~~~~~
21 | console.log(new Speciated().length);
24 | 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
array-subclass-fences.ts:24:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
20 | console.log(new Derived().length);
21 | console.log(new Speciated().length);
23 | console.log(new Derived().length);
24 | console.log(new Speciated().length);
| ^~~~~~~~~~~~~~~
22 |
25 | console.log(new Nested().length);
array-subclass-fences.ts:25:13 - error SC1090: constructing through a class value whose class has no lowering (the class declaration itself was rejected — see its own diagnostic) is not supported yet
24 | console.log(new Speciated().length);
25 | console.log(new Nested().length);
| ^~~~~~~~~~~~
26 |
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }
@@ -0,0 +1 @@
{ "plain": 11000, "san": 9000 }