diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index 403edd34f5..f34e3949e1 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -1403,6 +1403,84 @@ class Interpreter { }) } + private assignPattern(pattern: AstNode, value: unknown, node: AstNode): Effect.Effect { + const self = this + return Effect.gen(function* () { + if (pattern.type === "Identifier") { + self.setIdentifierValue(getString(pattern, "name"), value, pattern) + return + } + + if (pattern.type === "MemberExpression") { + yield* self.writeMember(pattern, value) + return + } + + if (pattern.type === "AssignmentPattern") { + const resolved = value === undefined ? yield* self.evaluateExpression(getNode(pattern, "right")) : value + yield* self.assignPattern(getNode(pattern, "left"), resolved, node) + return + } + + if (pattern.type === "ObjectPattern") { + if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) { + throw new InterpreterRuntimeError( + "Object destructuring requires a data object value.", + pattern, + "InvalidDataValue", + ) + } + + const source = value as SafeObject + const consumed = new Set() + for (const propertyValue of getArray(pattern, "properties")) { + const property = asNode(propertyValue, "properties") + if (property.type === "RestElement") { + const rest: SafeObject = Object.create(null) as SafeObject + for (const [key, item] of Object.entries(source)) { + if (!consumed.has(key) && !isBlockedMember(key)) rest[key] = item + } + yield* self.assignPattern(getNode(property, "argument"), rest, property) + continue + } + if ( + property.type !== "Property" || + getBoolean(property, "computed") || + getString(property, "kind") !== "init" + ) { + throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property) + } + const keyNode = getNode(property, "key") + const key = keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value) + if (isBlockedMember(key)) { + throw new InterpreterRuntimeError(`Property '${key}' is not available in CodeMode.`, keyNode) + } + consumed.add(key) + yield* self.assignPattern(getNode(property, "value"), source[key], property) + } + return + } + + if (pattern.type === "ArrayPattern") { + if (!Array.isArray(value)) { + throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern) + } + for (const [index, item] of getArray(pattern, "elements").entries()) { + if (item === null) continue + const element = asNode(item, `elements[${index}]`) + if (element.type === "RestElement") { + yield* self.assignPattern(getNode(element, "argument"), value.slice(index), element) + break + } + yield* self.assignPattern(element, value[index], pattern) + } + return + } + + throw new InterpreterRuntimeError(`Unsupported assignment pattern '${pattern.type}'.`, node) + }) + } + private evaluateExpression(node: AstNode): Effect.Effect { switch (node.type) { case "Literal": { @@ -1804,6 +1882,11 @@ class Interpreter { if (operator === "??=" || operator === "||=" || operator === "&&=") { return yield* self.evaluateLogicalAssignment(node, left, operator) } + if (operator === "=" && (left.type === "ObjectPattern" || left.type === "ArrayPattern")) { + const rightValue = yield* self.evaluateExpression(getNode(node, "right")) + yield* self.assignPattern(left, rightValue, node) + return rightValue + } if (left.type === "Identifier") { const name = getString(left, "name") if (operator !== "=") { diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index 39f2ba02af..1831a7222d 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -463,3 +463,38 @@ describe("H5: builtin coercion functions work as array callbacks", () => { expect(err.message).toContain("callback") }) }) + +describe("destructuring assignment", () => { + test("assigns object and array patterns to existing bindings", async () => { + expect( + await value(` + let a = 0 + let b = 0 + ;({ a } = { a: 2 }) + ;[a, b] = [3, 4] + return [a, b] + `), + ).toEqual([3, 4]) + }) + + test("supports defaults, nesting, rest, and member targets", async () => { + expect( + await value(` + let first = 0 + let fallback = 0 + let rest = {} + const target = {} + ;[first, fallback = 2, ...target.tail] = [1] + ;({ nested: { value: target.value }, kept: target.kept = 3, ...rest } = { + nested: { value: 4 }, + extra: 5, + }) + return { first, fallback, target, rest } + `), + ).toEqual({ first: 1, fallback: 2, target: { tail: [], value: 4, kept: 3 }, rest: { extra: 5 } }) + }) + + test("returns the assigned value", async () => { + expect(await value(`let a = 0; const result = ([a] = [7]); return [a, result]`)).toEqual([7, [7]]) + }) +})