Address review comments

This commit is contained in:
Chris Tate
2026-10-23 23:49:40 -05:00
parent c9346c1583
commit cbb87e8812
7 changed files with 127 additions and 31 deletions
@@ -225,6 +225,11 @@ function classConversion(
plan.method === "sym:toPrimitive" ? [{ kind: "strLit", value: hint, type: STRING, loc }] : [];
return finish(accessorCall(lowerer, cls.def.name, plan.method, object, args, plan.ret, loc));
};
// "[object Object]" reads nothing of the instance, but the receiver
// expression still runs, once.
const own = plans.get(info)!;
if (tops.length === 0 && own.method === null && !own.date && !own.tag && !isSafeToRepeat(value))
prefix.push({ kind: "exprStmt", expr: value, loc });
let result = convert(info, receiver);
for (let i = tops.length - 1; i >= 0; i--) {
const top = tops[i]!;
@@ -18,6 +18,7 @@ import { jsFuncNameOf, type Lowerer } from "./lowerer.js";
import { isJsSourceFile } from "../program.js";
import {
BOOL,
CAUGHT,
DYN,
F64,
type IrExpr,
@@ -1876,7 +1877,8 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
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.
// nonempty string other than the name. A getter that throws shows
// no tag, as in Node.
const tagGetter = info.fields.has(TO_STRING_TAG_FIELD)
? null
: findMethodOn(lowerer, info, "get:sym:toStringTag");
@@ -1884,21 +1886,36 @@ function inspectHelper(lowerer: Lowerer, t: IrType, loc: SrcLoc): string {
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,
locals.push(
{ id: "tag.0", name: "tag", type: STRING, mutable: true },
{ id: "tagError.0", name: "tagError", type: CAUGHT, mutable: false },
);
body.push(
{ kind: "varDecl", localId: "tag.0", init: strLit("", loc), loc },
{
kind: "tryCatch",
tryBody: [
{
kind: "assign",
localId: "tag.0",
value: accessorCall(
lowerer,
owner,
"get:sym:toStringTag",
lowerer.upcastTo(v(), owner),
[],
STRING,
loc,
),
loc,
},
],
catchBody: [],
catchLocalId: "tagError.0",
finallyBody: null,
loc,
),
loc,
});
},
);
const tag = (): IrExpr => varRef("tag.0", STRING, loc);
const shown: IrExpr = {
kind: "logical",
+4
View File
@@ -144,6 +144,10 @@ static void scr_dyn_display_buf(ScrJsonBuf *b, const ScrDyn *d, bool to_string)
return;
case SCR_DYN_TYPED_REF: {
/* ToString of a class instance that converts through
* Object.prototype.toString shows its Symbol.toStringTag getter's
* string. */
if (to_string && scr_dyn_typed_ref_put_tag(b, d)) return;
ScrDyn *materialized = scr_dyn_typed_ref_materialize(d);
scr_dyn_display_buf(b, materialized, to_string);
scr_dyn_release(materialized);
+2 -1
View File
@@ -1210,9 +1210,10 @@ static bool insp_tag_names_class(const char *tag, const char *cls) {
/* 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. */
* the fields instead. A getter that throws shows no tag, as in Node. */
static ScrStr *insp_inherited_tag(const ScrDyn *instance, const ScrStr *name) {
ScrStr *tag = scr_dyn_typed_ref_class_tag(instance);
if (!tag && scr_exc_pending()) scr_exc_clear();
if (tag && (tag->len == 0 || (tag->len == name->len && memcmp(tag->data, name->data, name->len) == 0))) {
scr_str_release(tag);
return NULL;
+40 -15
View File
@@ -4034,6 +4034,9 @@ ScrDyn *scr_dyn_index_get(const ScrDyn *d, double index) {
return scr_dyn_typed_ref_element(d, i);
}
static ScrStr *scr_dyn_typed_ref_conversion_tag(const ScrDyn *d);
static ScrStr *scr_dyn_tag_string(const ScrStr *tag);
ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) {
/* An instance of a class declared `extends Date` converts through its
* toString, declared or inherited. With an exception pending the view is
@@ -4047,6 +4050,16 @@ ScrDyn *scr_dyn_typed_ref_string_view(const ScrDyn *d) {
scr_str_release(empty);
return out;
}
/* A class instance converting through Object.prototype.toString shows
* its Symbol.toStringTag getter's string. */
ScrStr *class_tag = scr_dyn_typed_ref_conversion_tag(d);
if (class_tag || scr_exc_pending()) {
ScrStr *text = class_tag ? scr_dyn_tag_string(class_tag) : scr_str_new("", 0);
scr_str_release(class_tag);
ScrDyn *out = scr_dyn_new_str(text);
scr_str_release(text);
return out;
}
ScrDyn *view = scr_dyn_typed_ref_materialize(d);
const char *tag = scr_event_ref_hooks ? scr_event_ref_hooks->string_tag(d) : NULL;
if (!tag || view->kind != SCR_DYN_OBJ || scr_dyn_property_owner(view, "toString", 8)) return view;
@@ -8333,6 +8346,27 @@ static ScrStr *scr_dyn_tag_string(const ScrStr *tag) {
return scr_jb_finish(&b);
}
/* The Symbol.toStringTag getter's string of a class instance whose
* ToString is Object.prototype.toString (its class has no toString; the
* string hint never reaches valueOf, since that answers a string): +1, or
* NULL otherwise or with the getter's exception pending. */
static ScrStr *scr_dyn_typed_ref_conversion_tag(const ScrDyn *d) {
ScrStr *to_string_key = scr_str_new("toString", 8);
bool own_conversion = scr_dyn_typed_ref_class_has(d, to_string_key);
scr_str_release(to_string_key);
return own_conversion ? NULL : scr_dyn_typed_ref_class_tag(d);
}
bool scr_dyn_typed_ref_put_tag(ScrJsonBuf *b, const ScrDyn *d) {
ScrStr *tag = scr_dyn_typed_ref_conversion_tag(d);
if (!tag) return scr_exc_pending();
scr_jb_puts(b, "[object ");
scr_jb_write(b, tag->data, tag->len);
scr_jb_putc(b, ']');
scr_str_release(tag);
return true;
}
/* 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. */
@@ -9676,22 +9710,13 @@ 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;
ScrStr *tag = scr_dyn_typed_ref_conversion_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);
+5
View File
@@ -119,6 +119,11 @@ _Static_assert(offsetof(ScrJsonBuf, data) == 0 && offsetof(ScrJsonBuf, len) == s
"LLVM backend expects ScrJsonBuf to start with { data, len, cap }");
void scr_jb_init(ScrJsonBuf *b);
/* String conversion of a class instance whose ToString is
* Object.prototype.toString and whose class has a Symbol.toStringTag
* getter: appends "[object Tag]" and answers true, also with the getter's
* exception pending; false when the ordinary conversion applies. */
bool scr_dyn_typed_ref_put_tag(ScrJsonBuf *b, const ScrDyn *d);
/* Release a buffer's text without finishing it (error paths). */
void scr_jb_abandon(ScrJsonBuf *b);
/* Append a borrowed byte span verbatim, including embedded NUL bytes. */
@@ -74,3 +74,42 @@ console.log(values);
const literal: unknown = { [Symbol.toStringTag]: "Literal", a: 1 };
console.log(Object.prototype.toString.call(literal), String(literal), `${literal}`);
// util.inspect ignores a Symbol.toStringTag getter that throws, typed or
// held as an untyped value.
class Throwing {
get [Symbol.toStringTag](): string {
throw new Error("tag");
}
}
const throwing: unknown = new Throwing();
console.log(new Throwing(), [new Throwing()], throwing, [throwing]);
// A conversion to "[object Object]" still evaluates its receiver.
let made = 0;
function make(): Plain {
made++;
return new Plain();
}
function fail(): Plain {
throw new Error("receiver");
}
console.log(Object.prototype.toString.call(make()), String(make()), made);
try {
console.log(Object.prototype.toString.call(fail()));
} catch (error) {
console.log((error as Error).message);
}
// ToString of a class with valueOf and no toString reads its tag; the
// default hint calls valueOf.
class Valued {
valueOf(): number {
return 1;
}
get [Symbol.toStringTag](): string {
return "Valued";
}
}
const valued: unknown = new Valued();
console.log(String(valued), `${valued}`, [valued].join(), valued + "", Object.prototype.toString.call(valued));