Skip to content

Commit 98a36fb

Browse files
authored
feat(codemode): hoist var to function scope (#48284)
1 parent e22cd0a commit 98a36fb

3 files changed

Lines changed: 316 additions & 11 deletions

File tree

‎packages/codemode/interpreter-support.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,17 @@ ultimate source of truth.
4444

4545
## Bindings and destructuring
4646

47-
- [x] `const`, `let`, and accepted `var` declarations.
47+
- [x] `const`, `let`, and `var` declarations.
4848
- [x] Object and array destructuring in declarations, parameters, assignment expressions, and `for...of` bindings.
4949
- [x] Nested patterns, defaults, elisions, and rest elements.
5050
- [x] Assignment to identifiers, plain-object fields, non-negative integer array indexes, and writable URL
5151
fields.
5252
- [x] Direct function declarations are hoisted in program and block statement lists.
5353
- [x] Parameter defaults observe a temporal dead zone for later parameters.
54-
- [ ] JavaScript-correct function scoping, hoisting, and redeclaration for accepted `var` declarations.
54+
- [x] `var` is function-scoped and hoisted: names declared anywhere in a function or program body, including loop
55+
heads, blocks, `switch` cases, and `try`/`catch`, read as `undefined` before their statement runs; redeclaration
56+
assigns the one binding; a same-named parameter keeps its argument; closures in parameter defaults see outer
57+
names rather than body `var`s.
5558
- [x] Predeclare `let` and `const` bindings in every lexical scope, including program/block bodies, switch bodies, and
5659
loop headers, so reads before initialization and self- or cross-referential initializers observe the JavaScript
5760
temporal dead zone.

‎packages/codemode/src/interpreter/runtime.ts‎

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,49 @@ const collectPatternNames = (pattern: Pattern, out: Array<string> = []): Array<s
170170
return out
171171
}
172172

