Summary
var x; re-declared inside a loop body is reset to undefined on each iteration. var is function-scoped and hoisted, so re-executing the declaration is a runtime no-op — the binding must keep its value.
This is the mirror image of #6871 (which was about lexical let not being reset). It is a pre-existing defect, present before and after the #6871 fix.
Repro
function varKeepsValue(): string {
const out: string[] = [];
for (let i = 0; i < 3; i++) {
var v: string | undefined;
if (i === 0) v = "kept";
out.push(v === undefined ? "u" : v);
}
return out.join(" ");
}
console.log(varKeepsValue());
node --experimental-strip-types kept kept kept
perry 0.5.1239 kept u u <-- wrong
Verified against released perry 0.5.1239 (i.e. before #6871's fix landed), so this is not a regression from that change — #6871 deliberately excludes var and neither fixes nor worsens this.
Notes
Per ECMA-262, a var declaration's binding is created once at function entry (FunctionDeclarationInstantiation, initialized to undefined); the declaration site itself only performs the assignment when one is written. var v; with no initializer is therefore a no-op every time control reaches it, including on a loop back-edge.
Likely relevant: HIR emits Stmt::Let { init: None } for the hoisted binding, and stmt/let_stmt.rs has a redeclaration path (if ctx.locals.contains_key(&id)) whose no-init arm clears the slot to undefined when the id is in ctx.tdz_boxes. That arm's comment already states the intended semantics — "A redeclaration with no initializer (var x;) keeps the prior value, matching JS" — so the reset is coming from somewhere that path does not intend. I did not chase it further.
Test coverage
test-files/test_gap_uninit_let_loop_reset.ts (added by #6871) documents this case in a comment rather than asserting it, so it does not bake in a known failure. Once fixed, the assertion can be restored there:
console.log(varKeepsValue()); // kept kept kept
Environment
- perry 0.5.1239, macOS arm64
Summary
var x;re-declared inside a loop body is reset toundefinedon each iteration.varis function-scoped and hoisted, so re-executing the declaration is a runtime no-op — the binding must keep its value.This is the mirror image of #6871 (which was about lexical
letnot being reset). It is a pre-existing defect, present before and after the #6871 fix.Repro
Verified against released perry 0.5.1239 (i.e. before #6871's fix landed), so this is not a regression from that change — #6871 deliberately excludes
varand neither fixes nor worsens this.Notes
Per ECMA-262, a
vardeclaration's binding is created once at function entry (FunctionDeclarationInstantiation, initialized toundefined); the declaration site itself only performs the assignment when one is written.var v;with no initializer is therefore a no-op every time control reaches it, including on a loop back-edge.Likely relevant: HIR emits
Stmt::Let { init: None }for the hoisted binding, andstmt/let_stmt.rshas a redeclaration path (if ctx.locals.contains_key(&id)) whose no-init arm clears the slot toundefinedwhen the id is inctx.tdz_boxes. That arm's comment already states the intended semantics — "A redeclaration with no initializer (var x;) keeps the prior value, matching JS" — so the reset is coming from somewhere that path does not intend. I did not chase it further.Test coverage
test-files/test_gap_uninit_let_loop_reset.ts(added by #6871) documents this case in a comment rather than asserting it, so it does not bake in a known failure. Once fixed, the assertion can be restored there:Environment