diff --git a/bin/repl b/bin/repl index 052f481..c6ff98c 100755 --- a/bin/repl +++ b/bin/repl @@ -144,19 +144,33 @@ async function repl() { return } - codeHistory.push(trimmed) - try { const compiler = new Compiler(trimmed, Object.keys(globals)) - vm.appendBytecode(compiler.bytecode) + // Save VM state before appending bytecode, in case execution fails + const savedInstructions = [...vm.instructions] + const savedConstants = [...vm.constants] + const savedPc = vm.pc + const savedScope = vm.scope + const savedStopped = vm.stopped - const result = await vm.continue() + try { + vm.appendBytecode(compiler.bytecode) + const result = await vm.continue() - console.log(`${colors.dim}=>${colors.reset} ${formatValue(result)}`) + codeHistory.push(trimmed) + console.log(`${colors.dim}=>${colors.reset} ${formatValue(result)}`) + } catch (error: any) { + vm.instructions = savedInstructions + vm.constants = savedConstants + vm.pc = savedPc + vm.scope = savedScope + vm.stopped = savedStopped + + console.log(`\n${colors.red}Error:${colors.reset} ${error.message}`) + } } catch (error: any) { console.log(`\n${colors.red}Error:${colors.reset} ${error.message}`) - codeHistory.pop() } rl.prompt() diff --git a/bun.lock b/bun.lock index e8b8c92..afb8aaa 100644 --- a/bun.lock +++ b/bun.lock @@ -62,7 +62,7 @@ "hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="], - "reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#9618dd64148ccf6f0cdfd8a80a0f58efe3e0819d", { "peerDependencies": { "typescript": "^5" } }, "9618dd64148ccf6f0cdfd8a80a0f58efe3e0819d"], + "reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#c69b172c78853756ec8acba5bc33d93eb6a571c6", { "peerDependencies": { "typescript": "^5" } }, "c69b172c78853756ec8acba5bc33d93eb6a571c6"], "style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="], diff --git a/package.json b/package.json index 2ac8e0c..cdf55cc 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "dev": "bun generate-parser && bun --hot src/server/server.tsx", "generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts", "repl": "bun generate-parser && bun bin/repl", - "update-reef": "cd packages/ReefVM && git pull origin main" + "update-reef": "rm -rf ~/.bun/install/cache/ && bun update reefvm" }, "dependencies": { "@codemirror/view": "^6.38.3", diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 4e35747..b4e93e4 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -237,11 +237,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 c5fb786..2198444 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 80cdef5..7997ec1 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -178,7 +178,7 @@ Params { } Assign { - AssignableIdentifier Eq consumeToTerminator + (AssignableIdentifier | Array) Eq consumeToTerminator } BinOp { diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index ec84f41..b472c18 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:82, finally:88, end:90, null:96, try:106, throw:110, if:114, elseif:122, else:126} export const parser = LRParser.deserialize({ version: 14, - states: "8xQYQbOOO#tQcO'#CvO$qOSO'#CxO%PQbO'#E`OOQ`'#DR'#DROOQa'#DO'#DOO&SQbO'#D]O'XQcO'#ETOOQa'#ET'#ETO)cQcO'#ESO)vQRO'#CwO+SQcO'#EOO+dQcO'#EOO+nQbO'#CuO,fOpO'#CsOOQ`'#EP'#EPO,kQbO'#EOO,rQQO'#EfOOQ`'#Db'#DbO,wQbO'#DdO,wQbO'#EhOOQ`'#Df'#DfO-lQRO'#DnOOQ`'#EO'#EOO-qQQO'#D}OOQ`'#D}'#D}OOQ`'#Do'#DoQYQbOOO-yQbO'#DPOOQa'#ES'#ESOOQ`'#D`'#D`OOQ`'#Ee'#EeOOQ`'#Dv'#DvO.TQbO,59^O.nQbO'#CzO.vQWO'#C{OOOO'#EV'#EVOOOO'#Dp'#DpO/[OSO,59dOOQa,59d,59dOOQ`'#Dr'#DrO/jQbO'#DSO/rQQO,5:zOOQ`'#Dq'#DqO/wQbO,59wO0OQQO,59jOOQa,59w,59wO0ZQbO,59wO,wQbO,59cO,wQbO,59cO,wQbO,59cO,wQbO,59yO,wQbO,59yO,wQbO,59yO0eQRO,59aO0lQRO,59aO0}QRO,59aO0xQQO,59aO1YQQO,59aO1bObO,59_O1mQbO'#DwO1xQbO,59]O2aQbO,5;QO2tQcO,5:OO3jQcO,5:OO3zQcO,5:OO4pQRO,5;SO4wQRO,5;SO5SQbO,5:YOOQ`,5:i,5:iOOQ`-E7m-E7mOOQ`,59k,59kOOQ`-E7t-E7tOOOO,59f,59fOOOO,59g,59gOOOO-E7n-E7nOOQa1G/O1G/OOOQ`-E7p-E7pO5dQbO1G0fOOQ`-E7o-E7oO5wQQO1G/UOOQa1G/c1G/cO6SQbO1G/cOOQO'#Dt'#DtO5wQQO1G/UOOQa1G/U1G/UOOQ`'#Du'#DuO6SQbO1G/cOOQa1G.}1G.}O6{QcO1G.}O7VQcO1G.}O7aQcO1G.}OOQa1G/e1G/eO9PQcO1G/eO9WQcO1G/eO9_QcO1G/eOOQa1G.{1G.{OOQa1G.y1G.yO!aQbO'#CvO&ZQbO'#CrOOQ`,5:c,5:cOOQ`-E7u-E7uO9fQbO1G0lO9qQbO1G0mO:_QbO1G0nOOQ`1G/t1G/tO:rQbO7+&QO9qQbO7+&SO:}QQO7+$pOOQa7+$p7+$pO;YQbO7+$}OOQa7+$}7+$}OOQO-E7r-E7rOOQ`-E7s-E7sO;dQbO'#DUO;iQQO'#DXOOQ`7+&W7+&WO;nQbO7+&WO;sQbO7+&WOOQ`'#Ds'#DsO;{QQO'#DsOPQbO<[QQO,59pO>aQbO,59sOOQ`<tQbO<yQbO<aQbO<fQbO<nQbO<yQQO,59pO?OQbO,59sOOQ`< (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 15, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], - tokenPrec: 1547 + tokenPrec: 1576 }) 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(`