Compile instanceof against constructors held as values (#1909)

This commit is contained in:
Chris Tate
2026-10-23 01:56:13 -05:00
committed by GitHub
31 changed files with 541 additions and 58 deletions
@@ -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 },
+1
View File
@@ -391,6 +391,7 @@ const DYN_READ_ONLY = new Set<string>([
"dyn.forInStable",
"dyn.getOwnPropertyNames",
"dyn.getPrototype",
"dyn.getPrototypeForIdentity",
"dyn.hasKey",
"dyn.hasOwn",
"dyn.in",
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/6349-instanceof-subclass-capsules/errors.cjs",
"<repo>/tests/corpus/6349-instanceof-subclass-capsules/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6923-instanceof-builtin-constructor-values.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6924-instanceof-class-values-native.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6925-instanceof-dynamic-constructors.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7541-prototype-identity-stand-ins.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7542-prototype-identity-without-data.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/7544-local-class-prototype-identity.ts"
],
"diags": []
}
+1
View File
@@ -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);
+36 -11
View File
@@ -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 }