You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
hir: builder fold treats an implicit ToPrimitive as "cannot execute user code" — o.a = "" + weird throws ReferenceError #10357
The builder fold (#6812, crates/perry-hir/src/lower/builder_fold.rs) rewrites
consto: any={};o.a=<value>;
into const o: any = { a: <value> }, which moves <value> from after the allocation to beforeo is initialized. value_is_fold_safe is what makes that sound, and its doc says it admits "only expressions that provably cannot execute user code".
It doesn't. It admits every Expr::Bin operator except in/instanceof, every Expr::Unary except delete, and Expr::Tpl with substitutions. Each of those runs an implicit ToPrimitive/ToNumber/ToString on its operands, and that conversion calls a user valueOf/toString/Symbol.toPrimitive. If that user code reads o, the fold turns a successful read into a TDZ ReferenceError.
Every value (or, since #10355, every skipped gap statement) that converts an operand:
arithmetic/relational/bitwise Bin: a + b, a - b, a < b, a | b, a == b, …
numeric Unary: -x, +x, ~x
templates with substitutions: `${x}`
Conversion-free forms are fine: literals, identifiers, typeof, void, !, ===/!==, &&/||/??, ?:, and array/object literals built from those.
Fix and its cost
Remove the converting operators from value_is_fold_safe, keeping only the conversion-free forms above. That is a correctness fix, but it narrows #6812's coverage: o.b = r + i, the module doc's own motivating example, would stop folding. It should be measured before landing (benchmarks/object-write-6812, and any real module graph that relies on the fold). Anything smarter needs a type proof that the operands are primitive.
#10355 reuses this predicate for its gap test on purpose, so the two sides stay in sync, and did not change it. Found while validating that PR.
Correction to "Affected forms" above: bare identifiers are not fine. A name that resolves to no binding reads the global object, and Object.defineProperty(globalThis, "g", { get() { return typeof o; }, configurable: true }); const o = {}; o.a = g; throws ReferenceError on perry where node prints object. I missed it at first because my probe used declare const g, and perry materializes that as a real undefined binding (#10363). #10361 fixes both the conversions and the identifier reads.
Fixed by 204f9f8065 (changelog.d/10361-builder-fold-toprimitive.md). This issue's exact reproducer returned ReferenceError when filed; it now prints string:str, matching node.
One caveat worth recording: this was reported on Linux and verified on macOS. The fix commit is named and specific, so I am closing on that rather than on the platform match alone — reopen if it persists on Linux.
Activity
fix(hir): keep an observable conversion out of a folded builder (Perr…
fix(hir): keep observable user code out of a folded builder (PerryTS#…
proggeramlug commentedon Sep 16, 2026
Correction to "Affected forms" above: bare identifiers are not fine. A name that resolves to no binding reads the global object, and
Object.defineProperty(globalThis, "g", { get() { return typeof o; }, configurable: true }); const o = {}; o.a = g;throwsReferenceErroron perry where node printsobject. I missed it at first because my probe useddeclare const g, and perry materializes that as a realundefinedbinding (#10363). #10361 fixes both the conversions and the identifier reads.fix(hir): keep observable user code out of a folded builder (#10357)
refactor(hir): split builder_fold.rs width-hint scan into its own file
proggeramlug commentedon Sep 20, 2026
Fixed by
204f9f8065(changelog.d/10361-builder-fold-toprimitive.md). This issue's exact reproducer returnedReferenceErrorwhen filed; it now printsstring:str, matching node.One caveat worth recording: this was reported on Linux and verified on macOS. The fix commit is named and specific, so I am closing on that rather than on the platform match alone — reopen if it persists on Linux.