diff --git a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts index 5f8119079..4169e86d8 100644 --- a/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts +++ b/packages/compiler/src/backend/llvm/checked-value-lifetimes.ts @@ -5,6 +5,8 @@ export function preservesDynTest(test: string): boolean { switch (test) { case "truthy": + case "classCapsule": + case "funcKind": case "nullish": case "promise": case "buffer": diff --git a/packages/compiler/src/backend/llvm/expr-dynamic.ts b/packages/compiler/src/backend/llvm/expr-dynamic.ts index 434435eae..07a538184 100644 --- a/packages/compiler/src/backend/llvm/expr-dynamic.ts +++ b/packages/compiler/src/backend/llvm/expr-dynamic.ts @@ -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]!]); } diff --git a/packages/compiler/src/backend/llvm/lib-shared.ts b/packages/compiler/src/backend/llvm/lib-shared.ts index d118a99a2..1e8e69767 100644 --- a/packages/compiler/src/backend/llvm/lib-shared.ts +++ b/packages/compiler/src/backend/llvm/lib-shared.ts @@ -134,6 +134,9 @@ export const LIB_FN_SYMS: Record = { "error.setCodeDyn": "scr_error_set_code_dyn", "error.assign": "scr_error_assign_dyn", "error.hasKey": "scr_error_has_key", + "error.ownAccessor": "scr_error_own_accessor", + "error.accessorGet": "scr_error_accessor_get", + "error.accessorSet": "scr_error_accessor_set", "error.hasCode": "scr_error_has_code", "error.defineCause": "scr_error_define_cause", "error.deleteCause": "scr_error_delete_cause", @@ -1570,11 +1573,13 @@ export const LIB_FN_SYMS: Record = { "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", "dyn.objCreateWithProperties": "scr_dyn_obj_create_with_properties", "dyn.getPrototype": "scr_dyn_get_prototype", + "dyn.getPrototypeForIdentity": "scr_dyn_get_prototype_for_identity", "dyn.setPrototype": "scr_dyn_set_prototype", "dyn.getOwnPropertyNames": "scr_dyn_get_own_property_names", "dyn.ownKeys": "scr_dyn_own_keys", diff --git a/packages/compiler/src/frontend/decl-projection.ts b/packages/compiler/src/frontend/decl-projection.ts index 31d32834e..f2340e984 100644 --- a/packages/compiler/src/frontend/decl-projection.ts +++ b/packages/compiler/src/frontend/decl-projection.ts @@ -99,6 +99,70 @@ function declaredInPackage(symbol: ts.Symbol | undefined): boolean { return symbol.declarations.some((d) => isPackageDeclarationFile(ts.getSourceFileOfNode(d))); } +/** The parts of a call signature's declaration that pass-through detection + * reads, for the declaration kinds a resolved call signature has. */ +function signatureParts(node: ts.Node | undefined): { + typeParameters: readonly ts.TypeParameterDeclaration[]; + parameters: readonly ts.ParameterDeclaration[]; + type: ts.TypeNode | undefined; +} | null { + if (node === undefined) return null; + if (ts.isFunctionTypeNode(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isFunctionDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isMethodDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isMethodSignatureDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + if (ts.isCallSignatureDeclaration(node)) + return { + typeParameters: node.typeParameters?.nodes ?? [], + parameters: node.parameters.nodes, + type: node.type, + }; + return null; +} + +/** A signature that returns one of its own type parameters which also types + * a parameter (`(obj: T) => T`) hands back the argument + * or something standing in for it, a proxy or a marked object, whose + * identity the package may track (Effect's `Equal.byReference`). Its result + * type comes from the caller's argument, not from data the package + * declares, so the boundary leaves it alone: converting it would build a + * separate native value. */ +function passesArgumentThrough(declaration: ts.Node | undefined): boolean { + const sig = signatureParts(declaration); + if (sig === null || sig.type === undefined || !ts.isTypeReferenceNode(sig.type)) return false; + const ret = sig.type; + if (!ts.isIdentifier(ret.typeName) || ret.typeArguments !== undefined) return false; + const name = ret.typeName.text; + if (!sig.typeParameters.some((tp) => tp.name.text === name)) return false; + return sig.parameters.some( + (param) => + param.type !== undefined && + ts.isTypeReferenceNode(param.type) && + ts.isIdentifier(param.type.typeName) && + param.type.typeName.text === name, + ); +} + function declaredInUserFile(symbol: ts.Symbol | undefined): boolean { if (symbol === undefined) return false; const decls = symbol.declarations; @@ -511,16 +575,16 @@ export function planDeclProjection(input: DeclProjectionInput): DeclProjection | if (ts.isCallExpression(d) || ts.isNewExpression(d)) { const callee = d.expression; const target = ts.isPropertyAccessExpression(callee) ? callee.name : callee; - produced = declaredInPackage(declSymbol(target)); - if (!produced) { - try { - const sig = declChecker.getResolvedSignature(d); - const decl = sig?.declaration; - produced = decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl)); - } catch { - produced = false; - } + let decl: ts.Node | undefined; + try { + decl = declChecker.getResolvedSignature(d)?.declaration; + } catch { + decl = undefined; } + produced = + (declaredInPackage(declSymbol(target)) || + (decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl)))) && + !passesArgumentThrough(decl); } else if (ts.isTaggedTemplateExpression(d)) { produced = declaredInPackage(declSymbol(d.tag)); } else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) { diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts index b717452c2..1a21a6aa5 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.test.ts @@ -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: [ diff --git a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts index e394718d1..a1bc41782 100644 --- a/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts +++ b/packages/compiler/src/frontend/lowering/class-dynamic-dispatch.ts @@ -350,7 +350,7 @@ export function classInstanceOf( localId: "candidate", value: { kind: "libCall", - fn: "dyn.getPrototype", + fn: "dyn.getPrototypeForIdentity", args: [candidate], type: DYN, loc, @@ -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(); + 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 = `, 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, )`: + * 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 @@ -862,6 +1057,9 @@ export class ClassDynamicDispatch { private deletedNames: Set | null | undefined; private readonly boxed = new Set(); private readonly boxedConstructors = new Set(); + /** Every class a boxed value was statically typed as, runtime error + * classes included: the value's capsule can be any of its subclasses. */ + private readonly boxedStatic = new Set(); private readonly dispatches = new Map(); private readonly properties = new Map(); private readonly computed = new Map< @@ -906,8 +1104,15 @@ export class ClassDynamicDispatch { private descriptorHelper: IrFunction | null = null; private prototypeHelper: IrFunction | null = null; private readonly prototypeClasses = new Set(); - private prototypeIdentityHelper: IrFunction | null = null; - private readonly prototypeIdentityClasses = new Set(); + /** The stand-in prototype object of each class without prototype data + * (prototypeStandIn), by class name. */ + private readonly prototypeStandIns = new Set(); + /** The prototype reads of identity walks, by libCall, and the classes + * each one answers for boxed instances. */ + private readonly prototypeIdentityHelpers = new Map< + "dyn.getPrototype" | "dyn.getPrototypeForIdentity", + { fn: IrFunction; classes: Set } + >(); /** `value instanceof Error` through Error.prototype: boxed instances of * classes with a heritage evaluated at run time test their prototype * data's chain. */ @@ -961,6 +1166,7 @@ export class ClassDynamicDispatch { if (seenTypes.has(key)) return; seenTypes.add(key); if (type.kind === "object") { + this.boxedStatic.add(type.className); if (isDynTypedRefType(type)) this.boxed.add(type.className); } else if (type.kind === "classval") this.boxedConstructors.add(type.className); else if (type.kind === "array") discover(type.elem); @@ -1092,33 +1298,6 @@ export class ClassDynamicDispatch { }); changed = true; } - if (this.prototypeIdentityHelper && !this.prototypeIdentityClasses.has(className)) { - this.prototypeIdentityClasses.add(className); - const loc = this.prototypeIdentityHelper.loc; - const type: IrType = { kind: "object", className }; - const value = varRef("value", DYN, loc); - const prototype = classPrototypeData(lowerer, info, loc, { - kind: "dynCheck", - value, - type, - loc, - }); - if (prototype) - this.prototypeIdentityHelper.body.unshift({ - kind: "if", - cond: { - kind: "libCall", - fn: "dyn.typedRefIs", - args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], - type: BOOL, - loc, - }, - then: [{ kind: "return", value: prototype, loc }], - else_: null, - loc, - }); - changed = true; - } if ( this.errorPrototypeHelper && !this.errorPrototypeClasses.has(className) && @@ -1229,6 +1408,101 @@ export class ClassDynamicDispatch { } } } + // The runtime boxes a class instance as its exact class, so a value + // boxed at a base (or Error) static type reaches the identity views as + // any subclass's capsule. + for (const helper of this.prototypeIdentityHelpers.values()) + for (const [className, info] of lowerer.classes) { + if (helper.classes.has(className)) continue; + if (info.builtinEmitter || info.builtinStream || info.builtinError) continue; + if (!isDynTypedRefType({ kind: "object", className })) continue; + if ( + !this.boxed.has(className) && + ![...this.boxedStatic].some((base) => lowerer.isSubclassOf(className, base)) + ) + continue; + helper.classes.add(className); + const loc = helper.fn.loc; + const type: IrType = { kind: "object", className }; + const value = varRef("value", DYN, loc); + // A class with no prototype data in its chain (one extending + // Error, or with no methods) has no prototype a program can name: + // an object of the class's own stands in, so instances of one class + // share it (`Object.getPrototypeOf(a) === Object.getPrototypeOf(b)`), + // other prototypes differ from it, and an instanceof walk (through + // Object.prototype to null) answers false instead of reaching the + // reflection refusal. A function-local class is a new class at + // each evaluation of its declaration, so its instances answer with + // the prototype of their own class value instead. + const answer = (name: string): IrStmt[] | null => { + const of = lowerer.classes.get(name)!; + const prototype = + hasClassPrototypeData(of) || of.localClass + ? classPrototypeData(lowerer, of, loc, { + kind: "dynCheck", + value, + type: { kind: "object", className: name }, + loc, + }) + : null; + if (prototype) return [{ kind: "return", value: prototype, loc }]; + return hasClassPrototypeData(of) ? null : this.prototypeStandIn(lowerer, name, loc); + }; + const own = answer(className); + if (!own) { + changed = true; + continue; + } + // A capsule boxed at this class can hold a subclass instance: the + // most derived subclass the instance belongs to answers. + const depth = (name: string): number => { + let n = 0; + for (let at = lowerer.classes.get(name)?.base; at; at = at.base) n++; + return n; + }; + const subclasses = [...lowerer.classes.keys()] + .filter( + (name) => + name !== className && + lowerer.isSubclassOf(name, className) && + isDynTypedRefType({ kind: "object", className: name }), + ) + .sort((a, b) => depth(b) - depth(a)); + const then: IrStmt[] = []; + for (const name of subclasses) { + const sub = answer(name); + if (!sub) continue; + then.push({ + kind: "if", + cond: { + kind: "instanceOf", + value: { kind: "dynCheck", value, type, loc }, + className: name, + type: BOOL, + loc, + }, + then: sub, + else_: null, + loc, + }); + } + then.push(...own); + helper.fn.body.unshift({ + kind: "if", + cond: { + kind: "libCall", + fn: "dyn.typedRefIs", + args: [value, { kind: "strLit", value: typeKey(type), type: STRING, loc }], + type: BOOL, + loc, + }, + then, + else_: null, + loc, + }); + changed = true; + } + let live: Map[]> | null = null; for (const plan of this.propertyReceivers(lowerer, false, false)) { const info = plan.info; const className = info.def.name; @@ -1309,7 +1583,11 @@ export class ClassDynamicDispatch { ); const prototypeFor = (current: ClassInfo, receiver: IrExpr, loc: SrcLoc): IrExpr | null => this.bagPrototype(lowerer, current, receiver, loc); - for (const existingInit of this.bagInitializers.get(className) ?? []) { + live ??= this.liveBagInitializers(); + for (const existingInit of [ + ...(this.bagInitializers.get(className) ?? []), + ...(live.get(className) ?? []), + ]) { if (existingInit.value.kind === "dynObjLit" && needsPrototype(info)) { const prototype = prototypeFor(info, existingInit.obj, existingInit.loc); if (prototype) { @@ -1320,6 +1598,11 @@ export class ClassDynamicDispatch { type: DYN, loc: existingInit.loc, }; + // The initializer is a statement of the property bag helpers; + // their reused site summaries do not see an edit inside it. + if (this.propertyBag) this.sites.forget(this.propertyBag); + for (const fn of this.typedPropertyBags.values()) this.sites.forget(fn); + for (const fn of this.exactPropertyBags.values()) this.sites.forget(fn); changed = true; } } @@ -1591,29 +1874,41 @@ export class ClassDynamicDispatch { } if ( expr.kind === "libCall" && - expr.fn === "dyn.getPrototype" && + (expr.fn === "dyn.getPrototype" || expr.fn === "dyn.getPrototypeForIdentity") && expr.args[0]?.kind !== "dynObjLit" ) { if (expr.prototypeIdentityOnly) { - if (!this.prototypeIdentityHelper) { + let helper = this.prototypeIdentityHelpers.get(expr.fn); + if (!helper) { const loc = expr.loc; - this.prototypeIdentityHelper = { - name: "%dyn.class.prototypeIdentity", - params: [{ localId: "value", name: "value", type: DYN }], - locals: [{ id: "value", name: "value", type: DYN, mutable: false }], - returnType: DYN, - loc, - body: [ - { kind: "return", value: { ...expr, args: [varRef("value", DYN, loc)] }, loc }, - ], + helper = { + fn: { + name: + expr.fn === "dyn.getPrototype" + ? "%dyn.class.prototypeIdentity" + : "%dyn.class.prototypeChainIdentity", + params: [{ localId: "value", name: "value", type: DYN }], + locals: [{ id: "value", name: "value", type: DYN, mutable: false }], + returnType: DYN, + loc, + body: [ + { + kind: "return", + value: { ...expr, args: [varRef("value", DYN, loc)] }, + loc, + }, + ], + }, + classes: new Set(), }; - lowerer.liftedFns.push(this.prototypeIdentityHelper); - this.generated.add(this.prototypeIdentityHelper); + this.prototypeIdentityHelpers.set(expr.fn, helper); + lowerer.liftedFns.push(helper.fn); + this.generated.add(helper.fn); changed = true; } return { kind: "call", - callee: this.prototypeIdentityHelper.name, + callee: helper.fn.name, args: expr.args, type: DYN, loc: expr.loc, @@ -3561,6 +3856,43 @@ export class ClassDynamicDispatch { /** The receiver's own dynamic properties for a read: undefined while it has * none, and never stored, so reads stay valid on published objects. */ + /** Statements that return `className`'s stand-in prototype: one empty + * object per class, created on first use and kept in a module global. */ + private prototypeStandIn(lowerer: Lowerer, className: string, loc: SrcLoc): IrStmt[] { + const globalId = `%g.%prototype.standIn.${className}`; + const readyId = `${globalId}.ready`; + if (!this.prototypeStandIns.has(className)) { + this.prototypeStandIns.add(className); + lowerer.globalsList.push( + { id: globalId, name: `${className}.prototype`, type: DYN, mutable: true }, + { id: readyId, name: `${className}.prototype.ready`, type: BOOL, mutable: true }, + ); + } + return [ + { + kind: "if", + cond: { kind: "unary", op: "!", operand: varRef(readyId, BOOL, loc), type: BOOL, loc }, + then: [ + { + kind: "assign", + localId: globalId, + value: { kind: "dynObjLit", fields: [], type: DYN, loc }, + loc, + }, + { + kind: "assign", + localId: readyId, + value: { kind: "boolLit", value: true, type: BOOL, loc }, + loc, + }, + ], + else_: null, + loc, + }, + { kind: "return", value: varRef(globalId, DYN, loc), loc }, + ]; + } + private readPropertyBag(lowerer: Lowerer, receiver: IrExpr, loc: SrcLoc): IrExpr { const helper = classPropertiesHelper(lowerer, loc, true); this.generated.add(helper); @@ -4440,6 +4772,27 @@ export class ClassDynamicDispatch { * A class with constructible subclasses that have prototype data selects * it through its `%class.bagPrototype` helper; any other class reads its * own prototype data. */ + /** The lazy bag stores as they stand in the generated bodies, by class. + * Worklist passes that transform every function rebuild statement + * objects, so a recorded initializer can be a detached copy; a store that + * gains its prototype must be updated where it runs. */ + private liveBagInitializers(): Map[]> { + const live = new Map[]>(); + const add = (stmt: IrStmt | undefined): void => { + if (stmt?.kind !== "if") return; + const store = stmt.then[0]; + if (store?.kind !== "fieldSet" || store.field !== PROPERTY_BAG) return; + const list = live.get(store.className); + if (list) list.push(store); + else live.set(store.className, [store]); + }; + // The complete dispatch guards each class's lazy store by its receiver. + for (const stmt of this.propertyBag?.body ?? []) if (stmt.kind === "if") add(stmt.then[0]); + for (const helper of this.typedPropertyBags.values()) add(helper.body[0]); + for (const helper of this.exactPropertyBags.values()) add(helper.body[0]); + return live; + } + private bagPrototype( lowerer: Lowerer, current: ClassInfo, @@ -4516,6 +4869,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(); @@ -4536,6 +4890,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[] { @@ -4609,10 +5039,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", @@ -4653,6 +5090,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 }); } diff --git a/packages/compiler/src/frontend/lowering/class-prototypes.ts b/packages/compiler/src/frontend/lowering/class-prototypes.ts index 3ecdd84aa..cb2ab2280 100644 --- a/packages/compiler/src/frontend/lowering/class-prototypes.ts +++ b/packages/compiler/src/frontend/lowering/class-prototypes.ts @@ -289,6 +289,16 @@ export function hasClassPrototypeData(info: ClassInfo): boolean { ); } +/** Methods and accessors that live on the class's prototype. */ +function declaresPrototypeMembers(decl: ts.ClassLikeDeclaration): boolean { + return decl.members.nodes.some( + (member) => + (ts.isMethodDeclaration(member) || ts.isAccessor(member)) && + !ts.isPrivateIdentifier(member.name) && + !ts.getModifiers(member)?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword), + ); +} + /** Populate the shared prototype view before exposing it to reflection. * Methods and accessors are non-enumerable and retain declaration identity. */ export function reflectClassPrototype( @@ -318,7 +328,10 @@ export function reflectClassPrototype( // Local JavaScript factories initialize captured prototype members on // their constructor value, so the shared runtime view is already complete. if (info.localClass && info.runtimePrototypeMembers) return prototype; - if (info.localClass) + // A function-local class declaring no prototype members (zod 4's + // `class Definition extends Parent {}`) has nothing to install: its + // shared runtime view is complete once its base's is. + if (info.localClass && declaresPrototypeMembers(info.decl)) return refuse("prototype reflection on a function-local class is not supported yet"); if (info.base && !info.base.builtinError) { reflectClassPrototype( @@ -544,6 +557,15 @@ export function lowerClassPrototypeData( if (name === "constructor") { return reflectClassPrototype(lowerer, info, locOf(expr), classValueRef(lowerer, info, expr)); } + // The whole prototype of a function-local class: the shared view + // Object.getPrototypeOf answers for its instances. + if (name === null && info.localClass) { + const receiver = lowerer.lowerExprExpecting(expr.expression, { + kind: "classval", + className: info.def.name, + }); + return reflectClassPrototype(lowerer, info, locOf(expr), receiver); + } if (name === null || (isCompiledPrototypeMember(lowerer, info, name) && !method)) { lowerer.unsupported( "SC1090", diff --git a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts index 1947f0d75..d55f71ff0 100644 --- a/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts +++ b/packages/compiler/src/frontend/lowering/containers/string-and-regexp.ts @@ -16,6 +16,7 @@ import { type IrExpr, type IrStmt, type IrType, + REGEX, STRING, type SrcLoc, UNDEFINED_T, @@ -920,12 +921,15 @@ export function lowerRegexMethodCall( })(); if ((receiverKind === "string" || nullableStringRecv) && name === "match") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern match: the SC2020 fence - if (call.arguments.nodes.length !== 1) return null; + if (!arg0 || call.arguments.nodes.length !== 1) return null; + const checkedPattern = isCheckedPatternArgument(lowerer, arg0); + if (!checkedPattern && !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern match: the SC2020 fence const receiver = nullableStringRecv ? lowerMethodReceiver(lowerer, access.expression, STRING, access.name.text) : lowerReceiver(); - const re = lowerRegexPatternArgument(lowerer, arg0); + const re = checkedPattern + ? lowerCheckedPatternArgument(lowerer, arg0) + : lowerRegexPatternArgument(lowerer, arg0); // RegExpMatchArray | null maps to the string[] | null union by // itself; intern it directly when the checker's spelling doesn't — // or when it maps to something WIDER (an optional-chain call node @@ -983,10 +987,13 @@ export function lowerRegexMethodCall( // 0, exactly Node. Never throws. if (receiverKind === "string" && name === "search") { const arg0 = call.arguments.nodes[0]; - if (!arg0 || !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern form: the SC2020 fence - if (call.arguments.nodes.length !== 1) return null; + if (!arg0 || call.arguments.nodes.length !== 1) return null; + const checkedPattern = isCheckedPatternArgument(lowerer, arg0); + if (!checkedPattern && !isRegexPatternArgument(lowerer, arg0)) return null; // string-pattern form: the SC2020 fence const receiver = lowerReceiver(); - const re = lowerRegexPatternArgument(lowerer, arg0); + const re = checkedPattern + ? lowerCheckedPatternArgument(lowerer, arg0) + : lowerRegexPatternArgument(lowerer, arg0); return { kind: "regexIntrinsic", method: "search", receiver, args: [re], type: F64, loc }; } if (receiverKind === "string" && (name === "replace" || name === "replaceAll")) { @@ -1936,6 +1943,46 @@ function isRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): boolean ); } +/** True when a `match` or `search` pattern is checker-untyped (`any` or + * `unknown`), so its runtime value decides the protocol. */ +function isCheckedPatternArgument(lowerer: Lowerer, node: ts.Expression): boolean { + if (ts.isSpreadElement(node)) return false; + return (lowerer.typeOf(node).flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0; +} + +/** An untyped `match` or `search` pattern as a RegExp, chosen at run time as + * Node does: a RegExp is used as is (its lastIndex state included), and any + * other value builds `new RegExp(value)` (undefined is the empty pattern, + * other values convert with ToString). */ +function lowerCheckedPatternArgument(lowerer: Lowerer, node: ts.Expression): IrExpr { + const loc = locOf(node); + const value = lowerer.lowerExpr(node); + if (value.type.kind === "regex") return value; + const checked = lowerer.coerceInto(node, value, DYN); + const local = lowerer.declareHiddenLocal("%matchPattern", DYN); + const ref = varRef(local.id, DYN, loc); + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: local.id, init: checked, loc }], + result: { + kind: "ternary", + cond: { kind: "libCall", fn: "dyn.nativeRegexIs", args: [ref], type: BOOL, loc }, + then: lowerer.coerceInto(node, ref, REGEX), + else_: { + kind: "libCall", + fn: "regex.newChecked", + args: [ref, dynUndefinedExpr(loc)], + type: REGEX, + loc, + }, + type: REGEX, + loc, + }, + type: REGEX, + loc, + }; +} + /** The pattern of `match` or `search` as a RegExp: an undefined pattern is * `new RegExp(undefined)`, the empty pattern, as in Node. */ function lowerRegexPatternArgument(lowerer: Lowerer, node: ts.Expression): IrExpr { diff --git a/packages/compiler/src/frontend/lowering/dispatch-sites.ts b/packages/compiler/src/frontend/lowering/dispatch-sites.ts index f5b8ebdb2..b9ab8f069 100644 --- a/packages/compiler/src/frontend/lowering/dispatch-sites.ts +++ b/packages/compiler/src/frontend/lowering/dispatch-sites.ts @@ -81,6 +81,7 @@ const DISPATCH_LIB_CALLS = new Set([ "dyn.propertyIsEnumerableComputed", "dyn.defineProperty", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.inheritsErrorPrototype", "dyn.reflectGet", "dyn.reflectSet", diff --git a/packages/compiler/src/frontend/lowering/error-methods.ts b/packages/compiler/src/frontend/lowering/error-methods.ts index b545f4147..067f9c286 100644 --- a/packages/compiler/src/frontend/lowering/error-methods.ts +++ b/packages/compiler/src/frontend/lowering/error-methods.ts @@ -94,12 +94,15 @@ export function refreshErrorPropertyDispatch( lowerer.isSubclassOf(info.def.name, "%Error"), ); const properties: ("name" | "message")[] = ["name", "message"]; + // Test lowerers built from partial objects leave the location undefined. + const accessors = Boolean(lowerer.errorAccessorReflection); for (const property of properties) { if ( - overrides.length && + (overrides.length || accessors) && !lowerer.liftedFns.some((fn) => fn.name === `%error.${property}.read`) ) { - errorPropertyRead(lowerer, varRef("error.0", errorType, overrides[0]!.def.loc), property); + const loc = overrides[0]?.def.loc ?? lowerer.errorAccessorReflection!; + errorPropertyRead(lowerer, varRef("error.0", errorType, loc), property); return true; } } @@ -115,6 +118,7 @@ export function refreshErrorPropertyDispatch( const revision = JSON.stringify([ readers.map((fn) => fn.name), overrides.map((info) => [info.def.name, hasClassPrototypeData(info)]), + accessors, ]); if (lowerer.errorPropertyDispatchRevision === revision) return false; lowerer.errorPropertyDispatchRevision = revision; @@ -183,6 +187,50 @@ export function refreshErrorPropertyDispatch( }, ]; const body: IrStmt[] = [{ kind: "if", cond: own, then: fallback, else_: null, loc }]; + // An own accessor a reflective definition installed (the slot reads as + // absent then): the read calls its getter, the write its setter. + if (accessors) + body.push({ + kind: "if", + cond: { kind: "libCall", fn: "error.ownAccessor", args: [receiver, key], type: BOOL, loc }, + then: write + ? [ + { + kind: "exprStmt", + expr: { + kind: "libCall", + fn: "error.accessorSet", + args: [ + receiver, + key, + lowerer.coerceToExpected(varRef("value", STRING, loc), DYN), + ], + type: VOID, + loc, + }, + loc, + }, + { kind: "return", value: null, loc }, + ] + : [ + { + kind: "return", + value: lowerer.coerceToExpected( + { + kind: "libCall", + fn: "error.accessorGet", + args: [receiver, key], + type: DYN, + loc, + }, + STRING, + ), + loc, + }, + ], + else_: null, + loc, + }); helper.locals = helper.locals.filter((local) => !local.id.startsWith("descriptor.")); for (const info of overrides) { const member = `${write ? "set" : "get"}:${property}`; diff --git a/packages/compiler/src/frontend/lowering/expressions/element-access.ts b/packages/compiler/src/frontend/lowering/expressions/element-access.ts index 0be9be81a..0ea94ec7a 100644 --- a/packages/compiler/src/frontend/lowering/expressions/element-access.ts +++ b/packages/compiler/src/frontend/lowering/expressions/element-access.ts @@ -566,8 +566,8 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } if (receiverIr?.kind !== "array") { + const receiver = tryLowerExpression(lowerer, expr.expression); { - const receiver = tryLowerExpression(lowerer, expr.expression); if ( receiver?.type.kind === "dyn" || (receiver && @@ -618,7 +618,13 @@ export function lowerElementAccess(lowerer: Lowerer, expr: ts.ElementAccessExpre } } } - if (receiverIr?.kind === "string") { + // A checker-`any` receiver that lowers to a string (an implicit-any + // package parameter specialized for a string argument) indexes the + // string the same way. + if ( + receiverIr?.kind === "string" || + (receiverIr === null && receiver?.type.kind === "string") + ) { // An indexed string property is absent for non-integer and missing // indexes. charAt has different coercion and out-of-range semantics. const recv = lowerer.lowerExpr(expr.expression); diff --git a/packages/compiler/src/frontend/lowering/expressions/relational.ts b/packages/compiler/src/frontend/lowering/expressions/relational.ts index b731e4912..f1590e9cc 100644 --- a/packages/compiler/src/frontend/lowering/expressions/relational.ts +++ b/packages/compiler/src/frontend/lowering/expressions/relational.ts @@ -58,6 +58,44 @@ import { isExecExceptionType, refuseExecExceptionCode } from "../builtins/errors import { hasRuntimeStatics } from "../class-runtime-statics.js"; import { nullProtoRecordShape, recordNullProtoTest } from "../null-prototype.js"; +/** The class value an `instanceof` right-hand side holds. A JavaScript + * object's property holding a program class (`holder.type`) reads as a + * checked-dynamic value that the checker types as the class: it converts + * to the class value. Runtime-provided classes keep their own lowering. */ +function classValueOperand( + lowerer: Lowerer, + right: ts.Expression, + type: IrType & { kind: "classval" }, +): IrExpr { + if ( + !isJsSourceFile(ts.getSourceFileOfNode(right)) || + ts.isIdentifier(right) || + lowerer.classes.get(type.className)?.def.runtime + ) + return lowerer.lowerExpr(right); + const value = lowerer.lowerExprExpecting(right, DYN); + if (value.type.kind !== "dyn") return value; + return { kind: "dynCheck", value, type, loc: locOf(right) }; +} + +/** The value kinds that never hold an instance of a program class. */ +const NO_CLASS_INSTANCE_KINDS: ReadonlySet = new Set([ + "f64", + "bigint", + "date", + "string", + "bool", + "array", + "map", + "set", + "regex", + "bytes", + "symbol", + "promise", + "undefinedT", + "nullT", +]); + /** `x instanceof C` for a program-declared class C. When x's static * class and C are both in extends-hierarchies the test is dynamic — the * O(1) preorder-interval check against the vtable (`instanceOf` node). @@ -400,7 +438,58 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // (itself — no descendants can flow into the slot), so the answer // folds statically exactly like the named-target folds below. const rhsClassval = storedClassValueType(lowerer, expr.right); - if (rhsClassval?.kind === "classval" && !lowerer.caughtLocalOf(expr.left)) { + // A catch binding against a class held by a binding (`var E = class + // extends Error {}`). The binding holds the declared class or one of its + // subclasses. Without subclasses the test is the class's caught test, + // after the right operand's read; otherwise a payload of the declared + // class is tested against the class the binding holds. + const caught = lowerer.caughtLocalOf(expr.left); + const caughtTarget = + rhsClassval?.kind === "classval" && caught + ? lowerer.classes.get(rhsClassval.className) + : undefined; + if (caught && caughtTarget && !caughtTarget.localClass && !caughtTarget.def.runtime) { + const classValue = lowerer.lowerExpr(expr.right); + if (caughtTarget.subclasses.length === 0) { + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: classValue, loc }], + result: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + type: BOOL, + loc, + }; + } + if (classValue.type.kind !== "classval") + lowerer.badType(expr.right, lowerer.typeOf(expr.right)); + const held = lowerer.declareHiddenLocal("%instanceofClass", classValue.type); + const instance: IrType = { kind: "object", className: caughtTarget.def.name }; + return { + kind: "seqExpr", + stmts: [{ kind: "varDecl", localId: held.id, init: classValue, loc }], + result: { + kind: "logical", + op: "&&", + left: caughtClassInstanceOf(lowerer, caught.id, caughtTarget, loc), + right: { + kind: "instanceOfValue", + value: { + kind: "caughtNarrow", + value: varRef(caught.id, CAUGHT, loc), + type: instance, + loc, + }, + classValue: varRef(held.id, classValue.type, loc), + type: BOOL, + loc, + }, + type: BOOL, + loc, + }, + type: BOOL, + loc, + }; + } + if (rhsClassval?.kind === "classval" && !caught) { const targetInfo = lowerer.classes.get(rhsClassval.className); if (!targetInfo) { // The type world names a class the lowering never registered @@ -432,7 +521,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc "'instanceof' through a runtime-provided class value on an untyped value (give the operand a class type first)", ); } - const classValue = lowerer.lowerExpr(expr.right); + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); return classInstanceOf(lowerer, left, targetInfo, loc, classValue); } // A union of class instances against a runtime class value (`new K()` @@ -441,6 +530,22 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc const tested = unionClassValueInstanceOf(lowerer, left, expr.right, targetInfo, loc); if (tested) return tested; } + // A value whose type holds no class instance (a primitive, a native + // collection, a date, a regex, an array) is never an instance + // of a program class: false, after both operands evaluate. + if (!targetInfo.def.runtime && NO_CLASS_INSTANCE_KINDS.has(left.type.kind)) { + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); + return { + kind: "seqExpr", + stmts: [ + { kind: "exprStmt", expr: left, loc }, + { kind: "exprStmt", expr: classValue, loc }, + ], + result: { kind: "boolLit", value: false, type: BOOL, loc }, + type: BOOL, + loc, + }; + } if (left.type.kind !== "object") { lowerer.unsupported( "SC1090", @@ -455,7 +560,7 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc (lowerer.inHierarchy(targetInfo) && lowerer.inHierarchy(lhsInfo)) || (targetInfo.localClass !== undefined && lhsInfo.localClass !== undefined) ) { - const classValue = lowerer.lowerExpr(expr.right); + const classValue = classValueOperand(lowerer, expr.right, rhsClassval); if (classValue.type.kind !== "classval") lowerer.badType(expr.right, lowerer.typeOf(expr.right)); const writableTest = writableClassValueTest( @@ -493,8 +598,11 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc "statically-decided 'instanceof' on computed operands (bind the values to variables first)", ); } + // A checked-dynamic right-hand side (a constructor read from a table, a + // structural constructor type, a builtin constructor held as `any`): + // the runtime's instanceof over the value, in both source languages. const constructor = tryLowerExpression(lowerer, expr.right); - if (constructor?.type.kind === "dyn" && isJsSourceFile(ts.getSourceFileOfNode(expr))) { + if (constructor?.type.kind === "dyn") { const value = lowerer.coerceInto(expr.left, lowerer.lowerExpr(expr.left), DYN); return checkedClassInstanceOf( lowerer, @@ -551,39 +659,8 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc // narrowing then types the branches; reads bridge through caughtRead. const caughtLocal = lowerer.caughtLocalOf(expr.left); if (caughtLocal) { - const caughtInstanceOf = (info: ClassInfo): IrExpr => { - if (!lowerer.inHierarchy(info)) { - const value = varRef(caughtLocal.id, CAUGHT, loc); - return { - kind: "ternary", - cond: { - kind: "caughtTest", - value, - test: "instanceof", - className: info.def.name, - type: BOOL, - loc, - }, - then: { kind: "boolLit", value: true, type: BOOL, loc }, - else_: classInstanceOf( - lowerer, - { kind: "caughtToDyn", value, type: DYN, loc }, - info, - loc, - ), - type: BOOL, - loc, - }; - } - return { - kind: "caughtTest", - value: { kind: "varRef", localId: caughtLocal.id, type: CAUGHT, loc }, - test: "instanceof", - className: info.def.name, - type: BOOL, - loc, - }; - }; + const caughtInstanceOf = (info: ClassInfo): IrExpr => + caughtClassInstanceOf(lowerer, caughtLocal.id, info, loc); let test = caughtInstanceOf(target); // A thrown checked-dynamic object whose prototype chain reaches // Error.prototype (an ES5 constructor function inheriting from it). @@ -978,6 +1055,42 @@ function writableClassValueTest( }; } +/** A catch binding tested against a program class: the runtime test against + * the class's preorder interval (false for non-hierarchy-object payloads). */ +function caughtClassInstanceOf( + lowerer: Lowerer, + caughtId: string, + info: ClassInfo, + loc: SrcLoc, +): IrExpr { + if (!lowerer.inHierarchy(info)) { + const value = varRef(caughtId, CAUGHT, loc); + return { + kind: "ternary", + cond: { + kind: "caughtTest", + value, + test: "instanceof", + className: info.def.name, + type: BOOL, + loc, + }, + then: { kind: "boolLit", value: true, type: BOOL, loc }, + else_: classInstanceOf(lowerer, { kind: "caughtToDyn", value, type: DYN, loc }, info, loc), + type: BOOL, + loc, + }; + } + return { + kind: "caughtTest", + value: { kind: "varRef", localId: caughtId, type: CAUGHT, loc }, + test: "instanceof", + className: info.def.name, + type: BOOL, + loc, + }; +} + /** A statically decided `instanceof` still reads its class after the left * operand: a class with an initialization flag throws there before its * declaration ran. */ diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index dcb2d1183..71a0a1ebc 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -291,8 +291,46 @@ function indirectUtilCalleeOf( return bi?.module === "util" ? bi : null; } +/** `Object.defineProperty(target, ...)`, `Object.defineProperties`, + * `Reflect.defineProperty`, and `target.__defineGetter__/__defineSetter__` + * over a target that may hold an Error can install an accessor over an + * error's `name` or `message` (error-methods.ts). */ +function noteErrorAccessorReflection(lowerer: Lowerer, expr: ts.CallExpression): void { + const callee = expr.expression; + if (!ts.isPropertyAccessExpression(callee)) return; + const member = callee.name.text; + const target = + member === "defineProperty" || member === "defineProperties" + ? expr.arguments.nodes[0] + : member === "__defineGetter__" || member === "__defineSetter__" + ? callee.expression + : undefined; + if (!target) return; + const error = lowerer.checker.resolveName("Error", ts.SymbolFlags.Type, target, false); + const errorType = error ? lowerer.checker.getDeclaredTypeOfSymbol(error) : null; + if (mayHoldError(lowerer, lowerer.checker.getTypeAtLocation(target), errorType)) + lowerer.errorAccessorReflection = locOf(expr); +} + +/** An untyped or generic type, a type Error is assignable to (`object`, + * `{}`, `Object`, `{ message?: string }`), or + * an Error-shaped type with both `name` and `message`, alone or in a + * union or intersection. */ +function mayHoldError(lowerer: Lowerer, type: ts.Type, errorType: ts.Type | null): boolean { + if (type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown | ts.TypeFlags.Instantiable)) + return true; + if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) + return ts.constituentTypes(type).some((part) => mayHoldError(lowerer, part, errorType)); + return ( + (lowerer.checker.getPropertyOfType(type, "message") !== undefined && + lowerer.checker.getPropertyOfType(type, "name") !== undefined) || + (errorType !== null && lowerer.checker.isTypeAssignableTo(errorType, type)) + ); +} + export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr { const loc = locOf(expr); + if (lowerer.errorAccessorReflection === null) noteErrorAccessorReflection(lowerer, expr); // Object's checker return type is `any`. Claim known object inputs // before generic checked-dynamic dispatch tries to load the constructor. if ( diff --git a/packages/compiler/src/frontend/lowering/lowerer.ts b/packages/compiler/src/frontend/lowering/lowerer.ts index 6987a1071..b6ef9f68d 100644 --- a/packages/compiler/src/frontend/lowering/lowerer.ts +++ b/packages/compiler/src/frontend/lowering/lowerer.ts @@ -3253,6 +3253,11 @@ export class Lowerer { * dispatch revision needs no weak-key identity or process-global state. */ errorMethodDispatchRevision: string | null = null; errorPropertyDispatchRevision: string | null = null; + /** The program defines properties by reflection on an untyped or + * Error-shaped target, so an own accessor may replace an error's `name` + * or `message`: typed reads and writes of those slots check for one. + * Holds the first such call's location. */ + errorAccessorReflection: SrcLoc | null = null; /** Structural source positions (structural-positions.ts) for names that * stay the same when unrelated code moves. */ /** Files are named by their path relative to the entry's directory, so @@ -3646,6 +3651,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(); constructor( readonly program: ts.Program, diff --git a/packages/compiler/src/ir/builtin-effects.ts b/packages/compiler/src/ir/builtin-effects.ts index 86b6ef03d..c99a6a370 100644 --- a/packages/compiler/src/ir/builtin-effects.ts +++ b/packages/compiler/src/ir/builtin-effects.ts @@ -458,6 +458,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "global.undefRead", // The compiler-resolved Node-parity throw: always throws, catchably. "error.nodeThrow", + // An accessor over an error's own name or message runs user code. + "error.accessorGet", + "error.accessorSet", // USVString coercion runs user toString/valueOf — throws propagate. "dyn.toStringCoerce", "dyn.stringConstructor", @@ -645,6 +648,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet = new Set([ "dyn.objCreate", "dyn.objCreateWithProperties", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.setPrototype", // Error.captureStackTrace: Node's TypeError for a primitive target, and // the error string conversion of name and message diff --git a/packages/compiler/src/ir/builtin-signatures.ts b/packages/compiler/src/ir/builtin-signatures.ts index 4cf579da4..085521fad 100644 --- a/packages/compiler/src/ir/builtin-signatures.ts +++ b/packages/compiler/src/ir/builtin-signatures.ts @@ -3429,6 +3429,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ * throw-site fields, dynamic writes) or an Object.prototype member. */ "error.hasKey": { argTypes: [null, STRING], result: BOOL }, "error.hasCode": { argTypes: [null], result: BOOL }, + /** Whether the error's own `name` or `message` (the key) is an accessor + * a reflective definition installed. Borrows both; never throws. */ + "error.ownAccessor": { argTypes: [null, STRING], result: BOOL }, + /** That accessor's getter called on the error (owned result), and its + * setter called with the value (V8's TypeError without one). Borrow + * their arguments; throw what the accessor throws. */ + "error.accessorGet": { argTypes: [null, STRING], result: DYN }, + "error.accessorSet": { argTypes: [null, STRING, DYN], result: VOID }, "error.defineCause": { argTypes: [null, DYN], result: VOID }, "error.deleteCause": { argTypes: [null], result: VOID }, /** The compiler-resolved Node-parity throw for always-throwing lowered @@ -3745,6 +3753,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 @@ -3752,6 +3764,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({ "dyn.nameClassPrototype": { argTypes: [DYN, STRING], result: VOID }, "dyn.objCreateWithProperties": { argTypes: [DYN, DYN], result: DYN }, "dyn.getPrototype": { argTypes: [DYN], result: DYN }, + // dyn.getPrototype for walks that compare each prototype with a program + // class's prototype: native builtin values end the walk (null). + "dyn.getPrototypeForIdentity": { argTypes: [DYN], result: DYN }, "dyn.setPrototype": { argTypes: [DYN, DYN], result: DYN }, "dyn.getOwnPropertyNames": { argTypes: [DYN], result: DYN }, "dyn.ownKeys": { argTypes: [DYN], result: DYN }, diff --git a/packages/compiler/src/ir/int32-slots.ts b/packages/compiler/src/ir/int32-slots.ts index 5e9a87319..fba3d96a5 100644 --- a/packages/compiler/src/ir/int32-slots.ts +++ b/packages/compiler/src/ir/int32-slots.ts @@ -391,6 +391,7 @@ const DYN_READ_ONLY = new Set([ "dyn.forInStable", "dyn.getOwnPropertyNames", "dyn.getPrototype", + "dyn.getPrototypeForIdentity", "dyn.hasKey", "dyn.hasOwn", "dyn.in", diff --git a/packages/compiler/src/ir/ir.ts b/packages/compiler/src/ir/ir.ts index 63286ff54..6d0760b81 100644 --- a/packages/compiler/src/ir/ir.ts +++ b/packages/compiler/src/ir/ir.ts @@ -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; diff --git a/packages/compiler/src/ir/js-ic.test.ts b/packages/compiler/src/ir/js-ic.test.ts index 1b566fd00..f71a1e9db 100644 --- a/packages/compiler/src/ir/js-ic.test.ts +++ b/packages/compiler/src/ir/js-ic.test.ts @@ -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 (!(v)) return `: 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(); +}); diff --git a/packages/compiler/src/ir/js-ic.ts b/packages/compiler/src/ir/js-ic.ts index 316b3c958..752316db3 100644 --- a/packages/compiler/src/ir/js-ic.ts +++ b/packages/compiler/src/ir/js-ic.ts @@ -170,6 +170,18 @@ export class JsIcSites { ); } + /** An early exit for values a class table never matches: `if (!(value)) return `, 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); diff --git a/packages/compiler/src/ir/validate.ts b/packages/compiler/src/ir/validate.ts index 0e990918b..02825f0de 100644 --- a/packages/compiler/src/ir/validate.ts +++ b/packages/compiler/src/ir/validate.ts @@ -4146,6 +4146,9 @@ function validateFunction( e.fn === "error.assign" || e.fn === "error.hasKey" || e.fn === "error.hasCode" || + e.fn === "error.ownAccessor" || + e.fn === "error.accessorGet" || + e.fn === "error.accessorSet" || e.fn === "error.defineCause" || e.fn === "error.deleteCause" ) { diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json new file mode 100644 index 000000000..6b31ee9ee --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6349-instanceof-subclass-capsules/main.ts.json @@ -0,0 +1,7 @@ +{ + "order": [ + "/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs", + "/tests/corpus/6349-instanceof-subclass-capsules/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json new file mode 100644 index 000000000..a2184f9a3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6923-instanceof-builtin-constructor-values.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6923-instanceof-builtin-constructor-values.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json new file mode 100644 index 000000000..54090ccc3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6924-instanceof-class-values-native.mjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6924-instanceof-class-values-native.mjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json new file mode 100644 index 000000000..20085dc0d --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6925-instanceof-dynamic-constructors.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6925-instanceof-dynamic-constructors.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json new file mode 100644 index 000000000..8bf8f11da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7263-error-accessor-object-helper.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7263-error-accessor-object-helper.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json new file mode 100644 index 000000000..c5179b475 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7541-prototype-identity-stand-ins.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7541-prototype-identity-stand-ins.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json new file mode 100644 index 000000000..09f1a6369 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7542-prototype-identity-without-data.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7542-prototype-identity-without-data.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7544-local-class-prototype-identity.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7544-local-class-prototype-identity.ts.json new file mode 100644 index 000000000..80cebd695 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7544-local-class-prototype-identity.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7544-local-class-prototype-identity.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json new file mode 100644 index 000000000..ef262841b --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7611-error-message-accessor.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7611-error-message-accessor.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json new file mode 100644 index 000000000..bc852b7bd --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7612-error-message-accessor-lazy.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7612-error-message-accessor-lazy.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json new file mode 100644 index 000000000..bbf827605 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7613-local-class-prototype-reflection.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7613-local-class-prototype-reflection.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json new file mode 100644 index 000000000..229a4e2ff --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7615-local-class-prototype-reflection-ts.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7615-local-class-prototype-reflection-ts.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json new file mode 100644 index 000000000..78fc40580 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7731-npm-any-string-index/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7731-npm-any-string-index/main.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json new file mode 100644 index 000000000..f6355a2ae --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7732-untyped-match-pattern.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7732-untyped-match-pattern.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json new file mode 100644 index 000000000..2b9778666 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7733-property-bag-late-prototype.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7733-property-bag-late-prototype.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json new file mode 100644 index 000000000..9ce0ed4f0 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7734-caught-instanceof-class-binding.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7734-caught-instanceof-class-binding.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json new file mode 100644 index 000000000..7ca5d34da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/7735-untyped-headers-iterators.js.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/7735-untyped-headers-iterators.js" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json new file mode 100644 index 000000000..1fd5083da --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/npm/cases/decl-pass-through/main.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/npm/cases/decl-pass-through/main.ts" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 210e4429b..b16d070c4 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -938,6 +938,9 @@ void *scr_dyn_obj_source_cast(const ScrDyn *d, ScrDyn *scr_dyn_new_obj_null_proto(void); ScrDyn *scr_dyn_obj_create(ScrDyn *prototype); ScrDyn *scr_dyn_obj_create_with_properties(ScrDyn *prototype, ScrDyn *descriptors); +ScrDyn *scr_dyn_get_prototype_for_identity(ScrDyn *object); +bool scr_dyn_bag_create_field(ScrDyn *bag, const ScrStr *key); +bool scr_dyn_is_class_capsule(const ScrDyn *d); ScrDyn *scr_dyn_get_prototype(ScrDyn *object); ScrDyn *scr_dyn_get_own_property_descriptors(ScrDyn *object); ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype); diff --git a/packages/runtime/src/scr_error.h b/packages/runtime/src/scr_error.h index a7c921f23..fbb64aad9 100644 --- a/packages/runtime/src/scr_error.h +++ b/packages/runtime/src/scr_error.h @@ -296,6 +296,9 @@ void scr_error_assign_dyn(ScrError *e, const struct ScrDyn *sources); /* `key in err` for a key no compiled class declares: a runtime-set own * property or an Object.prototype member. Borrows both (scr_json.c). */ bool scr_error_has_key(ScrError *e, const ScrStr *key); +bool scr_error_own_accessor(ScrError *e, const ScrStr *key); +struct ScrDyn *scr_error_accessor_get(ScrError *e, const ScrStr *key); +void scr_error_accessor_set(ScrError *e, const ScrStr *key, struct ScrDyn *value); bool scr_error_has_code(ScrError *e); const char *scr_error_message_code(const ScrStr *msg); ScrStr *scr_error_code(ScrError *e); diff --git a/packages/runtime/src/scr_fetch.c b/packages/runtime/src/scr_fetch.c index 96e90e47c..7cc2beef4 100644 --- a/packages/runtime/src/scr_fetch.c +++ b/packages/runtime/src/scr_fetch.c @@ -2491,11 +2491,44 @@ static bool sf_headers_sorted_entry(SfHeaders *h, size_t index, return found; } +/* One step of a live Headers iterator (entries, keys or values): the + * `*index`th entry of the current sorted list, as Node's iterator re-reads + * the list on every step. The result is { value, done }. */ +static ScrDyn *sf_headers_step(void *ptr, double *index, unsigned selection) { + ScrStr *name = NULL; + ScrStr *value = NULL; + bool found = sf_headers_sorted_entry(ptr, (size_t)*index, &name, &value); + ScrDyn *item; + if (found) { + (*index)++; + if (selection == 2) { + item = scr_dyn_new_arr(); + scr_dyn_arr_push(item, scr_dyn_new_str(name)); + scr_dyn_arr_push(item, scr_dyn_new_str(value)); + } else { + item = scr_dyn_new_str(selection == 1 ? name : value); + } + scr_str_release(name); + scr_str_release(value); + } else { + item = scr_dyn_retain(scr_dyn_undefined()); + } + ScrDyn *result = scr_dyn_new_obj(); + scr_dyn_obj_set(result, "value", 5, item); + scr_dyn_obj_set(result, "done", 4, scr_dyn_new_bool(!found)); + return result; +} + static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self, const char *method, ScrDyn *const *args, size_t argc, const char *what) { SfHeaders *h = ptr; + if (strcmp(method, "entries") == 0 || strcmp(method, "keys") == 0 || + strcmp(method, "values") == 0) { + return scr_dyn_native_handle_iterator( + self, strcmp(method, "entries") == 0 ? 2 : strcmp(method, "keys") == 0 ? 1 : 0); + } if (strcmp(method, "get") == 0 || strcmp(method, "has") == 0) { if (argc < 1) { sf_type_error("Headers.get/has requires a header name"); @@ -5487,7 +5520,8 @@ static const ScrDynHandleOps sf_response_ops = { &sf_response_invoke, &sf_response_get, &sf_no_set, NULL}; static const ScrDynHandleOps sf_headers_ops = { "Headers", &sf_headers_retain_v, &sf_headers_release_v, - &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL}; + &sf_headers_invoke, &sf_headers_get, &sf_no_set, NULL, NULL, + &sf_headers_step}; static void sf_teardown(void) { while (sf_live) { diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index aac38f0c1..44b653d2a 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -2241,6 +2241,26 @@ ScrDyn *scr_dyn_get_prototype(ScrDyn *object) { return NULL; } +/* scr_dyn_get_prototype for a walk that compares each prototype with a + * program class's prototype (instanceof against a class value): a native + * Date, RegExp, Map, Set, WeakMap or WeakSet, or a promise, has only builtin + * prototypes on its chain, and no program class extends these (a class + * extending Date makes instances of its own), so the walk ends there with + * null. */ +ScrDyn *scr_dyn_get_prototype_for_identity(ScrDyn *object) { + if (object->kind == SCR_DYN_PROMISE) return scr_dyn_new_null(); + if (object->kind == SCR_DYN_HANDLE) { + switch (object->v.handle.tag) { + case SCR_DYNH_DATE: case SCR_DYNH_REGEXP: case SCR_DYNH_MAP: case SCR_DYNH_SET: + case SCR_DYNH_WEAK_MAP: case SCR_DYNH_WEAK_SET: + return scr_dyn_new_null(); + default: + break; + } + } + return scr_dyn_get_prototype(object); +} + ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) { /* A primitive receiver is returned unchanged when the prototype is null or * any object or function; nothing is mutated, so the prototype's kind is the @@ -4120,28 +4140,33 @@ ScrDyn *scr_array_buffer_constructor(void) { /* `value instanceof C` for C a global constructor held as a value * (`{ instanceOf: Date }`, scr_dyn_global_constructor): 1 or 0 by the - * value's native kind, or -1 when callee is not one of them, or for Object - * and for class instances, which these kinds cannot answer (except Date, - * whose subclasses' instances are known). A typed record or array is none - * of these kinds. */ + * value's native kind, or -1 when callee is not one of them, or for Object. + * A typed record or array is none of these kinds, and neither is an + * instance of a program class: a class can extend Date (answered through + * scr_dyn_date_instance_is), and extending RegExp, Map, Set, WeakMap, + * WeakSet or Promise refuses where the class is defined. The error + * constructors (scr_dyn_error_constructor) answer -1. */ static int scr_dyn_global_constructor_instanceof(const ScrDyn *value, const ScrDyn *callee) { if (!callee->v.fn.sig || !callee->v.fn.name || strcmp(callee->v.fn.sig, "native:constructor") != 0 || strcmp(callee->v.fn.name, "Object") == 0) return -1; /* A native Date, or an instance of a class declared `extends Date`. */ if (strcmp(callee->v.fn.name, "Date") == 0) return scr_dyn_date_instance_is(value); - if (value->kind == SCR_DYN_TYPED_REF) - return strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value) ? 0 : -1; static const struct { const char *name; int tag; } kinds[] = { - {"Date", SCR_DYNH_DATE}, {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, + {"RegExp", SCR_DYNH_REGEXP}, {"Map", SCR_DYNH_MAP}, {"Set", SCR_DYNH_SET}, {"WeakMap", SCR_DYNH_WEAK_MAP}, {"WeakSet", SCR_DYNH_WEAK_SET}, {"FinalizationRegistry", SCR_DYNH_FINALIZATION_REGISTRY}, }; - if (strcmp(callee->v.fn.name, "Promise") == 0) return value->kind == SCR_DYN_PROMISE; - for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) { - if (strcmp(callee->v.fn.name, kinds[i].name) == 0) - return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == kinds[i].tag; + bool promise = strcmp(callee->v.fn.name, "Promise") == 0; + int tag = -1; + for (size_t i = 0; i < sizeof kinds / sizeof kinds[0]; i++) + if (strcmp(callee->v.fn.name, kinds[i].name) == 0) tag = kinds[i].tag; + if (value->kind == SCR_DYN_TYPED_REF) { + if (strncmp(value->v.typed_ref.type_key, "record:", 7) == 0 || scr_dyn_ref_is_array(value)) return 0; + return (promise || tag >= 0) && strncmp(value->v.typed_ref.type_key, "object:", 7) == 0 ? 0 : -1; } + if (promise) return value->kind == SCR_DYN_PROMISE; + if (tag >= 0) return value->kind == SCR_DYN_HANDLE && (int)value->v.handle.tag == tag; return -1; } @@ -4434,6 +4459,11 @@ ScrDyn *scr_dyn_class_prototype(ScrDyn *value, ScrDyn *base) { * the class object and no registry keeps it alive, so evaluating a * function-local class again releases the previous one's prototype. */ if (cls->name && cls->name->len) data->v.obj.class_name = cls->name->data; + /* Like every class prototype, it starts with its own non-enumerable + * `constructor`: the class value. The cycle it closes through the class + * object is one the collector traces. */ + scr_dyn_obj_set(data, "constructor", 11, scr_dyn_retain(value)); + data->v.obj.entries[data->v.obj.len - 1].enumerable = false; cls->prototype_data = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); scr_box_set_ref(cls->prototype_data, data); } @@ -8031,35 +8061,36 @@ ScrDyn *scr_errdyn_view_of(const ScrError *e) { static void scr_error_cause_drop_impl(void *obj); void scr_error_commit_dyn(ScrError *e, const ScrDyn *view) { - ScrDynEntry *name_entry = NULL; - for (size_t i = 0; i < view->v.obj.len; i++) { - ScrDynEntry *entry = &view->v.obj.entries[i]; - if (entry->key_len == 4 && memcmp(entry->key, "name", 4) == 0) { name_entry = entry; break; } - } - ScrDyn *name = name_entry ? name_entry->value : NULL; + ScrDynEntry *name_entry = scr_dyn_find_entry(view, "name", 4); + ScrDynEntry *message_entry = scr_dyn_find_entry(view, "message", 7); + /* An own accessor over `name` or `message` (zod's lazy error message) + * has no value to snapshot: the slot keeps its last value, and the + * property reads as absent so typed reads take the accessor path + * (scr_error_own_accessor) and toString asks the installed reader. */ + ScrDyn *name = name_entry && !name_entry->accessor ? name_entry->value : NULL; if (name && name->kind != SCR_DYN_STR) { scr_dyn_check_fail(NULL, "string", name); return; } - ScrStr *next_name = name ? scr_str_retain(name->v.str) : scr_error_default_name(e); - scr_str_release(e->name); - e->name = next_name; - e->name_present = name != NULL; - e->name_enumerable = name_entry && name_entry->enumerable; - ScrDyn *message = scr_dyn_obj_get(view, "message", 7); + ScrDyn *message = message_entry && !message_entry->accessor ? message_entry->value : NULL; if (message && message->kind != SCR_DYN_STR) { scr_dyn_check_fail(NULL, "string", message); return; } - ScrStr *next_message = message ? scr_str_retain(message->v.str) : scr_str_new("", 0); - scr_str_release(e->message); - e->message = next_message; - e->message_present = message != NULL; - e->message_enumerable = false; - for (size_t i = 0; message && i < view->v.obj.len; i++) { - const ScrDynEntry *entry = &view->v.obj.entries[i]; - if (entry->key_len == 7 && memcmp(entry->key, "message", 7) == 0) e->message_enumerable = entry->enumerable; + if (!name_entry || !name_entry->accessor) { + ScrStr *next_name = name ? scr_str_retain(name->v.str) : scr_error_default_name(e); + scr_str_release(e->name); + e->name = next_name; } + e->name_present = name != NULL; + e->name_enumerable = name_entry && name_entry->enumerable; + if (!message_entry || !message_entry->accessor) { + ScrStr *next_message = message ? scr_str_retain(message->v.str) : scr_str_new("", 0); + scr_str_release(e->message); + e->message = next_message; + } + e->message_present = message != NULL; + e->message_enumerable = message_entry && message_entry->enumerable; ScrDyn *stack = scr_dyn_obj_get(view, "stack", 5); if (stack && stack->kind == SCR_DYN_STR) { ScrStr *replacement = scr_str_retain(stack->v.str); @@ -8155,8 +8186,20 @@ static void scr_error_sync_slots(const ScrDyn *view, ScrDyn *obj, const ScrStr * return; } if (name || message) { - /* The native error's toString and inspect read these fields. */ - if (!entry || entry->accessor || entry->value->kind != SCR_DYN_STR) return; + /* The native error's toString and inspect read these fields. An own + * accessor leaves the slot, which then reads as absent: typed reads + * call it (scr_error_own_accessor). */ + if (entry && entry->accessor) { + if (name) { + e->name_present = false; + e->name_enumerable = entry->enumerable; + } else { + e->message_present = false; + e->message_enumerable = entry->enumerable; + } + return; + } + if (!entry || entry->value->kind != SCR_DYN_STR) return; ScrStr **field = name ? &e->name : &e->message; scr_str_release(*field); *field = scr_str_retain(entry->value->v.str); @@ -16065,6 +16108,61 @@ bool scr_error_has_code(ScrError *e) { * property set at runtime, which lives on the error's checked-dynamic node * (that node exists once anything reflected or wrote through it), or a * member of Object.prototype. Borrows both; never throws. */ +/* Whether the error's own `name` or `message` (the key) is an accessor a + * reflective definition installed on its checked-dynamic node (the slot + * then reads as absent): typed reads call its getter and typed writes its + * setter through that node. A class view keeps the accessor in its last + * materialized object. Borrows both; never throws. */ +static ScrDynEntry *scr_error_accessor_entry(ScrError *e, const ScrStr *key) { + const ScrDyn *view = scr_errdyn_view_of(e); + if (view != NULL && view->kind == SCR_DYN_TYPED_REF) view = view->v.typed_ref.materialized; + if (view == NULL || view->kind != SCR_DYN_OBJ) return NULL; + ScrDynEntry *entry = scr_dyn_find_entry(view, key->data, key->len); + return entry != NULL && entry->accessor ? entry : NULL; +} + +bool scr_error_own_accessor(ScrError *e, const ScrStr *key) { return scr_error_accessor_entry(e, key) != NULL; } + +/* A typed read of that accessor: its getter called on the error's node + * (the undefined value without one). Owned result, or NULL with the + * getter's exception pending. */ +ScrDyn *scr_error_accessor_get(ScrError *e, const ScrStr *key) { + ScrDynEntry *entry = scr_error_accessor_entry(e, key); + if (entry == NULL || entry->getter == NULL) return scr_dyn_retain(scr_dyn_undefined()); + ScrDyn *getter = scr_dyn_retain(entry->getter); + ScrDyn *receiver = scr_dyn_from_error(e); + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(getter, NULL, 0, "getter"); + scr_dyn_this_pop(); + scr_dyn_release(receiver); + scr_dyn_release(getter); + return result; +} + +/* A typed write of that accessor: its setter called on the error's node + * with the value, or V8's strict-mode TypeError without one. Borrows all; + * the setter's exception stays pending. */ +void scr_error_accessor_set(ScrError *e, const ScrStr *key, ScrDyn *value) { + ScrDynEntry *entry = scr_error_accessor_entry(e, key); + if (entry == NULL) return; + if (entry->setter == NULL) { + char message[256]; + int n = snprintf(message, sizeof message, "Cannot set property %.*s of %.*s which has only a getter", + (int)key->len, key->data, (int)e->name->len, e->name->data); + scr_throw_error_msg(SCR_ERR_TYPE, message, (size_t)n < sizeof message ? (size_t)n : sizeof message - 1); + return; + } + ScrDyn *setter = scr_dyn_retain(entry->setter); + ScrDyn *receiver = scr_dyn_from_error(e); + ScrDyn *args[1] = {value}; + scr_dyn_this_push_dyn(receiver); + ScrDyn *result = scr_dyn_call(setter, args, 1, "setter"); + scr_dyn_this_pop(); + scr_dyn_release(result); + scr_dyn_release(receiver); + scr_dyn_release(setter); +} + bool scr_error_has_key(ScrError *e, const ScrStr *key) { const ScrDyn *view = scr_errdyn_view_of(e); if (view != NULL && view->kind == SCR_DYN_OBJ && scr_dyn_find_entry(view, key->data, key->len) != NULL) @@ -16130,3 +16228,25 @@ 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) { + 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) { + 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; +} diff --git a/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs b/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs new file mode 100644 index 000000000..bf32f8550 --- /dev/null +++ b/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs @@ -0,0 +1,24 @@ +'use strict'; +class UndiciError extends Error { + constructor(message) { + super(message); + this.name = 'UndiciError'; + this.code = 'UND_ERR'; + } + get kind() { + return 'undici'; + } +} +class SocketError extends UndiciError { + constructor(message) { + super(message); + this.name = 'SocketError'; + } +} +class HTTPParserError extends Error { + constructor(message) { + super(message); + this.name = 'HTTPParserError'; + } +} +module.exports = { UndiciError, SocketError, HTTPParserError }; diff --git a/tests/corpus/6349-instanceof-subclass-capsules/main.ts b/tests/corpus/6349-instanceof-subclass-capsules/main.ts new file mode 100644 index 000000000..59a545198 --- /dev/null +++ b/tests/corpus/6349-instanceof-subclass-capsules/main.ts @@ -0,0 +1,27 @@ +// `instanceof` against a JavaScript class reads the prototype chain of a +// value that crossed into the dynamic tier at a base static type (here +// Error): the value is its exact class at runtime, including a subclass +// without methods of its own, which answers false rather than throwing. +import errors from "./errors.cjs"; + +function describe(err: unknown): string { + if (err instanceof errors.UndiciError) return `undici ${err.name}`; + if (err instanceof Error) return `error ${err.name}`; + return "other"; +} +function isSocket(err: unknown): boolean { + return err instanceof errors.SocketError; +} +function fail(kind: number): Error { + if (kind === 0) return new errors.HTTPParserError("bad chunk"); + if (kind === 1) return new errors.SocketError("closed"); + return new RangeError("range"); +} +for (let kind = 0; kind < 3; kind++) { + const err: Error = fail(kind); + console.log(describe(err), isSocket(err)); +} +const base: Error = new errors.UndiciError("base"); +console.log(describe(base), isSocket(base)); +const rejected = Promise.reject(fail(0)); +rejected.catch((err: unknown) => console.log("rejected", describe(err))); diff --git a/tests/corpus/6923-instanceof-builtin-constructor-values.mjs b/tests/corpus/6923-instanceof-builtin-constructor-values.mjs new file mode 100644 index 000000000..929b048e3 --- /dev/null +++ b/tests/corpus/6923-instanceof-builtin-constructor-values.mjs @@ -0,0 +1,13 @@ +// `instanceof` against builtin constructors held as values, with instances of +// program classes and native values on the left. +class Dog { + constructor(name) { + this.name = name; + } +} +class Puppy extends Dog {} +const kinds = [Map, Set, RegExp, Promise, WeakMap, WeakSet, Date]; +const values = []; +values.push(new Dog("rex"), new Puppy("bit"), new Map(), new Set(), /x/g, Promise.resolve(1)); +values.push(new WeakMap(), new WeakSet(), new Date(0), { name: "plain" }, [1, 2]); +for (const C of kinds) console.log(C.name, values.map((v) => (v instanceof C ? 1 : 0)).join("")); diff --git a/tests/corpus/6924-instanceof-class-values-native.mjs b/tests/corpus/6924-instanceof-class-values-native.mjs new file mode 100644 index 000000000..82e8bdcf2 --- /dev/null +++ b/tests/corpus/6924-instanceof-class-values-native.mjs @@ -0,0 +1,19 @@ +// `instanceof` against program classes held as values, with native builtin +// values on the left: their prototype chains hold no program class. +class Animal { + constructor(name) { + this.name = name; + } +} +class Dog extends Animal {} +class Cat extends Animal {} +const classes = { Animal: Animal, Dog: Dog, Cat: Cat }; +const values = []; +values.push(new Map(), new Set(), /x/, Promise.resolve(1), new Date(0), new WeakMap(), new WeakSet()); +values.push(new Dog("rex"), new Cat("tom"), new Animal("any"), {}, [], null); +for (const name of Object.keys(classes)) { + const C = classes[name]; + console.log(name, values.map((v) => (v instanceof C ? 1 : 0)).join("")); +} +const holder = { type: Dog }; +console.log(new Map() instanceof holder.type, new Dog("a") instanceof holder.type); diff --git a/tests/corpus/6925-instanceof-dynamic-constructors.ts b/tests/corpus/6925-instanceof-dynamic-constructors.ts new file mode 100644 index 000000000..0a39b92b2 --- /dev/null +++ b/tests/corpus/6925-instanceof-dynamic-constructors.ts @@ -0,0 +1,64 @@ +// `instanceof` in TypeScript against right-hand sides that are not class names: +// constructor-typed parameters, constructors read from tables as any, builtin +// constructors held as values, and class values held in properties. +class Animal { + name: string; + constructor(name: string) { this.name = name; } +} +class Dog extends Animal { + bark(): string { return this.name + " barks"; } +} +class Plain { v = 1; } + +// Constructor-typed parameters. +function isA(value: unknown, C: new (...args: never[]) => unknown): boolean { + return value instanceof C; +} +const dog = new Dog("rex"); +console.log(isA(dog, Animal), isA(dog, Dog), isA(new Animal("a"), Dog), isA(new Plain(), Animal), isA(dog, Plain)); +console.log(isA(new Date(0), Date), isA(new Map(), Map), isA({}, Map), isA("text", Animal), isA(null, Animal)); + +// Narrowing through a constructor-typed value. +function describe(value: Animal, C: typeof Dog): string { + if (value instanceof C) return value.bark(); + return value.name; +} +console.log(describe(dog, Dog), describe(new Animal("cat"), Dog)); + +// A table of constructors read as any. +const table: Record = { Date, Map, Set, RegExp, Promise, WeakMap, WeakSet, Dog }; +const samples: unknown[] = [new Date(0), new Map(), new Set(), /x/, Promise.resolve(1), new WeakMap(), new WeakSet(), [1], dog, { a: 1 }]; +for (const name of Object.keys(table)) { + const C = table[name]; + console.log(name, samples.map((s) => (s instanceof C ? 1 : 0)).join("")); +} + +// A right-hand side that is not callable throws Node's TypeError. +const notCallable: any = {}; +try { + console.log(dog instanceof notCallable); +} catch (e) { + console.log((e as Error).name, (e as Error).message); +} + +// Typed native values passed as unknown, against builtin constructors held as any. +const typedValues: [string, unknown][] = [ + ["map", new Map([["a", 1]])], + ["set", new Set([1])], + ["regex", /a/g], + ["promise", Promise.resolve(1)], + ["weak", new WeakMap()], + ["date", new Date(5)], + ["array", [1]], + ["record", { a: 1 }], +]; +const builtins: Record = { Map, Set, RegExp, Promise, WeakMap, Date }; +for (const [label, value] of typedValues) { + console.log(label, Object.keys(builtins).map((name) => (value instanceof builtins[name] ? 1 : 0)).join("")); +} + +// Class values held in properties, against native values and instances. +const holder = { type: Dog, base: Animal }; +const map = new Map(); +const pattern = /x/; +console.log(map instanceof holder.type, pattern instanceof holder.base, dog instanceof holder.type, dog instanceof holder.base); diff --git a/tests/corpus/7263-error-accessor-object-helper.ts b/tests/corpus/7263-error-accessor-object-helper.ts new file mode 100644 index 000000000..8e8a4a532 --- /dev/null +++ b/tests/corpus/7263-error-accessor-object-helper.ts @@ -0,0 +1,54 @@ +// An accessor installed over an Error's own `message` and `name` by +// helpers whose target parameter is typed `object`, `{}` or generic: +// typed reads, writes and String() go through the accessors. +class ValidationError extends Error {} + +function defineLazyMessage(target: object, compute: () => string): void { + let cached: string | undefined; + Object.defineProperty(target, "message", { + get() { + cached ??= compute(); + return cached; + }, + set(value: string) { + cached = `set:${value}`; + }, + enumerable: true, + configurable: true, + }); +} + +function defineName(target: {}, name: string): void { + Object.defineProperty(target, "name", { get: () => name, configurable: true }); +} + +function defineFixedMessage(target: T, message: string): T { + Object.defineProperty(target, "message", { get: () => message, configurable: true }); + return target; +} + +let computed = 0; +const e = new ValidationError("orig"); +defineLazyMessage(e, () => { + computed++; + return "too small"; +}); +console.log(e.message, computed); +console.log(String(e), computed); +e.message = "typed"; +console.log(e.message, Object.keys(e).join()); + +const named = new ValidationError("bad input"); +defineName(named, "ZodError"); +console.log(named.name, String(named)); +const fixed = defineFixedMessage(new ValidationError("orig"), "fixed"); +console.log(fixed.message, String(fixed)); + +const plain = new Error("start"); +defineLazyMessage(plain, () => "computed"); +console.log(plain.message, `${plain}`); +try { + throw plain; +} catch (caught) { + console.log(caught instanceof Error && caught.message); +} diff --git a/tests/corpus/7541-prototype-identity-stand-ins.ts b/tests/corpus/7541-prototype-identity-stand-ins.ts new file mode 100644 index 000000000..81b1e8c71 --- /dev/null +++ b/tests/corpus/7541-prototype-identity-stand-ins.ts @@ -0,0 +1,22 @@ +// Instances held as unknown, of a class whose chain has no prototype data +// (a subclass without methods, or one extending Error), still share one +// prototype: comparing the prototypes of two instances of the same class is +// true, and of instances of different classes is false. +class Base { + tag = "b"; +} +class Sub extends Base {} +class Other extends Base {} +class Oops extends Error {} +const bases: Base[] = [new Sub(), new Sub(), new Other()]; +const errors: Error[] = [new Oops("a"), new Oops("b")]; +const first: unknown = bases[0]; +const second: unknown = bases[1]; +const other: unknown = bases[2]; +const e1: unknown = errors[0]; +const e2: unknown = errors[1]; +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(second)); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(other)); +console.log(Object.getPrototypeOf(e1) === Object.getPrototypeOf(e2)); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(e1)); +console.log(first instanceof Sub, other instanceof Sub, e1 instanceof Oops, e1 instanceof Error); diff --git a/tests/corpus/7542-prototype-identity-without-data.ts b/tests/corpus/7542-prototype-identity-without-data.ts new file mode 100644 index 000000000..9ac1209f8 --- /dev/null +++ b/tests/corpus/7542-prototype-identity-without-data.ts @@ -0,0 +1,19 @@ +// Instances held as unknown, of classes whose chains have no prototype data +// a program can name (no methods, or a subclass of Error): each class's +// instances share one prototype, and different classes' differ, as in Node. +class Base { + tag = "b"; +} +class Sub extends Base {} +class Other extends Base {} +class Oops extends Error {} +const bases: Base[] = [new Sub(), new Sub(), new Other(), new Base()]; +const errors: Error[] = [new Oops("a"), new Oops("b")]; +const [a, b, c, d] = bases.map((value): unknown => value); +const e1: unknown = errors[0]; +const e2: unknown = errors[1]; +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(c)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(d)); +console.log(Object.getPrototypeOf(e1) === Object.getPrototypeOf(e2)); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(e1)); diff --git a/tests/corpus/7544-local-class-prototype-identity.ts b/tests/corpus/7544-local-class-prototype-identity.ts new file mode 100644 index 000000000..fc465f0ff --- /dev/null +++ b/tests/corpus/7544-local-class-prototype-identity.ts @@ -0,0 +1,23 @@ +// A function-local class is a new class at each evaluation of its +// declaration: instances from separate calls of the factory have different +// prototypes, while instances from one call share theirs. +function make() { + class Local { + tag = "l"; + } + return new Local(); +} +const first: unknown = make(); +const second: unknown = make(); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(second)); +console.log(Object.getPrototypeOf(first) === Object.getPrototypeOf(first)); +function pair() { + class Twin { + n = 1; + } + return [new Twin(), new Twin()]; +} +const [t1, t2] = pair().map((value): unknown => value); +const [t3] = pair().map((value): unknown => value); +console.log(Object.getPrototypeOf(t1) === Object.getPrototypeOf(t2)); +console.log(Object.getPrototypeOf(t1) === Object.getPrototypeOf(t3)); diff --git a/tests/corpus/7611-error-message-accessor.ts b/tests/corpus/7611-error-message-accessor.ts new file mode 100644 index 000000000..c4528e488 --- /dev/null +++ b/tests/corpus/7611-error-message-accessor.ts @@ -0,0 +1,43 @@ +// An accessor defined over an Error subclass instance's own `message` +// (zod 4's lazy error message): typed reads and writes, String(), and +// reads through an untyped alias all go through the accessor, and a later +// data definition replaces it. +class E extends Error {} +const e = new E("orig"); +let store = "lazy"; +let reads = 0; +Object.defineProperty(e, "message", { + get() { + reads++; + return store; + }, + set(value: string) { + store = `set:${value}`; + }, + enumerable: true, + configurable: true, +}); +console.log(e.message, String(e), reads); +e.message = "typed"; +console.log(e.message, store); +const loose: any = e; +console.log(loose.message); +loose.message = "loose"; +console.log(e.message, Object.keys(e).join()); +const desc = Object.getOwnPropertyDescriptor(e, "message")!; +console.log(typeof desc.get, typeof desc.set, desc.enumerable); +Object.defineProperty(e, "message", { value: "data", writable: true, configurable: true }); +console.log(e.message, String(e), reads); +e.message = "after"; +console.log(e.message, store); + +class Named extends Error {} +const n = new Named("m"); +const loosen: any = n; +Object.defineProperty(loosen, "name", { get() { return "Custom"; }, configurable: true }); +console.log(n.name, String(n)); + +class Plain extends Error {} +const p = new Plain("kept"); +p.message = "changed"; +console.log(p.message, String(p)); diff --git a/tests/corpus/7612-error-message-accessor-lazy.js b/tests/corpus/7612-error-message-accessor-lazy.js new file mode 100644 index 000000000..26d54c09c --- /dev/null +++ b/tests/corpus/7612-error-message-accessor-lazy.js @@ -0,0 +1,36 @@ +// zod 4's lazy error message: an initializer defines a shared accessor +// descriptor over `message` on an instance whose class extends a runtime +// Error parent, and the message is computed on first read and cached. +function _getMessage() { + const internals = this._zod; + internals.message ?? (internals.message = JSON.stringify(internals.def)); + return internals.message; +} +function _setMessage(value) { + this._zod.message = value; +} +const _messageDesc = { get: _getMessage, set: _setMessage, enumerable: true, configurable: true }; +function initializer(inst, def) { + Object.defineProperty(inst, "_zod", { value: { def }, enumerable: false }); + inst.name = "$ZodError"; + Object.defineProperty(inst, "message", _messageDesc); +} +function make(Parent, def) { + class Definition extends Parent {} + const inst = new Definition(); + initializer(inst, def); + return inst; +} +const err = make(Error, [{ code: "too_small", path: ["a"] }]); +console.log(err._zod.message, err.message, err._zod.message === err.message); +console.log(String(err), Object.keys(err).join()); +err.message = "replaced"; +console.log(err.message, err._zod.message, `${err}`); +try { + throw err; +} catch (caught) { + console.log(caught === err, caught.message, caught instanceof Error); +} +const plain = new Error("start"); +Object.defineProperty(plain, "message", { get() { return "computed"; }, configurable: true }); +console.log(plain.message, String(plain)); diff --git a/tests/corpus/7613-local-class-prototype-reflection.js b/tests/corpus/7613-local-class-prototype-reflection.js new file mode 100644 index 000000000..a48bd305c --- /dev/null +++ b/tests/corpus/7613-local-class-prototype-reflection.js @@ -0,0 +1,55 @@ +// Prototype reflection on instances of a function-local class without +// methods (zod 4's `$constructor` Definition): the prototype is the class's +// shared prototype object, it chains to the parent's, and members defined +// on it reach every instance. +const installed = new WeakSet(); +function factory(name, Parent) { + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + return function make(label) { + const inst = new Definition(); + inst.label = label; + const proto = Object.getPrototypeOf(inst); + console.log(name, proto === Definition.prototype, proto === Object.getPrototypeOf(new Definition())); + if (!installed.has(proto)) { + installed.add(proto); + Object.defineProperty(proto, "describe", { + configurable: true, + enumerable: false, + get() { + return () => `${name}:${this.label}`; + }, + }); + console.log("installed", name); + } + return inst; + }; +} +const makeError = factory("Failure", Error); +const makePlain = factory("Thing", Object); +const a = makeError("a"); +const b = makeError("b"); +const c = makePlain("c"); +console.log(a.describe(), b.describe(), c.describe()); +console.log(a instanceof Error, c instanceof Error, Object.keys(a).join(), Object.keys(c).join()); +console.log(Object.getPrototypeOf(a) === Object.getPrototypeOf(b), Object.getPrototypeOf(a) === Object.getPrototypeOf(c)); +console.log(typeof Object.getPrototypeOf(c).describe, "describe" in a, Object.hasOwn(a, "describe")); + +// The prototype has its own non-enumerable `constructor`, the class value +// of that evaluation, as every class prototype does. +function local(Parent) { + class Definition extends Parent {} + const proto = Object.getPrototypeOf(new Definition()); + const descriptor = Object.getOwnPropertyDescriptor(proto, "constructor"); + console.log( + proto.constructor === Definition, + Object.hasOwn(proto, "constructor"), + JSON.stringify(Object.getOwnPropertyNames(proto)), + descriptor.value === Definition, + descriptor.writable, + descriptor.enumerable, + descriptor.configurable, + ); + return Definition; +} +console.log(local(Object) !== local(Error)); diff --git a/tests/corpus/7615-local-class-prototype-reflection-ts.ts b/tests/corpus/7615-local-class-prototype-reflection-ts.ts new file mode 100644 index 000000000..92d3b5668 --- /dev/null +++ b/tests/corpus/7615-local-class-prototype-reflection-ts.ts @@ -0,0 +1,14 @@ +// Prototype reflection on an instance of a TypeScript function-local class +// without methods: one shared prototype per class evaluation. +function make(label: string): { label: string } { + class Box { + label = ""; + } + const box = new Box(); + box.label = label; + return box; +} +const a = make("a"); +const b = make("b"); +const pa = Object.getPrototypeOf(a); +console.log(pa === Object.getPrototypeOf(a), pa === Object.getPrototypeOf(b), typeof pa, Object.keys(a).join()); diff --git a/tests/corpus/7731-npm-any-string-index/main.ts b/tests/corpus/7731-npm-any-string-index/main.ts new file mode 100644 index 000000000..21e7d1c10 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/main.ts @@ -0,0 +1,9 @@ +// An implicit-any package function specialized for a string argument indexes +// the string (`base?.[0]`), as hono's mergePath does. +import { App, mergePath } from "merge-path"; +console.log(new App().request("x")); +console.log(new App().request(new URL("http://h/p"))); +console.log(new App().basePath("/api").basePath("v1/")._basePath); +console.log(mergePath("/api/", "/"), mergePath("a", "b", "/c")); +console.log(new App().basePath("").basePath("").request("")); +console.log(mergePath("", "x"), mergePath("/", "/"), mergePath("//", "y/")); diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts new file mode 100644 index 000000000..aa5142307 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.d.ts @@ -0,0 +1,2 @@ +export declare const mergePath: (...paths: string[]) => string; +export declare class App { _basePath: string; request: (input: string | URL) => string; basePath(path: string): App; } diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js new file mode 100644 index 000000000..6a5c7c86b --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/index.js @@ -0,0 +1,10 @@ +import { mergePath } from "./url.js"; +class App { + _basePath = "/"; + request = (input) => { + input = input.toString(); + return mergePath("/", input); + }; + basePath(path) { const s = new App(); s._basePath = mergePath(this._basePath, path); return s; } +} +export { App, mergePath }; diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json new file mode 100644 index 000000000..ddc3db4ef --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/package.json @@ -0,0 +1 @@ +{ "name": "merge-path", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" } diff --git a/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js new file mode 100644 index 000000000..e27469714 --- /dev/null +++ b/tests/corpus/7731-npm-any-string-index/node_modules/merge-path/url.js @@ -0,0 +1,10 @@ +/** +* Merge paths. +* @param {string[]} ...paths - The paths to merge. +* @returns {string} The merged path. +*/ +const mergePath = (base, sub, ...rest) => { + if (rest.length) sub = mergePath(sub, ...rest); + return `${base?.[0] === "/" ? "" : "/"}${base}${sub === "/" ? "" : `${base?.[base.length - 1] === "/" ? "" : "/"}${sub?.[0] === "/" ? sub.slice(1) : sub}`}`; +}; +export { mergePath }; diff --git a/tests/corpus/7732-untyped-match-pattern.js b/tests/corpus/7732-untyped-match-pattern.js new file mode 100644 index 000000000..2079bb8ef --- /dev/null +++ b/tests/corpus/7732-untyped-match-pattern.js @@ -0,0 +1,15 @@ +// String.prototype.match and search with an untyped pattern: a RegExp is used as is, +// any other value builds new RegExp(value), as hono's matcher does. +const matchers = { GET: [/^\/a\/([^/]+)$()|^\/b$()/, [0, 1], {}] }; +function match(method, path) { + const matcher = matchers[method] || matchers["ALL"]; + const m = path.match(matcher[0]); + if (!m) return null; + return [m.indexOf("", 1), m.length, m[1], m.index]; +} +console.log(JSON.stringify(match("GET", "/a/xy"))); +console.log(JSON.stringify(match("GET", "/b"))); +console.log(JSON.stringify(match("GET", "/c"))); +const g = { re: /o/g, s: "o.", n: null }; +function m2(s, k) { return s.match(g[k]); } +console.log(JSON.stringify(m2("foo boo", "re")), JSON.stringify(m2("go.o", "s")), JSON.stringify(m2("null", "n"))); diff --git a/tests/corpus/7733-property-bag-late-prototype.js b/tests/corpus/7733-property-bag-late-prototype.js new file mode 100644 index 000000000..f88212b62 --- /dev/null +++ b/tests/corpus/7733-property-bag-late-prototype.js @@ -0,0 +1,52 @@ +// A class whose property bag is first created without prototype data, while a local +// class with an untyped base rewrites the shared bag dispatch: the store gains the +// prototype later, so inherited methods stay readable (hono's SmartRouter with zod). +function $constructor(name, initializer, params) { + const Parent = params?.Parent ?? Object; + class Definition extends Parent {} + Object.defineProperty(Definition, "name", { value: name }); + function _(def) { + const inst = params?.Parent ? new Definition() : this; + inst.def = def; + initializer(inst, def); + return inst; + } + Object.defineProperty(_, "name", { value: name }); + return _; +} +class Smart { + name = "S"; + #inner = []; + constructor(init) { this.#inner = init.routers; } + add(x) { this.#inner.push(x); } + match(m) { + const r = this.#inner[0]; + this.match = r.match.bind(r); + this.name = `S + ${r.name}`; + return "first " + m; + } +} +class Inner { name = "Inner"; match(m) { return "inner " + m; } } +class App { + router; + constructor(options = {}) { + const { strict, ...rest } = options; + Object.assign(this, rest); + this.router = options.router ?? new Smart({ routers: [new Inner()] }); + } + dispatch(m) { return this.router.match(m); } +} +const app = new App(); +console.log(Object.keys(app.router).join(",")); +const $Err = $constructor("$Err", (inst, def) => { inst.issues = def.issues; }, { Parent: Error }); +const e = new $Err({ issues: [1] }); +console.log(Object.keys(e).join(","), e.issues.length); +const run = [(a) => a.dispatch("a"), (a) => a.dispatch("b")]; +for (const f of run) console.log(f(app), app.router.name); +const holder = JSON.parse("{}"); +holder.r = new Smart({ routers: [new Inner()] }); +console.log(typeof holder.r.match, "match" in holder.r, Object.keys(holder.r).join(",")); +console.log(holder.r.match("c"), holder.r.match("d"), holder.r.name); +const scratch = JSON.parse("{\"name\":\"x\",\"other\":1}"); +delete scratch.name; +console.log(Object.keys(scratch).join(",")); diff --git a/tests/corpus/7734-caught-instanceof-class-binding.js b/tests/corpus/7734-caught-instanceof-class-binding.js new file mode 100644 index 000000000..4332daab0 --- /dev/null +++ b/tests/corpus/7734-caught-instanceof-class-binding.js @@ -0,0 +1,42 @@ +// A catch binding tested against a class held by a binding (var/const class expression), +// as hono's SmartRouter tests UnsupportedPathError. +var UnsupportedPathError = class extends Error {}; +class Strict { + add(p) { if (p.includes("*")) throw new UnsupportedPathError(); return p; } +} +class Loose { add(p) { return "loose " + p; } } +function pick(p) { + const routers = [new Strict(), new Loose()]; + for (const r of routers) { + try { + return r.add(p); + } catch (e) { + if (e instanceof UnsupportedPathError) continue; + throw e; + } + } +} +console.log(pick("/a"), pick("/b/*")); +try { throw new Error("x"); } catch (e) { console.log(e instanceof UnsupportedPathError, e instanceof Error); } +const u = new UnsupportedPathError("m"); +console.log(u instanceof UnsupportedPathError, u.message, u.name, u instanceof Error); + +// A binding that holds a subclass of the class it was declared with: the +// test reads the class the binding holds at run time. +class BaseError extends Error {} +class DerivedError extends BaseError {} +class LeafError extends DerivedError {} +var Current = BaseError; +function caughtIs(value) { + try { + throw value; + } catch (e) { + return e instanceof Current; + } +} +const samples = () => [new BaseError("b"), new DerivedError("d"), new LeafError("l"), new Error("e"), "text"]; +console.log("base", samples().map(caughtIs).join(",")); +Current = DerivedError; +console.log("derived", samples().map(caughtIs).join(",")); +Current = LeafError; +console.log("leaf", samples().map(caughtIs).join(",")); diff --git a/tests/corpus/7735-untyped-headers-iterators.js b/tests/corpus/7735-untyped-headers-iterators.js new file mode 100644 index 000000000..40937fe5c --- /dev/null +++ b/tests/corpus/7735-untyped-headers-iterators.js @@ -0,0 +1,23 @@ +// Headers entries/keys/values on an untyped Headers value: live iterators over the +// sorted list, with set-cookie values one by one, as hono's Context copies headers. +const holder = JSON.parse("{}"); +holder.res = new Response("body", { headers: { "x-b": "2", "X-A": "1", "set-cookie": "s=1" } }); +holder.res.headers.append("set-cookie", "t=2"); +const headers = holder.res.headers; +for (const [k, v] of headers.entries()) console.log("entry", k, v); +console.log(JSON.stringify([...headers.keys()]), JSON.stringify([...headers.values()])); +const it = headers.entries(); +console.log(JSON.stringify(it.next())); +headers.append("a-first", "0"); +headers.append("z-last", "9"); +console.log(JSON.stringify(it.next()), JSON.stringify(it.next())); +const rest = []; +for (const entry of it) rest.push(entry.join("=")); +console.log(rest.join(" "), JSON.stringify(it.next())); +const copy = JSON.parse("{}"); +copy.res = new Response(null, { status: 204 }); +for (const [k, v] of headers.entries()) { + if (k === "set-cookie") continue; + copy.res.headers.set(k, v); +} +console.log(JSON.stringify([...copy.res.headers.entries()])); diff --git a/tests/fixtures/npm/cases/decl-pass-through/main.ts b/tests/fixtures/npm/cases/decl-pass-through/main.ts new file mode 100644 index 000000000..a8168edc4 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/main.ts @@ -0,0 +1,16 @@ +// Package functions declared to return their argument's own type +// (`(obj: T) => T`) hand back the argument, or a proxy +// standing in for it, and the package tracks that object's identity: those +// results stay the package's objects. Reads of data the declarations +// describe (`point(): Point`) are still typed. +import { byReference, equals, mark, point } from "ref-marks"; + +const a = byReference({ n: 42 }); +const b = byReference({ n: 42 }); +console.log(equals(a, a), equals(a, b), a.n + b.n); +const m = mark({ n: 1 }); +const m2 = mark({ n: 1 }); +console.log(equals(m, m), equals(m, m2)); +const p = point(1, 2); +const q = point(1, 2); +console.log(equals(p, q), p.x + q.y); diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts new file mode 100644 index 000000000..aa0df42f4 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.d.ts @@ -0,0 +1,8 @@ +export interface Point { + x: number; + y: number; +} +export declare function byReference(obj: T): T; +export declare const mark: (obj: T) => T; +export declare function equals(a: unknown, b: unknown): boolean; +export declare function point(x: number, y: number): Point; diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js new file mode 100644 index 000000000..796efb19b --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/lib/index.js @@ -0,0 +1,18 @@ +const marked = new WeakSet(); +export function byReference(obj) { + const proxy = new Proxy(obj, {}); + marked.add(proxy); + return proxy; +} +export function mark(obj) { + marked.add(obj); + return obj; +} +export function equals(a, b) { + if (a === b) return true; + if (marked.has(a) || marked.has(b)) return false; + return JSON.stringify(a) === JSON.stringify(b); +} +export function point(x, y) { + return { x, y }; +} diff --git a/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json new file mode 100644 index 000000000..5ae95f315 --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/node_modules/ref-marks/package.json @@ -0,0 +1 @@ +{ "name": "ref-marks", "version": "1.0.0", "type": "module", "main": "./lib/index.js", "types": "./lib/index.d.ts" } diff --git a/tests/fixtures/npm/cases/decl-pass-through/package.json b/tests/fixtures/npm/cases/decl-pass-through/package.json new file mode 100644 index 000000000..4895e9aec --- /dev/null +++ b/tests/fixtures/npm/cases/decl-pass-through/package.json @@ -0,0 +1 @@ +{ "name": "decl-pass-through-case", "private": true, "type": "module" } diff --git a/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json b/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json new file mode 100644 index 000000000..6474582a0 --- /dev/null +++ b/tests/harness/corpus-costs/6349-instanceof-subclass-capsules/main.ts.json @@ -0,0 +1 @@ +{ "plain": 1600, "san": 2000 } diff --git a/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json b/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json new file mode 100644 index 000000000..53f54c1dd --- /dev/null +++ b/tests/harness/corpus-costs/6923-instanceof-builtin-constructor-values.mjs.json @@ -0,0 +1 @@ +{ "plain": 3810, "san": 5921 } diff --git a/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json b/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json new file mode 100644 index 000000000..8de22d657 --- /dev/null +++ b/tests/harness/corpus-costs/6924-instanceof-class-values-native.mjs.json @@ -0,0 +1 @@ +{ "plain": 3033, "san": 4212 } diff --git a/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json b/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json new file mode 100644 index 000000000..e2d372e0c --- /dev/null +++ b/tests/harness/corpus-costs/6925-instanceof-dynamic-constructors.ts.json @@ -0,0 +1 @@ +{ "plain": 3097, "san": 4707 } diff --git a/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json b/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json new file mode 100644 index 000000000..b56ad904d --- /dev/null +++ b/tests/harness/corpus-costs/7263-error-accessor-object-helper.ts.json @@ -0,0 +1 @@ +{ "plain": 2600, "san": 4000 } diff --git a/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json b/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json new file mode 100644 index 000000000..046e0461c --- /dev/null +++ b/tests/harness/corpus-costs/7541-prototype-identity-stand-ins.ts.json @@ -0,0 +1 @@ +{ "plain": 4700, "san": 6500 } diff --git a/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json b/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json new file mode 100644 index 000000000..d2c83f967 --- /dev/null +++ b/tests/harness/corpus-costs/7542-prototype-identity-without-data.ts.json @@ -0,0 +1 @@ +{ "plain": 2500, "san": 3300 } diff --git a/tests/harness/corpus-costs/7544-local-class-prototype-identity.ts.json b/tests/harness/corpus-costs/7544-local-class-prototype-identity.ts.json new file mode 100644 index 000000000..87dec518c --- /dev/null +++ b/tests/harness/corpus-costs/7544-local-class-prototype-identity.ts.json @@ -0,0 +1 @@ +{ "plain": 3900, "san": 5100 } diff --git a/tests/harness/corpus-costs/7611-error-message-accessor.ts.json b/tests/harness/corpus-costs/7611-error-message-accessor.ts.json new file mode 100644 index 000000000..8c97e80f9 --- /dev/null +++ b/tests/harness/corpus-costs/7611-error-message-accessor.ts.json @@ -0,0 +1 @@ +{ "plain": 2962, "san": 3600 } diff --git a/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json b/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json new file mode 100644 index 000000000..bbc81f519 --- /dev/null +++ b/tests/harness/corpus-costs/7612-error-message-accessor-lazy.js.json @@ -0,0 +1 @@ +{ "plain": 2217, "san": 2721 } diff --git a/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json b/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json new file mode 100644 index 000000000..9c4d9369a --- /dev/null +++ b/tests/harness/corpus-costs/7613-local-class-prototype-reflection.js.json @@ -0,0 +1 @@ +{ "plain": 1967, "san": 2510 } diff --git a/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json b/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json new file mode 100644 index 000000000..2e0a455c2 --- /dev/null +++ b/tests/harness/corpus-costs/7615-local-class-prototype-reflection-ts.ts.json @@ -0,0 +1 @@ +{ "plain": 1466, "san": 2143 } diff --git a/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json b/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json new file mode 100644 index 000000000..9fe9ad101 --- /dev/null +++ b/tests/harness/corpus-costs/7731-npm-any-string-index/main.ts.json @@ -0,0 +1 @@ +{ "plain": 6200, "san": 5800 } diff --git a/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json b/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json new file mode 100644 index 000000000..2de3f6b94 --- /dev/null +++ b/tests/harness/corpus-costs/7732-untyped-match-pattern.js.json @@ -0,0 +1 @@ +{ "plain": 3600, "san": 3300 } diff --git a/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json b/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json new file mode 100644 index 000000000..706d1a2a5 --- /dev/null +++ b/tests/harness/corpus-costs/7733-property-bag-late-prototype.js.json @@ -0,0 +1 @@ +{ "plain": 4400, "san": 3900 } diff --git a/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json b/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json new file mode 100644 index 000000000..71aa92bd3 --- /dev/null +++ b/tests/harness/corpus-costs/7734-caught-instanceof-class-binding.js.json @@ -0,0 +1 @@ +{ "plain": 3300, "san": 3600 } diff --git a/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json b/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json new file mode 100644 index 000000000..8836ebdf7 --- /dev/null +++ b/tests/harness/corpus-costs/7735-untyped-headers-iterators.js.json @@ -0,0 +1 @@ +{ "plain": 4000, "san": 4000 } diff --git a/tests/harness/coverage.test.ts b/tests/harness/coverage.test.ts index 3ed562aea..e1831c609 100644 --- a/tests/harness/coverage.test.ts +++ b/tests/harness/coverage.test.ts @@ -561,13 +561,14 @@ test("types only a package's .d.ts declares stay static through re-exports and . // shape-kinds re-exports types its .d.ts entry imports; row-module's // runtime entry is an .mjs file. Without their declared types the // program's uses of Form, Kind and Row run in the dynamic tier (12 - // statements); with them only the packages' own JavaScript and one - // arithmetic on a JavaScript return value do (4). + // statements); with them only the packages' own JavaScript does (3). + // readRows's result keeps its declared number type, so `count - 1` is + // native arithmetic too. const { coverage } = analyze( join(repoRoot, "tests/corpus/7004-package-declared-type-reexports/main.ts"), ); expect(coverage.diagnostics).toEqual([]); - expect(coverage.stats.statementsDynamic).toBe(4); + expect(coverage.stats.statementsDynamic).toBe(3); }); test("any-typed checked-dynamic operators compile natively", () => { diff --git a/tests/harness/error-accessor-reflection.test.ts b/tests/harness/error-accessor-reflection.test.ts new file mode 100644 index 000000000..5f50d8a66 --- /dev/null +++ b/tests/harness/error-accessor-reflection.test.ts @@ -0,0 +1,77 @@ +/* Reflection that can install an accessor over an error's `name` or + * `message`: a defineProperty call over a target that may hold an Error + * turns on the accessor checks of typed Error reads and writes + * (error-methods.ts), whatever the target parameter's declared type. */ +import { createHash } from "node:crypto"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { expect, test } from "vitest"; +import { compile } from "@scriptc/compiler"; + +const repoRoot = join(import.meta.dirname, "../.."); +const cacheDir = join(repoRoot, "node_modules/.cache/scriptc-tests"); + +async function emittedIr(source: string): Promise { + const key = createHash("sha256").update(source).digest("hex").slice(0, 16); + const outDir = join(cacheDir, `error-accessor-${key}`); + mkdirSync(outDir, { recursive: true }); + const file = join(outDir, "main.ts"); + writeFileSync(file, source); + const result = await compile(file, { + outPath: join(outDir, "main.ir"), + outDir, + outputKind: "ir", + }); + if (!result.ok) + throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n")); + return readFileSync(result.artifact.path, "utf8"); +} + +const program = ( + param: string, + define = "Object.defineProperty", +): string => `class E extends Error {} +function install${param} { + ${define}(target, "message", { get: () => "lazy", configurable: true }); +} +const e = new E("orig"); +install(e); +console.log(e.message); +`; + +const checksAccessors = async (source: string): Promise => + (await emittedIr(source)).includes("error.ownAccessor"); + +test.each([ + ["object", "(target: object)"], + ["{}", "(target: {})"], + ["Object", "(target: Object)"], + ["a weak type", "(target: { message?: string })"], + ["{ message: string }", "(target: { message: string })"], + ["a type parameter", "(target: T)"], + ["a constrained type parameter", "(target: T)"], + ["object | undefined", "(target: object | undefined)"], + ["Error", "(target: Error)"], + ["unknown", "(target: unknown)"], +])("a definition over %s checks for accessors", async (_, param) => { + expect(await checksAccessors(program(param))).toBe(true); +}); + +test("Reflect.defineProperty over object checks for accessors", async () => { + expect(await checksAccessors(program("(target: object)", "Reflect.defineProperty"))).toBe(true); +}); + +test("a definition over a type that cannot hold an Error leaves typed reads plain", async () => { + const source = `class E extends Error {} +class Box { + id = 0; +} +function install(target: Box) { + Object.defineProperty(target, "extra", { get: () => 1, configurable: true }); +} +install(new Box()); +const e = new E("orig"); +console.log(e.message); +`; + expect(await checksAccessors(source)).toBe(false); +});