fix(codemode): enforce lexical temporal dead zones (#37358)
This commit is contained in:
parent
faf964691b
commit
91238441a6
5 changed files with 431 additions and 128 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -144,6 +144,20 @@ const collectPatternNames = (pattern: AstNode, out: Array<string> = []): Array<s
|
|||
return out
|
||||
}
|
||||
|
||||
const loopDeclaration = (left: AstNode, statement: "for...of" | "for...in") => {
|
||||
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<R> {
|
||||
private scopes: ScopeStack
|
||||
private readonly invokeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>
|
||||
|
|
@ -207,6 +221,7 @@ export class Interpreter<R> {
|
|||
// 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<R> {
|
|||
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<R> {
|
|||
}
|
||||
}
|
||||
|
||||
private predeclareLexical(statements: Array<unknown>): 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<StatementResult, unknown, R> {
|
||||
const testNode = getNode(node, "test")
|
||||
const consequentNode = getNode(node, "consequent")
|
||||
|
|
@ -359,7 +394,6 @@ export class Interpreter<R> {
|
|||
|
||||
private evaluateSwitchStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||
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<R> {
|
|||
"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<StatementResult, unknown, R> {
|
||||
|
|
@ -465,6 +503,10 @@ export class Interpreter<R> {
|
|||
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<R> {
|
|||
? 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<R> {
|
|||
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<R> {
|
|||
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<R> {
|
|||
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<R> {
|
|||
}
|
||||
|
||||
if (result.kind === "break") {
|
||||
return { kind: "none" }
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}
|
||||
|
||||
if (result.kind === "continue") {
|
||||
|
|
@ -589,8 +620,14 @@ export class Interpreter<R> {
|
|||
}
|
||||
}
|
||||
|
||||
return { kind: "none" }
|
||||
})
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}).pipe(
|
||||
Effect.ensuring(
|
||||
Effect.sync(() => {
|
||||
if (declared?.lexical) self.scopes.pop()
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private enumerableKeys(value: unknown): Array<string> | undefined {
|
||||
|
|
@ -607,9 +644,13 @@ export class Interpreter<R> {
|
|||
}
|
||||
|
||||
private evaluateForInStatement(node: AstNode): Effect.Effect<StatementResult, unknown, R> {
|
||||
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<R> {
|
|||
)
|
||||
}
|
||||
|
||||
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<R> {
|
|||
}
|
||||
|
||||
if (result.kind === "break") {
|
||||
return { kind: "none" }
|
||||
return { kind: "none" } satisfies StatementResult
|
||||
}
|
||||
|
||||
if (result.kind === "continue") {
|
||||
|
|
@ -667,8 +701,14 @@ export class Interpreter<R> {
|
|||
}
|
||||
}
|
||||
|
||||
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<R> {
|
|||
|
||||
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<R> {
|
|||
value: unknown,
|
||||
mutable: boolean,
|
||||
node: AstNode,
|
||||
initialize = false,
|
||||
): Effect.Effect<void, unknown, R> {
|
||||
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<R> {
|
|||
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<R> {
|
|||
self.destructuringPropertyValue(value as SafeObject | Array<unknown>, key),
|
||||
mutable,
|
||||
property,
|
||||
initialize,
|
||||
)
|
||||
}
|
||||
return
|
||||
|
|
@ -823,10 +867,10 @@ export class Interpreter<R> {
|
|||
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<R> {
|
|||
}
|
||||
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") {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
188
packages/codemode/test/lexical-test262.test.ts
Normal file
188
packages/codemode/test/lexical-test262.test.ts
Normal file
|
|
@ -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"])
|
||||
})
|
||||
})
|
||||
|
|
@ -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 () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue