From 91238441a6ff15666ab9ae13b531acc7ea34c8e7 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:26:45 -0500 Subject: [PATCH] fix(codemode): enforce lexical temporal dead zones (#37358) --- packages/codemode/interpreter-support.md | 2 +- packages/codemode/src/interpreter/runtime.ts | 270 ++++++++++-------- packages/codemode/src/interpreter/scope.ts | 26 +- .../codemode/test/lexical-test262.test.ts | 188 ++++++++++++ packages/codemode/test/parity.test.ts | 73 ++++- 5 files changed, 431 insertions(+), 128 deletions(-) create mode 100644 packages/codemode/test/lexical-test262.test.ts diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 70d34f2408..600d2b5b2a 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -50,7 +50,7 @@ ultimate source of truth. - [x] Direct function declarations are hoisted in program and block statement lists. - [x] Parameter defaults observe a temporal dead zone for later parameters. - [ ] JavaScript-correct function scoping, hoisting, and redeclaration for accepted `var` declarations. -- [ ] Predeclare `let` and `const` bindings in every lexical scope, including program/block bodies, switch bodies, and +- [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. diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index e4529028fb..332389d966 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -144,6 +144,20 @@ const collectPatternNames = (pattern: AstNode, out: Array = []): Array { + if (left.type !== "VariableDeclaration") return undefined + const declarations = getArray(left, "declarations") + if (declarations.length !== 1) { + throw new InterpreterRuntimeError(`${statement} supports one declared binding.`, left) + } + const kind = getString(left, "kind") + return { + pattern: getNode(asNode(declarations[0], "declarations[0]"), "id"), + mutable: kind !== "const", + lexical: kind !== "var", + } +} + export class Interpreter { private scopes: ScopeStack private readonly invokeTool: (path: ReadonlyArray, args: Array) => Effect.Effect @@ -207,6 +221,7 @@ export class Interpreter { // Keep top-level declarations separate so they can shadow builtins. this.scopes.push() return Effect.gen(function* () { + self.predeclareLexical(program.body) self.hoistFunctions(program.body) let value: unknown = undefined for (const [index, statement] of program.body.entries()) { @@ -303,6 +318,7 @@ export class Interpreter { const self = this return Effect.gen(function* () { const body = getArray(node, "body") + self.predeclareLexical(body) self.hoistFunctions(body) for (const statementValue of body) { @@ -343,6 +359,25 @@ export class Interpreter { } } + private predeclareLexical(statements: Array): void { + for (const statementValue of statements) { + if (!isRecord(statementValue) || statementValue.type !== "VariableDeclaration") continue + const statement = statementValue as AstNode + const kind = getString(statement, "kind") + if (kind === "var") continue + for (const declarationValue of getArray(statement, "declarations")) { + const declaration = asNode(declarationValue, "declarations") + for (const name of collectPatternNames(getNode(declaration, "id"))) { + this.scopes.reserve(name, kind !== "const", declaration) + } + } + } + } + + private predeclarePattern(pattern: AstNode, mutable: boolean, node: AstNode): void { + for (const name of collectPatternNames(pattern)) this.scopes.reserve(name, mutable, node) + } + private evaluateIfStatement(node: AstNode): Effect.Effect { const testNode = getNode(node, "test") const consequentNode = getNode(node, "consequent") @@ -359,7 +394,6 @@ export class Interpreter { private evaluateSwitchStatement(node: AstNode): Effect.Effect { const self = this - this.scopes.push() return Effect.gen(function* () { const discriminant = yield* self.evaluateExpression(getNode(node, "discriminant")) if (containsOpaqueReference(discriminant)) { @@ -369,39 +403,43 @@ export class Interpreter { "InvalidDataValue", ) } - const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`)) - let defaultIndex: number | undefined - let selected: number | undefined - for (const [index, branch] of cases.entries()) { - const test = getOptionalNode(branch, "test") - if (!test) { - defaultIndex = index - continue + self.scopes.push() + return yield* Effect.gen(function* () { + const cases = getArray(node, "cases").map((value, index) => asNode(value, `cases[${index}]`)) + self.predeclareLexical(cases.flatMap((branch) => getArray(branch, "consequent"))) + let defaultIndex: number | undefined + let selected: number | undefined + for (const [index, branch] of cases.entries()) { + const test = getOptionalNode(branch, "test") + if (!test) { + defaultIndex = index + continue + } + const candidate = yield* self.evaluateExpression(test) + if (containsOpaqueReference(candidate)) { + throw new InterpreterRuntimeError( + "Switch case values must be data values in CodeMode.", + test, + "InvalidDataValue", + ) + } + if (candidate === discriminant) { + selected = index + break + } } - const candidate = yield* self.evaluateExpression(test) - if (containsOpaqueReference(candidate)) { - throw new InterpreterRuntimeError( - "Switch case values must be data values in CodeMode.", - test, - "InvalidDataValue", - ) + const start = selected ?? defaultIndex + if (start === undefined) return { kind: "none" } satisfies StatementResult + for (let index = start; index < cases.length; index += 1) { + for (const statementValue of getArray(cases[index]!, "consequent")) { + const result = yield* self.evaluateStatement(asNode(statementValue, "consequent")) + if (result.kind === "break") return { kind: "none" } satisfies StatementResult + if (result.kind === "return" || result.kind === "continue") return result + } } - if (candidate === discriminant) { - selected = index - break - } - } - const start = selected ?? defaultIndex - if (start === undefined) return { kind: "none" } satisfies StatementResult - for (let index = start; index < cases.length; index += 1) { - for (const statementValue of getArray(cases[index]!, "consequent")) { - const result = yield* self.evaluateStatement(asNode(statementValue, "consequent")) - if (result.kind === "break") return { kind: "none" } satisfies StatementResult - if (result.kind === "return" || result.kind === "continue") return result - } - } - return { kind: "none" } satisfies StatementResult - }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) + return { kind: "none" } satisfies StatementResult + }).pipe(Effect.ensuring(Effect.sync(() => self.scopes.pop()))) + }) } private evaluateWhileStatement(node: AstNode): Effect.Effect { @@ -465,6 +503,10 @@ export class Interpreter { const updateNode = getOptionalNode(node, "update") const bodyNode = getNode(node, "body") + if (initNode?.type === "VariableDeclaration" && getString(initNode, "kind") !== "var") { + self.predeclareLexical([initNode]) + } + if (initNode) { if (initNode.type === "VariableDeclaration") { yield* self.evaluateVariableDeclaration(initNode) @@ -478,21 +520,18 @@ export class Interpreter { ? Array.from(self.scopes.current().keys()) : [] - while (testNode ? yield* self.evaluateExpression(testNode) : true) { - const iterationScope = - perIterationBindings.length > 0 - ? new Map( - perIterationBindings.map((name): [string, Binding] => [name, { ...self.scopes.current().get(name)! }]), - ) - : undefined - if (iterationScope) self.scopes.push(iterationScope) - const result = yield* self.evaluateStatement(bodyNode).pipe( - Effect.ensuring( - Effect.sync(() => { - if (iterationScope) self.scopes.pop() - }), - ), + const nextIteration = () => { + if (perIterationBindings.length === 0) return + const current = self.scopes.current() + self.scopes.pop() + self.scopes.push( + new Map(perIterationBindings.map((name): [string, Binding] => [name, { ...current.get(name)! }])), ) + } + nextIteration() + + while (testNode ? yield* self.evaluateExpression(testNode) : true) { + const result = yield* self.evaluateStatement(bodyNode) if (result.kind === "return") { return result @@ -502,13 +541,7 @@ export class Interpreter { return { kind: "none" } satisfies StatementResult } - if (iterationScope) { - const loopScope = self.scopes.current() - for (const name of perIterationBindings) { - loopScope.set(name, { ...iterationScope.get(name)! }) - } - } - + nextIteration() if (updateNode) { yield* self.evaluateExpression(updateNode) } @@ -527,9 +560,13 @@ export class Interpreter { throw new InterpreterRuntimeError("for await...of is not supported.", node) } + const left = getNode(node, "left") + const declared = loopDeclaration(left, "for...of") + if (declared?.lexical) this.scopes.push() + const self = this return Effect.gen(function* () { - const left = getNode(node, "left") + if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) const right = yield* self.evaluateExpression(getNode(node, "right")) const body = getNode(node, "body") @@ -538,40 +575,34 @@ export class Interpreter { throw new InterpreterRuntimeError("for...of requires an array, string, Map, or Set value in CodeMode.", node) } - let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined let assignment: AstNode | undefined - if (left.type === "VariableDeclaration") { - const declarations = getArray(left, "declarations") - if (declarations.length !== 1) { - throw new InterpreterRuntimeError("for...of supports one declared binding.", left) - } - - const declarator = asNode(declarations[0], "declarations[0]") - declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" } - } else if ( - left.type === "Identifier" || - left.type === "MemberExpression" || - left.type === "ArrayPattern" || - left.type === "ObjectPattern" + if ( + left.type !== "VariableDeclaration" && + (left.type === "Identifier" || + left.type === "MemberExpression" || + left.type === "ArrayPattern" || + left.type === "ObjectPattern") ) { assignment = left - } else { + } else if (left.type !== "VariableDeclaration") { throw new InterpreterRuntimeError("Unsupported for...of binding.", left) } for (const value of iterable) { - if (declaration) { - self.scopes.push() - yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left) - } else if (assignment) { - yield* self.assignPattern(assignment, value, left) - } - - const result = yield* self.evaluateStatement(body).pipe( + const result = yield* Effect.gen(function* () { + if (declared) { + self.scopes.push() + if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) + yield* self.declarePattern(declared.pattern, value, declared.mutable, left, declared.lexical) + } else if (assignment) { + yield* self.assignPattern(assignment, value, left) + } + return yield* self.evaluateStatement(body) + }).pipe( Effect.ensuring( Effect.sync(() => { - if (declaration) self.scopes.pop() + if (declared) self.scopes.pop() }), ), ) @@ -581,7 +612,7 @@ export class Interpreter { } if (result.kind === "break") { - return { kind: "none" } + return { kind: "none" } satisfies StatementResult } if (result.kind === "continue") { @@ -589,8 +620,14 @@ export class Interpreter { } } - return { kind: "none" } - }) + return { kind: "none" } satisfies StatementResult + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (declared?.lexical) self.scopes.pop() + }), + ), + ) } private enumerableKeys(value: unknown): Array | undefined { @@ -607,9 +644,13 @@ export class Interpreter { } private evaluateForInStatement(node: AstNode): Effect.Effect { + const left = getNode(node, "left") + const declared = loopDeclaration(left, "for...in") + if (declared?.lexical) this.scopes.push() + const self = this return Effect.gen(function* () { - const left = getNode(node, "left") + if (declared?.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) const right = yield* self.evaluateExpression(getNode(node, "right")) const body = getNode(node, "body") @@ -621,35 +662,28 @@ export class Interpreter { ) } - let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined let assignmentName: string | undefined - if (left.type === "VariableDeclaration") { - const declarations = getArray(left, "declarations") - if (declarations.length !== 1) { - throw new InterpreterRuntimeError("for...in supports one declared binding.", left) - } - - const declarator = asNode(declarations[0], "declarations[0]") - declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" } - } else if (left.type === "Identifier") { + if (left.type === "Identifier") { assignmentName = getString(left, "name") - } else { + } else if (left.type !== "VariableDeclaration") { throw new InterpreterRuntimeError("Unsupported for...in binding.", left) } for (const key of keys) { - if (declaration) { - self.scopes.push() - yield* self.declarePattern(declaration.pattern, key, declaration.mutable, left) - } else if (assignmentName) { - self.scopes.set(assignmentName, key, left) - } - - const result = yield* self.evaluateStatement(body).pipe( + const result = yield* Effect.gen(function* () { + if (declared) { + self.scopes.push() + if (declared.lexical) self.predeclarePattern(declared.pattern, declared.mutable, left) + yield* self.declarePattern(declared.pattern, key, declared.mutable, left, declared.lexical) + } else if (assignmentName) { + self.scopes.set(assignmentName, key, left) + } + return yield* self.evaluateStatement(body) + }).pipe( Effect.ensuring( Effect.sync(() => { - if (declaration) self.scopes.pop() + if (declared) self.scopes.pop() }), ), ) @@ -659,7 +693,7 @@ export class Interpreter { } if (result.kind === "break") { - return { kind: "none" } + return { kind: "none" } satisfies StatementResult } if (result.kind === "continue") { @@ -667,8 +701,14 @@ export class Interpreter { } } - return { kind: "none" } - }) + return { kind: "none" } satisfies StatementResult + }).pipe( + Effect.ensuring( + Effect.sync(() => { + if (declared?.lexical) self.scopes.pop() + }), + ), + ) } private evaluateBreakStatement(node: AstNode): StatementResult { @@ -752,7 +792,7 @@ export class Interpreter { const init = getOptionalNode(declaration, "init") const value = init ? yield* self.evaluateExpression(init) : undefined - yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration) + yield* self.declarePattern(getNode(declaration, "id"), value, kind !== "const", declaration, kind !== "var") } }) } @@ -762,17 +802,20 @@ export class Interpreter { value: unknown, mutable: boolean, node: AstNode, + initialize = false, ): Effect.Effect { const self = this return Effect.gen(function* () { if (pattern.type === "Identifier") { - self.scopes.declare(getString(pattern, "name"), value, mutable, node) + const name = getString(pattern, "name") + if (initialize) self.scopes.initialize(name, value, node) + else self.scopes.declare(name, value, mutable, node) return } if (pattern.type === "AssignmentPattern") { const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value - yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node) + yield* self.declarePattern(getNode(pattern, "left"), resolved, mutable, node, initialize) return } @@ -794,7 +837,7 @@ export class Interpreter { for (const [key, item] of Object.entries(value as SafeObject)) { if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item } - yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property) + yield* self.declarePattern(getNode(property, "argument"), rest, mutable, property, initialize) continue } @@ -808,6 +851,7 @@ export class Interpreter { self.destructuringPropertyValue(value as SafeObject | Array, key), mutable, property, + initialize, ) } return @@ -823,10 +867,10 @@ export class Interpreter { if (item === null) continue const element = asNode(item, `elements[${index}]`) if (element.type === "RestElement") { - yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element) + yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element, initialize) break } - yield* self.declarePattern(element, items[index], mutable, pattern) + yield* self.declarePattern(element, items[index], mutable, pattern, initialize) } return } @@ -1577,10 +1621,10 @@ export class Interpreter { } for (const [index, parameter] of fn.parameters.entries()) { if (parameter.type === "RestElement") { - yield* invocation.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter) + yield* invocation.declarePattern(getNode(parameter, "argument"), args.slice(index), true, parameter, true) break } - yield* invocation.declarePattern(parameter, args[index], true, parameter) + yield* invocation.declarePattern(parameter, args[index], true, parameter, true) } if (fn.body.type === "BlockStatement") { diff --git a/packages/codemode/src/interpreter/scope.ts b/packages/codemode/src/interpreter/scope.ts index f5ee137cbc..ae3257de3f 100644 --- a/packages/codemode/src/interpreter/scope.ts +++ b/packages/codemode/src/interpreter/scope.ts @@ -7,14 +7,28 @@ export class ScopeStack { this.scopes = scopes } - declare(name: string, value: unknown, mutable: boolean, node: AstNode): void { + reserve(name: string, mutable: boolean, node: AstNode): void { const scope = this.current() - - const existing = scope.get(name) - if (existing && existing.initialized !== false) { + if (scope.has(name)) { throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node) } + scope.set(name, { mutable, value: undefined, initialized: false }) + } + initialize(name: string, value: unknown, node: AstNode): void { + const binding = this.current().get(name) + if (!binding || binding.initialized !== false) { + throw new InterpreterRuntimeError(`Identifier '${name}' has not been reserved for initialization.`, node) + } + binding.value = value + binding.initialized = true + } + + declare(name: string, value: unknown, mutable: boolean, node: AstNode): void { + const scope = this.current() + if (scope.has(name)) { + throw new InterpreterRuntimeError(`Identifier '${name}' has already been declared.`, node) + } scope.set(name, { mutable, value, initialized: true }) } @@ -39,6 +53,10 @@ export class ScopeStack { throw new InterpreterRuntimeError(`Unknown identifier '${name}'.`, node).as("ReferenceError") } + if (binding.initialized === false) { + throw new InterpreterRuntimeError(`Cannot access '${name}' before initialization.`, node).as("ReferenceError") + } + if (!binding.mutable) { throw new InterpreterRuntimeError(`Cannot assign to constant '${name}'.`, node).as("TypeError") } diff --git a/packages/codemode/test/lexical-test262.test.ts b/packages/codemode/test/lexical-test262.test.ts new file mode 100644 index 0000000000..6f67a9acb9 --- /dev/null +++ b/packages/codemode/test/lexical-test262.test.ts @@ -0,0 +1,188 @@ +/* + * Portions adapted from Test262 at revision 250f204f23a9249ff204be2baec29600faae7b75: + * - test/language/statements/let/global-use-before-initialization-in-prior-statement.js + * - test/language/statements/let/block-local-use-before-initialization-in-prior-statement.js + * - test/language/statements/const/global-use-before-initialization-in-prior-statement.js + * - test/language/statements/const/block-local-use-before-initialization-in-prior-statement.js + * - test/language/statements/let/block-local-use-before-initialization-in-declaration-statement.js + * - test/language/statements/const/block-local-use-before-initialization-in-declaration-statement.js + * - test/language/statements/let/block-local-closure-set-before-initialization.js + * - test/language/statements/for-of/head-let-bound-names-fordecl-tdz.js + * - test/language/statements/for-in/head-let-bound-names-fordecl-tdz.js + * - test/language/statements/let/syntax/let-iteration-variable-is-freshly-allocated-for-each-iteration-single-let-binding.js + * - test/language/statements/let/syntax/let-iteration-variable-is-freshly-allocated-for-each-iteration-multi-let-binding.js + * - test/language/statements/for-of/head-let-fresh-binding-per-iteration.js + * - test/language/statements/for-in/head-let-fresh-binding-per-iteration.js + * - test/language/statements/for/scope-head-lex-open.js + * - test/language/statements/for/scope-body-lex-open.js + * - test/language/statements/switch/scope-lex-open-case.js + * - test/language/statements/switch/scope-lex-close-case.js + * - test/language/statements/function/dflt-params-ref-prior.js + * - test/language/statements/function/dflt-params-ref-later.js + * - test/language/statements/function/dflt-params-ref-self.js + * + * Copyright (C) 2011, 2014, 2016 the V8 project authors. All rights reserved. + * Test262 portions are governed by the BSD license in LICENSE.test262. + */ +import { describe, expect, test } from "bun:test" +import { Effect } from "effect" +import { CodeMode } from "../src/index.js" + +const value = async (code: string) => { + const result = await Effect.runPromise(CodeMode.execute({ code, tools: {} })) + if (!result.ok) throw new Error(`expected success, got ${result.error.kind}: ${result.error.message}`) + return result.value +} + +describe("Test262 lexical temporal dead zones", () => { + test("program and block bindings exist before initialization", async () => { + expect( + await value(` + const errors = [] + try { programLet; let programLet } catch (error) { errors.push(error.name) } + try { programConst; const programConst = 1 } catch (error) { errors.push(error.name) } + try { { blockLet; let blockLet } } catch (error) { errors.push(error.name) } + try { { blockConst; const blockConst = 1 } } catch (error) { errors.push(error.name) } + return errors + `), + ).toEqual(["ReferenceError", "ReferenceError", "ReferenceError", "ReferenceError"]) + }) + + test("self-initialization and closure assignment observe the TDZ", async () => { + expect( + await value(` + const errors = [] + try { { let item = item + 1 } } catch (error) { errors.push(error.name) } + try { { const item = item + 1 } } catch (error) { errors.push(error.name) } + try { + { + function assign() { item = 1 } + assign() + let item + } + } catch (error) { errors.push(error.name) } + return errors + `), + ).toEqual(["ReferenceError", "ReferenceError", "ReferenceError"]) + }) + + test("for-of and for-in bound names are in the head TDZ", async () => { + expect( + await value(` + const errors = [] + try { let item = [1]; for (let item of item) {} } catch (error) { errors.push(error.name) } + try { let item = { value: 1 }; for (let item in item) {} } catch (error) { errors.push(error.name) } + return errors + `), + ).toEqual(["ReferenceError", "ReferenceError"]) + }) +}) + +describe("Test262 lexical loop environments", () => { + test("classic for creates fresh single and multiple bindings", async () => { + expect( + await value(` + const single = [] + for (let index = 0; index < 5; ++index) single.push(() => index) + + const left = [] + const right = [] + for (let first = 0, second = 10; first < 5; ++first, ++second) { + left.push(() => first) + right.push(() => second) + } + return [ + single.map((read) => read()), + left.map((read) => read()), + right.map((read) => read()), + ] + `), + ).toEqual([ + [0, 1, 2, 3, 4], + [0, 1, 2, 3, 4], + [10, 11, 12, 13, 14], + ]) + }) + + test("for-of and for-in create fresh bindings", async () => { + expect( + await value(` + const values = [] + for (let item of [1, 2, 3]) values.push(() => item) + + const keys = {} + for (let key in { first: 1, second: 2, third: 3 }) keys[key] = () => key + return [values.map((read) => read()), keys.first(), keys.second(), keys.third()] + `), + ).toEqual([[1, 2, 3], "first", "second", "third"]) + }) + + test("classic for separates declaration and per-iteration environments", async () => { + expect( + await value(` + let before + let testRead + let bodyRead + let updateRead + let run = true + for ( + let item = "outside", ignored = before = () => item; + run && (item = "inside", testRead = () => item); + updateRead = () => item + ) bodyRead = () => item, run = false + return [before(), testRead(), bodyRead(), updateRead()] + `), + ).toEqual(["outside", "inside", "inside", "inside"]) + }) +}) + +describe("Test262 switch and parameter environments", () => { + test("switch creates its lexical environment after the discriminant", async () => { + expect( + await value(` + let item = "outside" + let discriminantRead + let selectorRead + let statementRead + switch ((discriminantRead = () => item, null)) { + case (selectorRead = () => item, null): + statementRead = () => item + let item = "inside" + } + return [discriminantRead(), selectorRead(), statementRead()] + `), + ).toEqual(["outside", "inside", "inside"]) + }) + + test("all switch cases share one lexical environment that closes afterward", async () => { + expect( + await value(` + let item = "outside" + let firstRead + let secondRead + switch (null) { + case null: + let item = "inside" + firstRead = () => item + case null: + secondRead = () => item + } + return [firstRead(), secondRead(), item] + `), + ).toEqual(["inside", "inside", "outside"]) + }) + + test("parameter defaults see prior bindings but not self or later bindings", async () => { + expect( + await value(` + function prior(first, second = first, third = second) { return [first, second, third] } + function later(first = second, second) { return first } + function self(item = item) { return item } + function failure(run) { + try { return run() } catch (error) { return error.name } + } + return [prior(3), failure(later), failure(self)] + `), + ).toEqual([[3, 3, 3], "ReferenceError", "ReferenceError"]) + }) +}) diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 0326c13c54..7ab0fbf348 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -99,12 +99,66 @@ describe("H4: typeof on an undeclared identifier is 'undefined'", () => { }) }) +describe("CodeMode lexical scope integration", () => { + test("keeps self, cross, and destructuring defaults in the TDZ", async () => { + expect( + await value(` + const outer = 1 + const errors = [] + try { const first = second, second = 2 } catch (error) { errors.push(error.name) } + try { const [first = second, second = 2] = [] } catch (error) { errors.push(error.name) } + return errors + `), + ).toEqual(["ReferenceError", "ReferenceError"]) + }) + + test("keeps typeof and constant assignment inside the TDZ", async () => { + expect( + await value(` + const errors = [] + try { { errors.push(typeof item); let item } } catch (error) { errors.push(error.name) } + try { { constant = 1; const constant = 2 } } catch (error) { errors.push(error.name) } + return errors + `), + ).toEqual(["ReferenceError", "ReferenceError"]) + }) + + test("shadows builtins from the start of the program scope", async () => { + expect( + await value(` + let observed + try { observed = typeof Promise } catch (error) { observed = error.name } + const Promise = 1 + return observed + `), + ).toBe("ReferenceError") + }) + + test("keeps classic for initializers inside the header TDZ", async () => { + expect( + await value(` + let index = 1 + try { for (let index = index; index < 2; index++) {} } catch (error) { return error.name } + `), + ).toBe("ReferenceError") + }) + + test("removes loop scopes when per-iteration initialization fails", async () => { + expect( + await value(` + const value = "outer" + try { for (let [value] of [1]) {} } catch {} + return value + `), + ).toBe("outer") + }) +}) + describe("unary void", () => { test("evaluates its operand and returns undefined", async () => { - expect(await value(`let count = 0; const result = void (count += 1); return [count, result === undefined]`)).toEqual([ - 1, - true, - ]) + expect( + await value(`let count = 0; const result = void (count += 1); return [count, result === undefined]`), + ).toEqual([1, true]) }) test("discards opaque values", async () => { @@ -137,12 +191,11 @@ describe("property deletion", () => { }) test("deleting an array index creates a hole without changing its length", async () => { - expect(await value(`const values = [1, 2, 3]; const removed = delete values[1]; return [removed, values.length, 1 in values, values]`)).toEqual([ - true, - 3, - false, - [1, null, 3], - ]) + expect( + await value( + `const values = [1, 2, 3]; const removed = delete values[1]; return [removed, values.length, 1 in values, values]`, + ), + ).toEqual([true, 3, false, [1, null, 3]]) }) test("array length is not configurable", async () => {