mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
Merge main
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
) {
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs",
|
||||
"<repo>/tests/corpus/6349-instanceof-subclass-capsules/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6923-instanceof-builtin-constructor-values.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6924-instanceof-class-values-native.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6925-instanceof-dynamic-constructors.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7263-error-accessor-object-helper.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7541-prototype-identity-stand-ins.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7542-prototype-identity-without-data.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7544-local-class-prototype-identity.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7611-error-message-accessor.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7612-error-message-accessor-lazy.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7613-local-class-prototype-reflection.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7615-local-class-prototype-reflection-ts.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7731-npm-any-string-index/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7732-untyped-match-pattern.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7733-property-bag-late-prototype.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7734-caught-instanceof-class-binding.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7735-untyped-headers-iterators.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/decl-pass-through/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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/"));
|
||||
+2
@@ -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; }
|
||||
+10
@@ -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 };
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "merge-path", "version": "1.0.0", "type": "module", "main": "index.js", "types": "index.d.ts" }
|
||||
+10
@@ -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()]));
|
||||
@@ -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);
|
||||
+8
@@ -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;
|
||||
+18
@@ -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 };
|
||||
}
|
||||
+1
@@ -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 }
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user