mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Create untyped class instances' property-bag entries without the class tables
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
468e0592bd
commit
14a2769f58
@@ -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",
|
||||
|
||||
@@ -685,16 +685,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 +907,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
|
||||
@@ -4516,6 +4677,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();
|
||||
@@ -4538,6 +4700,21 @@ export class ClassDynamicDispatch {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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);
|
||||
}
|
||||
}
|
||||
|
||||
private normalizedDispatches(): IrFunction[] {
|
||||
return [
|
||||
...(this.propertyBag ? [this.propertyBag] : []),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16110,3 +16110,18 @@ 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user