From 08ff21179c28efcb22f2115cdf25396ef1a316ef Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 10 Sep 2026 00:42:08 -0500 Subject: [PATCH] fix(codemode): hoist switch-case functions and memoize var names (#48287) --- packages/codemode/interpreter-support.md | 8 ++--- packages/codemode/src/interpreter/runtime.ts | 30 +++++++++---------- .../test/var-hoisting-test262.test.ts | 7 +++++ 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 3a6e2d69bfb..cb50b25f358 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -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. diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 166b028dadd..8fed3a5b071 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -171,6 +171,8 @@ const collectPatternNames = (pattern: Pattern, out: Array = []): Array, ReadonlyArray>() const collectVarNames = ( node: Statement | ModuleDeclaration | null | undefined, out: Array = [], @@ -446,12 +448,14 @@ class Frame { // 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, parameters?: Map): void { + const names = + varNames.get(statements) ?? + statements.reduce>((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 { 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 { }) 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)), ) } diff --git a/packages/codemode/test/var-hoisting-test262.test.ts b/packages/codemode/test/var-hoisting-test262.test.ts index ec936111705..24d3b00b41f 100644 --- a/packages/codemode/test/var-hoisting-test262.test.ts +++ b/packages/codemode/test/var-hoisting-test262.test.ts @@ -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") + }) +})