mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Let objects inherit from JavaScript class prototypes
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
- **Prototypes:** Named properties and methods on program class prototypes support replacement, inheritance, and deletion.
|
||||
Bare prototype reflection, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
|
||||
A module-level JavaScript class's prototype can become the prototype of other objects: `Object.create(C.prototype)`, `Object.setPrototypeOf(F.prototype, C.prototype)`, `Object.setPrototypeOf(object, C.prototype)`, and `util.inherits(F, C)` with an ordinary constructor `F`.
|
||||
The inherited methods and accessors run with the inheriting object as `this`.
|
||||
Methods that use `#private` members or `super` throw a TypeError for a receiver that is not an instance of the class.
|
||||
`C.prototype` also reads as a value for `Object.getPrototypeOf` and for `===` and `!==` comparisons.
|
||||
Changing the prototype of a class prototype (`Object.setPrototypeOf(C.prototype, X)`, `util.inherits(C, F)`, or `Object.setPrototypeOf` on a class prototype reached through an instance) is unsupported.
|
||||
Other bare prototype reflection, TypeScript class prototypes as values, accessor replacement, and prototype storage on local, generic, mixin, or runtime-provided classes are unsupported.
|
||||
Plain objects, arrays, functions, class instances, and Date, RegExp, Map, Set, WeakMap, WeakSet, and Promise values inherit `constructor`, `valueOf`, `toString`, `hasOwnProperty`, and `propertyIsEnumerable` from `Object.prototype` as in Node, so `value.constructor === Object` and `value.valueOf === Object.prototype.valueOf` compare identities exactly.
|
||||
Named reads such as `value.valueOf` answer these inherited members; computed-key reads such as `value[key]` do not.
|
||||
`Object.prototype`'s own members read as values; its mutation and other reflection, such as `isPrototypeOf` and `__proto__`, are unsupported.
|
||||
|
||||
@@ -82,6 +82,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"util.isDeepStrictEqual": "scr_util_is_deep_strict_equal",
|
||||
"util.styleText": "scr_util_style_text",
|
||||
"util.inherits": "scr_util_inherits",
|
||||
"util.inheritsPrototype": "scr_util_inherits_prototype",
|
||||
"util.deprecate": "scr_util_deprecate",
|
||||
"util.callbackify": "scr_util_callbackify",
|
||||
"util.promisifyDyn": "scr_util_promisify",
|
||||
|
||||
@@ -4105,6 +4105,94 @@ export class ClassDynamicDispatch {
|
||||
return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** `%dyn.class.bagHasOwn[OrPrototype]`: whether an instance's property bag
|
||||
* holds `key` as its own property, or, with `prototype`, whether the bag's
|
||||
* prototype object does. False before the instance has a bag. */
|
||||
private readBagHasOwn(
|
||||
lowerer: Lowerer,
|
||||
bag: IrExpr,
|
||||
key: IrExpr,
|
||||
prototype: boolean,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const name = prototype ? "%dyn.class.bagHasOwnOrPrototype" : "%dyn.class.bagHasOwn";
|
||||
let helper = lowerer.liftedFnNamed(name);
|
||||
if (!helper) {
|
||||
const value = varRef("p.0", DYN, loc);
|
||||
const keyRef = varRef("p.1", STRING, loc);
|
||||
const proto = varRef("proto", DYN, loc);
|
||||
const hasOwn = (target: IrExpr): IrExpr => ({
|
||||
kind: "libCall",
|
||||
fn: "dyn.hasOwn",
|
||||
args: [target, keyRef],
|
||||
type: BOOL,
|
||||
loc,
|
||||
});
|
||||
const returnFalse: IrStmt = {
|
||||
kind: "return",
|
||||
value: { kind: "boolLit", value: false, type: BOOL, loc },
|
||||
loc,
|
||||
};
|
||||
helper = {
|
||||
name,
|
||||
params: [
|
||||
{ localId: "p.0", name: "bag", type: DYN },
|
||||
{ localId: "p.1", name: "key", type: STRING },
|
||||
],
|
||||
returnType: BOOL,
|
||||
locals: [
|
||||
{ id: "p.0", name: "bag", type: DYN, mutable: false },
|
||||
{ id: "p.1", name: "key", type: STRING, mutable: false },
|
||||
...(prototype ? [{ id: "proto", name: "proto", type: DYN, mutable: false }] : []),
|
||||
],
|
||||
body: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "undefined", value, type: BOOL, loc },
|
||||
then: [returnFalse],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
...(prototype
|
||||
? ([
|
||||
{
|
||||
kind: "if",
|
||||
cond: hasOwn(value),
|
||||
then: [
|
||||
{
|
||||
kind: "return",
|
||||
value: { kind: "boolLit", value: true, type: BOOL, loc },
|
||||
loc,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: "proto",
|
||||
init: { kind: "libCall", fn: "dyn.getPrototype", args: [value], type: DYN, loc },
|
||||
loc,
|
||||
},
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: "nullish", value: proto, type: BOOL, loc },
|
||||
then: [returnFalse],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
{ kind: "return", value: hasOwn(proto), loc },
|
||||
] satisfies IrStmt[])
|
||||
: [{ kind: "return", value: hasOwn(value), loc } satisfies IrStmt]),
|
||||
],
|
||||
loc,
|
||||
};
|
||||
lowerer.liftedFns.push(helper);
|
||||
}
|
||||
this.generated.add(helper);
|
||||
return { kind: "call", callee: name, args: [bag, key], type: BOOL, loc };
|
||||
}
|
||||
|
||||
/** The hints for which some boxed class declares a conversion method. A
|
||||
* string conversion calls toString first, so without a declared toString
|
||||
* or Symbol.toPrimitive it answers Object.prototype.toString's
|
||||
@@ -5866,12 +5954,18 @@ export class ClassDynamicDispatch {
|
||||
const cls = lowerer.coerceToExpected(classValueRef(lowerer, info, info.decl), DYN);
|
||||
const bag = this.branchPropertyBag(lowerer, info, receiver, false, loc);
|
||||
const key: IrExpr = { kind: "strLit", value: name, type: STRING, loc };
|
||||
// `constructor` is an own property of every class prototype, so an
|
||||
// ancestor's prototype never answers for an instance of this class:
|
||||
// the instance's own property answers, then one this class's
|
||||
// prototype object defines (a reflected prototype, or a write to
|
||||
// `C.prototype.constructor`), and otherwise the class itself.
|
||||
const answered = this.readBagHasOwn(lowerer, bag, key, hasClassPrototypeData(info), loc);
|
||||
return [
|
||||
{
|
||||
kind: "return",
|
||||
value: {
|
||||
kind: "ternary",
|
||||
cond: this.readBagHas(lowerer, "dyn.hasKey", bag, key, loc),
|
||||
cond: answered,
|
||||
then: {
|
||||
kind: "libCall",
|
||||
fn: "dyn.bagGet",
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
type IrStmt,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { isJsSourceFile, locOf } from "../program.js";
|
||||
import {
|
||||
classMemberNameOf,
|
||||
classValueRef,
|
||||
@@ -503,6 +503,69 @@ export function isErrorPrototypeValue(
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether a bare `C.prototype` read becomes the prototype of another object
|
||||
* or is only compared or inspected: the argument of `Object.create` or
|
||||
* `Object.getPrototypeOf` / `Reflect.getPrototypeOf`, the prototype argument
|
||||
* of `Object.setPrototypeOf` / `Reflect.setPrototypeOf`, or an operand of
|
||||
* `===` / `!==`. In these positions the complete view of the
|
||||
* prototype (reflectClassPrototype) answers. Objects that inherit from it
|
||||
* run the class's methods with themselves as the receiver, which the
|
||||
* methods' receiver-independent lowering (reflectedClassMethodValue)
|
||||
* supports. Other positions, such as `Object.setPrototypeOf(C.prototype, X)`,
|
||||
* which would relink the class, keep the diagnostic. */
|
||||
function prototypeViewPosition(lowerer: Lowerer, expr: ts.PropertyAccessExpression): boolean {
|
||||
let node: ts.Node = expr;
|
||||
let parent = node.parent;
|
||||
while (parent && ts.isParenthesizedExpression(parent)) {
|
||||
node = parent;
|
||||
parent = parent.parent;
|
||||
}
|
||||
if (!parent) return false;
|
||||
if (ts.isCallExpression(parent) && ts.isPropertyAccessExpression(parent.expression)) {
|
||||
const args = parent.arguments.nodes;
|
||||
const objectMethod = lowerer.stdlibGlobalMember(parent.expression, "Object");
|
||||
const reflectMethod = lowerer.stdlibGlobalMember(parent.expression, "Reflect");
|
||||
if (
|
||||
objectMethod === "create" ||
|
||||
objectMethod === "getPrototypeOf" ||
|
||||
reflectMethod === "getPrototypeOf"
|
||||
)
|
||||
return args[0] === node;
|
||||
if (objectMethod === "setPrototypeOf" || reflectMethod === "setPrototypeOf")
|
||||
return args[1] === node;
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
ts.isBinaryExpression(parent) &&
|
||||
(parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken)
|
||||
);
|
||||
}
|
||||
|
||||
/** A module-level JavaScript class named directly (not a class value that
|
||||
* may hold a subclass at run time): its prototype has a complete view whose
|
||||
* methods accept any receiver. */
|
||||
function viewableClass(lowerer: Lowerer, info: ClassInfo, receiver: ts.Expression): boolean {
|
||||
return (
|
||||
info.decl !== null &&
|
||||
info.localClass === undefined &&
|
||||
isJsSourceFile(ts.getSourceFileOfNode(info.decl)) &&
|
||||
exactClassOfReceiver(lowerer, receiver) === info
|
||||
);
|
||||
}
|
||||
|
||||
/** The complete prototype view of the class `node` names, when it is a
|
||||
* viewable class (util.inherits's superCtor); null otherwise. */
|
||||
export function classPrototypeView(
|
||||
lowerer: Lowerer,
|
||||
node: ts.Expression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
const info = exactClassOfReceiver(lowerer, node);
|
||||
if (!info || !viewableClass(lowerer, info, node)) return null;
|
||||
return reflectClassPrototype(lowerer, info, loc, classValueRef(lowerer, info, node));
|
||||
}
|
||||
|
||||
/** Named data and method slots have native descriptors. Bare reflection and
|
||||
* accessor replacement still require a complete view of the prototype. */
|
||||
export function lowerClassPrototypeData(
|
||||
@@ -566,6 +629,12 @@ export function lowerClassPrototypeData(
|
||||
});
|
||||
return reflectClassPrototype(lowerer, info, locOf(expr), receiver);
|
||||
}
|
||||
if (
|
||||
name === null &&
|
||||
prototypeViewPosition(lowerer, expr) &&
|
||||
viewableClass(lowerer, info, expr.expression)
|
||||
)
|
||||
return reflectClassPrototype(lowerer, info, locOf(expr), classValueRef(lowerer, info, expr));
|
||||
if (name === null || (isCompiledPrototypeMember(lowerer, info, name) && !method)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { perKeyOrder } from "./key-order-witness.js";
|
||||
import { classPrototypeView } from "./class-prototypes.js";
|
||||
import {
|
||||
dynUndefinedExpr,
|
||||
boolLit,
|
||||
@@ -3433,8 +3434,14 @@ function lowerUtilCallableCall(
|
||||
member === "inherits" && expr.arguments.nodes[1] !== undefined
|
||||
? builtinClassKindOf(lowerer, expr.arguments.nodes[1])
|
||||
: null;
|
||||
// A superCtor class whose prototype has a complete view (a module-level
|
||||
// JavaScript class named directly) passes that view to the runtime.
|
||||
const superView =
|
||||
member === "inherits" && builtinBase === null && expr.arguments.nodes[1] !== undefined
|
||||
? classPrototypeView(lowerer, expr.arguments.nodes[1], loc)
|
||||
: null;
|
||||
for (const [index, arg] of expr.arguments.nodes.slice(0, classArgs).entries()) {
|
||||
if (index === 1 && builtinBase !== null) continue;
|
||||
if (index === 1 && (builtinBase !== null || superView !== null)) continue;
|
||||
if (lowerer.mapTypeOf(lowerer.typeOf(arg))?.kind === "classval")
|
||||
lowerer.noLowering(
|
||||
`util.${member} over a class constructor`,
|
||||
@@ -3466,7 +3473,9 @@ function lowerUtilCallableCall(
|
||||
for (const arg of expr.arguments.nodes.slice(arity))
|
||||
stmts.push({ kind: "exprStmt", expr: lowerer.lowerExpr(arg), loc });
|
||||
const type = fn === "util.inherits" ? VOID : DYN;
|
||||
const result: IrExpr = { kind: "libCall", fn, args, type, loc };
|
||||
const result: IrExpr = superView
|
||||
? { kind: "libCall", fn: "util.inheritsPrototype", args: [...args, superView], type, loc }
|
||||
: { kind: "libCall", fn, args, type, loc };
|
||||
return { kind: "seqExpr", stmts, result, type: result.type, loc };
|
||||
}
|
||||
|
||||
|
||||
@@ -591,6 +591,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"util.styleText",
|
||||
"util.toUSVString",
|
||||
"util.inherits",
|
||||
"util.inheritsPrototype",
|
||||
"util.deprecate",
|
||||
"util.callbackify",
|
||||
"util.promisifyDyn",
|
||||
|
||||
@@ -1146,6 +1146,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* arguments (scr_util_callable.c). The function results are native dyn
|
||||
* wrappers; call sites with a static function type check them back. */
|
||||
"util.inherits": { argTypes: [DYN, DYN], result: VOID },
|
||||
"util.inheritsPrototype": { argTypes: [DYN, DYN, DYN], result: VOID },
|
||||
"util.deprecate": { argTypes: [DYN, DYN, DYN, DYN], result: DYN },
|
||||
"util.callbackify": { argTypes: [DYN], result: DYN },
|
||||
/** util.promisify over an untyped function value: a native dyn function
|
||||
|
||||
@@ -144,6 +144,7 @@ const DYN_ASYNC_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"util.debuglog",
|
||||
"util.deprecate",
|
||||
"util.inherits",
|
||||
"util.inheritsPrototype",
|
||||
"util.styleText",
|
||||
]);
|
||||
|
||||
@@ -173,6 +174,7 @@ const UTIL_CALLABLE_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"util.debuglog",
|
||||
"util.deprecate",
|
||||
"util.inherits",
|
||||
"util.inheritsPrototype",
|
||||
]);
|
||||
|
||||
/** Entry points that link the regex unit: the regex and glob matchers (fs.glob's walk compiles its patterns with the glob matcher), and readline's terminal unit (scr_readline_tty.c), which measures with the regex unit's stripVTControlCharacters. */
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8260-class-prototype-views.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -2169,6 +2169,40 @@ static bool scr_dyn_prototype_valid(const ScrDyn *prototype) {
|
||||
}
|
||||
|
||||
/* A valid receiver or prototype whose kind this operation does not handle. */
|
||||
/* Compiled class prototypes. Compiled code binds a class's methods, its
|
||||
* base chain and its instanceof answers when it compiles, so the
|
||||
* prototype's own [[Prototype]] is fixed: scr_dyn_set_prototype refuses to
|
||||
* change it. Each module-level class prototype registers when it is
|
||||
* created (scr_dyn_name_class_prototype) and stays alive until its thread
|
||||
* exits, as the class does. */
|
||||
static SCR_TL ScrDyn **scr_dyn_fixed_prototypes;
|
||||
static SCR_TL size_t scr_dyn_fixed_count, scr_dyn_fixed_cap;
|
||||
|
||||
static void scr_dyn_fixed_prototypes_cleanup(void) {
|
||||
for (size_t i = 0; i < scr_dyn_fixed_count; i++) scr_dyn_release(scr_dyn_fixed_prototypes[i]);
|
||||
free(scr_dyn_fixed_prototypes);
|
||||
scr_dyn_fixed_prototypes = NULL;
|
||||
scr_dyn_fixed_count = scr_dyn_fixed_cap = 0;
|
||||
}
|
||||
|
||||
static void scr_dyn_fix_prototype(ScrDyn *prototype) {
|
||||
if (scr_dyn_fixed_count == scr_dyn_fixed_cap) {
|
||||
if (scr_dyn_fixed_cap == 0) scr_atexit(scr_dyn_fixed_prototypes_cleanup);
|
||||
size_t cap = scr_dyn_fixed_cap ? scr_dyn_fixed_cap * 2 : 8;
|
||||
ScrDyn **grown = realloc(scr_dyn_fixed_prototypes, cap * sizeof *grown);
|
||||
if (!grown) scr_json_oom();
|
||||
scr_dyn_fixed_prototypes = grown;
|
||||
scr_dyn_fixed_cap = cap;
|
||||
}
|
||||
scr_dyn_fixed_prototypes[scr_dyn_fixed_count++] = scr_dyn_retain(prototype);
|
||||
}
|
||||
|
||||
static bool scr_dyn_prototype_fixed(const ScrDyn *object) {
|
||||
for (size_t i = 0; i < scr_dyn_fixed_count; i++)
|
||||
if (scr_dyn_fixed_prototypes[i] == object) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
static void scr_dyn_prototype_unsupported(const char *operation) {
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
@@ -2285,6 +2319,11 @@ ScrDyn *scr_dyn_set_prototype(ScrDyn *object, ScrDyn *prototype) {
|
||||
return NULL;
|
||||
}
|
||||
if (!scr_dyn_prototype_valid(prototype)) return NULL;
|
||||
if (object->kind == SCR_DYN_OBJ && scr_dyn_fixed_count && scr_dyn_prototype_fixed(object) &&
|
||||
!(prototype->kind == SCR_DYN_NULL ? object->null_proto : prototype == object->prototype)) {
|
||||
scr_dyn_prototype_unsupported("Object.setPrototypeOf");
|
||||
return NULL;
|
||||
}
|
||||
bool typed = scr_dyn_typed_ref_prototype(prototype);
|
||||
if (scr_exc_pending()) return NULL;
|
||||
if ((object->kind != SCR_DYN_OBJ && object->kind != SCR_DYN_ARR) ||
|
||||
@@ -6354,7 +6393,9 @@ static void scr_dyn_name_prototype_chain(const ScrDyn *object) {
|
||||
}
|
||||
|
||||
void scr_dyn_name_class_prototype(ScrDyn *prototype, const ScrStr *name) {
|
||||
if (prototype->kind != SCR_DYN_OBJ || prototype->v.obj.class_name || name->len == 0) return;
|
||||
if (prototype->kind != SCR_DYN_OBJ) return;
|
||||
if (!scr_dyn_prototype_fixed(prototype)) scr_dyn_fix_prototype(prototype);
|
||||
if (prototype->v.obj.class_name || name->len == 0) return;
|
||||
scr_dyn_stamp_prototype_name(prototype, name->data, name->len);
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ bool scr_util_terminal_colors(void);
|
||||
* (scr_util_promisify.c). Arguments are borrowed; function results are +1
|
||||
* native dyn functions, or NULL with Node's TypeError pending. */
|
||||
void scr_util_inherits(struct ScrDyn *ctor, struct ScrDyn *super_ctor);
|
||||
void scr_util_inherits_prototype(struct ScrDyn *ctor, struct ScrDyn *super_ctor,
|
||||
struct ScrDyn *super_prototype);
|
||||
struct ScrDyn *scr_util_deprecate(struct ScrDyn *fn, struct ScrDyn *msg, struct ScrDyn *code,
|
||||
struct ScrDyn *options);
|
||||
struct ScrDyn *scr_util_callbackify(struct ScrDyn *original);
|
||||
|
||||
@@ -149,23 +149,9 @@ static bool uc_define(ScrDyn *target, const char *key, ScrDyn *descriptor) {
|
||||
* Node 24's lib/util.js: nullish arguments and an undefined
|
||||
* superCtor.prototype throw ERR_INVALID_ARG_TYPE; then super_ becomes a
|
||||
* writable, configurable, non-enumerable own property and
|
||||
* ctor.prototype's prototype becomes superCtor.prototype. */
|
||||
void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) {
|
||||
if (uc_nullish(ctor)) {
|
||||
scr_dyn_arg_type_fail("ctor", "of type function", ctor);
|
||||
return;
|
||||
}
|
||||
if (uc_nullish(super_ctor)) {
|
||||
scr_dyn_arg_type_fail("superCtor", "of type function", super_ctor);
|
||||
return;
|
||||
}
|
||||
if ((ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) ||
|
||||
(super_ctor->kind == SCR_DYN_FUNC && super_ctor->v.fn.class_obj)) {
|
||||
uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends");
|
||||
return;
|
||||
}
|
||||
ScrDyn *super_prototype = uc_get(super_ctor, "prototype");
|
||||
if (!super_prototype) return;
|
||||
* ctor.prototype's prototype becomes superCtor.prototype. super_prototype
|
||||
* is superCtor.prototype (owned), or NULL when superCtor is no object. */
|
||||
static void uc_inherits(ScrDyn *ctor, ScrDyn *super_ctor, ScrDyn *super_prototype) {
|
||||
if (super_prototype->kind == SCR_DYN_UNDEF) {
|
||||
scr_dyn_prop_type_fail("superCtor.prototype", "of type object", super_prototype);
|
||||
scr_dyn_release(super_prototype);
|
||||
@@ -198,6 +184,40 @@ void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) {
|
||||
scr_dyn_release(super_prototype);
|
||||
}
|
||||
|
||||
void scr_util_inherits(ScrDyn *ctor, ScrDyn *super_ctor) {
|
||||
if (uc_nullish(ctor)) {
|
||||
scr_dyn_arg_type_fail("ctor", "of type function", ctor);
|
||||
return;
|
||||
}
|
||||
if (uc_nullish(super_ctor)) {
|
||||
scr_dyn_arg_type_fail("superCtor", "of type function", super_ctor);
|
||||
return;
|
||||
}
|
||||
if ((ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) ||
|
||||
(super_ctor->kind == SCR_DYN_FUNC && super_ctor->v.fn.class_obj)) {
|
||||
uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends");
|
||||
return;
|
||||
}
|
||||
ScrDyn *super_prototype = uc_get(super_ctor, "prototype");
|
||||
if (!super_prototype) return;
|
||||
uc_inherits(ctor, super_ctor, super_prototype);
|
||||
}
|
||||
|
||||
/* util.inherits(ctor, C) where C is a compiled class whose prototype the
|
||||
* compiler exposes as super_prototype (borrowed): the class value's own
|
||||
* properties do not hold it. ctor itself may not be a class. */
|
||||
void scr_util_inherits_prototype(ScrDyn *ctor, ScrDyn *super_ctor, ScrDyn *super_prototype) {
|
||||
if (uc_nullish(ctor)) {
|
||||
scr_dyn_arg_type_fail("ctor", "of type function", ctor);
|
||||
return;
|
||||
}
|
||||
if (ctor->kind == SCR_DYN_FUNC && ctor->v.fn.class_obj) {
|
||||
uc_fence("util.inherits over class constructors has no lowering; declare the subclass with extends");
|
||||
return;
|
||||
}
|
||||
uc_inherits(ctor, super_ctor, scr_dyn_retain(super_prototype));
|
||||
}
|
||||
|
||||
/* ── util.deprecate ────────────────────────────────────────────────────
|
||||
* The wrapper warns once (and once per code across wrappers) before each
|
||||
* call reaches fn with the call's receiver and arguments; construction
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// A class's prototype as the prototype of other objects: ES5 constructors
|
||||
// that inherit from a class through Object.create, Object.setPrototypeOf and
|
||||
// util.inherits, and plain objects that adopt a class prototype. Inherited
|
||||
// methods, accessors and calls between methods run with the object as the
|
||||
// receiver.
|
||||
import util from "node:util";
|
||||
|
||||
class Shape {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
get label() {
|
||||
return `${this.name} (${this.sides()} sides)`;
|
||||
}
|
||||
sides() {
|
||||
return 0;
|
||||
}
|
||||
describe() {
|
||||
return `${this.label}: area ${this.area().toFixed(2)}`;
|
||||
}
|
||||
area() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
class Polygon extends Shape {
|
||||
sides() {
|
||||
return this.points.length;
|
||||
}
|
||||
perimeter() {
|
||||
let total = 0;
|
||||
for (let i = 0; i < this.points.length; i++) {
|
||||
const [x1, y1] = this.points[i];
|
||||
const [x2, y2] = this.points[(i + 1) % this.points.length];
|
||||
total += Math.hypot(x2 - x1, y2 - y1);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
// Object.create(Class.prototype) for an ES5 subclass.
|
||||
function Square(size) {
|
||||
this.name = "square";
|
||||
this.size = size;
|
||||
this.points = [
|
||||
[0, 0],
|
||||
[size, 0],
|
||||
[size, size],
|
||||
[0, size],
|
||||
];
|
||||
}
|
||||
Square.prototype = Object.create(Polygon.prototype);
|
||||
Square.prototype.constructor = Square;
|
||||
Square.prototype.area = function () {
|
||||
return this.size * this.size;
|
||||
};
|
||||
|
||||
const square = new Square(3);
|
||||
console.log(square.describe());
|
||||
console.log(square.perimeter(), square.sides(), square.label);
|
||||
console.log(square instanceof Square, square instanceof Polygon, square instanceof Shape);
|
||||
console.log(Object.getPrototypeOf(Square.prototype) === Polygon.prototype);
|
||||
console.log(Object.getPrototypeOf(Polygon.prototype) === Shape.prototype);
|
||||
console.log(Object.keys(square).join(","));
|
||||
const inherited = [];
|
||||
for (const key in square) inherited.push(key);
|
||||
console.log(inherited.join(","));
|
||||
|
||||
// Object.setPrototypeOf on an ES5 constructor's prototype.
|
||||
function Circle(radius) {
|
||||
this.name = "circle";
|
||||
this.radius = radius;
|
||||
}
|
||||
Circle.prototype.area = function () {
|
||||
return Math.PI * this.radius * this.radius;
|
||||
};
|
||||
Object.setPrototypeOf(Circle.prototype, Shape.prototype);
|
||||
const circle = new Circle(2);
|
||||
console.log(circle.describe(), circle instanceof Shape, circle instanceof Polygon);
|
||||
|
||||
// util.inherits with a class as the superCtor.
|
||||
function Triangle(a, b, c) {
|
||||
this.name = "triangle";
|
||||
this.points = [a, b, c];
|
||||
}
|
||||
util.inherits(Triangle, Polygon);
|
||||
Triangle.prototype.area = function () {
|
||||
const [[x1, y1], [x2, y2], [x3, y3]] = this.points;
|
||||
return Math.abs((x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)) / 2);
|
||||
};
|
||||
const triangle = new Triangle([0, 0], [4, 0], [0, 3]);
|
||||
console.log(Triangle.super_ === Polygon, triangle instanceof Polygon);
|
||||
console.log(triangle.describe(), triangle.perimeter());
|
||||
|
||||
// A plain object that adopts a class prototype (rehydrating parsed data).
|
||||
const parsed = JSON.parse('{"name":"lot","points":[[0,0],[2,0],[2,1]]}');
|
||||
Object.setPrototypeOf(parsed, Polygon.prototype);
|
||||
console.log(parsed.sides(), parsed.perimeter().toFixed(3), parsed.label, parsed instanceof Polygon);
|
||||
const fresh = Object.create(Shape.prototype);
|
||||
fresh.name = "blank";
|
||||
console.log(fresh.describe(), fresh instanceof Shape, Object.getPrototypeOf(fresh) === Shape.prototype);
|
||||
|
||||
// Native instances keep their own methods and answers.
|
||||
const native = new Polygon("native");
|
||||
native.points = [
|
||||
[0, 0],
|
||||
[1, 0],
|
||||
[0, 1],
|
||||
];
|
||||
console.log(native.describe(), native.perimeter().toFixed(3), native instanceof Polygon);
|
||||
|
||||
// util.inspect names each object by its prototype's constructor.
|
||||
function Plain() {
|
||||
this.v = 1;
|
||||
}
|
||||
Object.setPrototypeOf(Plain.prototype, Shape.prototype);
|
||||
console.log(new Square(1).size, fresh, new Plain(), Object.prototype.toString.call(fresh));
|
||||
console.log(util.inspect(new Plain()), util.inspect(parsed, { depth: 0 }));
|
||||
|
||||
// A subclass keeps its own constructor below a base whose prototype other
|
||||
// objects inherit: `new this.constructor()` in a base method builds the
|
||||
// subclass, also while the subclass's prototype holds a replaceable method.
|
||||
class Disc extends Shape {
|
||||
area() {
|
||||
return 3;
|
||||
}
|
||||
rebuild() {
|
||||
return new this.constructor("copy of " + this.name);
|
||||
}
|
||||
}
|
||||
const areaValue = Shape.prototype.area;
|
||||
const disc = new Disc("d");
|
||||
const rebuilt = disc.rebuild();
|
||||
console.log(disc.constructor === Disc, rebuilt instanceof Disc, rebuilt.area(), rebuilt.name, typeof areaValue);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 8037, "san": 6172 }
|
||||
@@ -2190,3 +2190,33 @@ for (const run of runs) {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("class prototype relinks (JS lane)", () => {
|
||||
test("relinking a class prototype reached through an instance throws", async () => {
|
||||
// Compiled code binds a class's inherited methods, so its prototype's
|
||||
// [[Prototype]] cannot change at run time. Node would print "other";
|
||||
// the native program refuses instead of answering A's method.
|
||||
const r = await compileAndRun(
|
||||
"class-prototype-relink",
|
||||
`'use strict';
|
||||
class A {
|
||||
a() {
|
||||
return 'A.a';
|
||||
}
|
||||
}
|
||||
class B extends A {}
|
||||
const prototype = Object.getPrototypeOf(new B());
|
||||
Object.setPrototypeOf(prototype, Object.getPrototypeOf(prototype));
|
||||
console.log('same prototype', new B().a());
|
||||
Object.setPrototypeOf(prototype, { a: () => 'other' });
|
||||
console.log(new B().a());
|
||||
`,
|
||||
"cjs",
|
||||
);
|
||||
expect(r.exitCode).toBe(1);
|
||||
expect(r.stdout).toBe("same prototype A.a\n");
|
||||
expect(r.stderr).toBe(
|
||||
"Uncaught Error: Object.setPrototypeOf with this value is not supported yet\n",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user