mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Run private members and super calls over foreign receivers like Node (#1975)
This commit is contained in:
@@ -378,8 +378,33 @@ function reflectedMethodValue(
|
||||
!isJsSourceFile(ts.getSourceFileOfNode(declaration)) ||
|
||||
info.localClass !== undefined ||
|
||||
lowerer.errorHierarchyClassOf(info.def.name);
|
||||
// `super.m(...)` in a prototype method reads the base class's prototype,
|
||||
// which lowerSuperMethodCall answers over the call-site receiver when the
|
||||
// base has a prototype object. Other uses of `super` keep the native
|
||||
// receiver.
|
||||
const superCalls = !staticMethod && info.base !== null && prototypeChainViewable(info.base);
|
||||
// Over a dynamic receiver, private fields are brand-checked and read, and private methods
|
||||
// brand-checked and called; a private accessor, or a private method read as a value, would become a
|
||||
// lookup of its name.
|
||||
const privateAccessors = new Set<string>();
|
||||
const privateMethods = new Set<string>();
|
||||
for (const other of declaration.members.nodes)
|
||||
if (other.name !== undefined && ts.isPrivateIdentifier(other.name)) {
|
||||
if (ts.isAccessor(other)) privateAccessors.add(other.name.text);
|
||||
else if (ts.isMethodDeclaration(other)) privateMethods.add(other.name.text);
|
||||
}
|
||||
ts.walkPreorder(member, (node) => {
|
||||
if (ts.isPrivateIdentifier(node) || node.kind === ts.SyntaxKind.SuperKeyword)
|
||||
if (node.kind === ts.SyntaxKind.SuperKeyword && !(superCalls && isSuperMethodCallee(node)))
|
||||
needsNativeReceiver = true;
|
||||
if (!ts.isPrivateIdentifier(node)) return;
|
||||
// Private members on a static method's receiver (the class value) keep
|
||||
// the native receiver; prototype methods brand-check theirs, except
|
||||
// private accessors and private methods used other than by a call.
|
||||
if (
|
||||
staticMethod ||
|
||||
privateAccessors.has(node.text) ||
|
||||
(privateMethods.has(node.text) && !isPrivateMethodCallee(node))
|
||||
)
|
||||
needsNativeReceiver = true;
|
||||
});
|
||||
if (needsNativeReceiver) return null;
|
||||
@@ -403,7 +428,11 @@ function reflectedMethodValue(
|
||||
loc: locOf(member),
|
||||
};
|
||||
}
|
||||
// The method may be lowered on top of an unrelated caller's frames (a
|
||||
// `super` call in another class's method reflects this class): its `this`
|
||||
// is its own receiver, never an enclosing function's.
|
||||
const context = newFnCtx(false, null, null, DYN);
|
||||
context.thisBoundary = true;
|
||||
const previousClass = lowerer.currentClass;
|
||||
lowerer.currentClass = info;
|
||||
lowerer.fnStack.push(context);
|
||||
@@ -441,6 +470,55 @@ function reflectedMethodValue(
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether `base` and every class above it have a complete prototype object
|
||||
* (reflectClassPrototype): module-level program classes that neither extend
|
||||
* Date nor a runtime-provided class. */
|
||||
function prototypeChainViewable(base: ClassInfo): boolean {
|
||||
for (let c: ClassInfo | null = base; c; c = c.base)
|
||||
if (
|
||||
c.decl === null ||
|
||||
c.localClass !== undefined ||
|
||||
c.generic !== undefined ||
|
||||
c.genericInstance !== undefined ||
|
||||
c.mixinInstance !== undefined ||
|
||||
c.callableBase !== undefined ||
|
||||
c.extendsDate !== undefined ||
|
||||
c.def.runtime ||
|
||||
c.builtinEmitter ||
|
||||
c.builtinStream ||
|
||||
c.builtinError ||
|
||||
c.builtinEventTarget
|
||||
)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** `super` as the receiver of a called member: `super.m(...)`. */
|
||||
/** Whether `node`, a private name, is the method a call calls (`x.#m(...)`). */
|
||||
function isPrivateMethodCallee(node: ts.Node): boolean {
|
||||
const access = node.parent;
|
||||
return (
|
||||
access !== undefined &&
|
||||
ts.isPropertyAccessExpression(access) &&
|
||||
access.name === node &&
|
||||
access.parent !== undefined &&
|
||||
ts.isCallExpression(access.parent) &&
|
||||
access.parent.expression === access
|
||||
);
|
||||
}
|
||||
|
||||
function isSuperMethodCallee(node: ts.Node): boolean {
|
||||
const access = node.parent;
|
||||
return (
|
||||
access !== undefined &&
|
||||
ts.isPropertyAccessExpression(access) &&
|
||||
access.expression === node &&
|
||||
access.parent !== undefined &&
|
||||
ts.isCallExpression(access.parent) &&
|
||||
access.parent.expression === access
|
||||
);
|
||||
}
|
||||
|
||||
/** Complete adapters after all native class bodies and instantiations exist. */
|
||||
export function finalizeClassMethodValues(lowerer: Lowerer, functions: IrFunction[]): void {
|
||||
const byName = new Map(functions.map((fn) => [fn.name, fn]));
|
||||
|
||||
@@ -423,7 +423,10 @@ export function reflectClassPrototype(
|
||||
loc,
|
||||
},
|
||||
);
|
||||
// The members lower on top of whatever frames requested the view (a
|
||||
// `super` call in a subclass's method); their `this` is their own receiver.
|
||||
const context = newFnCtx(false, null, null, DYN);
|
||||
context.thisBoundary = true;
|
||||
lowerer.fnStack.push(context);
|
||||
try {
|
||||
for (const member of info.decl.members.nodes) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js";
|
||||
import { assertFunctionAliasOf } from "./lower-assert.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { perKeyOrder } from "./key-order-witness.js";
|
||||
@@ -589,6 +590,17 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
) {
|
||||
return lowerer.lowerSuperMethodCall(expr, expr.expression);
|
||||
}
|
||||
// `value.#m(...)` on a checked-dynamic value (`this` in a JavaScript
|
||||
// class's receiver-independent method): the private method of the
|
||||
// brand-checked receiver.
|
||||
if (
|
||||
ts.isPropertyAccessExpression(expr.expression) &&
|
||||
ts.isPrivateIdentifier(expr.expression.name) &&
|
||||
!expr.expression.questionDotToken
|
||||
) {
|
||||
const privateCall = lowerCheckedPrivateMethodCall(lowerer, expr, expr.expression);
|
||||
if (privateCall) return privateCall;
|
||||
}
|
||||
|
||||
const consoleMember = lowerer.consoleCallMember(expr);
|
||||
const consoleMethod = consoleMember === null ? lowerer.consoleMethodMember(expr) : null;
|
||||
@@ -11430,3 +11442,35 @@ function lowerShimIntrinsicCall(
|
||||
);
|
||||
return { kind: "libCall", fn: intrinsic.fn, args, type: intrinsic.ret, loc };
|
||||
}
|
||||
|
||||
/** `value.#m(args)` where `value` lowers to a checked-dynamic value and the
|
||||
* declaration-level class whose body declares `#m` declares it as a method:
|
||||
* the method called on the brand-checked receiver
|
||||
* (checkedPrivateMethodReceiver). Null for any other private member call. */
|
||||
function lowerCheckedPrivateMethodCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (!ts.isPrivateIdentifier(access.name)) return null;
|
||||
const name = access.name.text;
|
||||
const owner = privateNameClass(lowerer, access.name);
|
||||
const found = owner ? lowerer.findMethodOn(owner, name) : null;
|
||||
if (!owner || !found || found.declarer !== owner || found.sig.abstract === true) return null;
|
||||
const probed = tryLowerExpression(lowerer, access.expression);
|
||||
if (!probed || !checkedDynamicOperand(probed)) return null;
|
||||
const value = checkedDynamicOperand(lowerer.lowerExpr(access.expression));
|
||||
if (!value) return null;
|
||||
const loc = locOf(call);
|
||||
const callee = `%${owner.def.name}.${name}`;
|
||||
lowerer.noteEdge(callee);
|
||||
const receiver = checkedPrivateMethodReceiver(lowerer, owner, name, value, loc);
|
||||
const args = lowerer.completeArgs(call.arguments.nodes, found.sig.params, loc, call);
|
||||
return { kind: "call", callee, args: [receiver, ...args], type: found.sig.ret, loc };
|
||||
}
|
||||
|
||||
/** A checked-dynamic value, or the dynamic value under a checked extraction. */
|
||||
function checkedDynamicOperand(expr: IrExpr): IrExpr | null {
|
||||
if (expr.type.kind === "dyn") return expr;
|
||||
return expr.kind === "dynCheck" && expr.value.type.kind === "dyn" ? expr.value : null;
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ import {
|
||||
import { lowerObjectFactoryNew } from "./object-factory-new.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerInstanceConstructorNew } from "./class-instance-constructor.js";
|
||||
import { classPrototypeData } from "./class-prototypes.js";
|
||||
import { classPrototypeData, reflectClassPrototype } from "./class-prototypes.js";
|
||||
import { constructionEscape } from "./construction-escapes.js";
|
||||
import { checkedClassConstruction, checkedClassConstructionPacked } from "./class-construction.js";
|
||||
import { initializeRuntimeStatics, hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||
@@ -9430,8 +9430,16 @@ export function lowerSuperMethodCall(
|
||||
if (!thisLocal) lowerer.unsupported("SC1080", access);
|
||||
const loc = locOf(call);
|
||||
const thisRef: IrExpr = { kind: "varRef", localId: thisLocal.id, type: thisLocal.type, loc };
|
||||
if (lowerer.prototypeMethodAccesses.has(access.name.text)) {
|
||||
const prototype = classPrototypeData(lowerer, cls.base, loc);
|
||||
// A method whose `this` is the call-site receiver (a JavaScript class's
|
||||
// receiver-independent method value, reflectedClassMethodValue) looks the
|
||||
// method up on the base class's complete prototype, as `super` reads
|
||||
// [[HomeObject]].[[Prototype]], and calls it with that receiver.
|
||||
const dynamicThis = thisRef.type.kind === "dyn";
|
||||
if (dynamicThis || lowerer.prototypeMethodAccesses.has(access.name.text)) {
|
||||
const prototype =
|
||||
dynamicThis && cls.base.decl
|
||||
? reflectClassPrototype(lowerer, cls.base, loc, classValueRef(lowerer, cls.base, access))
|
||||
: classPrototypeData(lowerer, cls.base, loc);
|
||||
if (prototype) {
|
||||
const callee: IrExpr = {
|
||||
kind: "dynKeyGet",
|
||||
@@ -9440,12 +9448,21 @@ export function lowerSuperMethodCall(
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// Spread arguments are spread into the dynamic call, as in a dynamic method call.
|
||||
const spreads: { arg: number; what: string }[] = [];
|
||||
const args = call.arguments.nodes.map((arg, index) => {
|
||||
if (!ts.isSpreadElement(arg)) return lowerer.lowerExprExpecting(arg, DYN);
|
||||
const spelling = ts.getTextOfNode(arg.expression);
|
||||
spreads.push({ arg: index, what: spelling });
|
||||
return checkedIterableSpread(lowerer, lowerer.lowerExpr(arg.expression), spelling, loc);
|
||||
});
|
||||
const value: IrExpr = {
|
||||
kind: "dynCall",
|
||||
callee,
|
||||
receiver: lowerer.coerceToExpected(thisRef, DYN),
|
||||
calleeName: ts.getTextOfNode(access),
|
||||
args: call.arguments.nodes.map((arg) => lowerer.lowerExprExpecting(arg, DYN)),
|
||||
args,
|
||||
...(spreads.length > 0 ? { spreads } : {}),
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
|
||||
@@ -17,8 +17,9 @@ import type { Lowerer } from "./lowerer.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
|
||||
/** How a brand helper uses its receiver: `in` answers `#x in v`, `read` and
|
||||
* `write` return the receiver as the declaring class for a field access. */
|
||||
type BrandUse = "in" | "read" | "write";
|
||||
* `write` return the receiver as the declaring class for a field access, and
|
||||
* `call` for a private method call. */
|
||||
type BrandUse = "in" | "read" | "write" | "call";
|
||||
|
||||
const PREFIX = "%private.";
|
||||
|
||||
@@ -214,6 +215,35 @@ export function checkedPrivateValue(
|
||||
return { kind: "call", callee: helper, args: [receiver], type: resultType, loc };
|
||||
}
|
||||
|
||||
/** `value.#m(...)` on a checked-dynamic `value` in a declaration-level class
|
||||
* `info` that declares the private method `#m`: the receiver as an instance
|
||||
* of `info` when the value is one, else Node's TypeError ("Receiver must be
|
||||
* an instance of class C"). */
|
||||
export function checkedPrivateMethodReceiver(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
name: string,
|
||||
receiver: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrExpr {
|
||||
const className = info.def.name;
|
||||
const resultType: IrType = { kind: "object", className };
|
||||
const helper = `${PREFIX}call:${className}:${name}`;
|
||||
if (!lowerer.valueHelpers.has(helper)) {
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [{ localId: "value.0", name: "value", type: DYN }],
|
||||
returnType: resultType,
|
||||
locals: [{ id: "value.0", name: "value", type: DYN, mutable: false }],
|
||||
// finalizePrivateBrands fills the body.
|
||||
body: [],
|
||||
loc: helperLoc(loc),
|
||||
});
|
||||
lowerer.valueHelpers.set(helper, helper);
|
||||
}
|
||||
return { kind: "call", callee: helper, args: [receiver], type: resultType, loc };
|
||||
}
|
||||
|
||||
/** A plain assignment, a destructuring target, or a for-in/of target writes
|
||||
* the field. Compound assignments and updates read it first, and Node
|
||||
* reports the failed read. */
|
||||
@@ -358,6 +388,22 @@ export function finalizePrivateBrands(lowerer: Lowerer, functions: IrFunction[])
|
||||
},
|
||||
{ kind: "return", value: { kind: "boolLit", value: false, type: BOOL, loc }, loc },
|
||||
);
|
||||
} else if (use === "call") {
|
||||
// V8 names the class by its own identifier (not a name inferred from
|
||||
// a binding), "anonymous" without one, also for a nullish receiver.
|
||||
const owner = lowerer.classes.get(className);
|
||||
const identifier = owner?.decl?.name;
|
||||
const shown = identifier !== undefined ? identifier.text : "anonymous";
|
||||
for (const unit of ["undefined", "null"] as const) {
|
||||
body.push({
|
||||
kind: "if",
|
||||
cond: { kind: "dynTest", test: unit, value, type: BOOL, loc },
|
||||
then: [typeError(strLit(`Cannot read properties of ${unit} (reading '${shown}')`, loc))],
|
||||
else_: null,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
body.push(typeError(strLit(`Receiver must be an instance of class ${shown}`, loc)));
|
||||
} else {
|
||||
const verb = use === "read" ? "reading" : "setting";
|
||||
const action = use === "read" ? "Cannot read properties" : "Cannot set properties";
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7621-class-methods-foreign-receivers.mjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
// Prototype methods called with a receiver that is not an instance of
|
||||
// their class (`C.prototype.m.call(other)`): `#private` members throw
|
||||
// Node's TypeErrors for such a receiver, and `super.m()` looks the method up
|
||||
// on the base class's prototype and calls it with the receiver.
|
||||
function report(label, run) {
|
||||
try {
|
||||
console.log(label, run());
|
||||
} catch (error) {
|
||||
console.log(label, error.constructor.name, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
class Counter {
|
||||
#count = 0;
|
||||
#step = 1;
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
read() {
|
||||
return this.#count;
|
||||
}
|
||||
bump() {
|
||||
this.#count += this.#step;
|
||||
return this.#count;
|
||||
}
|
||||
reset(value) {
|
||||
this.#count = value;
|
||||
return this.name;
|
||||
}
|
||||
#label() {
|
||||
return `${this.name}#${this.#count}`;
|
||||
}
|
||||
label() {
|
||||
return this.#label();
|
||||
}
|
||||
owns(value) {
|
||||
return #count in value;
|
||||
}
|
||||
ownsSelf() {
|
||||
return #count in this;
|
||||
}
|
||||
}
|
||||
|
||||
const counter = new Counter("c");
|
||||
console.log(counter.bump(), counter.bump(), counter.read(), counter.label());
|
||||
console.log(counter.owns(counter), counter.owns({}), counter.ownsSelf(), counter.reset(7), counter.read());
|
||||
|
||||
const plain = { name: "plain" };
|
||||
report("read", () => Counter.prototype.read.call(plain));
|
||||
report("bump", () => Counter.prototype.bump.call(plain));
|
||||
report("reset", () => Counter.prototype.reset.call(plain, 3));
|
||||
report("label", () => Counter.prototype.label.call(plain));
|
||||
report("ownsSelf", () => Counter.prototype.ownsSelf.call(plain));
|
||||
report("owns primitive", () => Counter.prototype.owns.call(counter, 5));
|
||||
report("label undefined", () => Counter.prototype.label.call(undefined));
|
||||
report("label null", () => Counter.prototype.label.call(null));
|
||||
report("label number", () => Counter.prototype.label.call(4));
|
||||
report("read other instance", () => Counter.prototype.read.call(new Counter("d")));
|
||||
report("label other instance", () => Counter.prototype.label.call(new Counter("e")));
|
||||
|
||||
const Anonymous = class {
|
||||
#m() {
|
||||
return 1;
|
||||
}
|
||||
call() {
|
||||
return this.#m();
|
||||
}
|
||||
};
|
||||
const Named = class Inner {
|
||||
#m() {
|
||||
return 2;
|
||||
}
|
||||
call() {
|
||||
return this.#m();
|
||||
}
|
||||
};
|
||||
report("anonymous", () => Anonymous.prototype.call.call({}));
|
||||
report("named", () => Named.prototype.call.call({}));
|
||||
console.log(new Anonymous().call(), new Named().call());
|
||||
|
||||
class Shape {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
}
|
||||
describe() {
|
||||
return `shape ${this.name}`;
|
||||
}
|
||||
area() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
class Square extends Shape {
|
||||
describe() {
|
||||
return `square: ${super.describe()} area ${this.area()}`;
|
||||
}
|
||||
area() {
|
||||
return this.size * this.size;
|
||||
}
|
||||
both() {
|
||||
return [super.describe(), super.area()].join("|");
|
||||
}
|
||||
}
|
||||
class Tiny extends Square {
|
||||
#tag = "t";
|
||||
describe() {
|
||||
return `tiny<${super.describe()}>`;
|
||||
}
|
||||
tag() {
|
||||
return `${this.#tag}:${super.both()}`;
|
||||
}
|
||||
}
|
||||
|
||||
const square = new Square("s");
|
||||
square.size = 3;
|
||||
console.log(square.describe(), square.both());
|
||||
const tiny = new Tiny("t");
|
||||
tiny.size = 1;
|
||||
console.log(tiny.describe(), tiny.tag());
|
||||
const foreign = {
|
||||
name: "f",
|
||||
size: 2,
|
||||
area() {
|
||||
return 40;
|
||||
},
|
||||
};
|
||||
report("square describe", () => Square.prototype.describe.call(foreign));
|
||||
report("square both", () => Square.prototype.both.call(foreign));
|
||||
report("tiny describe", () => Tiny.prototype.describe.call(foreign));
|
||||
report("tiny tag", () => Tiny.prototype.tag.call(foreign));
|
||||
report("square describe on tiny", () => Square.prototype.describe.call(tiny));
|
||||
|
||||
// A base method that builds `new this.constructor()` on a subclass instance
|
||||
// builds the subclass, also once the base prototype is complete (a super call
|
||||
// above reads it) and `copy` is read through its prototype slot.
|
||||
class Node3 {
|
||||
constructor() {
|
||||
this.a = 0;
|
||||
}
|
||||
copy(source) {
|
||||
this.a = source.a;
|
||||
return this;
|
||||
}
|
||||
clone() {
|
||||
return new this.constructor().copy(this);
|
||||
}
|
||||
}
|
||||
class Mesh3 extends Node3 {
|
||||
constructor(geometry) {
|
||||
super();
|
||||
this.geometry = geometry;
|
||||
}
|
||||
copy(source) {
|
||||
super.copy(source);
|
||||
this.geometry = source.geometry;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
const copyValue = Node3.prototype.copy;
|
||||
const mesh = new Mesh3({ id: 1 });
|
||||
mesh.a = 5;
|
||||
const meshClone = mesh.clone();
|
||||
console.log(
|
||||
meshClone instanceof Mesh3,
|
||||
meshClone.constructor === Mesh3,
|
||||
mesh.constructor === Mesh3,
|
||||
meshClone.a,
|
||||
JSON.stringify(meshClone.geometry),
|
||||
typeof copyValue,
|
||||
);
|
||||
|
||||
// Methods that use a private accessor or return a private method keep their own receiver, as values too.
|
||||
class Vault {
|
||||
#v = 1;
|
||||
get #value() {
|
||||
return this.#v * 10;
|
||||
}
|
||||
set #value(x) {
|
||||
this.#v = x;
|
||||
}
|
||||
#hidden() {
|
||||
return "hidden " + this.#v;
|
||||
}
|
||||
read() {
|
||||
return this.#value;
|
||||
}
|
||||
bump() {
|
||||
this.#value = this.#v + 1;
|
||||
return this.#v;
|
||||
}
|
||||
hiddenValue() {
|
||||
return this.#hidden;
|
||||
}
|
||||
}
|
||||
const vault = new Vault();
|
||||
const readVault = Vault.prototype.read;
|
||||
const bumpVault = Vault.prototype.bump;
|
||||
const hiddenOf = Vault.prototype.hiddenValue;
|
||||
console.log(readVault.call(vault), bumpVault.call(vault), readVault.call(vault), typeof hiddenOf.call(vault));
|
||||
console.log(hiddenOf.call(vault).call(vault));
|
||||
try {
|
||||
readVault.call({});
|
||||
} catch (error) {
|
||||
console.log(error.constructor.name);
|
||||
}
|
||||
|
||||
// A method value whose body calls super with spread arguments.
|
||||
class Summer {
|
||||
add(...xs) {
|
||||
return xs.reduce((sum, x) => sum + x, 0);
|
||||
}
|
||||
}
|
||||
class Doubler extends Summer {
|
||||
add(...xs) {
|
||||
return super.add(...xs) * 2;
|
||||
}
|
||||
}
|
||||
const addValue = Doubler.prototype.add;
|
||||
console.log(addValue.call(new Doubler(), 1, 2, 3), addValue.apply(new Doubler(), [4, 5]));
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 6415, "san": 10107 }
|
||||
Reference in New Issue
Block a user