mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Test262: private static members with brand checks (#2549)
This commit is contained in:
@@ -100,6 +100,21 @@ export function initializeRuntimeStatics(
|
||||
}
|
||||
if (!member.name) continue;
|
||||
const memberLoc = locOf(member);
|
||||
// A private static field: its native global, assigned in order.
|
||||
if (ts.isPrivateIdentifier(member.name)) {
|
||||
const name = member.name.text;
|
||||
const field = info.staticFields.find((candidate) => candidate.name === name);
|
||||
if (!ts.isPropertyDeclaration(member) || !field) {
|
||||
lowerer.unsupported("SC1090", member, "private static fields without an initializer");
|
||||
}
|
||||
statements.push({
|
||||
kind: "assign",
|
||||
localId: field.globalId,
|
||||
value: lowerer.lowerExprExpecting(member.initializer!, field.type),
|
||||
loc: memberLoc,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const key: IrExpr = ts.isComputedPropertyName(member.name)
|
||||
? computedKeys.get(member)!
|
||||
: lowerer.coerceToExpected(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* Binary operators: arithmetic, comparisons and equality, logical and nullish pairs, and string conversion for concatenation. */
|
||||
import { lowerStaticPrivateAssignment } from "../private-statics.js";
|
||||
import { keyedHelperName } from "../stable-names.js";
|
||||
import * as ts from "../../ts7/adapter.js";
|
||||
import {
|
||||
@@ -632,7 +633,11 @@ export function lowerNativeFunctionAssignment(
|
||||
const classType = storedClassValueType(lowerer, target.expression);
|
||||
const staticClass =
|
||||
classType?.kind === "classval" ? lowerer.classes.get(classType.className) : undefined;
|
||||
if (staticClass && hasRuntimeStatics(staticClass)) {
|
||||
if (
|
||||
staticClass &&
|
||||
hasRuntimeStatics(staticClass) &&
|
||||
!(ts.isPropertyAccessExpression(target) && ts.isPrivateIdentifier(target.name))
|
||||
) {
|
||||
return lowerDynMemberAssignment(
|
||||
lowerer,
|
||||
expr,
|
||||
@@ -1110,6 +1115,8 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
|
||||
if (op === ts.SyntaxKind.EqualsToken) {
|
||||
const prototypeWrite = lowerClassPrototypeComputedAssignment(lowerer, expr);
|
||||
if (prototypeWrite) return prototypeWrite;
|
||||
const staticPrivateWrite = lowerStaticPrivateAssignment(lowerer, expr);
|
||||
if (staticPrivateWrite) return staticPrivateWrite;
|
||||
const callableWrite = lowerNativeFunctionAssignment(lowerer, expr);
|
||||
if (callableWrite) return callableWrite;
|
||||
const dereferenced = lowerDereferencedElementAssignment(lowerer, expr);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { widensReducedRecords } from "../function-returns.js";
|
||||
import { lowerStaticPrivateRead } from "../private-statics.js";
|
||||
import { widthNarrowHelper } from "../coercions/unions.js";
|
||||
/* Property reads (`a.b`): lowerExprInner's property-access branch, intrinsic and union properties, field reads and targets, and match-group reads. */
|
||||
import { jsSpecialGlobalValue } from "../global-values.js";
|
||||
@@ -344,6 +345,12 @@ export function lowerPropertyAccessExpr(
|
||||
): IrExpr {
|
||||
const arrayClassConstructor = lowerArrayClassConstructorRead(lowerer, expr, loc);
|
||||
if (arrayClassConstructor !== null) return arrayClassConstructor;
|
||||
// `R.#x` for a static private on a checked-dynamic receiver: the brand
|
||||
// check, then the native storage (private-statics.ts).
|
||||
{
|
||||
const staticPrivate = lowerStaticPrivateRead(lowerer, expr, loc);
|
||||
if (staticPrivate) return staticPrivate;
|
||||
}
|
||||
// `value.#x` on a checked-dynamic value, or on a checked extraction of
|
||||
// one: the field of the brand-checked receiver (checkedPrivateValue).
|
||||
if (ts.isPrivateIdentifier(expr.name)) {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* Increment, decrement and prefix unary operators, and compound field updates. */
|
||||
import { staticPrivateUpdateTarget } from "../private-statics.js";
|
||||
import { lowerCompoundValueToTarget } from "./binary.js";
|
||||
import {
|
||||
indexedUnionArms,
|
||||
lowerIndexedUnionNumberRead,
|
||||
@@ -269,6 +271,19 @@ export function lowerIncDec(
|
||||
}
|
||||
fenceNodeModuleMutation(lowerer, access, "assignment");
|
||||
if (ts.isPropertyAccessExpression(access)) {
|
||||
// `this.#x++` on a static private field (private-statics.ts): the
|
||||
// brand check, then the field's native global.
|
||||
const staticPrivate = staticPrivateUpdateTarget(lowerer, access);
|
||||
if (staticPrivate) {
|
||||
const update = lowerIncDecTarget(lowerer, expr, staticPrivate.target, prefix);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [staticPrivate.brand],
|
||||
result: update,
|
||||
type: update.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
const target =
|
||||
staticFieldWriteTarget(lowerer, access) ??
|
||||
expandoWritableTarget(lowerer, access) ??
|
||||
@@ -922,6 +937,27 @@ export function lowerFieldCompoundValue(
|
||||
"compound assignment through 'super' (read and write separately)",
|
||||
);
|
||||
}
|
||||
// `this.#x op= v` on a static private field (private-statics.ts): the
|
||||
// brand check, then the field's native global read before the right side.
|
||||
if (
|
||||
ts.isPropertyAccessExpression(access) &&
|
||||
rhsNode !== null &&
|
||||
ts.isBinaryExpression(access.parent) &&
|
||||
access.parent.left === access
|
||||
) {
|
||||
const staticPrivate = staticPrivateUpdateTarget(lowerer, access);
|
||||
if (staticPrivate) {
|
||||
const { target } = staticPrivate;
|
||||
const value = lowerCompoundValueToTarget(lowerer, access.parent, op, target);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [staticPrivate.brand],
|
||||
result: { kind: "assignExpr", localId: target.id, value, type: target.type, loc },
|
||||
type: target.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
}
|
||||
const body: IrStmt[] = [];
|
||||
const save = (value: IrExpr, name: string): IrExpr => {
|
||||
const local = lowerer.declareHiddenLocal(name, value.type);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "./builtins/array-classes.js";
|
||||
import { structuralCallInstance } from "./structural-instances.js";
|
||||
import { checkedPrivateMethodReceiver, privateNameClass } from "./private-brands.js";
|
||||
import { lowerStaticPrivateCall } from "./private-statics.js";
|
||||
import { assertFunctionAliasOf } from "./lower-assert.js";
|
||||
import { keyedHelperName } from "./stable-names.js";
|
||||
import { perKeyOrder } from "./key-order-witness.js";
|
||||
@@ -691,6 +692,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
ts.isPrivateIdentifier(expr.expression.name) &&
|
||||
!expr.expression.questionDotToken
|
||||
) {
|
||||
const staticPrivate = lowerStaticPrivateCall(lowerer, expr, expr.expression);
|
||||
if (staticPrivate) return staticPrivate;
|
||||
const privateCall = lowerCheckedPrivateMethodCall(lowerer, expr, expr.expression);
|
||||
if (privateCall) return privateCall;
|
||||
}
|
||||
@@ -2146,7 +2149,7 @@ function callableInstanceStringCall(
|
||||
|
||||
/** A call through an evaluated callee value: checked-dynamic callees,
|
||||
* function unions, typed-rest and runtime-arity closures. */
|
||||
function lowerCallThroughValue(
|
||||
export function lowerCallThroughValue(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression,
|
||||
callee: IrExpr,
|
||||
|
||||
@@ -2453,9 +2453,15 @@ export function collectClassShapeInner(
|
||||
(modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword,
|
||||
),
|
||||
)) &&
|
||||
// A private static ACCESSOR has no native storage yet, and a private
|
||||
// static method that reads `arguments` would lose the arguments past
|
||||
// its parameters when called through its value (`this.#m` returned by
|
||||
// a getter): keep such a class on the static path, whose public
|
||||
// static accessors refuse.
|
||||
!decl.members.nodes.some(
|
||||
(member) =>
|
||||
member.name &&
|
||||
(ts.isAccessor(member) ||
|
||||
(ts.isMethodDeclaration(member) && bodyReadsArguments(member))) &&
|
||||
ts.isPrivateIdentifier(member.name) &&
|
||||
ts
|
||||
.getModifiers(member)
|
||||
@@ -2524,8 +2530,22 @@ export function collectClassShapeInner(
|
||||
.getModifiers(member)
|
||||
?.some((modifier) => modifier.kind === ts.SyntaxKind.StaticKeyword))
|
||||
) {
|
||||
runtimeStatics.push(member);
|
||||
continue;
|
||||
// A private static of a declaration-level class keeps its native
|
||||
// storage (the %C.static:#m function, the %g.s.C.#x global): the
|
||||
// constructor object never carries a property for it, and every
|
||||
// access brand-checks its receiver (private-statics.ts). A field
|
||||
// also stays in runtimeStatics, so its initializer runs in source
|
||||
// order with the constructor's other initializers.
|
||||
const privateStatic =
|
||||
checkedStatics &&
|
||||
!functionLocalClass(decl) &&
|
||||
(member as { name?: ts.PropertyName }).name !== undefined &&
|
||||
ts.isPrivateIdentifier((member as { name: ts.PropertyName }).name);
|
||||
if (!privateStatic) {
|
||||
runtimeStatics.push(member);
|
||||
continue;
|
||||
}
|
||||
if (ts.isPropertyDeclaration(member)) runtimeStatics.push(member);
|
||||
}
|
||||
if (
|
||||
functionLocalClass(decl) &&
|
||||
@@ -5140,11 +5160,15 @@ function lowerStaticFieldInitsInner(lowerer: Lowerer, info: ClassInfo): IrStmt[]
|
||||
| { pos: number; kind: "field"; f: ClassInfo["staticFields"][number] }
|
||||
| { pos: number; kind: "block"; b: ts.ClassStaticBlockDeclaration };
|
||||
const items: Item[] = [
|
||||
...info.staticFields.map((f): Item => ({
|
||||
pos: ts.tokenStartOfNode(f.initializer),
|
||||
kind: "field",
|
||||
f,
|
||||
})),
|
||||
// A runtime-statics class initializes its private static fields with
|
||||
// its other initializers (initializeRuntimeStatics).
|
||||
...info.staticFields
|
||||
.filter((f) => !(info.runtimeStatics && f.name.startsWith("#")))
|
||||
.map((f): Item => ({
|
||||
pos: ts.tokenStartOfNode(f.initializer),
|
||||
kind: "field",
|
||||
f,
|
||||
})),
|
||||
...(info.staticBlocks ?? []).map((b): Item => ({
|
||||
pos: ts.tokenStartOfNode(b),
|
||||
kind: "block",
|
||||
@@ -5849,7 +5873,7 @@ export function lowerClassStaticRead(
|
||||
!(ts.isCallExpression(site.parent) && site.parent.expression === site)
|
||||
)
|
||||
lowerer.noLowering(`Date.${name} read through a class that extends Date`, site);
|
||||
if (hasRuntimeStatics(info) && !found?.field)
|
||||
if (hasRuntimeStatics(info) && !found?.field && !name.startsWith("#"))
|
||||
return {
|
||||
kind: "dynKeyGet",
|
||||
value: lowerer.coerceToExpected(classValueRef(lowerer, info, receiver), DYN),
|
||||
@@ -6747,7 +6771,9 @@ function staticCallOn(
|
||||
if (date) return date;
|
||||
}
|
||||
if (
|
||||
(hasRuntimeStatics(info) && !findStaticOn(lowerer, info, access.name.text)?.field) ||
|
||||
(hasRuntimeStatics(info) &&
|
||||
!access.name.text.startsWith("#") &&
|
||||
!findStaticOn(lowerer, info, access.name.text)?.field) ||
|
||||
(info.callableBase &&
|
||||
!findStaticOn(lowerer, info, access.name.text) &&
|
||||
!findGenericStaticOn(lowerer, info, access.name.text))
|
||||
@@ -7202,7 +7228,9 @@ export function lowerClassValueProperty(
|
||||
if (info?.def.runtime && member !== "name") return null;
|
||||
if (
|
||||
info &&
|
||||
((hasRuntimeStatics(info) && !findStaticOn(lowerer, info, member)?.field) ||
|
||||
((hasRuntimeStatics(info) &&
|
||||
!member.startsWith("#") &&
|
||||
!findStaticOn(lowerer, info, member)?.field) ||
|
||||
(info.callableBase && !findStaticOn(lowerer, info, member)))
|
||||
)
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { lowerStaticPrivateAssignment } from "./private-statics.js";
|
||||
import type { DiagnosticCode } from "../../diagnostics/codes.js";
|
||||
import { keepsInitializerLayout, notePreservedLayout } from "./structural-instances.js";
|
||||
import { jsGlobalValueByName } from "./global-values.js";
|
||||
@@ -8732,6 +8733,9 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
|
||||
if (urlWrite) return { kind: "exprStmt", expr: urlWrite, loc: locOf(expr) };
|
||||
}
|
||||
if (opKind === ts.SyntaxKind.EqualsToken) {
|
||||
const staticPrivateWrite = lowerStaticPrivateAssignment(lowerer, expr);
|
||||
if (staticPrivateWrite)
|
||||
return { kind: "exprStmt", expr: staticPrivateWrite, loc: locOf(expr) };
|
||||
const callableWrite = lowerNativeFunctionAssignment(lowerer, expr);
|
||||
if (callableWrite) return { kind: "exprStmt", expr: callableWrite, loc: locOf(expr) };
|
||||
// Expando function members (`foo.bar = 12`, `foo[SYM] = v` on a
|
||||
|
||||
@@ -0,0 +1,365 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import {
|
||||
BOOL,
|
||||
DYN,
|
||||
VOID,
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
} from "../../ir/ir.js";
|
||||
import { helperLoc, strLit, varRef } from "../../ir/build.js";
|
||||
import { locOf } from "../program.js";
|
||||
import { hasStaticThis } from "./static-this.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { lowerClassStaticRead, lowerStaticMethodCall, type ClassInfo } from "./lower-classes.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerCallThroughValue } from "./lower-calls.js";
|
||||
|
||||
/** How a static private name is used: a field read or write, or a method
|
||||
* (or accessor) read or call. V8 words each failure differently. */
|
||||
export type StaticPrivateUse = "read" | "write" | "method";
|
||||
|
||||
/** The declaration-level class whose body declares `priv` as a STATIC
|
||||
* member (the nearest enclosing class declaring the name, as JavaScript
|
||||
* scoping resolves it). Null for an instance private, a function-local or
|
||||
* generic class, or a class that was not collected. */
|
||||
export function staticPrivateOwner(lowerer: Lowerer, priv: ts.PrivateIdentifier): ClassInfo | null {
|
||||
for (let node: ts.Node | undefined = priv.parent; node; node = node.parent) {
|
||||
if (!ts.isClassDeclaration(node) && !ts.isClassExpression(node)) continue;
|
||||
const member = node.members.nodes.find((candidate) => {
|
||||
const name = (candidate as { name?: ts.PropertyName }).name;
|
||||
return name !== undefined && ts.isPrivateIdentifier(name) && name.text === priv.text;
|
||||
});
|
||||
if (!member) continue;
|
||||
const isStatic =
|
||||
ts.canHaveModifiers(member) &&
|
||||
ts.getModifiers(member)?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword) === true;
|
||||
if (!isStatic) return null;
|
||||
for (const info of lowerer.classes.values()) {
|
||||
if (info.decl !== node) continue;
|
||||
if (info.localClass || info.generic || info.genericInstance) return null;
|
||||
return info;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** V8's TypeError text for a static private `name` of `info` used on a
|
||||
* receiver that is not the declaring class object. */
|
||||
function brandMessage(info: ClassInfo, use: StaticPrivateUse, name: string): string {
|
||||
if (use === "read")
|
||||
return `Cannot read private member ${name} from an object whose class did not declare it`;
|
||||
if (use === "write")
|
||||
return `Cannot write private member ${name} to an object whose class did not declare it`;
|
||||
// V8 names the class by its own identifier; an anonymous class
|
||||
// expression prints ".".
|
||||
return `Receiver must be class ${info.decl?.name?.text ?? "."}`;
|
||||
}
|
||||
|
||||
/** The brand check of a static private: JavaScript installs static private
|
||||
* elements on the declaring class object alone, so the receiver must BE
|
||||
* that object (a subclass, an object inheriting from it, or a primitive
|
||||
* throws). An expression statement calling the lifted helper
|
||||
* `%private.static.<use>:<class>:<#name>(value: dyn): void`, which throws
|
||||
* V8's TypeError; `value` is the checked-dynamic receiver. */
|
||||
export function staticPrivateBrandCheck(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
use: StaticPrivateUse,
|
||||
name: string,
|
||||
value: IrExpr,
|
||||
loc: SrcLoc,
|
||||
): IrStmt {
|
||||
const helper = `%private.static.${use}:${info.def.name}:${name}`;
|
||||
if (!lowerer.valueHelpers.has(helper)) {
|
||||
const at = helperLoc(loc);
|
||||
const param = varRef("value.0", DYN, at);
|
||||
const classValue = lowerer.coerceToExpected(lowerer.classValueRef(info, info.decl!), DYN);
|
||||
lowerer.liftedFns.push({
|
||||
name: helper,
|
||||
params: [{ localId: "value.0", name: "value", type: DYN }],
|
||||
returnType: VOID,
|
||||
locals: [{ id: "value.0", name: "value", type: DYN, mutable: false }],
|
||||
body: [
|
||||
{
|
||||
kind: "if",
|
||||
cond: {
|
||||
kind: "dynScalarEq",
|
||||
left: param,
|
||||
right: classValue,
|
||||
negated: true,
|
||||
type: BOOL,
|
||||
loc: at,
|
||||
},
|
||||
then: [
|
||||
{
|
||||
kind: "throw",
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "error.new",
|
||||
args: [strLit(brandMessage(info, use, name), at)],
|
||||
type: { kind: "object", className: "%TypeError" },
|
||||
loc: at,
|
||||
},
|
||||
loc: at,
|
||||
},
|
||||
],
|
||||
else_: null,
|
||||
loc: at,
|
||||
},
|
||||
],
|
||||
loc: at,
|
||||
});
|
||||
lowerer.valueHelpers.set(helper, helper);
|
||||
}
|
||||
return {
|
||||
kind: "exprStmt",
|
||||
expr: { kind: "call", callee: helper, args: [value], type: VOID, loc },
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** A static private the native storage does not hold (an accessor, a
|
||||
* field without an initializer): a loud refusal, never a string-keyed
|
||||
* read of the class object. */
|
||||
function unsupportedStaticPrivate(lowerer: Lowerer, node: ts.Node, name: string): never {
|
||||
return lowerer.unsupported(
|
||||
"SC1090",
|
||||
node,
|
||||
`the private static '${name}' in this form on a class whose constructor carries runtime statics`,
|
||||
);
|
||||
}
|
||||
|
||||
/** The checked-dynamic value `expr` lowers to, or null. */
|
||||
function checkedDynamicReceiver(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
|
||||
const probed = tryLowerExpression(lowerer, node);
|
||||
if (!probed) return null;
|
||||
if (
|
||||
probed.type.kind !== "dyn" &&
|
||||
!(probed.kind === "dynCheck" && probed.value.type.kind === "dyn")
|
||||
)
|
||||
return null;
|
||||
const value = lowerer.lowerExpr(node);
|
||||
if (value.type.kind === "dyn") return value;
|
||||
return value.kind === "dynCheck" ? value.value : null;
|
||||
}
|
||||
|
||||
/** `R.#x` / `R.#m` read, where `#x` is a static private of a
|
||||
* declaration-level class and `R` is a checked-dynamic value (`this` in a
|
||||
* static accessor or method that the constructor object carries): the
|
||||
* brand check, then the member's native storage. Null for other reads,
|
||||
* which keep their paths (the class's own name reads storage directly). */
|
||||
export function lowerStaticPrivateRead(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.PropertyAccessExpression,
|
||||
loc: SrcLoc,
|
||||
): IrExpr | null {
|
||||
if (!ts.isPrivateIdentifier(expr.name) || expr.questionDotToken) return null;
|
||||
const info = staticPrivateOwner(lowerer, expr.name);
|
||||
if (!info || !info.runtimeStatics) return null;
|
||||
if (lowerer.exactClassOfReceiver(expr.expression) === info) return null;
|
||||
const value = checkedDynamicReceiver(lowerer, expr.expression);
|
||||
if (!value) return null;
|
||||
const name = expr.name.text;
|
||||
const found = lowerer.findStaticOn(info, name);
|
||||
if (!found) unsupportedStaticPrivate(lowerer, expr, name);
|
||||
const read = lowerClassStaticRead(lowerer, info, name, expr.expression, expr);
|
||||
if (!read) return null;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
staticPrivateBrandCheck(lowerer, info, found.field ? "read" : "method", name, value, loc),
|
||||
],
|
||||
result: read,
|
||||
type: read.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `R.#m(args)` for a static private method on a checked-dynamic receiver:
|
||||
* the brand check, then the call with `this` known to be the declaring
|
||||
* class. A receiver other than `this` reaches only methods that do not
|
||||
* read `this`; the others keep a loud refusal. */
|
||||
export function lowerStaticPrivateCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): IrExpr | null {
|
||||
if (!ts.isPrivateIdentifier(access.name) || access.questionDotToken) return null;
|
||||
const info = staticPrivateOwner(lowerer, access.name);
|
||||
if (!info || !info.runtimeStatics) return null;
|
||||
if (lowerer.exactClassOfReceiver(access.expression) === info) return null;
|
||||
const name = access.name.text;
|
||||
const found = lowerer.findStaticOn(info, name);
|
||||
if (!found) unsupportedStaticPrivate(lowerer, call, name);
|
||||
const value = checkedDynamicReceiver(lowerer, access.expression);
|
||||
if (!value) return null;
|
||||
const loc = locOf(call);
|
||||
if (found.field !== undefined)
|
||||
return lowerStaticPrivateFieldCall(lowerer, call, info, name, found.field, value);
|
||||
const brand = staticPrivateBrandCheck(lowerer, info, "method", name, value, loc);
|
||||
let result: IrExpr | null;
|
||||
if (access.expression.kind === ts.SyntaxKind.ThisKeyword) {
|
||||
// After the check `this` IS the class: lower the call against a
|
||||
// class-valued `this`, the exact-receiver static path.
|
||||
const classType: IrType = { kind: "classval", className: info.def.name };
|
||||
const local = lowerer.declareHiddenLocal("%staticPrivateThis", classType);
|
||||
const previous = lowerer.ctx.thisLocal;
|
||||
lowerer.ctx.thisLocal = local;
|
||||
try {
|
||||
result = lowerStaticMethodCall(lowerer, call, access);
|
||||
} finally {
|
||||
lowerer.ctx.thisLocal = previous;
|
||||
}
|
||||
if (!result) return null;
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
brand,
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: local.id,
|
||||
init: lowerer.coerceToExpected(lowerer.classValueRef(info, info.decl!), classType),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result,
|
||||
type: result.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (found.method.member.body && hasStaticThis(found.method.member)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`calling the private static '${name}' through a value other than 'this' when the method reads 'this'`,
|
||||
);
|
||||
}
|
||||
const fnName = `%${found.declarer.def.name}.static:${name}`;
|
||||
lowerer.noteEdge(fnName);
|
||||
const args = lowerer.completeArgs(call.arguments.nodes, found.method.params, loc, call);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [brand],
|
||||
result: { kind: "call", callee: fnName, args, type: found.method.ret, loc },
|
||||
type: found.method.ret,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `R.#f(args)` for a function-valued static private field on a
|
||||
* checked-dynamic receiver: the receiver, the brand check (PrivateGet's),
|
||||
* then a call through the field's value with `this` bound to the receiver,
|
||||
* as JavaScript calls a private field's function. The call takes the
|
||||
* ordinary call-through-value lanes, so rest parameters, typed rests and
|
||||
* checked-dynamic values receive every argument. */
|
||||
function lowerStaticPrivateFieldCall(
|
||||
lowerer: Lowerer,
|
||||
call: ts.CallExpression,
|
||||
info: ClassInfo,
|
||||
name: string,
|
||||
field: ClassInfo["staticFields"][number],
|
||||
value: IrExpr,
|
||||
): IrExpr {
|
||||
const loc = locOf(call);
|
||||
const receiver = lowerer.declareHiddenLocal("%staticPrivateReceiver", DYN);
|
||||
const lowered = lowerCallThroughValue(
|
||||
lowerer,
|
||||
call,
|
||||
varRef(field.globalId, field.type, loc),
|
||||
loc,
|
||||
);
|
||||
if (lowered.kind !== "callValue" && lowered.kind !== "dynCall") {
|
||||
return lowerer.unsupported(
|
||||
"SC1090",
|
||||
call,
|
||||
`this call of the private static field '${name}' (spread arguments)`,
|
||||
);
|
||||
}
|
||||
const result: IrExpr = { ...lowered, receiver: varRef(receiver.id, DYN, loc) };
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: receiver.id, init: value, loc },
|
||||
staticPrivateBrandCheck(lowerer, info, "read", name, varRef(receiver.id, DYN, loc), loc),
|
||||
],
|
||||
result,
|
||||
type: result.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** `R.#x = v` for a static private field on a checked-dynamic receiver:
|
||||
* the receiver, then the value, then the brand check (PrivateSet checks
|
||||
* after the right side runs), then the native global. */
|
||||
export function lowerStaticPrivateAssignment(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.BinaryExpression,
|
||||
): IrExpr | null {
|
||||
const target = expr.left;
|
||||
if (
|
||||
expr.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
!ts.isPropertyAccessExpression(target) ||
|
||||
!ts.isPrivateIdentifier(target.name)
|
||||
)
|
||||
return null;
|
||||
const info = staticPrivateOwner(lowerer, target.name);
|
||||
if (!info || !info.runtimeStatics) return null;
|
||||
if (lowerer.exactClassOfReceiver(target.expression) === info) return null;
|
||||
const name = target.name.text;
|
||||
const found = lowerer.findStaticOn(info, name);
|
||||
if (!found?.field) unsupportedStaticPrivate(lowerer, expr, name);
|
||||
const value = checkedDynamicReceiver(lowerer, target.expression);
|
||||
if (!value) return null;
|
||||
const loc = locOf(expr);
|
||||
const receiver = lowerer.declareHiddenLocal("%staticPrivateReceiver", DYN);
|
||||
const assigned = lowerer.declareHiddenLocal("%staticPrivateValue", found.field.type);
|
||||
return {
|
||||
kind: "seqExpr",
|
||||
stmts: [
|
||||
{ kind: "varDecl", localId: receiver.id, init: value, loc },
|
||||
{
|
||||
kind: "varDecl",
|
||||
localId: assigned.id,
|
||||
init: lowerer.lowerExprExpecting(expr.right, found.field.type),
|
||||
loc,
|
||||
},
|
||||
staticPrivateBrandCheck(lowerer, info, "write", name, varRef(receiver.id, DYN, loc), loc),
|
||||
{
|
||||
kind: "assign",
|
||||
localId: found.field.globalId,
|
||||
value: varRef(assigned.id, found.field.type, loc),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
result: varRef(assigned.id, found.field.type, loc),
|
||||
type: found.field.type,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
/** The update target of `R.#x++`, `--R.#x` or `R.#x op= v` for a static
|
||||
* private field on a checked-dynamic receiver: the brand check, which runs
|
||||
* where PrivateGet reads the old value (before the right side), and the
|
||||
* field's native global. The write after it needs no second check: the
|
||||
* receiver already passed. Null for other targets. */
|
||||
export function staticPrivateUpdateTarget(
|
||||
lowerer: Lowerer,
|
||||
access: ts.PropertyAccessExpression,
|
||||
): { brand: IrStmt; target: { id: string; type: IrType } } | null {
|
||||
if (!ts.isPrivateIdentifier(access.name) || access.questionDotToken) return null;
|
||||
const info = staticPrivateOwner(lowerer, access.name);
|
||||
if (!info || !info.runtimeStatics) return null;
|
||||
if (lowerer.exactClassOfReceiver(access.expression) === info) return null;
|
||||
const name = access.name.text;
|
||||
const found = lowerer.findStaticOn(info, name);
|
||||
if (!found?.field) unsupportedStaticPrivate(lowerer, access, name);
|
||||
const value = checkedDynamicReceiver(lowerer, access.expression);
|
||||
if (!value) return null;
|
||||
return {
|
||||
brand: staticPrivateBrandCheck(lowerer, info, "read", name, value, locOf(access)),
|
||||
target: { id: found.field.globalId, type: found.field.type },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { compile, deserializeModule } from "../src/index.js";
|
||||
|
||||
/** The names of the module's functions and of every call target. */
|
||||
async function lowered(
|
||||
source: string,
|
||||
): Promise<{ functions: string[]; callees: string[]; keys: string[] }> {
|
||||
const dir = await mkdtemp(join(tmpdir(), "scriptc-private-statics-"));
|
||||
try {
|
||||
const entry = join(dir, "main.js");
|
||||
await writeFile(entry, source);
|
||||
const outPath = join(dir, "main.ir.json");
|
||||
const result = await compile(entry, { outDir: dir, outPath, outputKind: "ir" });
|
||||
if (!result.ok)
|
||||
throw new Error(result.diagnostics.map((d) => `${d.code}: ${d.message}`).join("\n"));
|
||||
const mod = deserializeModule(await readFile(outPath, "utf8"));
|
||||
const callees: string[] = [];
|
||||
const keys: string[] = [];
|
||||
const visit = (value: unknown): void => {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) visit(item);
|
||||
return;
|
||||
}
|
||||
if (value === null || typeof value !== "object") return;
|
||||
const node = value as { kind?: unknown; callee?: unknown; value?: unknown };
|
||||
if (node.kind === "call" && typeof node.callee === "string") callees.push(node.callee);
|
||||
if (node.kind === "strLit" && typeof node.value === "string" && node.value.startsWith("#"))
|
||||
keys.push(node.value);
|
||||
for (const child of Object.values(value)) visit(child);
|
||||
};
|
||||
visit(mod.functions);
|
||||
return { functions: mod.functions.map((fn) => fn.name), callees, keys };
|
||||
} finally {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test("a static private read through `this` in a runtime static accessor checks the brand", async () => {
|
||||
const ir = await lowered(
|
||||
`class C {
|
||||
static #x = 1;
|
||||
static #m() { return 2; }
|
||||
static get x() { return this.#x; }
|
||||
static get m() { return this.#m; }
|
||||
}
|
||||
console.log(C.x, C.m());
|
||||
`,
|
||||
);
|
||||
expect(ir.callees).toContain("%private.static.read:C:#x");
|
||||
expect(ir.callees).toContain("%private.static.method:C:#m");
|
||||
// The private names never become string keys of the class object.
|
||||
expect(ir.keys).toEqual([]);
|
||||
});
|
||||
|
||||
test("the declaring class's own name reads native storage without a check", async () => {
|
||||
const ir = await lowered(
|
||||
`class C {
|
||||
static #x = 1;
|
||||
static get x() { return C.#x; }
|
||||
}
|
||||
console.log(C.x);
|
||||
`,
|
||||
);
|
||||
expect(ir.functions.some((name) => name.startsWith("%private.static."))).toBe(false);
|
||||
expect(ir.keys).toEqual([]);
|
||||
});
|
||||
|
||||
test("a function-valued static private field called through `this` calls the stored function", async () => {
|
||||
const ir = await lowered(
|
||||
`class C {
|
||||
static #fn = function (a) { return a + 1; };
|
||||
static get viaGetter() { return this.#fn(1); }
|
||||
}
|
||||
console.log(C.viaGetter);
|
||||
`,
|
||||
);
|
||||
expect(ir.callees).toContain("%private.static.read:C:#fn");
|
||||
expect(ir.keys).toEqual([]);
|
||||
});
|
||||
|
||||
test("updates of a static private field through `this` check the brand and keep native storage", async () => {
|
||||
const ir = await lowered(
|
||||
`class C {
|
||||
static #n = 1;
|
||||
static get bump() {
|
||||
this.#n++;
|
||||
--this.#n;
|
||||
this.#n += 2;
|
||||
return this.#n;
|
||||
}
|
||||
}
|
||||
console.log(C.bump);
|
||||
`,
|
||||
);
|
||||
expect(ir.callees).toContain("%private.static.read:C:#n");
|
||||
expect(ir.keys).toEqual([]);
|
||||
});
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/js-private-static-receivers.js"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
// Static private members of a JavaScript class whose constructor object is
|
||||
// materialized at run time (it has a public static accessor). The private
|
||||
// members keep their native storage; every access through a receiver other
|
||||
// than the class's own name checks that the receiver IS the declaring class,
|
||||
// as Node does: a subclass, an object inheriting from the class, or a
|
||||
// primitive throws V8's TypeError.
|
||||
|
||||
let calls = 0;
|
||||
var Getter = class {
|
||||
static #method(a, b) {
|
||||
calls++;
|
||||
return a + b;
|
||||
}
|
||||
static get method() {
|
||||
return this.#method;
|
||||
}
|
||||
};
|
||||
console.log("getter method", Getter.method(1, 2), calls);
|
||||
|
||||
var Generators = class {
|
||||
static *#gen(n) {
|
||||
yield n;
|
||||
yield n + 1;
|
||||
}
|
||||
static async *#agen(n) {
|
||||
yield n * 2;
|
||||
}
|
||||
static get gen() {
|
||||
return this.#gen;
|
||||
}
|
||||
static get agen() {
|
||||
return this.#agen;
|
||||
}
|
||||
};
|
||||
console.log("generator", [...Generators.gen(3)].join(","));
|
||||
|
||||
class Counter {
|
||||
static #count = 5;
|
||||
static #label() {
|
||||
return "count:" + Counter.#count;
|
||||
}
|
||||
static get count() {
|
||||
return this.#count;
|
||||
}
|
||||
static set count(value) {
|
||||
this.#count = value;
|
||||
}
|
||||
static get label() {
|
||||
return this.#label;
|
||||
}
|
||||
static describe() {
|
||||
return this.#label();
|
||||
}
|
||||
}
|
||||
console.log("counter", Counter.count, Counter.label(), Counter.describe());
|
||||
Counter.count = 9;
|
||||
console.log("after write", Counter.count, Counter.describe());
|
||||
|
||||
class Sub extends Counter {}
|
||||
const probes = [
|
||||
["Sub.count", () => Sub.count],
|
||||
["Sub.count =", () => {
|
||||
Sub.count = 1;
|
||||
}],
|
||||
["Sub.label", () => Sub.label],
|
||||
["Sub.describe()", () => Sub.describe()],
|
||||
];
|
||||
for (const [label, probe] of probes) {
|
||||
try {
|
||||
console.log(label, "returned", String(probe()));
|
||||
} catch (error) {
|
||||
console.log(label, error.constructor.name, error.message);
|
||||
}
|
||||
}
|
||||
console.log("unchanged", Counter.count);
|
||||
|
||||
// A private name is never a string key of the class object.
|
||||
console.log("no string key", Object.hasOwn(Counter, "#count"), "#count" in Counter, Object.hasOwn(Getter, "#method"));
|
||||
|
||||
// Static initializers run in source order, private ones included.
|
||||
const order = [];
|
||||
class Ordered {
|
||||
static a = (order.push("a"), 1);
|
||||
static #b = (order.push("#b"), Ordered.a + 1);
|
||||
static c = (order.push("c"), Ordered.#b + 1);
|
||||
static {
|
||||
order.push("block " + Ordered.#b);
|
||||
}
|
||||
static get all() {
|
||||
return [this.a, this.#b, this.c].join(",");
|
||||
}
|
||||
}
|
||||
console.log("order", order.join(","), Ordered.all);
|
||||
|
||||
// A receiver passed in as a value: the same check. V8 names an anonymous
|
||||
// class expression ".".
|
||||
var Anonymous = class {
|
||||
static #m() {
|
||||
return "m";
|
||||
}
|
||||
static get m() {
|
||||
return this.#m;
|
||||
}
|
||||
static probe(other) {
|
||||
return typeof other.#m;
|
||||
}
|
||||
};
|
||||
console.log("probe self", Anonymous.probe(Anonymous), Anonymous.m());
|
||||
for (const other of [{}, 1, undefined]) {
|
||||
try {
|
||||
Anonymous.probe(other);
|
||||
} catch (error) {
|
||||
console.log("probe", typeof other, error.constructor.name, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
Generators.agen(4)
|
||||
.next()
|
||||
.then((step) => console.log("async generator", step.value, step.done));
|
||||
|
||||
// A function-valued private static field called through `this` runs with
|
||||
// `this` bound to the receiver; a receiver other than the class throws the
|
||||
// field read's TypeError.
|
||||
class Calls {
|
||||
static #fn = function (a) {
|
||||
return (this === Calls ? "self " : "other ") + a;
|
||||
};
|
||||
static #arrow = (a) => "arrow " + a;
|
||||
static get viaGetter() {
|
||||
return this.#fn(2) + " " + this.#arrow(2);
|
||||
}
|
||||
static viaOther(other, a) {
|
||||
return other.#fn(a);
|
||||
}
|
||||
static get fn() {
|
||||
return this.#fn;
|
||||
}
|
||||
}
|
||||
console.log("field call", Calls.viaGetter, Calls.viaOther(Calls, 3));
|
||||
try {
|
||||
Calls.viaOther({}, 4);
|
||||
} catch (error) {
|
||||
console.log("field call", error.constructor.name, error.message);
|
||||
}
|
||||
|
||||
// Updates of a private static field through `this` update its storage.
|
||||
class Updates {
|
||||
static #n = 1;
|
||||
static #s = "a";
|
||||
static get bump() {
|
||||
const a = this.#n++;
|
||||
const b = ++this.#n;
|
||||
const c = this.#n--;
|
||||
this.#n += 10;
|
||||
const d = (this.#n *= 2);
|
||||
this.#s += "b";
|
||||
return [a, b, c, d, this.#n, this.#s].join(",");
|
||||
}
|
||||
}
|
||||
console.log("updates", Updates.bump, Updates.bump, Object.hasOwn(Updates, "#n"));
|
||||
class SubUpdates extends Updates {}
|
||||
try {
|
||||
SubUpdates.bump;
|
||||
} catch (error) {
|
||||
console.log("updates", error.constructor.name, error.message);
|
||||
}
|
||||
|
||||
// A private static field holding a rest-parameter function, or one that
|
||||
// reads `arguments`, receives every argument of a call through `this`.
|
||||
class Rest {
|
||||
static #sum = function (...xs) {
|
||||
return (this === Rest ? "self " : "other ") + xs.length + ":" + xs.reduce((a, b) => a + b, 0);
|
||||
};
|
||||
static #arrow = (first, ...rest) => first + "|" + rest.join(",");
|
||||
static #plain = function (a, b) {
|
||||
return [a, b, arguments.length].join("/");
|
||||
};
|
||||
static get run() {
|
||||
return [this.#sum(1, 2, 3), this.#sum(), this.#arrow(1, 2, 3), this.#plain(1), this.#plain(1, 2, 3)].join(" ; ");
|
||||
}
|
||||
}
|
||||
console.log("rest", Rest.run);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2124, "san": 75913 }
|
||||
Reference in New Issue
Block a user