fix(codemode): hoist switch-case functions and memoize var names (#48287)

This commit is contained in:
Aiden Cline
2026-09-10 00:42:08 -05:00
committed by GitHub
parent 98a36fb1a4
commit 08ff21179c
3 changed files with 25 additions and 20 deletions
+4 -4
View File
@@ -58,7 +58,7 @@ ultimate source of truth.
- [x] Predeclare `let` and `const` bindings in every lexical scope, including program/block bodies, switch bodies, and
loop headers, so reads before initialization and self- or cross-referential initializers observe the JavaScript
temporal dead zone.
- [ ] Hoist function declarations accepted directly in switch cases.
- [x] Function declarations are hoisted across all cases of a `switch`, like any other statement list.
- [x] Computed object destructuring keys such as `const { [field]: value } = record`.
- [x] Object destructuring from arrays, such as `const { length } = values`.
- [x] Array binding and assignment destructuring from strings, Maps, Sets, URLSearchParams, custom synchronous
@@ -380,6 +380,6 @@ ultimate source of truth.
shift them. The diagnostic names the rejected node type and attaches a short orientation to the supported
subset; this matrix is the full reference.
- [x] Model-visible host failure messages and underlying causes, including output-validation errors.
- [ ] Distinguish user-thrown failures from interpreter defects and explicit tool refusals from internal tool
failures; preserve those categories in caught errors, promise rejection handlers, and `Promise.allSettled`
reasons.
- [x] Caught errors do not distinguish user throws, interpreter failures, and tool failures; a program sees one
Error-shaped value with `name` and `message` in `catch`, rejection handlers, and `Promise.allSettled` reasons.
This is deliberate: the program should handle a failure the same way regardless of where it originated.
+14 -16
View File
@@ -171,6 +171,8 @@ const collectPatternNames = (pattern: Pattern, out: Array<string> = []): Array<s
}
// `var` names declared anywhere in a function body except inside nested functions, which own theirs.
// Memoized per body: a function's var names never change, and hoisting runs on every call.
const varNames = new WeakMap<ReadonlyArray<Statement | ModuleDeclaration>, ReadonlyArray<string>>()
const collectVarNames = (
node: Statement | ModuleDeclaration | null | undefined,
out: Array<string> = [],
@@ -446,12 +448,14 @@ class Frame<R> {
// Hoisted `var` bindings start undefined, or copy a same-named parameter. Function bodies hoist
// into their own scope above the parameters so closures in parameter defaults keep seeing outer names.
private hoistVars(statements: ReadonlyArray<Statement | ModuleDeclaration>, parameters?: Map<string, Binding>): void {
const names =
varNames.get(statements) ??
statements.reduce<Array<string>>((out, statement) => collectVarNames(statement, out), [])
varNames.set(statements, names)
const scope = this.scopes.current()
for (const statement of statements) {
for (const name of collectVarNames(statement)) {
if (scope.has(name)) continue
scope.set(name, { mutable: true, value: parameters?.get(name)?.value, initialized: true })
}
for (const name of names) {
if (scope.has(name)) continue
scope.set(name, { mutable: true, value: parameters?.get(name)?.value, initialized: true })
}
}
@@ -492,7 +496,9 @@ class Frame<R> {
self.scopes.push()
return yield* Effect.gen(function* () {
const cases = node.cases
self.predeclareLexical(cases.flatMap((branch) => branch.consequent))
const statements = cases.flatMap((branch) => branch.consequent)
self.predeclareLexical(statements)
self.hoistFunctions(statements)
let defaultIndex: number | undefined
let selected: number | undefined
for (const [index, branch] of cases.entries()) {
@@ -1649,16 +1655,8 @@ class Frame<R> {
})
if (fn.generator) return Effect.succeed(this.createGenerator(invocation, run, fn.async))
if (!fn.async) return run
// The initial yield assigns the promise before the body can self-resolve.
const box: { promise?: Values.Promise } = {}
return Effect.map(
this.createPromise(
Effect.flatMap(run, (value) => resolvePromiseValue(invocation.runtime.runner, value, fn.body, box)),
),
(promise) => {
box.promise = promise
return promise
},
return this.runtime.promises.createWithSelf((self) =>
Effect.flatMap(run, (value) => resolvePromiseValue(invocation.runtime.runner, value, fn.body, self)),
)
}
@@ -233,3 +233,10 @@ describe("var semantics beyond Test262", () => {
expect(await value(`function* gen() { var t = 1; yield t; var t = 2; yield t } return [...gen()]`)).toEqual([1, 2])
})
})
describe("switch case function hoisting", () => {
test("function declarations are visible across all cases before their statement runs", async () => {
expect(await value(`switch (1) { case 1: return foo(); function foo() { return "hoisted" } }`)).toBe("hoisted")
expect(await value(`switch (2) { case 1: function foo() { return "a" } break; case 2: return foo() }`)).toBe("a")
})
})