mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Evaluate constructor arguments before the class-value check
`new kinds[i](args)` over an array of class values read the element and narrowed it to the class value before the arguments ran, so an index past the end threw "is not a constructor" without evaluating them. JavaScript evaluates the arguments first: the construction is now a call of a lifted helper (checkedElementConstruction) that takes the element read and the arguments, in that order, then runs the check and constructs. Keeping the arguments in one call also keeps a spread's argument pack alive until every argument has read it. A checked class-value boundary accepted every subclass's class value into a base class's slot, so construction through the slot could call a subclass constructor that takes different arguments with the base constructor's. It now accepts only the subclasses whose class-value constructor takes the same arguments (classValueAbiEquals, the rule the classval upcast validator already applied); any other subclass fails the check with a TypeError.
This commit is contained in:
@@ -174,6 +174,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[];
|
||||
}
|
||||
@@ -1808,8 +1812,10 @@ 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.
|
||||
const subclasses = host.classSubtypes(t.className).filter((name) => name !== t.className);
|
||||
// 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) {
|
||||
|
||||
@@ -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";
|
||||
@@ -1895,14 +1896,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),
|
||||
@@ -3965,6 +3966,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}`);
|
||||
|
||||
@@ -9782,6 +9782,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 {
|
||||
@@ -11045,6 +11114,9 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
}
|
||||
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
|
||||
@@ -11118,18 +11190,25 @@ export function lowerNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr {
|
||||
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,
|
||||
{
|
||||
kind: "strLit",
|
||||
value: `${expr.expression.getText()} is not a constructor`,
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
],
|
||||
args: [callee, narrowedCallee.message],
|
||||
type: calleeT,
|
||||
loc,
|
||||
};
|
||||
@@ -11175,13 +11254,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
|
||||
|
||||
@@ -1630,6 +1630,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 {
|
||||
|
||||
@@ -58,7 +58,12 @@ import {
|
||||
unionContainerArmsOk,
|
||||
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";
|
||||
@@ -2997,19 +3002,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);
|
||||
}
|
||||
|
||||
@@ -28,3 +28,26 @@ try {
|
||||
}
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -26,3 +26,23 @@ try {
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user