Merge main

This commit is contained in:
Chris Tate
2026-10-23 03:55:00 -05:00
89 changed files with 2107 additions and 213 deletions
@@ -5,6 +5,8 @@
export function preservesDynTest(test: string): boolean {
switch (test) {
case "truthy":
case "classCapsule":
case "funcKind":
case "nullish":
case "promise":
case "buffer":
@@ -719,6 +719,10 @@ export function emitDynamicExpr(
B.line(
`${test} = call zeroext i1 @scr_dyn_typed_array_is(ptr ${d.name}, i32 ${BYTES_ELEM_NUM[e.bytesElem ?? "u8"]})`,
);
} else if (e.test === "classCapsule") {
host.declare(`declare zeroext i1 @scr_dyn_is_class_capsule(ptr)`);
test = B.tmp();
B.line(`${test} = call zeroext i1 @scr_dyn_is_class_capsule(ptr ${d.name})`);
} else if (e.test === "truthy") {
host.declare(`declare zeroext i1 @scr_dyn_truthy(ptr)`);
test = B.tmp();
@@ -839,6 +843,7 @@ export function emitDynamicExpr(
undefined: DYN_KIND.UNDEF,
null: DYN_KIND.NULL,
bytes: DYN_KIND.BYTES,
funcKind: DYN_KIND.FUNC,
};
test = oneOf([kindOf[e.test]!]);
}
@@ -134,6 +134,9 @@ export const LIB_FN_SYMS: Record<string, string> = {
"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<string, string> = {
"dyn.assignPrototype": "scr_dyn_assign_prototype",
"dyn.definePrototypeProps": "scr_dyn_define_prototype_props",
"dyn.bagGet": "scr_dyn_bag_get",
"dyn.bagCreateField": "scr_dyn_bag_create_field",
"dyn.bagSet": "scr_dyn_bag_set",
"dyn.objCreate": "scr_dyn_obj_create",
"dyn.nameClassPrototype": "scr_dyn_name_class_prototype",
"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",
@@ -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 (`<T extends object>(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)) {
@@ -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: [
@@ -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<string, string>();
const helperFor = (className: string): string => {
const known = perClass.get(className);
if (known !== undefined) return known;
const helperName = `${name}:${className}`;
perClass.set(className, helperName);
const self = varRef("this", { kind: "object", className }, loc);
const bagRef = varRef("bag", DYN, loc);
functions.push({
name: helperName,
params: [
{ localId: "this", name: "this", type: { kind: "object", className } },
{ localId: "key", name: "key", type: DYN },
],
returnType: VOID,
locals: [
{ id: "this", name: "this", type: { kind: "object", className }, mutable: false },
{ id: "key", name: "key", type: DYN, mutable: false },
{ id: "bag", name: "bag", type: DYN, mutable: true },
],
body: [
{
kind: "varDecl",
localId: "bag",
init: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc },
loc,
},
{
kind: "if",
cond: { kind: "dynTest", test: "undefined", value: bagRef, type: BOOL, loc },
then: initializeBag(lowerer, className, self, loc),
else_: null,
loc,
},
...defineIfAbsent(lowerer, bagRef, key, loc),
],
loc,
});
return helperName;
};
// String-keyed fields take the runtime's direct insert for an ordinary
// bag; the general path stays for any other bag and for symbol keys.
const stringHelperFor = (className: string): string => {
const known = perClass.get(`str:${className}`);
if (known !== undefined) return known;
const general = helperFor(className);
const helperName = `${name}.string:${className}`;
perClass.set(`str:${className}`, helperName);
const type: IrType = { kind: "object", className };
const self = varRef("this", type, loc);
const bagRef = varRef("bag", DYN, loc);
const field = varRef("field", STRING, loc);
functions.push({
name: helperName,
params: [
{ localId: "this", name: "this", type },
{ localId: "field", name: "field", type: STRING },
],
returnType: VOID,
locals: [
{ id: "this", name: "this", type, mutable: false },
{ id: "field", name: "field", type: STRING, mutable: false },
{ id: "bag", name: "bag", type: DYN, mutable: true },
],
body: [
{
kind: "varDecl",
localId: "bag",
init: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc },
loc,
},
{
kind: "if",
cond: { kind: "dynTest", test: "undefined", value: bagRef, type: BOOL, loc },
then: initializeBag(lowerer, className, self, loc),
else_: null,
loc,
},
{
kind: "if",
cond: {
kind: "libCall",
fn: "dyn.bagCreateField",
args: [bagRef, field],
type: BOOL,
loc,
},
then: [{ kind: "return", value: null, loc }],
else_: null,
loc,
},
{
kind: "exprStmt",
expr: {
kind: "call",
callee: general,
args: [self, lowerer.coerceToExpected(field, DYN)],
type: VOID,
loc,
},
loc,
},
],
loc,
});
return helperName;
};
const created = (receiver: IrExpr, className: string, field: string, loc: SrcLoc): IrStmt => {
const symbolKey = symbolMemberKey(lowerer, lowerer.classes.get(className)!, field, loc);
if (symbolKey === null) {
return {
kind: "exprStmt",
expr: {
kind: "call",
callee: stringHelperFor(className),
args: [
receiver.type.kind === "object" && receiver.type.className !== className
? { kind: "upcast", value: receiver, type: { kind: "object", className }, loc }
: receiver,
{ kind: "strLit", value: field, type: STRING, loc },
],
type: VOID,
loc,
},
loc,
};
}
return createdGeneral(receiver, className, field, loc);
};
const createdGeneral = (
receiver: IrExpr,
className: string,
field: string,
loc: SrcLoc,
): IrStmt => ({
kind: "exprStmt",
expr: {
kind: "call",
callee: name,
callee: helperFor(className),
args: [
lowerer.coerceToExpected(receiver, DYN),
receiver.type.kind === "object" && receiver.type.className !== className
? { kind: "upcast", value: receiver, type: { kind: "object", className }, loc }
: receiver,
symbolMemberKey(lowerer, lowerer.classes.get(className)!, field, loc) ??
lowerer.coerceToExpected({ kind: "strLit", value: field, type: STRING, loc }, DYN),
],
@@ -769,80 +941,103 @@ export function trackClassFieldCreation(lowerer: Lowerer, functions: IrFunction[
},
});
}
functions.push({
name,
params: [
{ localId: "value", name: "value", type: DYN },
{ localId: "key", name: "key", type: DYN },
],
returnType: VOID,
locals: [
{ id: "value", name: "value", type: DYN, mutable: false },
{ id: "key", name: "key", type: DYN, mutable: false },
{ id: "bag", name: "bag", type: DYN, mutable: false },
],
body: [
}
/** `bag = <the class's new bag>`, stored in the instance's hidden field:
* the class's own initializer when recorded, else the class-table
* dispatch, which creates it. */
function initializeBag(lowerer: Lowerer, className: string, self: IrExpr, loc: SrcLoc): IrStmt[] {
const init = lowerer.classBagInitializers?.get(className);
if (init === undefined) {
return [
{
kind: "varDecl",
kind: "assign",
localId: "bag",
init: {
value: {
kind: "call",
callee: classPropertiesHelper(lowerer, loc).name,
args: [value],
args: [lowerer.coerceToExpected(self, DYN)],
type: DYN,
loc,
},
loc,
},
{
kind: "if",
cond: {
kind: "unary",
op: "!",
operand: { kind: "libCall", fn: "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc },
type: BOOL,
loc,
},
then: [
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.defineProperty",
args: [
bag,
lowerer.coerceToExpected(key, DYN),
{
kind: "dynObjLit",
fields: [
{
key: { kind: "strLit", value: "value", type: STRING, loc },
value: dynUndefinedExpr(loc),
},
...["writable", "enumerable", "configurable"].map((key) => ({
key: { kind: "strLit" as const, value: key, type: STRING, loc },
value: lowerer.coerceToExpected(
{ kind: "boolLit", value: true, type: BOOL, loc },
DYN,
),
})),
],
type: DYN,
loc,
},
],
type: DYN,
loc,
},
loc,
},
],
else_: null,
];
}
return [
{
kind: "fieldSet",
obj: self,
className,
field: PROPERTY_BAG,
value: structuredClone(init),
loc,
},
{
kind: "assign",
localId: "bag",
value: { kind: "fieldGet", obj: self, className, field: PROPERTY_BAG, type: DYN, loc },
loc,
},
];
}
/** Whether `value` reads local `id`. */
function mentionsLocal(value: IrExpr, id: string): boolean {
return JSON.stringify(value).includes(`"localId":${JSON.stringify(id)}`);
}
/** `if (!hasOwn(bag, key)) defineProperty(bag, key, <undefined data field>)`:
* a field's creation in its instance's property bag. */
function defineIfAbsent(lowerer: Lowerer, bag: IrExpr, key: IrExpr, loc: SrcLoc): IrStmt[] {
return [
{
kind: "if",
cond: {
kind: "unary",
op: "!",
operand: { kind: "libCall", fn: "dyn.hasOwnComputed", args: [bag, key], type: BOOL, loc },
type: BOOL,
loc,
},
],
loc,
});
then: [
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.defineProperty",
args: [
bag,
lowerer.coerceToExpected(key, DYN),
{
kind: "dynObjLit",
fields: [
{
key: { kind: "strLit", value: "value", type: STRING, loc },
value: dynUndefinedExpr(loc),
},
...["writable", "enumerable", "configurable"].map((key) => ({
key: { kind: "strLit" as const, value: key, type: STRING, loc },
value: lowerer.coerceToExpected(
{ kind: "boolLit", value: true, type: BOOL, loc },
DYN,
),
})),
],
type: DYN,
loc,
},
],
type: DYN,
loc,
},
loc,
},
],
else_: null,
loc,
},
];
}
/** Calls on native class capsules keep the instance's compiled methods. The
@@ -862,6 +1057,9 @@ export class ClassDynamicDispatch {
private deletedNames: Set<string> | null | undefined;
private readonly boxed = new Set<string>();
private readonly boxedConstructors = new Set<string>();
/** 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<string>();
private readonly dispatches = new Map<string, Dispatch>();
private readonly properties = new Map<string, PropertyDispatch>();
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<string>();
private prototypeIdentityHelper: IrFunction | null = null;
private readonly prototypeIdentityClasses = new Set<string>();
/** The stand-in prototype object of each class without prototype data
* (prototypeStandIn), by class name. */
private readonly prototypeStandIns = new Set<string>();
/** 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<string> }
>();
/** `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<string, Extract<IrStmt, { kind: "fieldSet" }>[]> | 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<string, Extract<IrStmt, { kind: "fieldSet" }>[]> {
const live = new Map<string, Extract<IrStmt, { kind: "fieldSet" }>[]>();
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 });
}
@@ -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",
@@ -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 {
@@ -81,6 +81,7 @@ const DISPATCH_LIB_CALLS = new Set<string>([
"dyn.propertyIsEnumerableComputed",
"dyn.defineProperty",
"dyn.getPrototype",
"dyn.getPrototypeForIdentity",
"dyn.inheritsErrorPrototype",
"dyn.reflectGet",
"dyn.reflectSet",
@@ -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}`;
@@ -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);
@@ -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<IrType["kind"]> = 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. */
@@ -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 (
@@ -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<string, IrExpr>();
constructor(
readonly program: ts.Program,
@@ -458,6 +458,9 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = 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<IrLibFn> = 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
@@ -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 },
+1
View File
@@ -391,6 +391,7 @@ const DYN_READ_ONLY = new Set<string>([
"dyn.forInStable",
"dyn.getOwnPropertyNames",
"dyn.getPrototype",
"dyn.getPrototypeForIdentity",
"dyn.hasKey",
"dyn.hasOwn",
"dyn.in",
+5 -1
View File
@@ -3445,7 +3445,11 @@ export type IrExpr =
| "array"
| "truthy"
| "error"
| "function";
| "function"
/** The value is a capsule of a compiled class instance. */
| "classCapsule"
/** The value is a boxed function or class value (exact kind test). */
| "funcKind";
bytesElem?: IrBytesElem;
negated?: true;
value: IrExpr;
+61
View File
@@ -176,3 +176,64 @@ test("a function holds a site when it calls a classified helper or dispatches a
expect(hasJsIcSite({ ...invoker, name: "%init.0" }, sites)).toBe(false);
expect(hasJsIcSite({ ...caller, name: "%init.0" }, sites)).toBe(true);
});
test("bag helpers that exit early for values no class branch matches are still the receiver", () => {
/** `if (!<test>(v)) return <value>`: the class tables' early exit. */
const exit = (test: "classCapsule" | "funcKind", value: IrExpr, negated = true): IrStmt => ({
kind: "if",
cond: {
kind: "dynTest",
test,
...(negated ? { negated: true as const } : {}),
value: ref("v"),
type: BOOL,
loc,
},
then: [ret(value)],
else_: null,
loc,
});
const guarded = (name: string, first: IrStmt): IrFunction =>
helper(name, ["v"], [first, capsuleBranch("v"), ret(ref("v"))]);
const readBag = guarded("%dyn.class.readPropertiesOrEmpty", exit("classCapsule", ref("v")));
const writeBag = guarded("%dyn.class.properties", exit("classCapsule", ref("v")));
const staticBag = guarded("%dyn.class.staticProperties", exit("funcKind", ref("v")));
const getter = getHelper(
"%dyn.class.property.0",
"x",
[],
call(staticBag.name, call(readBag.name, ref("p.0"))),
);
const setter = setHelper("%dyn.class.property.1", "y");
// An exit that returns another value, or a test that is not negated,
// makes the bag differ from the receiver.
const otherExit = guarded(
"%dyn.class.other",
exit("classCapsule", { kind: "dynObjLit", type: DYN, loc } as IrExpr),
);
const positive = guarded("%dyn.class.positive", exit("classCapsule", ref("v"), false));
const otherGetter = getHelper("%dyn.class.property.2", "x", [], call(otherExit.name, ref("p.0")));
const positiveGetter = getHelper(
"%dyn.class.property.3",
"x",
[],
call(positive.name, ref("p.0")),
);
const sites = new JsIcSites({
functions: [
readBag,
writeBag,
staticBag,
otherExit,
positive,
getter,
setter,
otherGetter,
positiveGetter,
],
});
expect(sites.site("%dyn.class.property.0")).toEqual({ kind: "get", key: "x" });
expect(sites.site("%dyn.class.property.1")).toEqual({ kind: "set", key: "y" });
expect(sites.site("%dyn.class.property.2")).toBeNull();
expect(sites.site("%dyn.class.property.3")).toBeNull();
});
+15 -1
View File
@@ -170,6 +170,18 @@ export class JsIcSites {
);
}
/** An early exit for values a class table never matches: `if (!<kind
* test>(value)) return <value>`, with no else. A plain object takes it
* and gets itself back. */
private kindExit(s: IrStmt, param: string): boolean {
if (s.kind !== "if" || s.else_ !== null || s.then.length !== 1) return false;
const { cond } = s;
if (cond.kind !== "dynTest" || cond.negated !== true || !isVar(cond.value, param)) return false;
if (cond.test !== "classCapsule" && cond.test !== "funcKind") return false;
const ret = s.then[0]!;
return ret.kind === "return" && ret.value !== null && this.bagOf(ret.value, param);
}
/** A property-bag helper that returns its argument unless a capsule
* branch is taken. */
private identityHelper(name: string): boolean {
@@ -190,7 +202,9 @@ export class JsIcSites {
const param = fn.params[0]!.localId;
const last = fn.body[fn.body.length - 1];
identity =
fn.body.slice(0, -1).every((s) => this.capsuleBranch(s, param)) &&
fn.body
.slice(0, -1)
.every((s) => this.capsuleBranch(s, param) || this.kindExit(s, param)) &&
last?.kind === "return" &&
last.value !== null &&
this.bagOf(last.value, param);
+3
View File
@@ -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"
) {
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs",
"<repo>/tests/corpus/6349-instanceof-subclass-capsules/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6923-instanceof-builtin-constructor-values.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6924-instanceof-class-values-native.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6925-instanceof-dynamic-constructors.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7263-error-accessor-object-helper.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7541-prototype-identity-stand-ins.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7542-prototype-identity-without-data.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7544-local-class-prototype-identity.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7611-error-message-accessor.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7612-error-message-accessor-lazy.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7613-local-class-prototype-reflection.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7615-local-class-prototype-reflection-ts.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7731-npm-any-string-index/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7732-untyped-match-pattern.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7733-property-bag-late-prototype.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7734-caught-instanceof-class-binding.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7735-untyped-headers-iterators.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/decl-pass-through/main.ts"
],
"diags": []
}
+3
View File
@@ -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);
+3
View File
@@ -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);
+35 -1
View File
@@ -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) {
+153 -33
View File
@@ -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;
}
@@ -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 };
@@ -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)));
@@ -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(""));
@@ -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);
@@ -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<string, any> = { 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<string, number>([["a", 1]])],
["set", new Set<number>([1])],
["regex", /a/g],
["promise", Promise.resolve(1)],
["weak", new WeakMap<object, number>()],
["date", new Date(5)],
["array", [1]],
["record", { a: 1 }],
];
const builtins: Record<string, any> = { 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<string, number>();
const pattern = /x/;
console.log(map instanceof holder.type, pattern instanceof holder.base, dog instanceof holder.type, dog instanceof holder.base);
@@ -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<T extends object>(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);
}
@@ -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);
@@ -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));
@@ -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));
@@ -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));
@@ -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));
@@ -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));
@@ -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());
@@ -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/"));
@@ -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; }
@@ -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 };
@@ -0,0 +1 @@
{ "name": "merge-path", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
+10
View File
@@ -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 };
@@ -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")));
@@ -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(","));
@@ -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(","));
@@ -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()]));
+16
View File
@@ -0,0 +1,16 @@
// Package functions declared to return their argument's own type
// (`<T extends object>(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);
@@ -0,0 +1,8 @@
export interface Point {
x: number;
y: number;
}
export declare function byReference<T extends object>(obj: T): T;
export declare const mark: <T extends object>(obj: T) => T;
export declare function equals(a: unknown, b: unknown): boolean;
export declare function point(x: number, y: number): Point;
@@ -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 };
}
@@ -0,0 +1 @@
{ "name": "ref-marks", "version": "1.0.0", "type": "module", "main": "./lib/index.js", "types": "./lib/index.d.ts" }
@@ -0,0 +1 @@
{ "name": "decl-pass-through-case", "private": true, "type": "module" }
@@ -0,0 +1 @@
{ "plain": 1600, "san": 2000 }
@@ -0,0 +1 @@
{ "plain": 3810, "san": 5921 }
@@ -0,0 +1 @@
{ "plain": 3033, "san": 4212 }
@@ -0,0 +1 @@
{ "plain": 3097, "san": 4707 }
@@ -0,0 +1 @@
{ "plain": 2600, "san": 4000 }
@@ -0,0 +1 @@
{ "plain": 4700, "san": 6500 }
@@ -0,0 +1 @@
{ "plain": 2500, "san": 3300 }
@@ -0,0 +1 @@
{ "plain": 3900, "san": 5100 }
@@ -0,0 +1 @@
{ "plain": 2962, "san": 3600 }
@@ -0,0 +1 @@
{ "plain": 2217, "san": 2721 }
@@ -0,0 +1 @@
{ "plain": 1967, "san": 2510 }
@@ -0,0 +1 @@
{ "plain": 1466, "san": 2143 }
@@ -0,0 +1 @@
{ "plain": 6200, "san": 5800 }
@@ -0,0 +1 @@
{ "plain": 3600, "san": 3300 }
@@ -0,0 +1 @@
{ "plain": 4400, "san": 3900 }
@@ -0,0 +1 @@
{ "plain": 3300, "san": 3600 }
@@ -0,0 +1 @@
{ "plain": 4000, "san": 4000 }
+4 -3
View File
@@ -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", () => {
@@ -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<string> {
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<boolean> =>
(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", "<T>(target: T)"],
["a constrained type parameter", "<T extends object>(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);
});