mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
Support Symbol.toStringTag class fields and getters
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
- **Symbol fields:** Instance fields require a stable module-level `Symbol()` or `Symbol.for()` binding with a literal description or constant string binding.
|
||||
A `[Symbol.toStringTag]` field or getter with a string value sets the tag that `String()`, `Object.prototype.toString` and `util.inspect` print; fields keyed by other well-known symbols are unsupported.
|
||||
Reassigned keys, runtime-computed descriptions, distinct symbols with the same description in one class, and copying symbol fields with rest bindings or `Object.assign` are unsupported.
|
||||
JavaScript object spreads copy represented own symbol fields.
|
||||
Equal registry keys share a field across modules.
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
isRefCounted,
|
||||
shapeHasAccessorSlots,
|
||||
typeKey,
|
||||
type IrClassDef,
|
||||
} from "../../ir/ir.js";
|
||||
import { mangleFunction, mangleGlobal, mangleRecordStruct } from "../mangle.js";
|
||||
import { BlockBuilder } from "./blocks.js";
|
||||
@@ -277,9 +278,7 @@ export function streamTypedRefCommitAdapter(
|
||||
);
|
||||
});
|
||||
}
|
||||
const symbols = new Map(
|
||||
(meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol.globalId]),
|
||||
);
|
||||
const symbols = new Map((meta.def.symbolFields ?? []).map((symbol) => [symbol.field, symbol]));
|
||||
const fields = meta.def.fields.filter(
|
||||
(field) =>
|
||||
(isClassOwnEnumerableFieldName(field.name) || symbols.has(field.name)) &&
|
||||
@@ -299,13 +298,15 @@ export function streamTypedRefCommitAdapter(
|
||||
const inline = host.inlineUnions.classField(t.className, field.name);
|
||||
const fieldTy = int32 ? "i32" : llFieldType(field.type, inline);
|
||||
const checkTy = host.llType(field.type);
|
||||
const symbolGlobal = symbols.get(field.name);
|
||||
if (symbolGlobal) {
|
||||
const symbolKey = symbols.get(field.name);
|
||||
if (symbolKey) {
|
||||
host.declare(`declare ptr @scr_dyn_symbol_key_get(ptr, ptr, i1 zeroext)`);
|
||||
host.declare(`declare void @scr_dyn_release_v(ptr)`);
|
||||
const keyLines = symbolKeyLines(host, symbolKey, `f${index}_keyraw`);
|
||||
lines.push(
|
||||
` %f${index}_keyraw = load ptr, ptr @${mangleGlobal(symbolGlobal)}`,
|
||||
...keyLines.load,
|
||||
` %f${index}_key = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %f${index}_keyraw)`,
|
||||
...keyLines.release,
|
||||
);
|
||||
if (field.type.kind === "symbol") {
|
||||
// A base constructor can mutate the instance before this
|
||||
@@ -333,7 +334,7 @@ export function streamTypedRefCommitAdapter(
|
||||
lines.push(
|
||||
` %${raw} = call ptr @scr_dyn_obj_get(ptr %d, ptr ${host.cstr(field.name)}, ${host.sizeType} ${Buffer.byteLength(field.name, "utf8")})`,
|
||||
);
|
||||
if (meta.def.tracksOwnFields && !symbolGlobal)
|
||||
if (meta.def.tracksOwnFields && !symbolKey)
|
||||
lines.push(
|
||||
` %f${index}_present = icmp ne ptr %${raw}, null`,
|
||||
` br i1 %f${index}_present, label %f${index}_read, label %${after}`,
|
||||
@@ -344,7 +345,7 @@ export function streamTypedRefCommitAdapter(
|
||||
` %${undef} = call ptr @scr_dyn_undefined()`,
|
||||
` %${input} = select i1 %${missing}, ptr %${undef}, ptr %${raw}`,
|
||||
` %${next} = call ${field.type.kind === "bool" ? "zeroext " : ""}${checkTy} @${host.dyn.dynCheckHelper(field.type)}(ptr %${input}, ptr null)`,
|
||||
...(symbolGlobal ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []),
|
||||
...(symbolKey ? [` call void @scr_dyn_release_v(ptr %${raw})`] : []),
|
||||
...host.pendingTestLines(`%${pending}`),
|
||||
` br i1 %${pending}, label %done, label %${store}`,
|
||||
`${store}:`,
|
||||
@@ -499,16 +500,18 @@ export function streamTypedRefCommitAdapter(
|
||||
? `declare void @scr_dyn_symbol_key_placeholder(ptr, ptr)`
|
||||
: `declare void @scr_dyn_key_delete_computed(ptr, ptr, i1 zeroext)`,
|
||||
);
|
||||
[...symbols.values()].forEach((globalId, index) =>
|
||||
[...symbols.values()].forEach((symbol, index) => {
|
||||
const keyLines = symbolKeyLines(host, symbol, `bag_symraw${index}`);
|
||||
lines.push(
|
||||
` %bag_symraw${index} = load ptr, ptr @${mangleGlobal(globalId)}`,
|
||||
...keyLines.load,
|
||||
` %bag_sym${index} = call ptr @${host.dyn.toDynHelper(SYMBOL_T)}(ptr %bag_symraw${index})`,
|
||||
...keyLines.release,
|
||||
meta.def.tracksOwnFields
|
||||
? ` call void @scr_dyn_symbol_key_placeholder(ptr %bag, ptr %bag_sym${index})`
|
||||
: ` call void @scr_dyn_key_delete_computed(ptr %bag, ptr %bag_sym${index}, i1 zeroext false)`,
|
||||
` call void @scr_dyn_release_v(ptr %bag_sym${index})`,
|
||||
),
|
||||
);
|
||||
);
|
||||
});
|
||||
const { index } = classFieldIndex(meta, DYN_CLASS_PROPERTIES);
|
||||
lines.push(
|
||||
` %bag_ptr = getelementptr inbounds %${classStructSym(t.className)}, ptr %target, i64 0, i32 ${index}`,
|
||||
@@ -705,6 +708,27 @@ export function liveDynUnionRefAdapter(
|
||||
return sym;
|
||||
}
|
||||
|
||||
/** The raw ScrSym of a class's symbol field key into `%name`: a module
|
||||
* key is borrowed from its global, a well-known one comes from the
|
||||
* runtime's registry (+1, released by the `release` lines once the key
|
||||
* value holds it). */
|
||||
function symbolKeyLines(
|
||||
host: LlvmEmitterContext,
|
||||
symbol: NonNullable<IrClassDef["symbolFields"]>[number],
|
||||
name: string,
|
||||
): { load: string[]; release: string[] } {
|
||||
if ("globalId" in symbol)
|
||||
return { load: [` %${name} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`], release: [] };
|
||||
host.declare("declare ptr @scr_sym_well_known(ptr)");
|
||||
host.declare("declare void @scr_sym_release(ptr)");
|
||||
return {
|
||||
load: [
|
||||
` %${name} = call ptr @scr_sym_well_known(ptr ${host.internLiteral(symbol.wellKnown)})`,
|
||||
],
|
||||
release: [` call void @scr_sym_release(ptr %${name})`],
|
||||
};
|
||||
}
|
||||
|
||||
export function streamTypedRefBoxValue(
|
||||
host: LlvmEmitterContext,
|
||||
B: BlockBuilder,
|
||||
@@ -929,8 +953,10 @@ export function streamTypedRefMaterializeAdapter(
|
||||
B.line(`${fieldValue} = sitofp i32 ${raw} to double`);
|
||||
} else if (inline) fieldValue = loadStructField(B, host.sizeType, fieldPtr, field.type, true);
|
||||
else B.line(`${fieldValue} = load ${llFieldType(field.type, false)}, ptr ${fieldPtr}`);
|
||||
B.line(`${rawKey} = load ptr, ptr @${mangleGlobal(symbol.globalId)}`);
|
||||
const keyLines = symbolKeyLines(host, symbol, rawKey.slice(1));
|
||||
for (const line of keyLines.load) B.line(line.trim());
|
||||
const key = host.streamTypedRefBoxValue(B, SYMBOL_T, rawKey);
|
||||
for (const line of keyLines.release) B.line(line.trim());
|
||||
const afterSymbol =
|
||||
meta.def.tracksOwnFields || field.type.kind === "symbol"
|
||||
? B.newLabel("symbol.after")
|
||||
@@ -1473,9 +1499,26 @@ function liveClassOps(host: LlvmEmitterContext, snapshot: string, meta: LlClassM
|
||||
// The constructor's name, as util.inspect prints before the fields.
|
||||
const jsName = meta.def.jsName ?? meta.def.name.replace(/^%/, "");
|
||||
const className = jsName === "" ? "null" : host.internLiteral(jsName);
|
||||
// Its Symbol.toStringTag getter's string, for Object.prototype.toString
|
||||
// and util.inspect.
|
||||
let stringTag = "null";
|
||||
if (meta.def.stringTagHelper) {
|
||||
// The runtime lends the instance; the helper takes its own reference.
|
||||
stringTag = `@${snapshot}_ctag`;
|
||||
const retain = vAdapters(host.shapeHost, { kind: "object", className: meta.def.name }).retain;
|
||||
host.resolveThunkDefs.push(
|
||||
`define internal ptr ${stringTag}(ptr %p) ${FN_ATTRS} { ; Symbol.toStringTag of ${meta.def.name}`,
|
||||
`entry:`,
|
||||
` %owned = call ptr ${retain}(ptr %p)`,
|
||||
` %tag = call ptr @${mangleFunction(meta.def.stringTagHelper)}(ptr %owned)`,
|
||||
` ret ptr %tag`,
|
||||
`}`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
host.resolveThunkDefs.push(
|
||||
`@${names} = internal constant [${members.length} x ptr] [${literals.join(", ")}]`,
|
||||
`@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className} }`,
|
||||
`@${ops} = internal constant { ptr, ${host.sizeType}, ptr, ptr, ptr } { ptr ${exact}, ${host.sizeType} ${members.length}, ptr @${names}, ptr ${className}, ptr ${stringTag} }`,
|
||||
``,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { primitiveConstructorOr } from "./lower-exprs.js";
|
||||
import { TO_STRING_TAG_FIELD, wellKnownSymbolOfField } from "./symbol-fields.js";
|
||||
import { InternalCompilerError } from "../../errors.js";
|
||||
import { dynUndefinedExpr, helperLoc, nodeThrowExpr, varRef } from "../../ir/build.js";
|
||||
import { constructedValue } from "./constructor-returns.js";
|
||||
@@ -26,6 +27,7 @@ import {
|
||||
isUnitType,
|
||||
typeEquals,
|
||||
typeKey,
|
||||
type IrClassDef,
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrParam,
|
||||
@@ -153,6 +155,41 @@ const STREAM_NUM_PROPERTIES = new Set([
|
||||
const streamProperty = (name: string): boolean =>
|
||||
STREAM_BOOL_PROPERTIES.has(name) || STREAM_NUM_PROPERTIES.has(name);
|
||||
|
||||
/** `(instance) => string`, the string a class's inherited
|
||||
* Symbol.toStringTag getter answers, which the runtime reads for
|
||||
* Object.prototype.toString and util.inspect of an instance seen as
|
||||
* `unknown`. None for a class without a string getter, or whose own tag
|
||||
* field shadows it. */
|
||||
function stringTagHelper(lowerer: Lowerer, info: ClassInfo, loc: SrcLoc): IrFunction | null {
|
||||
if (info.fields.has(TO_STRING_TAG_FIELD)) return null;
|
||||
const getter = findMethodOn(lowerer, info, "get:sym:toStringTag");
|
||||
if (getter?.sig.ret.kind !== "string") return null;
|
||||
const type: IrType = { kind: "object", className: info.def.name };
|
||||
const owner = getter.declarer.def.name;
|
||||
return {
|
||||
name: `%class.stringTag:${info.def.name}`,
|
||||
params: [{ localId: "this", name: "this", type }],
|
||||
locals: [{ id: "this", name: "this", type, mutable: false }],
|
||||
returnType: STRING,
|
||||
body: [
|
||||
{
|
||||
kind: "return",
|
||||
value: accessorCall(
|
||||
lowerer,
|
||||
owner,
|
||||
"get:sym:toStringTag",
|
||||
lowerer.upcastTo(varRef("this", type, loc), owner),
|
||||
[],
|
||||
STRING,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
},
|
||||
],
|
||||
loc,
|
||||
};
|
||||
}
|
||||
|
||||
function symbolMemberKey(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
@@ -1276,10 +1313,27 @@ export class ClassDynamicDispatch {
|
||||
const info = lowerer.classes.get(className);
|
||||
if (!info || info.builtinEmitter || info.builtinStream || info.builtinError) continue;
|
||||
if (info.symbolFields && info.def.symbolFields === undefined) {
|
||||
info.def.symbolFields = [...info.symbolFields].flatMap(([symbol, field]) => {
|
||||
const global = lowerer.globalsBySymbol.get(symbol);
|
||||
return global?.type.kind === "symbol" ? [{ field, globalId: global.id }] : [];
|
||||
});
|
||||
info.def.symbolFields = [...info.symbolFields].flatMap(
|
||||
([symbol, field]): NonNullable<IrClassDef["symbolFields"]> => {
|
||||
const global = lowerer.globalsBySymbol.get(symbol);
|
||||
if (global?.type.kind === "symbol") return [{ field, globalId: global.id }];
|
||||
const wellKnown = wellKnownSymbolOfField(field);
|
||||
return wellKnown === null ? [] : [{ field, wellKnown }];
|
||||
},
|
||||
);
|
||||
}
|
||||
const tagged = (current: ClassInfo): ClassInfo[] => [
|
||||
current,
|
||||
...current.subclasses.flatMap(tagged),
|
||||
];
|
||||
for (const current of tagged(info)) {
|
||||
if (current.def.stringTagHelper !== undefined) continue;
|
||||
const fn = stringTagHelper(lowerer, current, functions[0]!.loc);
|
||||
if (!fn) continue;
|
||||
current.def.stringTagHelper = fn.name;
|
||||
this.generated.add(fn);
|
||||
lowerer.liftedFns.push(fn);
|
||||
changed = true;
|
||||
}
|
||||
if (this.prototypeHelper && !this.prototypeClasses.has(className)) {
|
||||
this.prototypeClasses.add(className);
|
||||
@@ -3554,10 +3608,29 @@ export class ClassDynamicDispatch {
|
||||
name,
|
||||
lowerer.coerceToExpected(varRef(global.id, global.type, loc), DYN),
|
||||
);
|
||||
} else if (wellKnownSymbolOfField(name) !== null) {
|
||||
// A field keyed by a well-known symbol (Symbol.toStringTag).
|
||||
symbols.set(name, {
|
||||
kind: "dynFrom",
|
||||
type: DYN,
|
||||
loc,
|
||||
value: {
|
||||
kind: "libCall",
|
||||
fn: "sym.wellKnown",
|
||||
args: [
|
||||
{ kind: "strLit", value: wellKnownSymbolOfField(name)!, type: STRING, loc },
|
||||
],
|
||||
type: SYMBOL_T,
|
||||
loc,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
for (let owner: ClassInfo | null = info; owner; owner = owner.base) {
|
||||
for (const name of [...owner.methods.keys()])
|
||||
for (const method of [...owner.methods.keys()]) {
|
||||
// Accessors key by the property they define (`get [Symbol.toStringTag]`).
|
||||
const name =
|
||||
method.startsWith("get:") || method.startsWith("set:") ? method.slice(4) : method;
|
||||
if (name.startsWith("sym:")) {
|
||||
symbols.set(name, {
|
||||
kind: "dynFrom",
|
||||
@@ -3572,6 +3645,7 @@ export class ClassDynamicDispatch {
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
const branches: IrStmt[] = [];
|
||||
if (!dispatch.write && !dispatch.reflect && !info.def.tracksOwnFields) {
|
||||
|
||||
@@ -394,7 +394,19 @@ export function collectClassMethodMutations(
|
||||
(ts.isElementAccessExpression(base) &&
|
||||
ts.isStringLiteralLike(base.argumentExpression) &&
|
||||
base.argumentExpression.text === "prototype");
|
||||
if (prototype) {
|
||||
// `Object.prototype.toString.call(value)` calls the builtin and
|
||||
// keeps no reference to it, so it replaces nothing.
|
||||
const builtinCall =
|
||||
ts.isPropertyAccessExpression(node) &&
|
||||
ts.isPropertyAccessExpression(base) &&
|
||||
ts.isIdentifier(unwrap(base.expression)) &&
|
||||
lowerer.isStdlibGlobal(unwrap(base.expression) as ts.Identifier, "Object") &&
|
||||
ts.isPropertyAccessExpression(node.parent) &&
|
||||
node.parent.expression === node &&
|
||||
(node.parent.name.text === "call" || node.parent.name.text === "apply") &&
|
||||
ts.isCallExpression(node.parent.parent) &&
|
||||
node.parent.parent.expression === node.parent;
|
||||
if (prototype && !builtinCall) {
|
||||
if (ts.isPropertyAccessExpression(node)) lowerer.noteMethodAccess(node.name.text, node);
|
||||
else if (ts.isStringLiteralLike(node.argumentExpression))
|
||||
lowerer.noteMethodAccess(node.argumentExpression.text, node);
|
||||
|
||||
@@ -13,6 +13,7 @@ import * as ts from "../ts7/adapter.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { isClassCallback } from "./class-callbacks.js";
|
||||
import { TO_STRING_TAG_FIELD } from "./symbol-fields.js";
|
||||
import { isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { DATE_TIME_FIELD, dateRootOf } from "./builtins/date-classes.js";
|
||||
import { dateValueMethod } from "./builtins/dates.js";
|
||||
@@ -29,7 +30,15 @@ type ConversionMethod = "sym:toPrimitive" | "valueOf" | "toString";
|
||||
* Date`, or through Object.prototype.toString ("[object Object]"). */
|
||||
type Plan =
|
||||
| { method: ConversionMethod; ret: IrType }
|
||||
| { method: null; date?: "toString" | "valueOf" };
|
||||
| { method: null; date?: "toString" | "valueOf"; tag?: ToStringTag };
|
||||
|
||||
/** Where Object.prototype.toString finds a class's Symbol.toStringTag: a
|
||||
* string field (`[Symbol.toStringTag] = "LRUCache"`) or a getter that
|
||||
* returns a string, declared on `owner`. */
|
||||
interface ToStringTag {
|
||||
owner: ClassInfo;
|
||||
getter: boolean;
|
||||
}
|
||||
|
||||
const PRIMITIVE_KINDS = new Set(["f64", "string", "bool", "bigint", "undefinedT", "nullT"]);
|
||||
|
||||
@@ -52,6 +61,17 @@ export function classToStringConversion(
|
||||
);
|
||||
}
|
||||
|
||||
/** Object.prototype.toString of a program class instance: "[object " +
|
||||
* its Symbol.toStringTag + "]" (an own tag field, else an inherited
|
||||
* getter), or "[object Object]". The class's own conversion methods do not
|
||||
* take part. Null as for classToStringConversion, and for Date subclasses. */
|
||||
export function classObjectTag(lowerer: Lowerer, value: IrExpr, node: ts.Node): IrExpr | null {
|
||||
if (value.type.kind !== "object") return null;
|
||||
const info = lowerer.classes.get(value.type.className);
|
||||
if (!info || dateRootOf(info) !== null) return null;
|
||||
return classConversion(lowerer, value, "tag", node, STRING, (tag) => tag);
|
||||
}
|
||||
|
||||
/** ToNumber(ToPrimitive(value, "number")) of a program class instance
|
||||
* (`Number(x)`): `[Symbol.toPrimitive]("number")`, then `valueOf` and then
|
||||
* `toString`, and NaN for "[object Object]". Null as for
|
||||
@@ -122,7 +142,7 @@ function mayBeBigint(lowerer: Lowerer, type: IrType): boolean {
|
||||
function classConversion(
|
||||
lowerer: Lowerer,
|
||||
value: IrExpr,
|
||||
hint: ConversionHint,
|
||||
hint: ConversionHint | "tag",
|
||||
node: ts.Node,
|
||||
resultType: IrType,
|
||||
finish: (primitive: IrExpr) => IrExpr,
|
||||
@@ -131,7 +151,7 @@ function classConversion(
|
||||
const info = lowerer.classes.get(value.type.className);
|
||||
if (!info || !plainProgramClass(info)) return null;
|
||||
const classes = subtree(lowerer, info);
|
||||
for (const cls of classes) refuseReplacedConversions(lowerer, cls, node);
|
||||
if (hint !== "tag") for (const cls of classes) refuseReplacedConversions(lowerer, cls, node);
|
||||
const plans = new Map(classes.map((cls) => [cls, planOf(lowerer, cls, hint, node)]));
|
||||
// A class whose plan differs from its base's starts a group; the deepest
|
||||
// groups are tested first, so each instance takes its own class's plan.
|
||||
@@ -164,6 +184,41 @@ function classConversion(
|
||||
};
|
||||
return finish(dateValueMethod(plan.date, time, loc)!);
|
||||
}
|
||||
if (plan.method === null && plan.tag) {
|
||||
// Object.prototype.toString: "[object " + the tag + "]".
|
||||
const owner = plan.tag.owner.def.name;
|
||||
const tag: IrExpr = plan.tag.getter
|
||||
? accessorCall(
|
||||
lowerer,
|
||||
owner,
|
||||
"get:sym:toStringTag",
|
||||
lowerer.upcastTo(object, owner),
|
||||
[],
|
||||
STRING,
|
||||
loc,
|
||||
)
|
||||
: {
|
||||
kind: "fieldGet",
|
||||
obj: lowerer.upcastTo(object, owner),
|
||||
className: owner,
|
||||
field: TO_STRING_TAG_FIELD,
|
||||
type: STRING,
|
||||
loc,
|
||||
};
|
||||
return finish({
|
||||
kind: "strConcat",
|
||||
left: {
|
||||
kind: "strConcat",
|
||||
left: { kind: "strLit", value: "[object ", type: STRING, loc },
|
||||
right: tag,
|
||||
type: STRING,
|
||||
loc,
|
||||
},
|
||||
right: { kind: "strLit", value: "]", type: STRING, loc },
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
if (plan.method === null)
|
||||
return finish({ kind: "strLit", value: "[object Object]", type: STRING, loc });
|
||||
const args: IrExpr[] =
|
||||
@@ -228,10 +283,28 @@ function depth(info: ClassInfo): number {
|
||||
}
|
||||
|
||||
function samePlan(a: Plan, b: Plan): boolean {
|
||||
return (
|
||||
a.method === b.method &&
|
||||
(a.method !== null || b.method !== null || a.date === (b as { date?: string }).date)
|
||||
);
|
||||
if (a.method !== null || b.method !== null) return a.method === b.method;
|
||||
return a.date === b.date && a.tag?.owner === b.tag?.owner && a.tag?.getter === b.tag?.getter;
|
||||
}
|
||||
|
||||
/** The class's Symbol.toStringTag, when it has one: the class at the top
|
||||
* of the chain whose layout holds the tag field (an own property, which
|
||||
* shadows any getter), or its getter. Other tag types would make
|
||||
* Object.prototype.toString answer "Object" or the value depending on the
|
||||
* run, and are refused. */
|
||||
function toStringTagOf(lowerer: Lowerer, info: ClassInfo, node: ts.Node): ToStringTag | null {
|
||||
const type = info.fields.get(TO_STRING_TAG_FIELD);
|
||||
if (type !== undefined) {
|
||||
if (type.kind !== "string") refuse(lowerer, node, info, "a non-string Symbol.toStringTag");
|
||||
let owner = info;
|
||||
while (owner.base?.fields.has(TO_STRING_TAG_FIELD)) owner = owner.base;
|
||||
return { owner, getter: false };
|
||||
}
|
||||
const getter = findMethodOn(lowerer, info, "get:sym:toStringTag");
|
||||
if (!getter) return null;
|
||||
if (getter.sig.ret.kind !== "string")
|
||||
refuse(lowerer, node, info, "a non-string Symbol.toStringTag");
|
||||
return { owner: getter.declarer, getter: true };
|
||||
}
|
||||
|
||||
/** A conversion method the class declares or inherits (a field of that
|
||||
@@ -246,7 +319,16 @@ function declared(
|
||||
return found ? { ret: found.sig.ret } : null;
|
||||
}
|
||||
|
||||
function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: ts.Node): Plan {
|
||||
function planOf(
|
||||
lowerer: Lowerer,
|
||||
info: ClassInfo,
|
||||
hint: ConversionHint | "tag",
|
||||
node: ts.Node,
|
||||
): Plan {
|
||||
if (hint === "tag") {
|
||||
const tag = toStringTagOf(lowerer, info, node);
|
||||
return tag ? { method: null, tag } : { method: null };
|
||||
}
|
||||
const exotic = declared(lowerer, info, "sym:toPrimitive");
|
||||
if (exotic) {
|
||||
if (!primitive(lowerer, exotic.ret))
|
||||
@@ -271,7 +353,8 @@ function planOf(lowerer: Lowerer, info: ClassInfo, hint: ConversionHint, node: t
|
||||
refuse(lowerer, node, info, `a '${method}' method that can return an object`);
|
||||
return { method, ret: found.ret };
|
||||
}
|
||||
return { method: null };
|
||||
const tag = toStringTagOf(lowerer, info, node);
|
||||
return tag ? { method: null, tag } : { method: null };
|
||||
}
|
||||
|
||||
function primitive(lowerer: Lowerer, type: IrType): boolean {
|
||||
@@ -292,8 +375,6 @@ function refuseReplacedConversions(lowerer: Lowerer, info: ClassInfo, node: ts.N
|
||||
if (lowerer.prototypeMethodAccesses.has(method))
|
||||
refuse(lowerer, node, info, `a '${method}' the program reaches through a prototype`);
|
||||
}
|
||||
if (findMethodOn(lowerer, info, "get:sym:toStringTag") || info.fields.has("sym:toStringTag"))
|
||||
refuse(lowerer, node, info, "a Symbol.toStringTag");
|
||||
}
|
||||
|
||||
function refuse(lowerer: Lowerer, node: ts.Node, info: ClassInfo, what: string): never {
|
||||
|
||||
@@ -707,11 +707,17 @@ function rewriteFunction(
|
||||
* unchecked. */
|
||||
export function refuseUncheckedCycleReads(
|
||||
lowerer: Lowerer,
|
||||
classes: readonly { baseValueGlobal?: string; symbolFields?: { globalId: string }[] }[],
|
||||
classes: readonly {
|
||||
baseValueGlobal?: string;
|
||||
symbolFields?: readonly ({ globalId: string } | { wellKnown: string })[];
|
||||
}[],
|
||||
): void {
|
||||
if (lowerer.cycleInitFlags.size === 0) return;
|
||||
for (const cls of classes) {
|
||||
const ids = [cls.baseValueGlobal, ...(cls.symbolFields ?? []).map((f) => f.globalId)];
|
||||
const ids = [
|
||||
cls.baseValueGlobal,
|
||||
...(cls.symbolFields ?? []).map((f) => ("globalId" in f ? f.globalId : undefined)),
|
||||
];
|
||||
for (const id of ids) {
|
||||
const flag = id === undefined ? undefined : lowerer.cycleInitFlags.get(id);
|
||||
if (flag === undefined) continue;
|
||||
|
||||
@@ -183,7 +183,7 @@ import {
|
||||
import { isSafeToDiscard } from "./expressions/evaluation-safety.js";
|
||||
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
|
||||
import { lowerChildMethodValue, lowerChildPropertyCall } from "./handle-properties.js";
|
||||
import { classToNumberConversion } from "./class-to-primitive.js";
|
||||
import { classObjectTag, classToNumberConversion } from "./class-to-primitive.js";
|
||||
import { hasRuntimeStatics } from "./class-runtime-statics.js";
|
||||
import { structuralReadType } from "./class-unions.js";
|
||||
import {
|
||||
@@ -3103,6 +3103,10 @@ function lowerObjectPrototypeCall(
|
||||
const immediate = immediateObjectTagReceiver(lowerer, receiverNode);
|
||||
const receiver = immediate?.effect ?? lowerer.lowerExpr(receiverNode);
|
||||
const tag = immediate?.tag ?? objectToStringTag(lowerer, receiver.type);
|
||||
if (tag === null && immediate === null && call.arguments.nodes.length === 1) {
|
||||
const classTag = classObjectTag(lowerer, receiver, receiverNode);
|
||||
if (classTag) return classTag;
|
||||
}
|
||||
// A generator object answers its tag through its checked capsule.
|
||||
if (tag === null && receiver.type.kind !== "dyn" && receiver.type.kind !== "generator") {
|
||||
lowerer.noLowering(
|
||||
|
||||
@@ -8156,7 +8156,10 @@ export function classMemberNameOf(lowerer: Lowerer, name: ts.PropertyName): stri
|
||||
member === "asyncIterator" ||
|
||||
member === "dispose" ||
|
||||
member === "asyncDispose" ||
|
||||
member === "toPrimitive"
|
||||
member === "toPrimitive" ||
|
||||
// A toStringTag field keeps its symbol field name; its accessor and
|
||||
// method forms take the method slot name.
|
||||
(member === "toStringTag" && !ts.isPropertyDeclaration(name.parent))
|
||||
) {
|
||||
return `sym:${member}`;
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ import {
|
||||
shapeKeyOrders,
|
||||
typeKey,
|
||||
} from "../../ir/ir.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import { accessorCall, findMethodOn, type ClassInfo } from "./lower-classes.js";
|
||||
import { isSafeToDiscard, isSafeToRepeat } from "./expressions/evaluation-safety.js";
|
||||
import { symbolFieldDisplayName } from "./symbol-fields.js";
|
||||
import { symbolFieldDisplayName, TO_STRING_TAG_FIELD } from "./symbol-fields.js";
|
||||
import { eventTargetRootOf } from "./lower-event-target.js";
|
||||
import { dateRootOf } from "./builtins/date-classes.js";
|
||||
import { lowerSystemErrorMap } from "./lower-system-errors.js";
|
||||
@@ -1870,8 +1870,70 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
// observable way — Node's inspect omits them entirely (verified),
|
||||
// so a class whose only fields are private prints as `C {}`.
|
||||
const visible = info.def.fields.filter((f) => isClassOwnEnumerableFieldName(f.name));
|
||||
// A Symbol.toStringTag getter (an own tag field is listed instead)
|
||||
// brackets the tag after the name, `Name [tag] {`, when it is a
|
||||
// nonempty string other than the name.
|
||||
const tagGetter = info.fields.has(TO_STRING_TAG_FIELD)
|
||||
? null
|
||||
: findMethodOn(lowerer, info, "get:sym:toStringTag");
|
||||
let named: (plain: string, make: (head: IrExpr) => IrExpr) => IrExpr = (plain) =>
|
||||
strLit(plain, loc);
|
||||
if (tagGetter?.sig.ret.kind === "string") {
|
||||
const owner = tagGetter.declarer.def.name;
|
||||
locals.push({ id: "tag.0", name: "tag", type: STRING, mutable: false });
|
||||
body.push({
|
||||
kind: "varDecl",
|
||||
localId: "tag.0",
|
||||
init: accessorCall(
|
||||
lowerer,
|
||||
owner,
|
||||
"get:sym:toStringTag",
|
||||
lowerer.upcastTo(v(), owner),
|
||||
[],
|
||||
STRING,
|
||||
loc,
|
||||
),
|
||||
loc,
|
||||
});
|
||||
const tag = (): IrExpr => varRef("tag.0", STRING, loc);
|
||||
const shown: IrExpr = {
|
||||
kind: "logical",
|
||||
op: "&&",
|
||||
left: {
|
||||
kind: "strEq",
|
||||
negated: true,
|
||||
left: tag(),
|
||||
right: strLit("", loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
right: {
|
||||
kind: "strEq",
|
||||
negated: true,
|
||||
left: tag(),
|
||||
right: strLit(display, loc),
|
||||
type: BOOL,
|
||||
loc,
|
||||
},
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
const head = concatAll(
|
||||
[strLit(`${display === "" ? "Object" : display} [`, loc), tag(), strLit("]", loc)],
|
||||
loc,
|
||||
);
|
||||
named = (plain, make) => ({
|
||||
kind: "ternary",
|
||||
cond: shown,
|
||||
then: make(head),
|
||||
else_: strLit(plain, loc),
|
||||
type: STRING,
|
||||
loc,
|
||||
});
|
||||
}
|
||||
const after = (text: string) => (head: IrExpr) => concatAll([head, strLit(text, loc)], loc);
|
||||
if (visible.length === 0) {
|
||||
body = [ret(strLit(display === "" ? "{}" : `${display} {}`, loc))];
|
||||
body.push(ret(named(display === "" ? "{}" : `${display} {}`, after(" {}"))));
|
||||
break;
|
||||
}
|
||||
const get = (field: string, type: IrType): IrExpr => ({
|
||||
@@ -1882,7 +1944,22 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
type,
|
||||
loc,
|
||||
});
|
||||
body = [depthGate(display === "" ? "[Object]" : `[${display}]`), ...begin()];
|
||||
body.push(
|
||||
{
|
||||
kind: "if",
|
||||
cond: { kind: "bin", op: ">", left: r(), right: d(), type: BOOL, loc },
|
||||
then: [
|
||||
ret(
|
||||
named(display === "" ? "[Object]" : `[${display}]`, (head) =>
|
||||
concatAll([strLit("[", loc), head, strLit("]", loc)], loc),
|
||||
),
|
||||
),
|
||||
],
|
||||
else_: null,
|
||||
loc,
|
||||
},
|
||||
...begin(),
|
||||
);
|
||||
// def.fields carries layout order: the base chain first, then own —
|
||||
// exactly the own-property insertion order of a constructor that
|
||||
// assigns in declaration order (SEMANTICS.md 36's stance). SYMBOL-
|
||||
@@ -1911,7 +1988,7 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
|
||||
ret(
|
||||
end(
|
||||
strLit("", loc),
|
||||
strLit(display === "" ? "{" : `${display} {`, loc),
|
||||
named(display === "" ? "{" : `${display} {`, after(" {")),
|
||||
strLit("}", loc),
|
||||
false,
|
||||
boolLit(false, loc),
|
||||
|
||||
@@ -77,6 +77,8 @@ export function fenceSymbolFieldCopy(
|
||||
}
|
||||
|
||||
export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSymbolKey | null {
|
||||
const wellKnown = wellKnownSymbolKeyOf(lowerer, key);
|
||||
if (wellKnown !== undefined) return wellKnown;
|
||||
if (
|
||||
!ts.isIdentifier(key) &&
|
||||
(!ts.isPropertyAccessExpression(key) || key.questionDotToken || !ts.isIdentifier(key.name))
|
||||
@@ -90,6 +92,53 @@ export function classSymbolKeyOf(lowerer: Lowerer, key: ts.Expression): ClassSym
|
||||
return sym ? classSymbolKeyOfSymbol(lowerer, sym) : null;
|
||||
}
|
||||
|
||||
/** Well-known symbols a class field may be keyed by: those no protocol
|
||||
* consults on an instance, or whose protocol the lowering reads from the
|
||||
* field (Symbol.toStringTag, by the class string conversions). A field
|
||||
* keyed by any other well-known symbol (Symbol.iterator, toPrimitive,
|
||||
* hasInstance, ...) would be skipped by its protocol, so it keeps the
|
||||
* computed-key refusal. */
|
||||
const FIELD_WELL_KNOWN_SYMBOLS: ReadonlySet<string> = new Set(["toStringTag"]);
|
||||
|
||||
/** The key of `Symbol.<name>` on the standard global: undefined when the
|
||||
* expression is not one, null when it is a well-known symbol a field may
|
||||
* not be keyed by. The identity is the lib's declaration of the property,
|
||||
* one symbol for every program reference. */
|
||||
function wellKnownSymbolKeyOf(
|
||||
lowerer: Lowerer,
|
||||
key: ts.Expression,
|
||||
): ClassSymbolKey | null | undefined {
|
||||
while (ts.isParenthesizedExpression(key)) key = key.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(key) ||
|
||||
key.questionDotToken ||
|
||||
!ts.isIdentifier(key.expression) ||
|
||||
!ts.isIdentifier(key.name) ||
|
||||
!lowerer.isStdlibGlobal(key.expression, "Symbol")
|
||||
)
|
||||
return undefined;
|
||||
const name = key.name.text;
|
||||
const sym = lowerer.checker.getSymbolAtLocation(key.name);
|
||||
if (!sym || !(lowerer.typeOf(key).flags & ts.TypeFlags.UniqueESSymbol)) return undefined;
|
||||
if (!FIELD_WELL_KNOWN_SYMBOLS.has(name)) return null;
|
||||
return { sym, identity: sym, fieldName: wellKnownSymbolFieldName(name) };
|
||||
}
|
||||
|
||||
/** The layout name of a field keyed by the well-known `Symbol.<name>`: a
|
||||
* reserved symbol-field name, printed as Node spells the key. */
|
||||
export function wellKnownSymbolFieldName(name: string): string {
|
||||
return `${FIELD_PREFIX}Symbol(Symbol.${name})`;
|
||||
}
|
||||
|
||||
/** The well-known symbol a field name is keyed by (`toStringTag`), or null. */
|
||||
export function wellKnownSymbolOfField(field: string): string | null {
|
||||
const match = /^%symbol:Symbol\(Symbol\.(\w+)\)$/.exec(field);
|
||||
return match === null ? null : match[1]!;
|
||||
}
|
||||
|
||||
/** The layout name of a `Symbol.toStringTag` field. */
|
||||
export const TO_STRING_TAG_FIELD = wellKnownSymbolFieldName("toStringTag");
|
||||
|
||||
/** A literal Symbol()/Symbol.for() initialized once at module scope. The
|
||||
* whole-file proof also covers var declarations emitted by bundlers. Keys
|
||||
* cannot be used before initialization: a computed class field evaluates
|
||||
|
||||
@@ -157,10 +157,12 @@ const MODULE_FIELDS: Record<StringFields<Omit<IrModule, "functions">>, Role> = {
|
||||
"classes[].errorNameAfter": "other",
|
||||
"classes[].prototypeDataHelper": "reference",
|
||||
"classes[].instancePrototypeHelper": "reference",
|
||||
"classes[].stringTagHelper": "reference",
|
||||
"classes[].localCaptures[].name": "other",
|
||||
"classes[].localCaptures[].localId": "other",
|
||||
"classes[].symbolFields[].field": "other",
|
||||
"classes[].symbolFields[].globalId": "other",
|
||||
"classes[].symbolFields[].wellKnown": "other",
|
||||
"classes[].fields[].name": "other",
|
||||
"classes[].methods[]": "method",
|
||||
"classes[].prototypeNames[]": "other",
|
||||
@@ -281,6 +283,10 @@ const REFERENCE_FIXTURES: Record<string, () => IrModule> = {
|
||||
...module(),
|
||||
classes: [{ name: "%m0.Cell", fields: [], instancePrototypeHelper: TARGET, loc }],
|
||||
}),
|
||||
"classes[].stringTagHelper": () => ({
|
||||
...module(),
|
||||
classes: [{ name: "%m0.Cell", fields: [], stringTagHelper: TARGET, loc }],
|
||||
}),
|
||||
"lib.exports[].fnName": () => ({
|
||||
...module(),
|
||||
lib: {
|
||||
|
||||
@@ -24,6 +24,7 @@ export function moduleFunctionReferences(mod: IrModule): string[] {
|
||||
for (const cls of mod.classes ?? []) {
|
||||
if (cls.prototypeDataHelper !== undefined) names.push(cls.prototypeDataHelper);
|
||||
if (cls.instancePrototypeHelper !== undefined) names.push(cls.instancePrototypeHelper);
|
||||
if (cls.stringTagHelper !== undefined) names.push(cls.stringTagHelper);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
@@ -1477,6 +1477,9 @@ export interface IrClassDef {
|
||||
prototypeDataHelper?: string;
|
||||
/** Native instance to its actual prototype, including factory captures and subclasses. */
|
||||
instancePrototypeHelper?: string;
|
||||
/** Native instance to the string its inherited Symbol.toStringTag getter
|
||||
* answers, read by the runtime for an instance seen as `unknown`. */
|
||||
stringTagHelper?: string;
|
||||
/** Public JavaScript fields record their creation in the shared property
|
||||
* bag. Its descriptors preserve presence and insertion order independently
|
||||
* of native layout order; field values remain in their native slots. */
|
||||
@@ -1486,8 +1489,10 @@ export interface IrClassDef {
|
||||
* class of the chain sets `name` during construction, and then an own
|
||||
* `name` follows every other own property. */
|
||||
errorNameAfter?: string | null;
|
||||
/** Stable module symbol identities for public instance layout fields. */
|
||||
symbolFields?: { field: string; globalId: string }[];
|
||||
/** Stable symbol identities for public instance layout fields: the
|
||||
* module global holding the key, or the name of the well-known symbol
|
||||
* (`toStringTag` for `Symbol.toStringTag`). */
|
||||
symbolFields?: ({ field: string } & ({ globalId: string } | { wellKnown: string }))[];
|
||||
/** RUNTIME-PROVIDED class (the builtin Error hierarchy): the struct, RC
|
||||
* helpers, and vtable live in the runtime (ScrError / scr_error_*), so
|
||||
* backends emit no definitions for it — only the preorder-interval
|
||||
|
||||
@@ -501,10 +501,29 @@ export function validateModule(mod: IrModule): IrValidationError[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const symbol of cls.symbolFields ?? []) {
|
||||
const global = mod.globals?.find((global) => global.id === symbol.globalId);
|
||||
if (cls.stringTagHelper !== undefined) {
|
||||
const helper = functionsByName.get(cls.stringTagHelper);
|
||||
const receiver = helper?.params[0]?.type;
|
||||
if (
|
||||
global?.type.kind !== "symbol" ||
|
||||
!helper ||
|
||||
helper.params.length !== 1 ||
|
||||
receiver?.kind !== "object" ||
|
||||
receiver.className !== cls.name ||
|
||||
helper.returnType.kind !== "string" ||
|
||||
helper.captures !== undefined ||
|
||||
helper.classCaptures !== undefined
|
||||
) {
|
||||
errors.push({
|
||||
message: `class ${cls.name}: string tag helper must be a noncapturing (instance) => string function`,
|
||||
loc: cls.loc,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const symbol of cls.symbolFields ?? []) {
|
||||
const global =
|
||||
"globalId" in symbol ? mod.globals?.find((global) => global.id === symbol.globalId) : null;
|
||||
if (
|
||||
("globalId" in symbol ? global?.type.kind !== "symbol" : symbol.wellKnown === "") ||
|
||||
!cls.fields.some((field) => field.name === symbol.field)
|
||||
) {
|
||||
errors.push({ message: `class ${cls.name}: invalid symbol field metadata`, loc: cls.loc });
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7751-class-well-known-symbol-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -398,6 +398,10 @@ typedef struct ScrDynTypedClassOps {
|
||||
size_t count;
|
||||
ScrStr *const *names; /* immortal member-name strings */
|
||||
ScrStr *class_name; /* immortal constructor name; NULL for an anonymous class */
|
||||
/* The string an inherited Symbol.toStringTag getter answers for the
|
||||
* instance (+1), or NULL with a pending exception from the getter;
|
||||
* NULL when the class has no such getter. */
|
||||
ScrStr *(*string_tag)(void *instance);
|
||||
} ScrDynTypedClassOps;
|
||||
|
||||
/* A compiled JavaScript class whose instances record their own properties
|
||||
@@ -915,6 +919,7 @@ void scr_dyn_install_iterator_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_has_instance_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_async_iterator_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_async_dispose_symbol(ScrDyn *key);
|
||||
void scr_dyn_install_to_string_tag_symbol(ScrDyn *key);
|
||||
/* True when key is Symbol.asyncDispose (registered by the symbol unit when
|
||||
* the program names it); handle symbol_get ops outside that unit use it. */
|
||||
bool scr_dyn_is_async_dispose_symbol(const ScrDyn *key);
|
||||
@@ -1072,6 +1077,9 @@ ScrDyn *scr_record_source_apply(ScrDyn *snapshot, const void *rec);
|
||||
/* Attach the prototype member names of a class instance capsule
|
||||
* (idempotent), so `in` sees its methods and accessors. */
|
||||
void scr_dyn_typed_ref_bind_class(ScrDyn *d, const ScrDynTypedClassOps *ops);
|
||||
/* A class instance capsule's inherited Symbol.toStringTag string from its
|
||||
* class's getter (+1); NULL when it has none or the getter threw. */
|
||||
ScrStr *scr_dyn_typed_ref_class_tag(const ScrDyn *d);
|
||||
/* Attach the bag and prototype hooks of a capsule of a bag-tracked class
|
||||
* instance (idempotent). */
|
||||
void scr_dyn_typed_ref_bind_bag_class(ScrDyn *d, const ScrDynTypedBagClassOps *ops);
|
||||
|
||||
@@ -1207,6 +1207,19 @@ static bool insp_tag_names_class(const char *tag, const char *cls) {
|
||||
memcmp(tag + tag_len - cls_len, cls, cls_len) == 0;
|
||||
}
|
||||
|
||||
/* The Symbol.toStringTag a class instance inherits from its class's
|
||||
* getter (+1), which Node brackets after the class name when it is a
|
||||
* nonempty string other than the name. An own tag field is listed with
|
||||
* the fields instead. */
|
||||
static ScrStr *insp_inherited_tag(const ScrDyn *instance, const ScrStr *name) {
|
||||
ScrStr *tag = scr_dyn_typed_ref_class_tag(instance);
|
||||
if (tag && (tag->len == 0 || (tag->len == name->len && memcmp(tag->data, name->data, name->len) == 0))) {
|
||||
scr_str_release(tag);
|
||||
return NULL;
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
/* A native Set or Map held as a checked-dynamic value: `Set(n) { a, b }`,
|
||||
* `Map(n) { k => v }`, at most 100 entries like Node's maxArrayLength. */
|
||||
/* Node's formatWeakCollection: the entries of a WeakMap or WeakSet are not
|
||||
@@ -1725,10 +1738,23 @@ ScrStr *scr_insp_dyn(ScrDyn *d, double recurse, double depth) {
|
||||
free(cls);
|
||||
} else if (name && materialized->kind == SCR_DYN_OBJ &&
|
||||
scr_dyn_obj_get(materialized, "%error", 6) == NULL) {
|
||||
char *cls = malloc(name->len + 1);
|
||||
ScrStr *tag = insp_inherited_tag(d, name);
|
||||
if (scr_exc_pending()) {
|
||||
scr_dyn_release(materialized);
|
||||
return scr_str_new("", 0);
|
||||
}
|
||||
size_t tag_len = tag ? tag->len + 3 : 0;
|
||||
char *cls = malloc(name->len + tag_len + 1);
|
||||
if (!cls) scr_trap("scriptc: out of memory\n");
|
||||
memcpy(cls, name->data, name->len);
|
||||
cls[name->len] = '\0';
|
||||
if (tag) {
|
||||
/* Node's `Name [tag]` prefix. */
|
||||
memcpy(cls + name->len, " [", 2);
|
||||
memcpy(cls + name->len + 2, tag->data, tag->len);
|
||||
cls[name->len + tag_len - 1] = ']';
|
||||
scr_str_release(tag);
|
||||
}
|
||||
cls[name->len + tag_len] = '\0';
|
||||
size_t nseen = g_nseen;
|
||||
g_pending_seen = instance;
|
||||
out = scr_insp_obj(materialized, recurse, depth, cls);
|
||||
|
||||
@@ -202,6 +202,19 @@ void scr_dyn_install_has_instance_symbol(ScrDyn *key) {
|
||||
scr_atexit(scr_has_instance_symbol_cleanup);
|
||||
}
|
||||
|
||||
/* Symbol.toStringTag, installed when the program first evaluates it: no
|
||||
* object holds the key before then. */
|
||||
static SCR_TL ScrDyn *scr_to_string_tag_symbol;
|
||||
static void scr_to_string_tag_symbol_cleanup(void) {
|
||||
scr_dyn_release(scr_to_string_tag_symbol);
|
||||
scr_to_string_tag_symbol = NULL;
|
||||
}
|
||||
void scr_dyn_install_to_string_tag_symbol(ScrDyn *key) {
|
||||
if (scr_to_string_tag_symbol) return;
|
||||
scr_to_string_tag_symbol = scr_dyn_retain(key);
|
||||
scr_atexit(scr_to_string_tag_symbol_cleanup);
|
||||
}
|
||||
|
||||
void scr_dyn_install_iterator_symbol(ScrDyn *key) {
|
||||
if (scr_iterator_symbol) return;
|
||||
scr_iterator_symbol = scr_dyn_retain(key);
|
||||
@@ -3884,6 +3897,12 @@ static bool scr_dyn_name_in_strs(const ScrStr *key, ScrStr *const *names, size_t
|
||||
return false;
|
||||
}
|
||||
|
||||
ScrStr *scr_dyn_typed_ref_class_tag(const ScrDyn *d) {
|
||||
if (!d || d->kind != SCR_DYN_TYPED_REF || d->prototype) return NULL;
|
||||
const ScrDynTypedClassOps *ops = scr_typed_ref_class_ops(d);
|
||||
return ops && ops->string_tag ? ops->string_tag(d->v.typed_ref.ptr) : NULL;
|
||||
}
|
||||
|
||||
bool scr_dyn_typed_ref_class_has(const ScrDyn *d, const ScrStr *key) {
|
||||
if (!d || d->kind != SCR_DYN_TYPED_REF || d->prototype) return false;
|
||||
const ScrDynTypedClassOps *ops = scr_typed_ref_class_ops(d);
|
||||
@@ -7616,11 +7635,45 @@ void scr_dyn_throw(ScrDyn *d) {
|
||||
scr_dyn_trace_v, scr_dyn_is_object(d));
|
||||
}
|
||||
|
||||
static ScrStr *scr_dyn_tag_string(const ScrStr *tag) {
|
||||
ScrJsonBuf b;
|
||||
scr_jb_init(&b);
|
||||
scr_jb_puts(&b, "[object ");
|
||||
scr_jb_write(&b, tag->data, tag->len);
|
||||
scr_jb_putc(&b, ']');
|
||||
return scr_jb_finish(&b);
|
||||
}
|
||||
|
||||
/* Object.prototype.toString of an object whose own or inherited
|
||||
* @@toStringTag is a string (+1), NULL otherwise or with a pending
|
||||
* exception from its getter. */
|
||||
static ScrStr *scr_dyn_string_tag(const ScrDyn *d) {
|
||||
ScrStr *getter_tag = scr_dyn_typed_ref_class_tag(d);
|
||||
if (getter_tag) {
|
||||
ScrStr *out = scr_dyn_tag_string(getter_tag);
|
||||
scr_str_release(getter_tag);
|
||||
return out;
|
||||
}
|
||||
if (scr_exc_pending() || !scr_to_string_tag_symbol) return NULL;
|
||||
ScrDyn *tag = scr_dyn_symbol_key_get(d, scr_to_string_tag_symbol, true);
|
||||
if (!tag) return NULL;
|
||||
ScrStr *out = tag->kind == SCR_DYN_STR ? scr_dyn_tag_string(tag->v.str) : NULL;
|
||||
scr_dyn_release(tag);
|
||||
return out;
|
||||
}
|
||||
|
||||
ScrStr *scr_dyn_object_tag(const ScrDyn *d) {
|
||||
if (d->kind == SCR_DYN_PROXY) {
|
||||
scr_dyn_proxy_unsupported("Symbol.toStringTag lookup");
|
||||
return NULL;
|
||||
}
|
||||
/* Object.prototype.toString answers a string @@toStringTag over the
|
||||
* builtin tag of a plain object or a class instance. */
|
||||
if ((d->kind == SCR_DYN_OBJ || (d->kind == SCR_DYN_TYPED_REF && !scr_dyn_ref_is_error(d) &&
|
||||
!scr_dyn_generator(d)))) {
|
||||
ScrStr *tag = scr_dyn_string_tag(d);
|
||||
if (tag || scr_exc_pending()) return tag;
|
||||
}
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
if (scr_dyn_ref_is_error(d)) return scr_str_new("[object Error]", 14);
|
||||
if (scr_dyn_generator(d)) {
|
||||
@@ -8838,6 +8891,22 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
|
||||
return NULL;
|
||||
}
|
||||
if (d->kind == SCR_DYN_TYPED_REF) {
|
||||
/* A class instance without its own conversion methods converts with
|
||||
* Object.prototype.toString, which reads its toStringTag getter. */
|
||||
ScrStr *to_string_key = scr_str_new("toString", 8), *value_of_key = scr_str_new("valueOf", 7);
|
||||
bool own_conversion = scr_dyn_typed_ref_class_has(d, to_string_key) ||
|
||||
scr_dyn_typed_ref_class_has(d, value_of_key);
|
||||
scr_str_release(to_string_key);
|
||||
scr_str_release(value_of_key);
|
||||
if (!own_conversion) {
|
||||
ScrStr *tag = scr_dyn_typed_ref_class_tag(d);
|
||||
if (tag) {
|
||||
ScrStr *out = scr_dyn_tag_string(tag);
|
||||
scr_str_release(tag);
|
||||
return out;
|
||||
}
|
||||
if (scr_exc_pending()) return NULL;
|
||||
}
|
||||
ScrDyn *materialized = scr_dyn_typed_ref_string_view(d);
|
||||
ScrStr *out = scr_dyn_string_coerce_js(materialized);
|
||||
scr_dyn_release(materialized);
|
||||
@@ -8858,7 +8927,7 @@ ScrStr *scr_dyn_string_coerce_js(const ScrDyn *d) {
|
||||
scr_error_release(error);
|
||||
return text;
|
||||
}
|
||||
return scr_str_new("[object Object]", 15);
|
||||
return scr_dyn_object_tag(d);
|
||||
}
|
||||
if (!m || m->kind != SCR_DYN_FUNC) { scr_dyn_release(m); continue; }
|
||||
/* OrdinaryToPrimitive performs a method call, not a bare function
|
||||
|
||||
@@ -90,6 +90,11 @@ ScrSym *scr_sym_well_known(ScrStr *name) {
|
||||
scr_dyn_install_has_instance_symbol(key);
|
||||
scr_dyn_release(key);
|
||||
}
|
||||
if (name->len == 11 && !memcmp(name->data, "toStringTag", 11)) {
|
||||
ScrDyn *key = scr_dyn_new_symbol(symbol);
|
||||
scr_dyn_install_to_string_tag_symbol(key);
|
||||
scr_dyn_release(key);
|
||||
}
|
||||
if (name->len == 13 && !memcmp(name->data, "asyncIterator", 13)) {
|
||||
ScrDyn *key = scr_dyn_new_symbol(symbol);
|
||||
scr_dyn_install_async_iterator_symbol(key);
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Class fields keyed by well-known symbols, Symbol.toStringTag first (the
|
||||
// lru-cache shape), beside toStringTag getters and module symbol keys.
|
||||
// String conversion, Object.prototype.toString and util.inspect read the
|
||||
// tag as Node does, through typed values and through `unknown`.
|
||||
|
||||
const limit = Symbol("limit");
|
||||
|
||||
class Cache {
|
||||
size = 0;
|
||||
[limit] = 10;
|
||||
[Symbol.toStringTag] = "LRUCache";
|
||||
}
|
||||
|
||||
class Sub extends Cache {
|
||||
extra = 1;
|
||||
}
|
||||
|
||||
class Retag extends Cache {
|
||||
constructor() {
|
||||
super();
|
||||
this[Symbol.toStringTag] = "Retagged";
|
||||
}
|
||||
}
|
||||
|
||||
class Tagged {
|
||||
x = 1;
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "Custom";
|
||||
}
|
||||
}
|
||||
|
||||
class TaggedSub extends Tagged {
|
||||
y = 2;
|
||||
}
|
||||
|
||||
class Named {
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "Named";
|
||||
}
|
||||
}
|
||||
|
||||
class Blank {
|
||||
get [Symbol.toStringTag](): string {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
class Plain {
|
||||
n = 3;
|
||||
}
|
||||
|
||||
const c = new Cache();
|
||||
console.log(String(c), `${c}`, Object.prototype.toString.call(c), c[Symbol.toStringTag]);
|
||||
console.log(c);
|
||||
console.log(Object.keys(c), JSON.stringify(c));
|
||||
console.log(new Sub(), String(new Sub()));
|
||||
const r: Cache = new Retag();
|
||||
console.log(String(r), r);
|
||||
c[Symbol.toStringTag] = "Other";
|
||||
console.log(`${c}`, c);
|
||||
|
||||
const t = new Tagged();
|
||||
console.log(String(t), `${t}`, t, [t], { deep: { deeper: { t } } });
|
||||
const ts: Tagged = new TaggedSub();
|
||||
console.log(String(ts), ts);
|
||||
console.log(new Named(), new Blank(), String(new Blank()));
|
||||
console.log(String(new Plain()), new Plain());
|
||||
|
||||
const values: unknown[] = [new Cache(), new Tagged(), new TaggedSub(), new Plain()];
|
||||
for (const v of values) {
|
||||
console.log(Object.prototype.toString.call(v), String(v), (v as { [Symbol.toStringTag]?: unknown })[Symbol.toStringTag]);
|
||||
}
|
||||
console.log(values);
|
||||
|
||||
const literal: unknown = { [Symbol.toStringTag]: "Literal", a: 1 };
|
||||
console.log(Object.prototype.toString.call(literal), String(literal), `${literal}`);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3800, "san": 4300 }
|
||||
Reference in New Issue
Block a user