mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-04 08:04:37 +08:00
Construct through arrays of class values that mix a class and its subclasses (#1868)
This commit is contained in:
@@ -176,6 +176,10 @@ export interface DynHost extends WalkerHost {
|
||||
dynPromiseAdapter(t: IrType): string;
|
||||
isErrorClass(className: string): boolean;
|
||||
classSubtypes(className: string): readonly string[];
|
||||
/** The strict subclasses whose class-value constructor takes the same
|
||||
* arguments as `className`'s (classValueAbiEquals): the class values a
|
||||
* checked `classval` slot accepts besides its own class. */
|
||||
classValueSubtypes(className: string): readonly string[];
|
||||
/** emitter.ts's pendingTestLines: the inline pending-exception test. */
|
||||
pendingTestLines(dest: string): string[];
|
||||
}
|
||||
@@ -1810,6 +1814,26 @@ export class LlDyn {
|
||||
const B = new BlockBuilder();
|
||||
if (t.kind === "classval") {
|
||||
host.declare(`declare ptr @scr_dyn_class_check(ptr, ptr, ptr)`);
|
||||
// A subclass's class value checks into the base class's slot when
|
||||
// construction through the slot can call its thunk with the base
|
||||
// constructor's arguments; any other subclass fails the check.
|
||||
const subclasses = host.classValueSubtypes(t.className);
|
||||
if (subclasses.length > 0) {
|
||||
host.declare(`declare zeroext i1 @scr_dyn_class_is(ptr, ptr)`);
|
||||
for (const className of subclasses) {
|
||||
const subKey = host.cstr(typeKey({ kind: "classval", className }));
|
||||
const matched = B.tmp();
|
||||
B.line(`${matched} = call zeroext i1 @scr_dyn_class_is(ptr %d, ptr ${subKey})`);
|
||||
const yes = B.newLabel("sub.yes");
|
||||
const no = B.newLabel("sub.no");
|
||||
B.condBr(matched, yes, no);
|
||||
B.startBlock(yes);
|
||||
const sub = B.tmp();
|
||||
B.line(`${sub} = call ptr @scr_dyn_class_check(ptr %d, ptr ${subKey}, ptr %path)`);
|
||||
B.terminate(`ret ptr ${sub}`);
|
||||
B.startBlock(no);
|
||||
}
|
||||
}
|
||||
const checked = B.tmp();
|
||||
B.line(
|
||||
`${checked} = call ptr @scr_dyn_class_check(ptr %d, ptr ${host.cstr(key)}, ptr %path)`,
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
typeKey,
|
||||
VOID,
|
||||
classJsName,
|
||||
classValueAbiEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { moduleRuntimeFeatures, type RuntimeFeatures } from "../../ir/runtime-features.js";
|
||||
import { captureFreeFunctions, observesStacks } from "../stack-observations.js";
|
||||
@@ -2053,14 +2054,14 @@ export class LlEmitter {
|
||||
const meta = this.classMeta.get(t.className);
|
||||
return meta !== undefined && programLifetimeClass(this.shapeHost, meta);
|
||||
},
|
||||
classSubtypes: (name) => {
|
||||
const target = this.classMetaOf(name);
|
||||
const intervals = classMembershipIntervals(this.classMeta, target.def.name);
|
||||
return [...this.classMeta.values()]
|
||||
.filter((meta) =>
|
||||
intervals.some((range) => range.pre <= meta.pre && meta.pre <= range.post),
|
||||
)
|
||||
.map((meta) => meta.def.name);
|
||||
classSubtypes: (name) => this.classSubtypes(name),
|
||||
classValueSubtypes: (name) => {
|
||||
const target = this.classMetaOf(name).def;
|
||||
return this.classSubtypes(name).filter(
|
||||
(sub) =>
|
||||
sub !== target.name &&
|
||||
classValueAbiEquals(this.classMetaOf(sub).def, target, this.fnByName),
|
||||
);
|
||||
},
|
||||
pendingTestLines: (dest) => this.pendingTestLines(dest),
|
||||
virtualEntry: (implFn) => this.virtualEntry(implFn),
|
||||
@@ -4185,6 +4186,15 @@ export class LlEmitter {
|
||||
|
||||
// ── class plumbing ──────────────────────────────────────────────────────
|
||||
|
||||
/** `name` and every class whose membership interval falls in its own. */
|
||||
private classSubtypes(name: string): string[] {
|
||||
const target = this.classMetaOf(name);
|
||||
const intervals = classMembershipIntervals(this.classMeta, target.def.name);
|
||||
return [...this.classMeta.values()]
|
||||
.filter((meta) => intervals.some((range) => range.pre <= meta.pre && meta.pre <= range.post))
|
||||
.map((meta) => meta.def.name);
|
||||
}
|
||||
|
||||
classMetaOf(className: string): LlClassMeta {
|
||||
const meta = this.classMeta.get(className);
|
||||
if (!meta) throw new InternalCompilerError(`llvm emitter bug: unknown class ${className}`);
|
||||
|
||||
@@ -9831,6 +9831,75 @@ function assignedThisFieldType(lowerer: Lowerer, expr: ts.NewExpression): IrType
|
||||
return lowerer.currentClass?.fields.get(parent.left.name.text) ?? null;
|
||||
}
|
||||
|
||||
/** An array element read that construction narrows to a class value with a
|
||||
* check (`%new.callee`, which throws "is not a constructor"). */
|
||||
interface NarrowedConstructorRead {
|
||||
read: IrExpr;
|
||||
check: string;
|
||||
unionT: IrType & { kind: "union" };
|
||||
calleeT: IrType & { kind: "classval" };
|
||||
message: IrExpr;
|
||||
}
|
||||
|
||||
/** `new arr[i](args)` through a narrowed element read: an interned lifted
|
||||
* function takes the read and the completed arguments, which evaluate in
|
||||
* that order as call arguments, then runs the check and constructs. JS
|
||||
* evaluates the arguments before it checks that the callee constructs, and
|
||||
* the arguments stay in the one call, where a spread's argument pack lives
|
||||
* until every argument has read it. */
|
||||
function checkedElementConstruction(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
narrowed: NarrowedConstructorRead,
|
||||
args: IrExpr[],
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const type: IrType = { kind: "object", className: info.def.name };
|
||||
const key = `newChecked:${narrowed.check}:${info.def.name}:${args.map((a) => typeKey(a.type)).join(",")}`;
|
||||
let name = lowerer.valueHelpers.get(key);
|
||||
if (!name) {
|
||||
name = keyedHelperName(lowerer, "%new.checked", key);
|
||||
lowerer.valueHelpers.set(key, name);
|
||||
const params: IrParam[] = [
|
||||
{ localId: "value", name: "value", type: narrowed.unionT },
|
||||
{ localId: "message", name: "message", type: STRING },
|
||||
...args.map((arg, i) => ({ localId: `arg.${i}`, name: `arg${i}`, type: arg.type })),
|
||||
];
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params,
|
||||
returnType: type,
|
||||
locals: params.map((p) => ({ id: p.localId, name: p.name, type: p.type, mutable: false })),
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "newValue",
|
||||
callee: {
|
||||
kind: "call",
|
||||
callee: narrowed.check,
|
||||
args: [varRef("value", narrowed.unionT, loc), varRef("message", STRING, loc)],
|
||||
type: narrowed.calleeT,
|
||||
loc,
|
||||
},
|
||||
args: params.slice(2).map((p) => varRef(p.localId, p.type, loc)),
|
||||
type,
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
return {
|
||||
kind: "call",
|
||||
callee: name,
|
||||
args: [narrowed.read, narrowed.message, ...args],
|
||||
type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
/** Map, Set and the error classes construct in lowerNew's identifier
|
||||
* branch, which a module member alias of one (`new mod.M()`) also takes. */
|
||||
function isCollectionOrErrorClass(ref: BuiltinClassRef | null): boolean {
|
||||
@@ -11169,8 +11238,109 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
"constructing through a class value of an uninstantiated generic type (annotate the slot with a concrete instantiation — e.g. 'new (v: number) => Box<number>')",
|
||||
);
|
||||
}
|
||||
const callee = lowerer.lowerExpr(expr.expression);
|
||||
let callee = lowerer.lowerExpr(expr.expression);
|
||||
if (callee.type.kind === "dyn") return lowerCheckedConstructor(lowerer, expr, callee);
|
||||
/** The union-typed element read a narrowing check guards, and the
|
||||
* check (`%new.callee`) that yields the class value or throws. */
|
||||
let narrowedCallee: NarrowedConstructorRead | null = null;
|
||||
// An array element read (`new T[0]()` over `[Dog, Animal]`, whose
|
||||
// checker type is the common base) keeps the undefined an index past
|
||||
// the end reads: the class value is narrowed out with a check, which
|
||||
// throws a TypeError for undefined.
|
||||
if (callee.type.kind === "union") {
|
||||
const arms = lowerer.unions.get(callee.type.unionId)?.arms ?? [];
|
||||
const classArms = arms.filter((arm) => arm.kind === "classval");
|
||||
if (
|
||||
classArms.length === 1 &&
|
||||
classArms[0]!.kind === "classval" &&
|
||||
classArms[0]!.className === calleeT.className &&
|
||||
arms.every((arm) => arm.kind === "classval" || arm.kind === "undefinedT")
|
||||
) {
|
||||
const unionT = callee.type;
|
||||
const helper = lowerer.narrowedArmHelper(unionT.unionId, calleeT, callee.loc);
|
||||
if (helper) {
|
||||
const loc = callee.loc;
|
||||
const name = `%new.callee:${unionT.unionId}`;
|
||||
if (!lowerer.liftedFns.some((fn) => fn.name === name)) {
|
||||
const undefinedTag = lowerer.armTag(unionT.unionId, UNDEFINED_T);
|
||||
const value: IrExpr = { kind: "varRef", localId: "value", type: unionT, loc };
|
||||
lowerer.liftedFns.push({
|
||||
name,
|
||||
params: [
|
||||
{ localId: "value", name: "value", type: unionT },
|
||||
{ localId: "message", name: "message", type: STRING },
|
||||
],
|
||||
locals: [
|
||||
{ id: "value", name: "value", type: unionT, mutable: false },
|
||||
{ id: "message", name: "message", type: STRING, mutable: false },
|
||||
],
|
||||
returnType: calleeT,
|
||||
body: [
|
||||
...(undefinedTag >= 0
|
||||
? [
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "unionIsTag",
|
||||
unionId: unionT.unionId,
|
||||
tag: undefinedTag,
|
||||
negated: false,
|
||||
value,
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "throw",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "error.new",
|
||||
args: [{ kind: "varRef", localId: "message", type: STRING, loc }],
|
||||
type: { kind: "object", className: "%TypeError" },
|
||||
loc,
|
||||
},
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
} satisfies IrStmt,
|
||||
]
|
||||
: []),
|
||||
{
|
||||
kind: "return",
|
||||
value: lowerer.checkedNarrowCall(helper, value, calleeT, loc),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
});
|
||||
}
|
||||
// JavaScript evaluates the arguments before it checks that the
|
||||
// callee constructs, so the check runs at the construction
|
||||
// (checkedElementConstruction).
|
||||
narrowedCallee = {
|
||||
read: callee,
|
||||
check: name,
|
||||
unionT,
|
||||
calleeT,
|
||||
message: {
|
||||
kind: "strLit",
|
||||
value: `${expr.expression.getText()} is not a constructor`,
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
};
|
||||
callee = {
|
||||
kind: "call",
|
||||
callee: name,
|
||||
args: [callee, narrowedCallee.message],
|
||||
type: calleeT,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (callee.type.kind !== "classval")
|
||||
lowerer.badType(expr.expression, lowerer.typeOf(expr.expression));
|
||||
lowerer.noteEdge(classValueConstructor(info.def).fn);
|
||||
@@ -11210,13 +11380,15 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
const built = constructedFromValue(
|
||||
lowerer,
|
||||
info,
|
||||
{
|
||||
kind: "newValue",
|
||||
callee,
|
||||
args,
|
||||
type: { kind: "object", className: info.def.name },
|
||||
loc,
|
||||
},
|
||||
narrowedCallee !== null
|
||||
? checkedElementConstruction(lowerer, info, narrowedCallee, args, loc)
|
||||
: {
|
||||
kind: "newValue",
|
||||
callee,
|
||||
args,
|
||||
type: { kind: "object", className: info.def.name },
|
||||
loc,
|
||||
},
|
||||
expr,
|
||||
);
|
||||
// A runtime class slot widened from a union (`[Readable, Writable]`'s
|
||||
|
||||
@@ -1623,6 +1623,28 @@ export function classValueConstructor(def: { name: string; runtime?: true }): {
|
||||
: { fn: `%${def.name}.constructor`, firstArg: 1 };
|
||||
}
|
||||
|
||||
/** Whether construction through a class value typed as `sup` may call
|
||||
* `sub`'s construct thunk: both class-value constructors exist and take the
|
||||
* same argument types after the receiver. The classval upcast rule and the
|
||||
* checked class-value boundary both rest on it. */
|
||||
export function classValueAbiEquals(
|
||||
sub: { name: string; runtime?: true },
|
||||
sup: { name: string; runtime?: true },
|
||||
functions: ReadonlyMap<string, IrFunction>,
|
||||
): boolean {
|
||||
const subValueCtor = classValueConstructor(sub);
|
||||
const supValueCtor = classValueConstructor(sup);
|
||||
const subCtor = functions.get(subValueCtor.fn);
|
||||
const supCtor = functions.get(supValueCtor.fn);
|
||||
if (!subCtor || !supCtor) return false;
|
||||
const subArgs = subCtor.params.slice(subValueCtor.firstArg);
|
||||
const supArgs = supCtor.params.slice(supValueCtor.firstArg);
|
||||
return (
|
||||
subArgs.length === supArgs.length &&
|
||||
subArgs.every((p, i) => typeEquals(p.type, supArgs[i]!.type))
|
||||
);
|
||||
}
|
||||
|
||||
/** The JavaScript `.name` of a class: its jsName, or the runtime-provided
|
||||
* class's own name (`%TypeError` is TypeError). */
|
||||
export function classJsName(def: { name: string; runtime?: true; jsName?: string }): string {
|
||||
|
||||
@@ -57,7 +57,12 @@ import {
|
||||
maybePromiseUnionArmsOk,
|
||||
VOID,
|
||||
} from "./ir.js";
|
||||
import { DYN_CLASS_PROPERTIES, callSiteReturnType, classValueConstructor } from "./ir.js";
|
||||
import {
|
||||
DYN_CLASS_PROPERTIES,
|
||||
callSiteReturnType,
|
||||
classValueAbiEquals,
|
||||
classValueConstructor,
|
||||
} from "./ir.js";
|
||||
import { unionWideningTags } from "./analysis.js";
|
||||
import { alwaysReturns } from "./control-flow.js";
|
||||
import { dynamicOperationOf, functionTier } from "./tiers.js";
|
||||
@@ -2993,19 +2998,13 @@ function validateFunction(
|
||||
}
|
||||
const subClass = classes.get(sub);
|
||||
const supClass = classes.get(sup);
|
||||
const subValueCtor = subClass ? classValueConstructor(subClass) : null;
|
||||
const supValueCtor = supClass ? classValueConstructor(supClass) : null;
|
||||
const subCtor = subValueCtor ? functions.get(subValueCtor.fn) : undefined;
|
||||
const supCtor = supValueCtor ? functions.get(supValueCtor.fn) : undefined;
|
||||
if (!subCtor || !supCtor || !subValueCtor || !supValueCtor) {
|
||||
const subCtor = subClass ? functions.get(classValueConstructor(subClass).fn) : undefined;
|
||||
const supCtor = supClass ? functions.get(classValueConstructor(supClass).fn) : undefined;
|
||||
if (!subClass || !supClass || !subCtor || !supCtor) {
|
||||
err(`classval upcast: "${sub}"/"${sup}" lack constructor functions`, e.loc);
|
||||
break;
|
||||
}
|
||||
const subArgs = subCtor.params.slice(subValueCtor.firstArg);
|
||||
const supArgs = supCtor.params.slice(supValueCtor.firstArg);
|
||||
const abiEqual =
|
||||
subArgs.length === supArgs.length &&
|
||||
subArgs.every((p, i) => typeEquals(p.type, supArgs[i]!.type));
|
||||
const abiEqual = classValueAbiEquals(subClass, supClass, functions);
|
||||
if (!abiEqual) {
|
||||
err(`classval upcast: "${sub}" and "${sup}" constructor ABIs differ`, e.loc);
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6937-class-value-arrays.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6938-class-value-arrays.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Arrays of class values that mix a base class and its subclasses: the
|
||||
// elements construct, compare and name like the classes themselves.
|
||||
class Animal {
|
||||
speak() {
|
||||
return "...";
|
||||
}
|
||||
}
|
||||
class Dog extends Animal {
|
||||
speak() {
|
||||
return "woof";
|
||||
}
|
||||
}
|
||||
class Cat extends Animal {
|
||||
speak() {
|
||||
return "meow";
|
||||
}
|
||||
}
|
||||
const kinds = [Dog, Cat, Animal];
|
||||
console.log(kinds.length, kinds.map((C) => C.name).join());
|
||||
console.log(new kinds[0]().speak(), new kinds[1]().speak(), new kinds[2]().speak());
|
||||
console.log(kinds[0] === Dog, kinds.indexOf(Cat), kinds.includes(Animal));
|
||||
const made = kinds.map((C) => new C());
|
||||
console.log(made.map((a) => a.speak()).join(), made[0] instanceof Animal, made[1] instanceof Dog);
|
||||
try {
|
||||
new kinds[5]();
|
||||
} catch (e) {
|
||||
console.log(e.name, e.message);
|
||||
}
|
||||
const byName = { dog: Dog, cat: Cat };
|
||||
console.log(new byName.dog().speak(), new byName["cat"]().speak());
|
||||
// The arguments are evaluated before the missing element throws.
|
||||
class Point {
|
||||
constructor(x, y) {
|
||||
this.sum = x + y;
|
||||
}
|
||||
}
|
||||
class Point3 extends Point {}
|
||||
const points = [Point3, Point];
|
||||
const evaluated = [];
|
||||
const arg = (v) => (evaluated.push(v), v);
|
||||
console.log(new points[0](arg(1), arg(2)).sum, evaluated.join());
|
||||
try {
|
||||
new points[2](arg(3), arg(4));
|
||||
} catch (e) {
|
||||
console.log(e.name, e.message, evaluated.join());
|
||||
}
|
||||
// Spread arguments too, through a valid index and past the end.
|
||||
console.log(new points[1](...[5, 6]).sum);
|
||||
try {
|
||||
new points[3](...[arg(7), arg(8)]);
|
||||
} catch (e) {
|
||||
console.log(e.name, e.message, evaluated.join());
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Arrays of class values that mix a base class and its subclasses, with the
|
||||
// element type the checker infers (the base class): elements construct and
|
||||
// dispatch to the subclass, and an index past the end throws Node's TypeError.
|
||||
class Shape {
|
||||
area(): number {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
class Square extends Shape {
|
||||
area(): number {
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
class Circle extends Shape {
|
||||
area(): number {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
const shapes = [Square, Circle, Shape];
|
||||
console.log(shapes.length, new shapes[0]().area(), new shapes[1]().area(), new shapes[2]().area());
|
||||
let total = 0;
|
||||
for (const S of shapes) total += new S().area();
|
||||
console.log(total, shapes[1] === Circle, shapes.map((S) => S.name).join());
|
||||
try {
|
||||
new shapes[3]();
|
||||
} catch (e) {
|
||||
console.log((e as Error).name, (e as Error).message);
|
||||
}
|
||||
// A class value checked into a `typeof Shape` slot: a subclass whose
|
||||
// constructor takes the same arguments constructs, and one whose constructor
|
||||
// takes an argument the slot does not pass throws a TypeError (Node's from
|
||||
// reading the missing argument, the compiled program's from the check).
|
||||
class Sized extends Shape {
|
||||
label: string;
|
||||
constructor(side: number) {
|
||||
super();
|
||||
this.label = side.toFixed(1);
|
||||
}
|
||||
}
|
||||
const values: any[] = [Square, Sized, Shape];
|
||||
for (const value of values) {
|
||||
try {
|
||||
const S: typeof Shape = value;
|
||||
console.log(new S().area());
|
||||
} catch (e) {
|
||||
console.log((e as Error).name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4211, "san": 6383 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2399, "san": 4080 }
|
||||
Reference in New Issue
Block a user