diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 84f4d7c53f7..2825e744b5f 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -1088,7 +1088,7 @@ class Interpreter { } let declaration: { readonly pattern: AstNode; readonly mutable: boolean } | undefined - let assignmentName: string | undefined + let assignment: AstNode | undefined if (left.type === "VariableDeclaration") { const declarations = getArray(left, "declarations") @@ -1098,8 +1098,13 @@ class Interpreter { const declarator = asNode(declarations[0], "declarations[0]") declaration = { pattern: getNode(declarator, "id"), mutable: getString(left, "kind") !== "const" } - } else if (left.type === "Identifier") { - assignmentName = getString(left, "name") + } else if ( + left.type === "Identifier" || + left.type === "MemberExpression" || + left.type === "ArrayPattern" || + left.type === "ObjectPattern" + ) { + assignment = left } else { throw new InterpreterRuntimeError("Unsupported for...of binding.", left) } @@ -1108,8 +1113,8 @@ class Interpreter { if (declaration) { self.pushScope() yield* self.declarePattern(declaration.pattern, value, declaration.mutable, left) - } else if (assignmentName) { - self.setIdentifierValue(assignmentName, value, left) + } else if (assignment) { + yield* self.assignPattern(assignment, value, left) } const result = yield* self.evaluateStatement(body).pipe( @@ -1507,6 +1512,16 @@ class Interpreter { return this.evaluateUnaryExpression(node) case "AssignmentExpression": return this.evaluateAssignmentExpression(node) + case "SequenceExpression": { + const self = this + return Effect.gen(function* () { + let result: unknown + for (const expression of getArray(node, "expressions")) { + result = yield* self.evaluateExpression(asNode(expression, "expressions")) + } + return result + }) + } case "CallExpression": return this.evaluateCallExpression(node) case "ArrowFunctionExpression": diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 1831a7222df..5593831127a 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -464,6 +464,54 @@ describe("H5: builtin coercion functions work as array callbacks", () => { }) }) +describe("for...of assignment destructuring", () => { + test("assigns entry pairs into predeclared variables", async () => { + expect( + await value(` + let key + let item + const out = [] + for ([key, item] of Object.entries({ a: 1, b: 2 })) out.push(key + item) + return { key, item, out } + `), + ).toEqual({ key: "b", item: 2, out: ["a1", "b2"] }) + }) + + test("assigns object patterns and defaults", async () => { + expect( + await value(` + let id + let label + const labels = [] + for ({ id, label = "unknown" } of [{ id: 1 }, { id: 2, label: "two" }]) labels.push(label) + return { id, label, labels } + `), + ).toEqual({ id: 2, label: "two", labels: ["unknown", "two"] }) + }) +}) + +describe("sequence expressions", () => { + test("evaluate left to right and return the final value", async () => { + expect(await value(`let x = 0; const result = (x += 1, x *= 3, x + 2); return { x, result }`)).toEqual({ + x: 3, + result: 5, + }) + }) + + test("support comma-separated for-loop updates", async () => { + expect( + await value(` + const pairs = [] + for (let left = 0, right = 3; left < right; left++, right--) pairs.push([left, right]) + return pairs + `), + ).toEqual([ + [0, 3], + [1, 2], + ]) + }) +}) + describe("destructuring assignment", () => { test("assigns object and array patterns to existing bindings", async () => { expect(