Create untyped class instances' property-bag entries without the class tables (#1961)

This commit is contained in:
Chris Tate
2026-10-23 02:18:39 -05:00
committed by GitHub
11 changed files with 482 additions and 74 deletions
@@ -5,6 +5,8 @@
export function preservesDynTest(test: string): boolean {
switch (test) {
case "truthy":
case "classCapsule":
case "funcKind":
case "nullish":
case "promise":
case "buffer":
@@ -719,6 +719,10 @@ export function emitDynamicExpr(
B.line(
`${test} = call zeroext i1 @scr_dyn_typed_array_is(ptr ${d.name}, i32 ${BYTES_ELEM_NUM[e.bytesElem ?? "u8"]})`,
);
} else if (e.test === "classCapsule") {
host.declare(`declare zeroext i1 @scr_dyn_is_class_capsule(ptr)`);
test = B.tmp();
B.line(`${test} = call zeroext i1 @scr_dyn_is_class_capsule(ptr ${d.name})`);
} else if (e.test === "truthy") {
host.declare(`declare zeroext i1 @scr_dyn_truthy(ptr)`);
test = B.tmp();
@@ -839,6 +843,7 @@ export function emitDynamicExpr(
undefined: DYN_KIND.UNDEF,
null: DYN_KIND.NULL,
bytes: DYN_KIND.BYTES,
funcKind: DYN_KIND.FUNC,
};
test = oneOf([kindOf[e.test]!]);
}
@@ -1553,6 +1553,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.assignPrototype": "scr_dyn_assign_prototype",
"dyn.definePrototypeProps": "scr_dyn_define_prototype_props",
"dyn.bagGet": "scr_dyn_bag_get",
"dyn.bagCreateField": "scr_dyn_bag_create_field",
"dyn.bagSet": "scr_dyn_bag_set",
"dyn.objCreate": "scr_dyn_obj_create",
"dyn.nameClassPrototype": "scr_dyn_name_class_prototype",
@@ -458,9 +458,11 @@ test("normalizes base capsules before dispatching each derived member once", ()
});
expect(dispatch.body.filter((statement) => statement.kind === "if")).toHaveLength(2);
const normalize = liftedFns.find((helper) => helper.name === "%dyn.class.normalize")!;
// The capsule branch unboxes its receiver once, then tests each subclass.
expect(normalize.body[0]).toMatchObject({
kind: "if",
then: [
{ kind: "varDecl", init: { kind: "dynCheck", type: { kind: "object", className: "Base" } } },
{
kind: "if",
then: [
@@ -394,6 +394,40 @@ export function classInstanceOf(
};
}
/** `value` is the parameter itself, possibly through js.fromDyn. */
function returnsParam(value: IrExpr, param: string): boolean {
if (value.kind === "varRef") return value.localId === param;
return (
value.kind === "libCall" &&
value.fn === "js.fromDyn" &&
value.args.length === 1 &&
value.args[0]!.kind === "varRef" &&
value.args[0]!.localId === param
);
}
/** The kind a class-table branch requires of `param`: a capsule-key test
* (alone or guarding a conjunction) needs a capsule, a class-key test a
* function value; null for any other condition. */
function branchKind(cond: IrExpr, param: string): "classCapsule" | "funcKind" | null {
if (
cond.kind === "libCall" &&
(cond.fn === "dyn.typedRefIs" ||
cond.fn === "dyn.typedRefKeyIs" ||
cond.fn === "dyn.classIs") &&
cond.args[0]?.kind === "varRef" &&
cond.args[0].localId === param
) {
if (cond.fn === "dyn.classIs") return "funcKind";
const key = cond.args[1];
return key?.kind === "strLit" && key.value.startsWith("object:") ? "classCapsule" : null;
}
if (cond.kind === "ternary" && cond.else_.kind === "boolLit" && cond.else_.value === false)
return branchKind(cond.cond, param);
if (cond.kind === "logical" && cond.op === "&&") return branchKind(cond.left, param);
return null;
}
const READ_PROPERTIES = "%dyn.class.readProperties";
const READ_PROPERTIES_OR_EMPTY = "%dyn.class.readPropertiesOrEmpty";
@@ -685,16 +719,154 @@ export function trackClassFieldCreation(lowerer: Lowerer, functions: IrFunction[
if (![...lowerer.classes.values()].some((info) => info.def.tracksOwnFields)) return;
const loc = functions[0]!.loc;
const name = "%dyn.class.fieldCreated";
const value = varRef("value", DYN, loc),
key = varRef("key", DYN, loc);
const bag = varRef("bag", DYN, loc);
const created = (receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrStmt => ({
const key = varRef("key", DYN, loc);
// One helper per class: the instance's bag is its own hidden field, so a
// creation reads it directly instead of boxing the instance and testing
// it against every class's capsule (%dyn.class.properties), which runs
// only to create the bag, once per instance.
const perClass = new Map<string, string>();
const helperFor = (className: string): string => {
const known = perClass.get(className);
if (known !== undefined) return known;
const helperName = `${name}:${className}`;
perClass.set(className, helperName);
const self = varRef("this", { kind: "object", className }, loc);
const bagRef = varRef("bag", DYN, loc);
functions.push({
name: helperName,
params: [
{ localId: "this", name: "this", type: { kind: "object", className } },
{ localId: "key", name: "key", type: DYN },
],
returnType: VOID,
locals: [
{ id: "this", name: "this", type: { kind: "object", className }, mutable: false },
{ id: "key", name: "key", type: DYN, mutable: false },
{ id: "bag", name: "bag", type: DYN, mutable: true },
],
body: [
{
kind: "varDecl",
localId: "bag",
init: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc },
loc,
},
{
kind: "if",
cond: { kind: "dynTest", test: "undefined", value: bagRef, type: BOOL, loc },
then: initializeBag(lowerer, className, self, loc),
else_: null,
loc,
},
...defineIfAbsent(lowerer, bagRef, key, loc),
],
loc,
});
return helperName;
};
// String-keyed fields take the runtime's direct insert for an ordinary
// bag; the general path stays for any other bag and for symbol keys.
const stringHelperFor = (className: string): string => {
const known = perClass.get(`str:${className}`);
if (known !== undefined) return known;
const general = helperFor(className);
const helperName = `${name}.string:${className}`;
perClass.set(`str:${className}`, helperName);
const type: IrType = { kind: "object", className };
const self = varRef("this", type, loc);
const bagRef = varRef("bag", DYN, loc);
const field = varRef("field", STRING, loc);
functions.push({
name: helperName,
params: [
{ localId: "this", name: "this", type },
{ localId: "field", name: "field", type: STRING },
],
returnType: VOID,
locals: [
{ id: "this", name: "this", type, mutable: false },
{ id: "field", name: "field", type: STRING, mutable: false },
{ id: "bag", name: "bag", type: DYN, mutable: true },
],
body: [
{
kind: "varDecl",
localId: "bag",
init: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc },
loc,
},
{
kind: "if",
cond: { kind: "dynTest", test: "undefined", value: bagRef, type: BOOL, loc },
then: initializeBag(lowerer, className, self, loc),
else_: null,
loc,
},
{
kind: "if",
cond: {
kind: "libCall",
fn: "dyn.bagCreateField",
args: [bagRef, field],
type: BOOL,
loc,
},
then: [{ kind: "return", value: null, loc }],
else_: null,
loc,
},
{
kind: "exprStmt",
expr: {
kind: "call",
callee: general,
args: [self, lowerer.coerceToExpected(field, DYN)],
type: VOID,
loc,
},
loc,
},
],
loc,
});
return helperName;
};
const created = (receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrStmt => {
const symbolKey = symbolMemberKey(lowerer, lowerer.classes.get(className)!, field, loc);
if (symbolKey === null) {
return {
kind: "exprStmt",
expr: {
kind: "call",
callee: stringHelperFor(className),
args: [
receiver.type.kind === "object" && receiver.type.className !== className
? { kind: "upcast", value: receiver, type: { kind: "object", className }, loc }
: receiver,
{ kind: "strLit", value: field, type: STRING, loc },
],
type: VOID,
loc,
},
loc,
};
}
return createdGeneral(receiver, className, field, loc);
};
const createdGeneral = (
receiver: IrExpr,
className: string,
field: string,
loc: SrcLoc,
): IrStmt => ({
kind: "exprStmt",
expr: {
kind: "call",
callee: name,
callee: helperFor(className),
args: [
lowerer.coerceToExpected(receiver, DYN),
receiver.type.kind === "object" && receiver.type.className !== className
? { kind: "upcast", value: receiver, type: { kind: "object", className }, loc }
: receiver,
symbolMemberKey(lowerer, lowerer.classes.get(className)!, field, loc) ??
lowerer.coerceToExpected({ kind: "strLit", value: field, type: STRING, loc }, DYN),
],
@@ -769,80 +941,103 @@ export function trackClassFieldCreation(lowerer: Lowerer, functions: IrFunction[
},
});
}
functions.push({
name,
params: [
{ localId: "value", name: "value", type: DYN },
{ localId: "key", name: "key", type: DYN },
],
returnType: VOID,
locals: [
{ id: "value", name: "value", type: DYN, mutable: false },
{ id: "key", name: "key", type: DYN, mutable: false },
{ id: "bag", name: "bag", type: DYN, mutable: false },
],
body: [
}
/** `bag = <the class's new bag>`, stored in the instance's hidden field:
* the class's own initializer when recorded, else the class-table
* dispatch, which creates it. */
function initializeBag(lowerer: Lowerer, className: string, self: IrExpr, loc: SrcLoc): IrStmt[] {
const init = lowerer.classBagInitializers?.get(className);
if (init === undefined) {
return [
{
kind: "varDecl",
kind: "assign",
localId: "bag",
init: {
value: {
kind: "call",
callee: classPropertiesHelper(lowerer, loc).name,
args: [value],
args: [lowerer.coerceToExpected(self, DYN)],
type: DYN,
loc,
},
loc,
},
{
kind: "if",
cond: {
kind: "unary",
op: "!",
operand: { kind: "libCall", fn: "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc },
type: BOOL,
loc,
},
then: [
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.defineProperty",
args: [
bag,
lowerer.coerceToExpected(key, DYN),
{
kind: "dynObjLit",
fields: [
{
key: { kind: "strLit", value: "value", type: STRING, loc },
value: dynUndefinedExpr(loc),
},
...["writable", "enumerable", "configurable"].map((key) => ({
key: { kind: "strLit" as const, value: key, type: STRING, loc },
value: lowerer.coerceToExpected(
{ kind: "boolLit", value: true, type: BOOL, loc },
DYN,
),
})),
],
type: DYN,
loc,
},
],
type: DYN,
loc,
},
loc,
},
],
else_: null,
];
}
return [
{
kind: "fieldSet",
obj: self,
className,
field: PROPERTY_BAG,
value: structuredClone(init),
loc,
},
{
kind: "assign",
localId: "bag",
value: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc },
loc,
},
];
}
/** Whether `value` reads local `id`. */
function mentionsLocal(value: IrExpr, id: string): boolean {
return JSON.stringify(value).includes(`"localId":${JSON.stringify(id)}`);
}
/** `if (!hasOwn(bag, key)) defineProperty(bag, key, <undefined data field>)`:
* a field's creation in its instance's property bag. */
function defineIfAbsent(lowerer: Lowerer, bag: IrExpr, key: IrExpr, loc: SrcLoc): IrStmt[] {
return [
{
kind: "if",
cond: {
kind: "unary",
op: "!",
operand: { kind: "libCall", fn: "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc },
type: BOOL,
loc,
},
],
loc,
});
then: [
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.defineProperty",
args: [
bag,
lowerer.coerceToExpected(key, DYN),
{
kind: "dynObjLit",
fields: [
{
key: { kind: "strLit", value: "value", type: STRING, loc },
value: dynUndefinedExpr(loc),
},
...["writable", "enumerable", "configurable"].map((key) => ({
key: { kind: "strLit" as const, value: key, type: STRING, loc },
value: lowerer.coerceToExpected(
{ kind: "boolLit", value: true, type: BOOL, loc },
DYN,
),
})),
],
type: DYN,
loc,
},
],
type: DYN,
loc,
},
loc,
},
],
else_: null,
loc,
},
];
}
/** Calls on native class capsules keep the instance's compiled methods. The
@@ -4648,6 +4843,7 @@ export class ClassDynamicDispatch {
finalize(lowerer?: Lowerer): void {
// Each table serves every site that reaches it.
if (lowerer) for (const fn of this.generated) lowerer.shareHelper(fn);
if (lowerer) this.recordBagInitializers(lowerer);
for (const { helper, initialize, orEmpty } of this.readPropertyBags) {
const absent = helper.body[0];
if (isEmptyBag(initialize.value) && !orEmpty) helper.body.shift();
@@ -4668,6 +4864,82 @@ export class ClassDynamicDispatch {
fn.name === READ_PROPERTIES_OR_EMPTY,
);
}
if (lowerer) this.guardClassTables(lowerer);
}
/** Each tracked class's bag initializer over a typed receiver (`this`),
* for the per-class field-creation helpers (trackClassFieldCreation),
* which create an instance's bag without the class-table dispatch. */
private recordBagInitializers(lowerer: Lowerer): void {
for (const [className, initializers] of this.bagInitializers) {
const init = initializers[0];
if (!init || !lowerer.classes.get(className)?.def.tracksOwnFields) continue;
const self = varRef("this", { kind: "object", className }, init.loc);
const value = withTypedReceiver(structuredClone(init.value), self);
if (mentionsLocal(value, "p.0")) continue;
// Lowerer stand-ins in unit tests may not carry the table.
lowerer.classBagInitializers?.set(className, value);
}
}
/** Fast exits for the generated class tables: every class branch of
* the property-bag, normalization and static-bag helpers tests the
* value's capsule key (or class key), so a value of another kind (a
* plain object, the common case in untyped code) skips straight to the
* table's fallthrough instead of testing every class in the program. */
private guardClassTables(lowerer: Lowerer): void {
const names = new Set([
"%dyn.class.properties",
READ_PROPERTIES,
READ_PROPERTIES_OR_EMPTY,
"%dyn.class.normalize",
"%dyn.class.staticProperties",
]);
for (const fn of lowerer.liftedFns) {
if (!names.has(fn.name) || fn.body.length < 2) continue;
const param = fn.params[0]?.localId;
if (param === undefined) continue;
const last = fn.body[fn.body.length - 1]!;
if (last.kind !== "return" || last.value === null || !returnsParam(last.value, param))
continue;
let test: "classCapsule" | "funcKind" | null = null;
let ok = true;
for (const stmt of fn.body.slice(0, -1)) {
if (
stmt.kind === "assign" &&
stmt.localId === param &&
stmt.value.kind === "call" &&
stmt.value.callee === "%dyn.class.normalize"
)
continue;
const kind =
stmt.kind === "if" && stmt.else_ === null ? branchKind(stmt.cond, param) : null;
if (kind === null || (test !== null && kind !== test)) {
ok = false;
break;
}
test = kind;
}
if (!ok || test === null) continue;
const first = fn.body[0]!;
if (first.kind === "if" && first.cond.kind === "dynTest" && first.cond.test === test)
continue;
const loc = fn.loc;
fn.body.unshift({
kind: "if",
cond: {
kind: "dynTest",
test,
negated: true,
value: varRef(param, DYN, loc),
type: BOOL,
loc,
},
then: [structuredClone(last)],
else_: null,
loc,
});
}
}
private normalizedDispatches(): IrFunction[] {
@@ -4741,10 +5013,17 @@ export class ClassDynamicDispatch {
this.normalizationKey = key;
const loc = this.normalization.loc;
const value = varRef("value", DYN, loc);
this.normalization.body = entries.map(({ capsule, children }): IrStmt => {
// The checked receiver is unboxed once per capsule branch, not once
// per subclass test.
this.normalization.locals = this.normalization.locals.filter(
(local) => !local.id.startsWith("%normalized."),
);
this.normalization.body = entries.map(({ capsule, children }, index): IrStmt => {
const type: IrType = { kind: "object", className: capsule.def.name };
const receiver: IrExpr = { kind: "dynCheck", value, type, loc };
return {
const id = `%normalized.${index}`;
this.normalization!.locals.push({ id, name: id, type, mutable: false });
const receiver: IrExpr = varRef(id, type, loc);
const branch: IrStmt = {
kind: "if",
cond: {
kind: "libCall",
@@ -4785,6 +5064,15 @@ export class ClassDynamicDispatch {
else_: null,
loc,
};
// The capsule test stays the branch condition; its body unboxes.
if (branch.kind === "if")
branch.then.unshift({
kind: "varDecl",
localId: id,
init: { kind: "dynCheck", value, type, loc },
loc,
});
return branch;
});
this.normalization.body.push({ kind: "return", value, loc });
}
@@ -3646,6 +3646,9 @@ export class Lowerer {
.some((sf) => isDeclBoundaryFile(sf.fileName));
return this.declBoundaryMemo;
}
/** Tracked classes' bag initializers over a typed `this`
* (class-dynamic-dispatch.ts recordBagInitializers). */
readonly classBagInitializers = new Map<string, IrExpr>();
constructor(
readonly program: ts.Program,
@@ -3745,6 +3745,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"dyn.assignPrototype": { argTypes: [DYN, DYN, DYN], result: DYN },
"dyn.definePrototypeProps": { argTypes: [DYN, DYN, DYN], result: DYN },
"dyn.bagGet": { argTypes: [DYN, STRING, DYN], result: DYN },
/** A class field's creation in its instance's property bag: adds the
* string key as a default data property when absent, answering false
* (nothing changed) when the bag needs the general defineProperty path. */
"dyn.bagCreateField": { argTypes: [DYN, STRING], result: BOOL },
"dyn.bagSet": { argTypes: [DYN, STRING, DYN, DYN], result: VOID },
"dyn.objCreate": { argTypes: [DYN], result: DYN },
// Names a compiled class's prototype object (args[0]) after the class
+5 -1
View File
@@ -3445,7 +3445,11 @@ export type IrExpr =
| "array"
| "truthy"
| "error"
| "function";
| "function"
/** The value is a capsule of a compiled class instance. */
| "classCapsule"
/** The value is a boxed function or class value (exact kind test). */
| "funcKind";
bytesElem?: IrBytesElem;
negated?: true;
value: IrExpr;
+61
View File
@@ -176,3 +176,64 @@ test("a function holds a site when it calls a classified helper or dispatches a
expect(hasJsIcSite({ ...invoker, name: "%init.0" }, sites)).toBe(false);
expect(hasJsIcSite({ ...caller, name: "%init.0" }, sites)).toBe(true);
});
test("bag helpers that exit early for values no class branch matches are still the receiver", () => {
/** `if (!<test>(v)) return <value>`: the class tables' early exit. */
const exit = (test: "classCapsule" | "funcKind", value: IrExpr, negated = true): IrStmt => ({
kind: "if",
cond: {
kind: "dynTest",
test,
...(negated ? { negated: true as const } : {}),
value: ref("v"),
type: BOOL,
loc,
},
then: [ret(value)],
else_: null,
loc,
});
const guarded = (name: string, first: IrStmt): IrFunction =>
helper(name, ["v"], [first, capsuleBranch("v"), ret(ref("v"))]);
const readBag = guarded("%dyn.class.readPropertiesOrEmpty", exit("classCapsule", ref("v")));
const writeBag = guarded("%dyn.class.properties", exit("classCapsule", ref("v")));
const staticBag = guarded("%dyn.class.staticProperties", exit("funcKind", ref("v")));
const getter = getHelper(
"%dyn.class.property.0",
"x",
[],
call(staticBag.name, call(readBag.name, ref("p.0"))),
);
const setter = setHelper("%dyn.class.property.1", "y");
// An exit that returns another value, or a test that is not negated,
// makes the bag differ from the receiver.
const otherExit = guarded(
"%dyn.class.other",
exit("classCapsule", { kind: "dynObjLit", type: DYN, loc } as IrExpr),
);
const positive = guarded("%dyn.class.positive", exit("classCapsule", ref("v"), false));
const otherGetter = getHelper("%dyn.class.property.2", "x", [], call(otherExit.name, ref("p.0")));
const positiveGetter = getHelper(
"%dyn.class.property.3",
"x",
[],
call(positive.name, ref("p.0")),
);
const sites = new JsIcSites({
functions: [
readBag,
writeBag,
staticBag,
otherExit,
positive,
getter,
setter,
otherGetter,
positiveGetter,
],
});
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" });
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" });
expect(sites.site("%dyn.class.property.2")).toBeNull();
expect(sites.site("%dyn.class.property.3")).toBeNull();
});
+15 -1
View File
@@ -170,6 +170,18 @@ export class JsIcSites {
);
}
/** An early exit for values a class table never matches: `if (!<kind
* test>(value)) return <value>`, with no else. A plain object takes it
* and gets itself back. */
private kindExit(s: IrStmt, param: string): boolean {
if (s.kind !== "if" || s.else_ !== null || s.then.length !== 1) return false;
const { cond } = s;
if (cond.kind !== "dynTest" || cond.negated !== true || !isVar(cond.value, param)) return false;
if (cond.test !== "classCapsule" && cond.test !== "funcKind") return false;
const ret = s.then[0]!;
return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param);
}
/** A property-bag helper that returns its argument unless a capsule
* branch is taken. */
private identityHelper(name: string): boolean {
@@ -190,7 +202,9 @@ export class JsIcSites {
const param = fn.params[0]!.localId;
const last = fn.body[fn.body.length - 1];
identity =
fn.body.slice(0, -1).every((s) => this.capsuleBranch(s, param)) &&
fn.body
.slice(0, -1)
.every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) &&
last?.kind === "return" &&
last.value !== null &&
this.bagOf(last.value, param);
+24
View File
@@ -16140,3 +16140,27 @@ void scr_dyn_decl_become(ScrDyn *n, const ScrDyn *capsule) {
}
scr_dyn_release(old);
}
/* A compiled class's field creation in its instance's property bag (the
* compiler's trackClassFieldCreation): an ordinary extensible bag gains
* `key` as a writable, enumerable, configurable data property when it
* does not have it yet. Answers false, changing nothing, for any other bag,
* whose creation takes the general defineProperty path. Calls only
* exported functions. */
bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key);
bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key) {
if (!bag || bag->kind != SCR_DYN_OBJ || bag->non_extensible || bag->v.obj.source_access)
return false;
if (scr_dyn_obj_get(bag, key->data, key->len)) return true;
scr_dyn_obj_set(bag, key->data, key->len, scr_dyn_retain(scr_dyn_undefined()));
return true;
}
/* Whether a value is a capsule of a compiled class instance (its type key
* names an object type): the class tables' fast exit for every other value,
* including record and array capsules. Calls only libc. */
bool scr_dyn_is_class_capsule(const ScrDyn *d);
bool scr_dyn_is_class_capsule(const ScrDyn *d) {
return d && d->kind == SCR_DYN_TYPED_REF && d->v.typed_ref.type_key_len > 7 &&
memcmp(d->v.typed_ref.type_key, "object:", 7) == 0;
}