mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Compile instanceof against constructors held as values (#1909)
This commit is contained in:
@@ -1558,6 +1558,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"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",
|
||||
|
||||
@@ -350,7 +350,7 @@ export function classInstanceOf(
|
||||
localId: "candidate",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.getPrototype",
|
||||
fn: "dyn.getPrototypeForIdentity",
|
||||
args: [candidate],
|
||||
type: DYN,
|
||||
loc,
|
||||
@@ -862,6 +862,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 +909,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 +971,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 +1103,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 +1213,100 @@ 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;
|
||||
}
|
||||
for (const plan of this.propertyReceivers(lowerer, false, false)) {
|
||||
const info = plan.info;
|
||||
const className = info.def.name;
|
||||
@@ -1320,6 +1398,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 +1674,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 +3656,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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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).
|
||||
@@ -432,7 +470,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 +479,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 +509,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 +547,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,
|
||||
|
||||
@@ -633,6 +633,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
|
||||
|
||||
@@ -3752,6 +3752,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",
|
||||
|
||||
+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/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": []
|
||||
}
|
||||
@@ -934,6 +934,7 @@ 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);
|
||||
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);
|
||||
|
||||
@@ -2223,6 +2223,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
|
||||
@@ -4102,28 +4122,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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 @@
|
||||
{ "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": 4700, "san": 6500 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2500, "san": 3300 }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3900, "san": 5100 }
|
||||
Reference in New Issue
Block a user