From 789481f4ef7b855ec59b8b03def9b351c81f9465 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 29 Oct 2025 19:13:03 -0700 Subject: [PATCH] [a b] = [1 2 3] --- src/compiler/compiler.ts | 28 ++++++++++++++++++--- src/compiler/tests/compiler.test.ts | 22 +++++++++++++++++ src/compiler/utils.ts | 16 +++++++++--- src/parser/shrimp.grammar | 2 +- src/parser/shrimp.ts | 8 +++--- src/parser/tests/basics.test.ts | 38 +++++++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 72b0f23..7581f17 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -235,11 +235,31 @@ export class Compiler { } case terms.Assign: { - const { identifier, right } = getAssignmentParts(node) + const assignParts = getAssignmentParts(node) const instructions: ProgramItem[] = [] - instructions.push(...this.#compileNode(right, input)) - instructions.push(['DUP']) // Keep a copy on the stack after storing - const identifierName = input.slice(identifier.from, identifier.to) + + // right-hand side + instructions.push(...this.#compileNode(assignParts.right, input)) + + // array destructuring: [ a b ] = [ 1 2 3 4 ] + if ('arrayPattern' in assignParts) { + const identifiers = assignParts.arrayPattern ?? [] + if (identifiers.length === 0) return instructions + + for (let i = 0; i < identifiers.length; i++) { + instructions.push(['DUP']) + instructions.push(['PUSH', i]) + instructions.push(['DOT_GET']) + instructions.push(['STORE', input.slice(identifiers[i]!.from, identifiers[i]!.to)]) + } + + // original array still on stack as the return value + return instructions + } + + // simple assignment: x = value + instructions.push(['DUP']) + const identifierName = input.slice(assignParts.identifier.from, assignParts.identifier.to) instructions.push(['STORE', identifierName]) return instructions diff --git a/src/compiler/tests/compiler.test.ts b/src/compiler/tests/compiler.test.ts index 4d662c3..2f5dcac 100644 --- a/src/compiler/tests/compiler.test.ts +++ b/src/compiler/tests/compiler.test.ts @@ -64,6 +64,28 @@ describe('compiler', () => { expect('sum = 2 + 3; sum').toEvaluateTo(5) }) + test('array destructuring with two variables', () => { + expect('[ a b ] = [ 1 2 3 4 ]; a').toEvaluateTo(1) + expect('[ a b ] = [ 1 2 3 4 ]; b').toEvaluateTo(2) + }) + + test('array destructuring with one variable', () => { + expect('[ x ] = [ 42 ]; x').toEvaluateTo(42) + }) + + test('array destructuring with missing elements assigns null', () => { + expect('[ a b c ] = [ 1 2 ]; c').toEvaluateTo(null) + }) + + test('array destructuring returns the original array', () => { + expect('[ a b ] = [ 1 2 3 4 ]').toEvaluateTo([1, 2, 3, 4]) + }) + + test('array destructuring with emoji identifiers', () => { + expect('[ 🚀 💎 ] = [ 1 2 ]; 🚀').toEvaluateTo(1) + expect('[ 🚀 💎 ] = [ 1 2 ]; 💎').toEvaluateTo(2) + }) + test('parentheses', () => { expect('(2 + 3) * 4').toEvaluateTo(20) }) diff --git a/src/compiler/utils.ts b/src/compiler/utils.ts index 82b4025..99b56c6 100644 --- a/src/compiler/utils.ts +++ b/src/compiler/utils.ts @@ -40,15 +40,23 @@ export const getAssignmentParts = (node: SyntaxNode) => { const children = getAllChildren(node) const [left, equals, right] = children - if (!left || left.type.id !== terms.AssignableIdentifier) { + if (!equals || !right) { throw new CompilerError( - `Assign left child must be an AssignableIdentifier, got ${left ? left.type.name : 'none'}`, + `Assign expected 3 children, got ${children.length}`, node.from, node.to ) - } else if (!equals || !right) { + } + + // array destructuring + if (left && left.type.id === terms.Array) { + const identifiers = getAllChildren(left).filter(child => child.type.id === terms.Identifier) + return { arrayPattern: identifiers, right } + } + + if (!left || left.type.id !== terms.AssignableIdentifier) { throw new CompilerError( - `Assign expected 3 children, got ${children.length}`, + `Assign left child must be an AssignableIdentifier or Array, got ${left ? left.type.name : 'none'}`, node.from, node.to ) diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 635035a..01c82f0 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -148,7 +148,7 @@ Params { } Assign { - AssignableIdentifier Eq consumeToTerminator + (AssignableIdentifier | Array) Eq consumeToTerminator } BinOp { diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 6e19a7f..e9ba516 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -7,9 +7,9 @@ import {highlighting} from "./highlight" const spec_Identifier = {__proto__:null,end:80, null:86, if:96, elseif:104, else:108} export const parser = LRParser.deserialize({ version: 14, - states: "3UQYQbOOO#hQcO'#CvO$eOSO'#CxO$sQbO'#EVOOQ`'#DR'#DROOQa'#DO'#DOO%vQbO'#DWO&{QcO'#DzOOQa'#Dz'#DzO)PQcO'#DyO)xQRO'#CwO*]QcO'#DuO*tQcO'#DuO+VQbO'#CuO+}OpO'#CsOOQ`'#Dv'#DvO,SQbO'#DuO,bQbO'#E]OOQ`'#D]'#D]O-VQRO'#DeOOQ`'#Du'#DuO-[QQO'#DtOOQ`'#Dt'#DtOOQ`'#Df'#DfQYQbOOO-dQbO'#DPOOQa'#Dy'#DyOOQ`'#DZ'#DZOOQ`'#E['#E[OOQ`'#Dm'#DmO-nQbO,59^O.RQbO'#CzO.ZQWO'#C{OOOO'#D|'#D|OOOO'#Dg'#DgO.oOSO,59dOOQa,59d,59dOOQ`'#Di'#DiO.}QbO'#DSO/VQQO,5:qOOQ`'#Dh'#DhO/[QbO,59rO/cQQO,59jOOQa,59r,59rO/nQbO,59rO,bQbO,59cO,bQbO,59cO,bQbO,59cO,bQbO,59tO,bQbO,59tO,bQbO,59tO/xQRO,59aO0PQRO,59aO0bQRO,59aO0]QQO,59aO0mQQO,59aO0uObO,59_O1QQbO'#DnO1]QbO,59]O1nQRO,5:wO1uQRO,5:wO2QQbO,5:POOQ`,5:`,5:`OOQ`-E7d-E7dOOQ`,59k,59kOOQ`-E7k-E7kOOOO,59f,59fOOOO,59g,59gOOOO-E7e-E7eOOQa1G/O1G/OOOQ`-E7g-E7gO2[QbO1G0]OOQ`-E7f-E7fO2iQQO1G/UOOQa1G/^1G/^O2tQbO1G/^OOQO'#Dk'#DkO2iQQO1G/UOOQa1G/U1G/UOOQ`'#Dl'#DlO2tQbO1G/^OOQa1G.}1G.}O3gQcO1G.}O3qQcO1G.}O3{QcO1G.}OOQa1G/`1G/`O5_QcO1G/`O5fQcO1G/`O5mQcO1G/`OOQa1G.{1G.{OOQa1G.y1G.yO!ZQbO'#CvO%}QbO'#CrOOQ`,5:Y,5:YOOQ`-E7l-E7lO5tQbO1G0cOOQ`1G/k1G/kO6RQbO7+%wO6WQbO7+%xO6hQQO7+$pOOQa7+$p7+$pO6sQbO7+$xOOQa7+$x7+$xOOQO-E7i-E7iOOQ`-E7j-E7jOOQ`'#D_'#D_O6}QbO7+%}O7SQbO7+&OOOQ`< (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 15, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], - tokenPrec: 1135 + tokenPrec: 1164 }) diff --git a/src/parser/tests/basics.test.ts b/src/parser/tests/basics.test.ts index da6d4bb..b9584ad 100644 --- a/src/parser/tests/basics.test.ts +++ b/src/parser/tests/basics.test.ts @@ -594,6 +594,44 @@ describe('Comments', () => { }) }) +describe('Array destructuring', () => { + test('parses array pattern with two variables', () => { + expect('[ a b ] = [ 1 2 3 4]').toMatchTree(` + Assign + Array + Identifier a + Identifier b + Eq = + Array + Number 1 + Number 2 + Number 3 + Number 4`) + }) + + test('parses array pattern with one variable', () => { + expect('[ x ] = [ 42 ]').toMatchTree(` + Assign + Array + Identifier x + Eq = + Array + Number 42`) + }) + + test('parses array pattern with emoji identifiers', () => { + expect('[ 🚀 💎 ] = [ 1 2 ]').toMatchTree(` + Assign + Array + Identifier 🚀 + Identifier 💎 + Eq = + Array + Number 1 + Number 2`) + }) +}) + describe('Conditional ops', () => { test('or can be chained', () => { expect(`