mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Read a class branch's own-property bag from its receiver
This commit is contained in:
@@ -475,3 +475,85 @@ test("normalizes base capsules before dispatching each derived member once", ()
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test("class branches read their exact class's bag, also for classes with subclasses", () => {
|
||||
const { lowerer, liftedFns } = context();
|
||||
const base = {
|
||||
def: { name: "Base", fields: [{ name: "value", type: F64 }], tracksOwnFields: true },
|
||||
fields: new Map([["value", F64]]),
|
||||
methods: new Map(),
|
||||
base: null,
|
||||
subclasses: [],
|
||||
} as unknown as ClassInfo;
|
||||
const child = {
|
||||
def: {
|
||||
name: "Child",
|
||||
base: "Base",
|
||||
fields: [{ name: "value", type: F64 }],
|
||||
tracksOwnFields: true,
|
||||
},
|
||||
fields: new Map([["value", F64]]),
|
||||
methods: new Map(),
|
||||
base,
|
||||
subclasses: [],
|
||||
} as unknown as ClassInfo;
|
||||
base.subclasses.push(child);
|
||||
lowerer.classes.set("Base", base);
|
||||
lowerer.classes.set("Child", child);
|
||||
lowerer.classCanBeConstructed = () => true;
|
||||
lowerer.isSubclassOf = (name, parent) => name === "Child" && parent === "Base";
|
||||
lowerer.coerceToExpected = (value, type) =>
|
||||
type.kind === "dyn" ? { kind: "dynFrom", value, type, loc } : value;
|
||||
const boxes = fn(
|
||||
"boxes",
|
||||
[base, child].map((info) =>
|
||||
statement({
|
||||
kind: "dynFrom",
|
||||
value: {
|
||||
kind: "varRef",
|
||||
localId: info.def.name,
|
||||
type: { kind: "object", className: info.def.name },
|
||||
loc,
|
||||
},
|
||||
type: DYN,
|
||||
loc,
|
||||
}),
|
||||
),
|
||||
);
|
||||
const read = fn("read", [
|
||||
statement({
|
||||
kind: "dynKeyGet",
|
||||
value: variable("value"),
|
||||
key: literal("value"),
|
||||
type: DYN,
|
||||
loc,
|
||||
}),
|
||||
]);
|
||||
const dispatch = new ClassDynamicDispatch();
|
||||
for (let pass = 0; pass < 4 && dispatch.process(lowerer, [boxes, read, ...liftedFns]); pass++);
|
||||
dispatch.finalize(lowerer);
|
||||
const property = liftedFns.find((helper) => helper.name.startsWith("%dyn.class.property."))!;
|
||||
// The class branches; the fallback for other receivers keeps the
|
||||
// dispatch over every class.
|
||||
const branches = property.body.filter(
|
||||
(statement) =>
|
||||
statement.kind === "if" &&
|
||||
statement.cond.kind === "libCall" &&
|
||||
statement.cond.fn === "dyn.typedRefIs",
|
||||
);
|
||||
expect(branches).toHaveLength(2);
|
||||
const text = JSON.stringify(branches);
|
||||
expect(text).toContain('"callee":"%class.branchPropertiesOrEmpty:Base"');
|
||||
expect(text).toContain('"callee":"%class.branchPropertiesOrEmpty:Child"');
|
||||
expect(text).not.toContain('"callee":"%dyn.class.readPropertiesOrEmpty"');
|
||||
const bag = liftedFns.find((helper) => helper.name === "%class.branchPropertiesOrEmpty:Base")!;
|
||||
// The bag is the receiver's own field; an absent bag reads as a fresh,
|
||||
// unstored empty object.
|
||||
expect(bag.params[0]!.type).toEqual({ kind: "object", className: "Base" });
|
||||
expect(JSON.stringify(bag.body)).toContain('"className":"Base","field":"%dynProperties"');
|
||||
expect(JSON.stringify(bag.body)).not.toContain('"kind":"fieldSet"');
|
||||
expect(bag.body[0]).toMatchObject({
|
||||
kind: "if",
|
||||
then: [{ kind: "return", value: { kind: "dynObjLit", fields: [] } }],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -386,6 +386,26 @@ function readOnlyBagBody(body: IrStmt[], orEmpty = false): IrStmt[] {
|
||||
return body;
|
||||
}
|
||||
|
||||
/** `value` with the class branch's checked receiver (`p.0` as a dynamic
|
||||
* value, possibly cast to the branch's class) replaced by `receiver`, the
|
||||
* typed receiver of a per-class helper. */
|
||||
function withTypedReceiver(value: IrExpr, receiver: IrExpr): IrExpr {
|
||||
const checked = (e: IrExpr): boolean =>
|
||||
e.kind === "dynCheck" && e.value.kind === "varRef" && e.value.localId === "p.0";
|
||||
return transformExpr(value, {
|
||||
stmt: (stmt) => stmt,
|
||||
expr: (expr) =>
|
||||
(expr.kind === "downcast" &&
|
||||
checked(expr.value) &&
|
||||
expr.type.kind === "object" &&
|
||||
receiver.type.kind === "object" &&
|
||||
expr.type.className === receiver.type.className) ||
|
||||
checked(expr)
|
||||
? receiver
|
||||
: expr,
|
||||
});
|
||||
}
|
||||
|
||||
export function classPropertiesHelper(lowerer: Lowerer, loc: SrcLoc, readOnly = false): IrFunction {
|
||||
const name = readOnly ? READ_PROPERTIES : "%dyn.class.properties";
|
||||
const existing = lowerer.liftedFnNamed(name);
|
||||
@@ -728,7 +748,11 @@ export class ClassDynamicDispatch {
|
||||
private readonly readPropertyBags: {
|
||||
helper: IrFunction;
|
||||
initialize: Extract<IrStmt, { kind: "fieldSet" }>;
|
||||
orEmpty?: boolean;
|
||||
}[] = [];
|
||||
/** Read-only bags of exactly one class (exactPropertyBag), by class and
|
||||
* whether an absent bag reads as a fresh empty object. */
|
||||
private readonly exactPropertyBags = new Map<string, IrFunction>();
|
||||
private readonly generated = new Set<IrFunction>();
|
||||
/** Protocol dispatch helpers and callee reads whose receivers normalize. */
|
||||
private readonly protocols: IrFunction[] = [];
|
||||
@@ -2557,7 +2581,6 @@ export class ClassDynamicDispatch {
|
||||
if (dispatch.classes.has(plan.key) || !matching!.has(info)) continue;
|
||||
dispatch.classes.add(plan.key);
|
||||
const loc = dispatch.source.loc;
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
const { receiver, condition } = this.propertyReceiver(plan, loc);
|
||||
if (isClassCallback(lowerer, info, dispatch.source.method)) {
|
||||
const prototypeMethod = observedPrototypeMethod(
|
||||
@@ -2567,7 +2590,7 @@ export class ClassDynamicDispatch {
|
||||
loc,
|
||||
receiver,
|
||||
);
|
||||
const bag = this.readPropertyBagOrEmpty(lowerer, value, loc);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc);
|
||||
const methodKey: IrExpr = {
|
||||
kind: "strLit",
|
||||
value: dispatch.source.method,
|
||||
@@ -2825,10 +2848,19 @@ export class ClassDynamicDispatch {
|
||||
dispatch.classes.add(plan.key);
|
||||
const loc = dispatch.fn.loc;
|
||||
const { receiver, condition } = this.propertyReceiver(plan, loc);
|
||||
// The branch checks its receiver once: each use of the checked
|
||||
// receiver would repeat the check.
|
||||
const local = `receiver.${dispatch.fn.locals.length}`;
|
||||
dispatch.fn.locals.push({
|
||||
id: local,
|
||||
name: "receiver",
|
||||
type: receiver.type,
|
||||
mutable: false,
|
||||
});
|
||||
const before = lowerer.diags.length;
|
||||
let branch: IrStmt[];
|
||||
try {
|
||||
branch = this.propertyBody(lowerer, dispatch, info, receiver);
|
||||
branch = this.propertyBody(lowerer, dispatch, info, varRef(local, receiver.type, loc));
|
||||
} catch (error) {
|
||||
if (!(error instanceof PoisonError) || !info.decl) throw error;
|
||||
const fence = lowerer.deferToRuntimeFence(before, info.decl, { kind: "statement" });
|
||||
@@ -2838,7 +2870,7 @@ export class ClassDynamicDispatch {
|
||||
dispatch.fn.body.unshift({
|
||||
kind: "if",
|
||||
cond: condition,
|
||||
then: branch,
|
||||
then: [{ kind: "varDecl", localId: local, init: receiver, loc }, ...branch],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
@@ -2938,11 +2970,7 @@ export class ClassDynamicDispatch {
|
||||
!info.fields.has(name) && observedPrototypeMethod(lowerer, info, name, loc, receiver);
|
||||
if (info.fields.has(name) || (dispatch.probe === "in" && !prototypeMethod)) {
|
||||
if (info.fields.has(name) && info.def.tracksOwnFields) {
|
||||
const bag = this.readPropertyBagOrEmpty(
|
||||
lowerer,
|
||||
lowerer.coerceToExpected(receiver, DYN),
|
||||
loc,
|
||||
);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc);
|
||||
const symbol = symbolMemberKey(lowerer, info, name, loc);
|
||||
const fn =
|
||||
dispatch.probe === "in"
|
||||
@@ -2968,11 +2996,7 @@ export class ClassDynamicDispatch {
|
||||
{ kind: "return", value: { kind: "boolLit", value: true, type: BOOL, loc }, loc },
|
||||
];
|
||||
}
|
||||
const bag = this.readPropertyBagOrEmpty(
|
||||
lowerer,
|
||||
lowerer.coerceToExpected(receiver, DYN),
|
||||
loc,
|
||||
);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc);
|
||||
const symbol = name.startsWith("sym:") || name.startsWith("%symbol:");
|
||||
const fn =
|
||||
dispatch.probe === "in"
|
||||
@@ -3042,9 +3066,8 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
const branches: IrStmt[] = [];
|
||||
if (!dispatch.write && !dispatch.reflect && !info.def.tracksOwnFields) {
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
const key = varRef("p.key", DYN, loc);
|
||||
const bag = this.readPropertyBagOrEmpty(lowerer, value, loc);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc);
|
||||
branches.push({
|
||||
kind: "if",
|
||||
cond: {
|
||||
@@ -4070,6 +4093,98 @@ export class ClassDynamicDispatch {
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** The read-only bag a class branch reads for its receiver: the
|
||||
* receiver's own field when the branch selected the receiver's exact
|
||||
* class, otherwise the dispatch over every class (readPropertyBag, or
|
||||
* readPropertyBagOrEmpty with `orEmpty`). */
|
||||
private branchPropertyBag(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
receiver: IrExpr,
|
||||
orEmpty: boolean,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const helper = this.exactPropertyBag(lowerer, info, receiver, orEmpty, loc);
|
||||
if (helper) return { kind: "call", callee: helper.name, args: [receiver], type: DYN, loc };
|
||||
return orEmpty
|
||||
? this.readPropertyBagOrEmpty(lowerer, lowerer.coerceToExpected(receiver, DYN), loc)
|
||||
: this.readPropertyBag(lowerer, receiver, loc);
|
||||
}
|
||||
|
||||
/** `%class.branchProperties[OrEmpty]:<class>(receiver)`: the read-only bag of
|
||||
* an instance of exactly `info`. A class branch of the dispatch helpers
|
||||
* selects a canonical receiver (normalizeDispatches gives an instance its
|
||||
* actual class's capsule), so its branch for `info` sees only instances
|
||||
* of `info`; instances of its subclasses take their own branches. Such a
|
||||
* receiver needs neither the dispatch over every class nor a boxed copy of
|
||||
* itself. Null
|
||||
* when `receiver` is not typed as `info`, when a runtime or builtin class
|
||||
* in the family keeps its own property contract, or before the class has
|
||||
* a bag. */
|
||||
private exactPropertyBag(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
receiver: IrExpr,
|
||||
orEmpty: boolean,
|
||||
loc: SrcLoc,
|
||||
): IrFunction | null {
|
||||
const type = receiver.type;
|
||||
if (type.kind !== "object" || type.className !== info.def.name) return null;
|
||||
for (let current: ClassInfo | null = info; current; current = current.base)
|
||||
if (
|
||||
current.def.runtime ||
|
||||
current.builtinEmitter ||
|
||||
current.builtinStream ||
|
||||
current.builtinError
|
||||
)
|
||||
return null;
|
||||
const key = `${type.className}:${orEmpty}`;
|
||||
const existing = this.exactPropertyBags.get(key);
|
||||
if (existing) return existing;
|
||||
const initializers = this.bagInitializers.get(type.className);
|
||||
if (!initializers) return null;
|
||||
const self = varRef("p.0", type, loc);
|
||||
const bag: IrExpr = {
|
||||
kind: "fieldGet",
|
||||
obj: self,
|
||||
className: type.className,
|
||||
field: PROPERTY_BAG,
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
const initialize: Extract<IrStmt, { kind: "fieldSet" }> = {
|
||||
...initializers[0]!,
|
||||
obj: self,
|
||||
value: withTypedReceiver(initializers[0]!.value, self),
|
||||
loc,
|
||||
};
|
||||
const helper: IrFunction = {
|
||||
name: `%class.branchProperties${orEmpty ? "OrEmpty" : ""}:${type.className}`,
|
||||
params: [{ localId: "p.0", name: "value", type }],
|
||||
returnType: DYN,
|
||||
locals: [{ id: "p.0", name: "value", type, mutable: false }],
|
||||
loc,
|
||||
body: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "undefined", value: bag, type: BOOL, loc },
|
||||
then: [initialize],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: bag, loc },
|
||||
],
|
||||
};
|
||||
// Initializers gain their prototype as subclasses become constructible;
|
||||
// finalize then turns the lazy store into the read's result.
|
||||
initializers.push(initialize);
|
||||
this.exactPropertyBags.set(key, helper);
|
||||
this.readPropertyBags.push({ helper, initialize, orEmpty });
|
||||
this.generated.add(helper);
|
||||
lowerer.liftedFns.push(helper);
|
||||
return helper;
|
||||
}
|
||||
|
||||
/** The prototype a new property bag of `current` (or of the subclass the
|
||||
* receiver is) starts from, or null when `current` has no prototype data.
|
||||
* A class with constructible subclasses that have prototype data selects
|
||||
@@ -4149,9 +4264,9 @@ export class ClassDynamicDispatch {
|
||||
* prototype-backed bag is produced for this read only. A published object
|
||||
* cannot take the store, and Node's reads never write. */
|
||||
finalize(lowerer?: Lowerer): void {
|
||||
for (const { helper, initialize } of this.readPropertyBags) {
|
||||
for (const { helper, initialize, orEmpty } of this.readPropertyBags) {
|
||||
const absent = helper.body[0];
|
||||
if (isEmptyBag(initialize.value)) helper.body.shift();
|
||||
if (isEmptyBag(initialize.value) && !orEmpty) helper.body.shift();
|
||||
else if (absent?.kind === "if")
|
||||
absent.then = [{ kind: "return", value: initialize.value, loc: initialize.loc }];
|
||||
}
|
||||
@@ -4733,7 +4848,7 @@ export class ClassDynamicDispatch {
|
||||
cond: this.readBagHas(
|
||||
lowerer,
|
||||
"dyn.hasKey",
|
||||
this.readPropertyBag(lowerer, receiver, loc),
|
||||
this.branchPropertyBag(lowerer, info, receiver, false, loc),
|
||||
key,
|
||||
loc,
|
||||
),
|
||||
@@ -4748,7 +4863,7 @@ export class ClassDynamicDispatch {
|
||||
{ kind: "return", value: null, loc },
|
||||
];
|
||||
}
|
||||
const bag = this.readPropertyBag(lowerer, receiver, loc);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc);
|
||||
return [
|
||||
{
|
||||
kind: "return",
|
||||
@@ -4816,7 +4931,7 @@ export class ClassDynamicDispatch {
|
||||
) {
|
||||
this.constructors.set(info.def.name, info);
|
||||
const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN);
|
||||
const bag = this.readPropertyBag(lowerer, receiver, loc);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc);
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
return [
|
||||
{
|
||||
@@ -4894,7 +5009,7 @@ export class ClassDynamicDispatch {
|
||||
if (symbol || (isClassOwnEnumerableFieldName(name) && !name.startsWith("sym:"))) {
|
||||
const prototypeMethod = observedPrototypeMethod(lowerer, info, name, loc, receiver);
|
||||
// Selecting the method must not give the object own storage.
|
||||
const bag = this.readPropertyBag(lowerer, receiver, loc);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc);
|
||||
const key: IrExpr = symbol ?? { kind: "strLit", value: name, type: STRING, loc };
|
||||
body.push({
|
||||
kind: "if",
|
||||
@@ -4962,7 +5077,7 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
if (!field && !accessor) {
|
||||
const instance = lowerer.coerceToExpected(receiver, DYN);
|
||||
const bag = this.readPropertyBagOrEmpty(lowerer, instance, loc);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc);
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
const own: IrExpr = {
|
||||
kind: "libCall",
|
||||
@@ -5009,11 +5124,7 @@ export class ClassDynamicDispatch {
|
||||
(t.kind === "union" && (getUnion(t.unionId)?.arms.some(mutable) ?? false));
|
||||
if (boxed.kind === "dynFrom" && mutable(boxed.value.type)) boxed.liveRef = true;
|
||||
if (accessor) {
|
||||
const bag = this.readPropertyBagOrEmpty(
|
||||
lowerer,
|
||||
lowerer.coerceToExpected(receiver, DYN),
|
||||
loc,
|
||||
);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc);
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
return [
|
||||
{
|
||||
@@ -5040,7 +5151,7 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
if (field && info.def.tracksOwnFields) {
|
||||
const instance = lowerer.coerceToExpected(receiver, DYN);
|
||||
const bag = this.readPropertyBagOrEmpty(lowerer, instance, loc);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, true, loc);
|
||||
const symbol = symbolMemberKey(lowerer, info, name, loc);
|
||||
const key: IrExpr = symbol ?? { kind: "strLit", value: name, type: STRING, loc };
|
||||
return [
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6943-js-class-branch-bags.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Untyped reads of class instances in a hierarchy: own properties added at
|
||||
// runtime shadow methods and accessors, prototype data answers while an
|
||||
// instance has no own properties, and computed keys and key probes see the
|
||||
// same members, for the base class and for each subclass.
|
||||
|
||||
class Shape {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.sides = 0;
|
||||
}
|
||||
area() {
|
||||
return 0;
|
||||
}
|
||||
get label() {
|
||||
return `${this.name}/${this.sides}`;
|
||||
}
|
||||
}
|
||||
class Square extends Shape {
|
||||
constructor(size) {
|
||||
super("square");
|
||||
this.sides = 4;
|
||||
this.size = size;
|
||||
}
|
||||
area() {
|
||||
return this.size * this.size;
|
||||
}
|
||||
}
|
||||
class Tile extends Square {
|
||||
constructor(size, color) {
|
||||
super(size);
|
||||
this.color = color;
|
||||
}
|
||||
}
|
||||
Shape.prototype.kind = "shape";
|
||||
Square.prototype.corner = "right";
|
||||
|
||||
function read(value, key) {
|
||||
return value[key];
|
||||
}
|
||||
function describe(value) {
|
||||
const parts = [];
|
||||
for (const key of ["name", "sides", "size", "color", "kind", "corner", "label", "missing"])
|
||||
parts.push(`${key}=${String(read(value, key))}`);
|
||||
parts.push(`in:${"area" in value},${"kind" in value},${"extra" in value}`);
|
||||
parts.push(`own:${Object.hasOwn(value, "kind")},${Object.hasOwn(value, "size")}`);
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
const shapes = [new Shape("dot"), new Square(3), new Tile(2, "red")];
|
||||
for (const shape of shapes) console.log(describe(shape), shape.area(), shape.label);
|
||||
|
||||
const untyped = shapes.map((shape) => /** @type {any} */ (shape));
|
||||
untyped[1].area = () => -1;
|
||||
untyped[2].kind = "tile";
|
||||
untyped[2].extra = 7;
|
||||
try {
|
||||
untyped[0].label = "plain";
|
||||
} catch (error) {
|
||||
console.log(error.name);
|
||||
}
|
||||
Object.defineProperty(untyped[0], "label", { value: "plain", enumerable: true });
|
||||
for (const shape of untyped) {
|
||||
console.log(describe(shape), typeof shape.area, shape.area(), shape.kind, shape.extra);
|
||||
console.log(read(shape, "area") === Shape.prototype.area, read(shape, "label"));
|
||||
}
|
||||
const area = untyped[2].area;
|
||||
console.log(area.call(new Tile(5, "blue")), Object.keys(untyped[2]).join(","));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 4400, "san": 4900 }
|
||||
Reference in New Issue
Block a user