Summary
JSON.stringify(url.toString()) returns undefined when the URL.prototype.toString() call is inlined directly as the argument to JSON.stringify. Storing the string in a variable first works.
Repro
const u = new URL("http://x/en", "http://x/");
console.log(JSON.stringify(u.toString())); // perry: undefined node: "http://x/en"
const r = u.toString();
console.log(JSON.stringify(r)); // both: "http://x/en" (stored works)
Scope (narrow)
- Only a URL receiver triggers it —
Date, numbers, and { toString(){…} } objects all stringify correctly inline into JSON.stringify.
- Only
JSON.stringify as the callee triggers it — String(u.toString()), encodeURIComponent(u.toString()), Object.assign({}, {x: u.toString()}), u.toString() + "", u.toString().slice(0), (u.toString()).length (==11), a user function id(u.toString()), and console.log(u.toString()) all work.
typeof (u.toString()) is "string", so the value itself is a valid string.
Notes for whoever picks this up
- Deterministic; reproduces with
PERRY_GEN_GC=0 / PERRY_WRITE_BARRIERS=0 / PERRY_GC_FORCE_EVACUATE=1 — not GC.
- HIR is
JsonStringifyFull(<arg>, Null, Null) in both cases → js_json_stringify_full(v,r,i) (crates/perry-codegen/src/expr/math_simple.rs). --trace llvm shows identical IR for the working vs failing case: %p = call i64 @js_jsvalue_to_string_method(double u) → or i64 %p, STRING_TAG → bitcast to double → js_json_stringify_full. The only difference is the working case shadow-slot-roots the string before the call; the failing case passes a bare SSA temp.
- So the divergence appears to be in
js_json_stringify_full (crates/perry-runtime/src/json/replacer.rs) misreading the URL.toString() string when it's the top-level value — suspect the try_stringify_lazy_array(value) fast path (only taken when no replacer/no spacer). Not root-caused.
Summary
JSON.stringify(url.toString())returnsundefinedwhen theURL.prototype.toString()call is inlined directly as the argument toJSON.stringify. Storing the string in a variable first works.Repro
Scope (narrow)
Date, numbers, and{ toString(){…} }objects all stringify correctly inline intoJSON.stringify.JSON.stringifyas the callee triggers it —String(u.toString()),encodeURIComponent(u.toString()),Object.assign({}, {x: u.toString()}),u.toString() + "",u.toString().slice(0),(u.toString()).length(==11), a user functionid(u.toString()), andconsole.log(u.toString())all work.typeof (u.toString())is"string", so the value itself is a valid string.Notes for whoever picks this up
PERRY_GEN_GC=0/PERRY_WRITE_BARRIERS=0/PERRY_GC_FORCE_EVACUATE=1— not GC.JsonStringifyFull(<arg>, Null, Null)in both cases →js_json_stringify_full(v,r,i)(crates/perry-codegen/src/expr/math_simple.rs).--trace llvmshows identical IR for the working vs failing case:%p = call i64 @js_jsvalue_to_string_method(double u)→or i64 %p, STRING_TAG→bitcast to double→js_json_stringify_full. The only difference is the working case shadow-slot-roots the string before the call; the failing case passes a bare SSA temp.js_json_stringify_full(crates/perry-runtime/src/json/replacer.rs) misreading theURL.toString()string when it's the top-level value — suspect thetry_stringify_lazy_array(value)fast path (only taken when no replacer/no spacer). Not root-caused.