diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 6934f06..efcff49 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -238,11 +238,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 7a67a9e..2f5dcac 100644 --- a/src/compiler/tests/compiler.test.ts +++ b/src/compiler/tests/compiler.test.ts @@ -64,36 +64,26 @@ describe('compiler', () => { expect('sum = 2 + 3; sum').toEvaluateTo(5) }) - test('compound assignment +=', () => { - expect('x = 10; x += 5; x').toEvaluateTo(15) + 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('compound assignment -= ', () => { - expect('x = 10; x -= 3; x').toEvaluateTo(7) + test('array destructuring with one variable', () => { + expect('[ x ] = [ 42 ]; x').toEvaluateTo(42) }) - test('compound assignment *=', () => { - expect('x = 5; x *= 3; x').toEvaluateTo(15) + test('array destructuring with missing elements assigns null', () => { + expect('[ a b c ] = [ 1 2 ]; c').toEvaluateTo(null) }) - test('compound assignment /=', () => { - expect('x = 20; x /= 4; x').toEvaluateTo(5) + test('array destructuring returns the original array', () => { + expect('[ a b ] = [ 1 2 3 4 ]').toEvaluateTo([1, 2, 3, 4]) }) - test('compound assignment %=', () => { - expect('x = 17; x %= 5; x').toEvaluateTo(2) - }) - - test('compound assignment with expression', () => { - expect('x = 10; x += 2 + 3; x').toEvaluateTo(15) - }) - - test('compound assignment returns value', () => { - expect('x = 5; x += 10; x').toEvaluateTo(15) - }) - - test('compound assignment fails on undefined variable', () => { - expect('undefined-var += 5').toFailEvaluation() + test('array destructuring with emoji identifiers', () => { + expect('[ 🚀 💎 ] = [ 1 2 ]; 🚀').toEvaluateTo(1) + expect('[ 🚀 💎 ] = [ 1 2 ]; 💎').toEvaluateTo(2) }) test('parentheses', () => { diff --git a/src/compiler/utils.ts b/src/compiler/utils.ts index 96f6bee..1f8f0c1 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/scopeTracker.ts b/src/parser/scopeTracker.ts index af2a32c..7ce09e0 100644 --- a/src/parser/scopeTracker.ts +++ b/src/parser/scopeTracker.ts @@ -2,7 +2,7 @@ import { ContextTracker, InputStream } from '@lezer/lr' import * as terms from './shrimp.terms' export class Scope { - constructor(public parent: Scope | null, public vars = new Set()) {} + constructor(public parent: Scope | null, public vars = new Set()) { } has(name: string): boolean { return this.vars.has(name) || (this.parent?.has(name) ?? false) @@ -42,7 +42,7 @@ export class Scope { // Tracker context that combines Scope with temporary pending identifiers class TrackerContext { - constructor(public scope: Scope, public pendingIds: string[] = []) {} + constructor(public scope: Scope, public pendingIds: string[] = []) { } } // Extract identifier text from input stream @@ -75,6 +75,12 @@ export const trackScope = new ContextTracker({ return new TrackerContext(context.scope, [...context.pendingIds, text]) } + // Track identifiers in array destructuring: [ a b ] = ... + if (!inParams && term === terms.Identifier && isArrayDestructuring(input)) { + const text = readIdentifierText(input, input.pos, stack.pos) + return new TrackerContext(Scope.add(context.scope, text), context.pendingIds) + } + return context }, @@ -98,3 +104,26 @@ export const trackScope = new ContextTracker({ hash: (context) => context.scope.hash(), }) + +// Check if we're parsing array destructuring: [ a b ] = ... +const isArrayDestructuring = (input: InputStream): boolean => { + let pos = 0 + + // Find closing bracket + while (pos < 200 && input.peek(pos) !== 93 /* ] */) { + if (input.peek(pos) === -1) return false // EOF + pos++ + } + + if (input.peek(pos) !== 93 /* ] */) return false + pos++ + + // Skip whitespace + while (input.peek(pos) === 32 /* space */ || + input.peek(pos) === 9 /* tab */ || + input.peek(pos) === 10 /* \n */) { + pos++ + } + + return input.peek(pos) === 61 /* = */ +} \ No newline at end of file diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 9da24da..af3069d 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -179,7 +179,7 @@ Params { } Assign { - AssignableIdentifier Eq consumeToTerminator + (AssignableIdentifier | Array) Eq consumeToTerminator } CompoundAssign { diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 172f6a7..1a81f28 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -7,9 +7,9 @@ import {highlighting} from "./highlight" const spec_Identifier = {__proto__:null,catch:92, finally:98, end:100, null:106, try:116, throw:120, if:124, elseif:132, else:136} export const parser = LRParser.deserialize({ version: 14, - states: "9UQYQbOOO#tQcO'#C{O$qOSO'#C}O%PQbO'#EfOOQ`'#DW'#DWOOQa'#DT'#DTO&SQbO'#DbO'XQcO'#EZOOQa'#EZ'#EZO)cQcO'#EYO)vQRO'#C|O+SQcO'#EUO+dQcO'#EUO+nQbO'#CzO,fOpO'#CxOOQ`'#EV'#EVO,kQbO'#EUO,rQQO'#ElOOQ`'#Dg'#DgO,wQbO'#DiO,wQbO'#EnOOQ`'#Dk'#DkO-lQRO'#DsOOQ`'#EU'#EUO.QQQO'#ETOOQ`'#ET'#ETOOQ`'#Du'#DuQYQbOOO.YQbO'#DUOOQa'#EY'#EYOOQ`'#De'#DeOOQ`'#Ek'#EkOOQ`'#D|'#D|O.dQbO,59cO.}QbO'#DPO/VQWO'#DQOOOO'#E]'#E]OOOO'#Dv'#DvO/kOSO,59iOOQa,59i,59iOOQ`'#Dx'#DxO/yQbO'#DXO0RQQO,5;QOOQ`'#Dw'#DwO0WQbO,59|O0_QQO,59oOOQa,59|,59|O0jQbO,59|O,wQbO,59hO,wQbO,59hO,wQbO,59hO,wQbO,5:OO,wQbO,5:OO,wQbO,5:OO0tQRO,59fO0{QRO,59fO1^QRO,59fO1XQQO,59fO1iQQO,59fO1qObO,59dO1|QbO'#D}O2XQbO,59bO2pQbO,5;WO3TQcO,5:TO3yQcO,5:TO4ZQcO,5:TO5PQRO,5;YO5WQRO,5;YO5cQbO,5:_O5cQbO,5:`OOQ`,5:o,5:oOOQ`-E7s-E7sOOQ`,59p,59pOOQ`-E7z-E7zOOOO,59k,59kOOOO,59l,59lOOOO-E7t-E7tOOQa1G/T1G/TOOQ`-E7v-E7vO5sQbO1G0lOOQ`-E7u-E7uO6WQQO1G/ZOOQa1G/h1G/hO6cQbO1G/hOOQO'#Dz'#DzO6WQQO1G/ZOOQa1G/Z1G/ZOOQ`'#D{'#D{O6cQbO1G/hOOQa1G/S1G/SO7[QcO1G/SO7fQcO1G/SO7pQcO1G/SOOQa1G/j1G/jO9`QcO1G/jO9gQcO1G/jO9nQcO1G/jOOQa1G/Q1G/QOOQa1G/O1G/OO!aQbO'#C{O&ZQbO'#CwOOQ`,5:i,5:iOOQ`-E7{-E7{O9uQbO1G0rO:QQbO1G0sO:nQbO1G0tOOQ`1G/y1G/yOOQ`1G/z1G/zO;RQbO7+&WO:QQbO7+&YO;^QQO7+$uOOQa7+$u7+$uO;iQbO7+%SOOQa7+%S7+%SOOQO-E7x-E7xOOQ`-E7y-E7yO;sQbO'#DZO;xQQO'#D^OOQ`7+&^7+&^O;}QbO7+&^ORQbO<WQbO<`QbO<kQQO,59uO>pQbO,59xOOQ`<SQbO7+&aOOQ`<pQbO<uQbO<}QbO<SQbO7+%aOOQ`7+%c7+%cOOQ`< (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 20, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], - tokenPrec: 1562 + tokenPrec: 1591 }) diff --git a/src/parser/tests/basics.test.ts b/src/parser/tests/basics.test.ts index 86e4a97..d890f91 100644 --- a/src/parser/tests/basics.test.ts +++ b/src/parser/tests/basics.test.ts @@ -660,6 +660,62 @@ 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`) + }) + + + test('works with dotget', () => { + expect('[ a ] = [ [1 2 3] ]; a.1').toMatchTree(` + Assign + Array + Identifier a + Eq = + Array + Array + Number 1 + Number 2 + Number 3 + FunctionCallOrIdentifier + DotGet + IdentifierBeforeDot a + Number 1`) + }) +}) + describe('Conditional ops', () => { test('or can be chained', () => { expect(`