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
module-level const/let reads before the declaration don't throw (no TDZ at module level) #11826
In a compound member assignment whose base is a const binding still in its temporal dead zone, perry evaluates the computed key first and then throws a TypeError. Per spec, the base is evaluated first, so reading the binding throws a ReferenceError before the key expression runs.
Two differences: the key's side effect (f()) runs when it shouldn't, and the error type is wrong. The TDZ check on the base is either skipped or ordered after the key spill in the compound-assignment lowering (hoist_compound_member_assign, crates/perry-hir/src/lower/expr_assign.rs).
Found while building #11825, which doesn't change this behaviour: that PR keeps the base temp, ahead of the key, whenever the key is spilled.
Correction to the diagnosis above, from a closer look on main 0a5bb47dc. The compound-assignment lowering is not at fault: it spills the base before the key. The real gap is that module-level const/let bindings have no TDZ at all. A read before the declaration yields undefined instead of throwing, so the compound form is only one symptom.
At module level (node throws ReferenceError in every case):
const y = tdz; before the declaration: no throw.
tdz.a, tdz["a"] += 1, tdz.a += 1, tdz[f()], tdz[f()] = 1, tdz[f()] += 1: TypeError, and the forms with a key call run f.
() => tdz called before the declaration: returns undefined.
Inside a function body every one of these forms already throws ReferenceError with zero key calls, so function-level TDZ works.
A fix needs TDZ for module-level lexical bindings. That means either a static rule (a read lexically before the declarator, at the same function level, throws) plus a check for closures that can run early, or a TDZ cell per module-level binding, which puts a cost on every global read. A prior attempt, #9762, was reverted in 1d63fa91f. Related: #6062, #6264, #6523, #11086. Retitling to match.
changed the title [-]tdz[f()] += 1 on a const in its TDZ runs the key and throws TypeError instead of ReferenceError[/-][+]module-level const/let reads before the declaration don't throw (no TDZ at module level)[/+]on Oct 4, 2026
Reopening: the cyclic-export TDZ follow-up (#11963) was reverted in #11989 because it broke tsc and qs (#11987). The cycle case still needs a correct fix.
Fixed: module-level TDZ landed in 4d740d2, and the import-cycle case re-landed in #11995 (with a CommonJS skip), verified against node on tsc, Zod, qs, commander and hello.
Activity
proggeramlug commentedon Oct 4, 2026
Correction to the diagnosis above, from a closer look on main
0a5bb47dc. The compound-assignment lowering is not at fault: it spills the base before the key. The real gap is that module-levelconst/letbindings have no TDZ at all. A read before the declaration yieldsundefinedinstead of throwing, so the compound form is only one symptom.At module level (node throws ReferenceError in every case):
const y = tdz;before the declaration: no throw.tdz.a,tdz["a"] += 1,tdz.a += 1,tdz[f()],tdz[f()] = 1,tdz[f()] += 1: TypeError, and the forms with a key call runf.() => tdzcalled before the declaration: returnsundefined.Inside a function body every one of these forms already throws ReferenceError with zero key calls, so function-level TDZ works.
A fix needs TDZ for module-level lexical bindings. That means either a static rule (a read lexically before the declarator, at the same function level, throws) plus a check for closures that can run early, or a TDZ cell per module-level binding, which puts a cost on every global read. A prior attempt, #9762, was reverted in
1d63fa91f. Related: #6062, #6264, #6523, #11086. Retitling to match.[-]tdz[f()] += 1 on a const in its TDZ runs the key and throws TypeError instead of ReferenceError[/-][+]module-level const/let reads before the declaration don't throw (no TDZ at module level)[/+]perf(modules): emit TDZ checks only for reads that can run before ini…
fix(hir): an exported lexical binding read across an import cycle hon…
fix(hir,codegen): module-level let/const temporal dead zone (PerryTS#…
Revert "fix(hir): an exported lexical binding read across an import c…
proggeramlug commentedon Oct 4, 2026
Reopening: the cyclic-export TDZ follow-up (#11963) was reverted in #11989 because it broke tsc and qs (#11987). The cycle case still needs a correct fix.
fix(hir): an exported lexical binding read across an import cycle hon…
fix(hir): an exported lexical binding read across an import cycle hon…
proggeramlug commentedon Oct 5, 2026
Fixed: module-level TDZ landed in 4d740d2, and the import-cycle case re-landed in #11995 (with a CommonJS skip), verified against node on tsc, Zod, qs, commander and hello.