173+
// `var` names declared anywhere in a function body except inside nested functions, which own theirs.
174+
const collectVarNames = (
175+
node: Statement | ModuleDeclaration | null | undefined,
176+
out: Array<string> = [],
177+
): Array<string> => {
178+
if (!node) return out
179+
switch (node.type) {
180+
case "VariableDeclaration":
181+
if (node.kind === "var") for (const declaration of node.declarations) collectPatternNames(declaration.id, out)
182+
break
183+
case "BlockStatement":
184+
for (const statement of node.body) collectVarNames(statement, out)
185+
break
186+
case "IfStatement":
187+
collectVarNames(node.consequent, out)
188+
collectVarNames(node.alternate, out)
189+
break
190+
case "ForStatement":
191+
if (node.init?.type === "VariableDeclaration") collectVarNames(node.init, out)
192+
collectVarNames(node.body, out)
193+
break
194+
case "ForInStatement":
195+
case "ForOfStatement":
196+
if (node.left.type === "VariableDeclaration") collectVarNames(node.left, out)
197+
collectVarNames(node.body, out)
198+
break
199+
case "WhileStatement":
200+
case "DoWhileStatement":
201+
case "LabeledStatement":
202+
collectVarNames(node.body, out)
203+
break
204+
case "SwitchStatement":
205+
for (const item of node.cases) for (const statement of item.consequent) collectVarNames(statement, out)
206+
break
207+
case "TryStatement":
208+
collectVarNames(node.block, out)
209+
collectVarNames(node.handler?.body, out)
210+
collectVarNames(node.finalizer, out)
211+
break
212+
}
213+
return out
214+
}
215+
173216
const loopDeclaration = (left: VariableDeclaration | Pattern, statement: "for...of" | "for...in") => {
174217
if (left.type !== "VariableDeclaration") return undefined
175218
const declaration = left.declarations.length === 1 ? left.declarations[0] : undefined
@@ -276,6 +319,7 @@ class Frame<R> {
276319
return Effect.gen(function* () {
277320
self.predeclareLexical(program.body)
278321
self.hoistFunctions(program.body)
322+
self.hoistVars(program.body)
279323
let value: unknown = undefined
280324
for (const [index, statement] of program.body.entries()) {
281325
if (index === program.body.length - 1 && statement.type === "ExpressionStatement") {
@@ -399,6 +443,18 @@ class Frame<R> {
399443
}
400444
}
401445

446+
// Hoisted `var` bindings start undefined, or copy a same-named parameter. Function bodies hoist
447+
// into their own scope above the parameters so closures in parameter defaults keep seeing outer names.
448+
private hoistVars(statements: ReadonlyArray<Statement | ModuleDeclaration>, parameters?: Map<string, Binding>): void {
449+
const scope = this.scopes.current()
450+
for (const statement of statements) {
451+
for (const name of collectVarNames(statement)) {
452+
if (scope.has(name)) continue
453+
scope.set(name, { mutable: true, value: parameters?.get(name)?.value, initialized: true })
454+
}
455+
}
456+
}
457+
402458
private predeclareLexical(statements: ReadonlyArray<Statement | ModuleDeclaration>): void {
403459
for (const statement of statements) {
404460
if (statement.type !== "VariableDeclaration") continue
@@ -589,18 +645,20 @@ class Frame<R> {
589645

590646
const evaluateBody = (value: unknown) =>
591647
Effect.gen(function* () {
592-
if (declared) {
648+
if (declared?.lexical) {
593649
self.scopes.push()
594-
if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
595-
yield* self.declarePattern(declared.pattern, value, declared.mutable, left, declared.lexical)
650+
self.predeclarePattern(declared.pattern, declared.mutable, left)
651+
yield* self.declarePattern(declared.pattern, value, declared.mutable, left, true)
652+
} else if (declared) {
653+
yield* self.assignPattern(declared.pattern, value, left)
596654
} else if (assignment) {
597655
yield* self.assignPattern(assignment, value, left)
598656
}
599657
return yield* self.evaluateStatement(node.body)
600658
}).pipe(
601659
Effect.ensuring(
602660
Effect.sync(() => {
603-
if (declared) self.scopes.pop()
661+
if (declared?.lexical) self.scopes.pop()
604662
}),
605663
),
606664
)
@@ -834,18 +892,20 @@ class Frame<R> {
834892

835893
for (const key of keys) {
836894
const result = yield* Effect.gen(function* () {
837-
if (declared) {
895+
if (declared?.lexical) {
838896
self.scopes.push()
839-
if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left)
840-
yield* self.declarePattern(declared.pattern, key, declared.mutable, left, declared.lexical)
897+
self.predeclarePattern(declared.pattern, declared.mutable, left)
898+
yield* self.declarePattern(declared.pattern, key, declared.mutable, left, true)
899+
} else if (declared) {
900+
yield* self.assignPattern(declared.pattern, key, left)
841901
} else if (assignmentName) {
842902
self.scopes.set(assignmentName, key, left)
843903
}
844904
return yield* self.evaluateStatement(node.body)
845905
}).pipe(
846906
Effect.ensuring(
847907
Effect.sync(() => {
848-
if (declared) self.scopes.pop()
908+
if (declared?.lexical) self.scopes.pop()
849909
}),
850910
),
851911
)
@@ -952,8 +1012,13 @@ class Frame<R> {
9521012
}
9531013

9541014
const init = declaration.init
1015+
// `var x` alone is a no-op: the binding was hoisted on function entry.
1016+
if (kind === "var") {
1017+
if (init) yield* self.assignPattern(declaration.id, yield* self.evaluateExpression(init), declaration)
1018+
continue
1019+
}
9551020
const value = init ? yield* self.evaluateExpression(init) : undefined
956-
yield* self.declarePattern(declaration.id, value, kind !== "const", declaration, kind !== "var")
1021+
yield* self.declarePattern(declaration.id, value, kind !== "const", declaration, true)
9571022
}
9581023
})
9591024
}
@@ -1574,6 +1639,8 @@ class Frame<R> {
15741639
}
15751640

15761641
if (fn.body.type === "BlockStatement") {
1642+
invocation.scopes.push()
1643+
invocation.hoistVars(fn.body.body, paramScope)
15771644
const result = yield* invocation.evaluateStatement(fn.body)
15781645
return result.kind === "return" ? result.value : undefined
15791646
}

0 commit comments

Comments
 (0)