diff --git a/packages/codemode/interpreter-support.md b/packages/codemode/interpreter-support.md index 2164e54569..70d34f2408 100644 --- a/packages/codemode/interpreter-support.md +++ b/packages/codemode/interpreter-support.md @@ -54,9 +54,9 @@ ultimate source of truth. 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. -- [ ] Computed object destructuring keys such as `const { [field]: value } = record`. -- [ ] Object destructuring from arrays, such as `const { length } = values`. -- [ ] Array destructuring from supported non-array iterables: strings, Maps, Sets, and URLSearchParams. +- [x] Computed object destructuring keys such as `const { [field]: value } = record`. +- [x] Object destructuring from arrays, such as `const { length } = values`. +- [x] Array destructuring from supported non-array iterables: strings, Maps, Sets, and URLSearchParams. ## Statements and control flow diff --git a/packages/codemode/src/interpreter/runtime.ts b/packages/codemode/src/interpreter/runtime.ts index e21fa37f9d..e4529028fb 100644 --- a/packages/codemode/src/interpreter/runtime.ts +++ b/packages/codemode/src/interpreter/runtime.ts @@ -777,9 +777,9 @@ export class Interpreter { } if (pattern.type === "ObjectPattern") { - if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) { + if (value === null || typeof value !== "object" || isRuntimeReference(value)) { throw new InterpreterRuntimeError( - "Object destructuring requires a data object value.", + "Object destructuring requires a data object or array value.", pattern, "InvalidDataValue", ) @@ -798,38 +798,35 @@ export class Interpreter { continue } - if ( - property.type !== "Property" || - getBoolean(property, "computed") || - getString(property, "kind") !== "init" - ) { - throw new InterpreterRuntimeError("Only named object destructuring properties are supported.", property) + const key = yield* self.destructuringPropertyKey(property) + if (isBlockedMember(String(key))) { + throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, 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.declarePattern(getNode(property, "value"), (value as SafeObject)[key], mutable, property) + consumed.add(String(key)) + yield* self.declarePattern( + getNode(property, "value"), + self.destructuringPropertyValue(value as SafeObject | Array, key), + mutable, + property, + ) } return } if (pattern.type === "ArrayPattern") { - if (!Array.isArray(value)) { - throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern) + const items = spreadItems(value) + if (items === undefined) { + throw new InterpreterRuntimeError("Array destructuring requires a supported iterable 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.declarePattern(getNode(element, "argument"), value.slice(index), mutable, element) + yield* self.declarePattern(getNode(element, "argument"), items.slice(index), mutable, element) break } - yield* self.declarePattern(element, value[index], mutable, pattern) + yield* self.declarePattern(element, items[index], mutable, pattern) } return } @@ -858,15 +855,15 @@ export class Interpreter { } if (pattern.type === "ObjectPattern") { - if (value === null || typeof value !== "object" || Array.isArray(value) || isRuntimeReference(value)) { + if (value === null || typeof value !== "object" || isRuntimeReference(value)) { throw new InterpreterRuntimeError( - "Object destructuring requires a data object value.", + "Object destructuring requires a data object or array value.", pattern, "InvalidDataValue", ) } - const source = value as SafeObject + const source = value as SafeObject | Array const consumed = new Set() for (const propertyValue of getArray(pattern, "properties")) { const property = asNode(propertyValue, "properties") @@ -878,36 +875,29 @@ export class Interpreter { 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 key = yield* self.destructuringPropertyKey(property) + if (isBlockedMember(String(key))) { + throw new InterpreterRuntimeError(`Property '${String(key)}' is not available in CodeMode.`, 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) + consumed.add(String(key)) + yield* self.assignPattern(getNode(property, "value"), self.destructuringPropertyValue(source, key), property) } return } if (pattern.type === "ArrayPattern") { - if (!Array.isArray(value)) { - throw new InterpreterRuntimeError("Array destructuring requires an array value.", pattern) + const items = spreadItems(value) + if (items === undefined) { + throw new InterpreterRuntimeError("Array destructuring requires a supported iterable 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) + yield* self.assignPattern(getNode(element, "argument"), items.slice(index), element) break } - yield* self.assignPattern(element, value[index], pattern) + yield* self.assignPattern(element, items[index], pattern) } return } @@ -916,6 +906,26 @@ export class Interpreter { }) } + private destructuringPropertyKey(property: AstNode): Effect.Effect { + if (property.type !== "Property" || getString(property, "kind") !== "init") { + throw new InterpreterRuntimeError("Unsupported object destructuring property.", property) + } + const keyNode = getNode(property, "key") + if (getBoolean(property, "computed")) { + return Effect.map(this.evaluateExpression(keyNode), (value) => this.toPropertyKey(value, keyNode)) + } + return Effect.succeed(keyNode.type === "Identifier" ? getString(keyNode, "name") : String(keyNode.value)) + } + + private destructuringPropertyValue(source: SafeObject | Array, key: string | number): unknown { + if (!Array.isArray(source)) return source[String(key)] + if (key === "length") return source.length + if (typeof key === "number") return source[key] + if (Object.hasOwn(source, key)) return (source as Record & Array)[key] + if (arrayMethods.has(key)) return new IntrinsicReference(source, key) + return undefined + } + private evaluateExpression(node: AstNode): Effect.Effect { switch (node.type) { case "Literal": { diff --git a/packages/codemode/test/parity.test.ts b/packages/codemode/test/parity.test.ts index ac19dcc3f7..0326c13c54 100644 --- a/packages/codemode/test/parity.test.ts +++ b/packages/codemode/test/parity.test.ts @@ -548,4 +548,71 @@ describe("destructuring assignment", () => { test("returns the assigned value", async () => { expect(await value(`let a = 0; const result = ([a] = [7]); return [a, result]`)).toEqual([7, [7]]) }) + + test("supports computed object keys and evaluates them once", async () => { + expect( + await value(` + let calls = 0 + const field = () => { calls++; return "name" } + const { [field()]: name, ...rest } = { name: "Ada", role: "engineer" } + return { calls, name, rest } + `), + ).toEqual({ calls: 1, name: "Ada", rest: { role: "engineer" } }) + }) + + test("supports object patterns over arrays", async () => { + expect( + await value(` + const { 0: first, length, slice, ...rest } = ["a", "b", "c"] + return { first, length, sliced: slice(1), rest } + `), + ).toEqual({ first: "a", length: 3, sliced: ["b", "c"], rest: { 1: "b", 2: "c" } }) + }) + + test("preserves exact computed property names on arrays", async () => { + expect( + await value(` + const { ["01"]: item, ...rest } = [10, 20] + return { missing: item === undefined, rest } + `), + ).toEqual({ missing: true, rest: { 0: 10, 1: 20 } }) + }) + + test("supports array patterns over strings, Maps, Sets, and URLSearchParams", async () => { + expect( + await value(` + const [letter, ...letters] = "A😀B" + const [[mapKey, mapValue]] = new Map([["key", 1]]) + const [setFirst, setSecond] = new Set([2, 3]) + const [[queryKey, queryValue]] = new URLSearchParams("q=test&page=2") + return { letter, letters, mapKey, mapValue, setFirst, setSecond, queryKey, queryValue } + `), + ).toEqual({ + letter: "A", + letters: ["😀", "B"], + mapKey: "key", + mapValue: 1, + setFirst: 2, + setSecond: 3, + queryKey: "q", + queryValue: "test", + }) + }) + + test("supports iterable patterns in assignment and parameters", async () => { + expect( + await value(` + let first + let rest + ;[first, ...rest] = new Set([1, 2, 3]) + const read = ([[key, value]]) => key + value + return { first, rest, entry: read(new Map([["a", 4]])) } + `), + ).toEqual({ first: 1, rest: [2, 3], entry: "a4" }) + }) + + test("rejects computed keys that are not confined property keys", async () => { + const err = await error(`const key = {}; const { [key]: value } = {}`) + expect(err.message).toContain("Property key must be a string or number") + }) })