Summary
String(x) on a builtin exotic with a Symbol.toStringTag renders "[object Object]" instead of "[object <Tag>]".
Repro
console.log(String(new Map())); // node: [object Map] perry: [object Object]
console.log(String(new Set())); // node: [object Set] perry: [object Object]
console.log(String(Promise.resolve(1))); // node: [object Promise] perry: [object Object]
console.log(`${new Map()}`); // node: [object Map] perry: [object Object]
Why
Object.prototype.toString is spec'd to read [[Get]](O, @@toStringTag) and use it when it is a string (ES2024 §20.1.3.6); Map.prototype[@@toStringTag] is "Map", Set's is "Set", Promise's is "Promise". Perry's ToString path (js_jsvalue_to_string in crates/perry-runtime/src/value/to_string.rs) ends in a hardcoded "[object Object]" fallback that never consults the tag. Note the runtime does have a to_string_tag.rs — it just isn't wired into this fallback.
An OWN toString on these instances is already honoured correctly (m.toString = () => "X"; String(m) → "X"), so this is purely the default-tag rendering.
Found while fixing #6370 (own toString overrides ignored on the ToString coercion path for RegExp/Date); pre-existing on main.
Summary
String(x)on a builtin exotic with aSymbol.toStringTagrenders"[object Object]"instead of"[object <Tag>]".Repro
Why
Object.prototype.toStringis spec'd to read[[Get]](O, @@toStringTag)and use it when it is a string (ES2024 §20.1.3.6);Map.prototype[@@toStringTag]is"Map",Set's is"Set",Promise's is"Promise". Perry's ToString path (js_jsvalue_to_stringincrates/perry-runtime/src/value/to_string.rs) ends in a hardcoded"[object Object]"fallback that never consults the tag. Note the runtime does have ato_string_tag.rs— it just isn't wired into this fallback.An OWN
toStringon these instances is already honoured correctly (m.toString = () => "X"; String(m)→"X"), so this is purely the default-tag rendering.Found while fixing #6370 (own
toStringoverrides ignored on the ToString coercion path for RegExp/Date); pre-existing onmain.