From 701ca98401e04bc8310f247b0b1035de28a02418 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 29 Oct 2025 14:22:57 -0700 Subject: [PATCH] try/catch/throw/finally --- src/compiler/compiler.ts | 87 +++++++++++- src/compiler/tests/exceptions.test.ts | 183 ++++++++++++++++++++++++++ src/compiler/utils.ts | 59 +++++++++ src/parser/shrimp.grammar | 32 ++++- src/parser/shrimp.terms.ts | 19 ++- src/parser/shrimp.ts | 18 +-- src/parser/tests/exceptions.test.ts | 177 +++++++++++++++++++++++++ 7 files changed, 554 insertions(+), 21 deletions(-) create mode 100644 src/compiler/tests/exceptions.test.ts create mode 100644 src/parser/tests/exceptions.test.ts diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 72b0f23..fb3dd9e 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -17,6 +17,7 @@ import { getNamedArgParts, getPipeExprParts, getStringParts, + getTryExprParts, } from '#compiler/utils' const DEBUG = false @@ -51,6 +52,7 @@ export class Compiler { instructions: ProgramItem[] = [] fnLabelCount = 0 ifLabelCount = 0 + tryLabelCount = 0 bytecode: Bytecode pipeCounter = 0 @@ -317,10 +319,87 @@ export class Compiler { } case terms.ThenBlock: - case terms.SingleLineThenBlock: { - const instructions = getAllChildren(node) - .map((child) => this.#compileNode(child, input)) - .flat() + case terms.SingleLineThenBlock: + case terms.TryBlock: { + const children = getAllChildren(node) + const instructions: ProgramItem[] = [] + + children.forEach((child, index) => { + instructions.push(...this.#compileNode(child, input)) + // keep only the last expression's value + if (index < children.length - 1) { + instructions.push(['POP']) + } + }) + + return instructions + } + + case terms.TryExpr: { + const { tryBlock, catchVariable, catchBody, finallyBody } = getTryExprParts(node, input) + + const instructions: ProgramItem[] = [] + this.tryLabelCount++ + const catchLabel: Label = `.catch_${this.tryLabelCount}` + const finallyLabel: Label = finallyBody ? `.finally_${this.tryLabelCount}` : null as any + const endLabel: Label = `.end_try_${this.tryLabelCount}` + + instructions.push(['PUSH_TRY', catchLabel]) + + const tryInstructions = this.#compileNode(tryBlock, input) + instructions.push(...tryInstructions) + + instructions.push(['POP_TRY']) + if (finallyBody) { + instructions.push(['JUMP', finallyLabel]) + } else { + instructions.push(['JUMP', endLabel]) + } + + instructions.push([`${catchLabel}:`]) + if (catchBody && catchVariable) { + // catch block + instructions.push(['STORE', catchVariable]) + const catchInstructions = this.#compileNode(catchBody, input) + instructions.push(...catchInstructions) + instructions.push(['JUMP', finallyBody ? finallyLabel : endLabel]) + } else { + // no catch block + if (finallyBody) { + instructions.push(['JUMP', finallyLabel]) + } else { + instructions.push(['THROW']) + } + } + + // finally block + if (finallyBody) { + instructions.push([`${finallyLabel}:`]) + const finallyInstructions = this.#compileNode(finallyBody, input) + instructions.push(...finallyInstructions) + // finally doesn't return a value + instructions.push(['POP']) + } + + instructions.push([`${endLabel}:`]) + + return instructions + } + + case terms.ThrowStatement: { + const children = getAllChildren(node) + const [_throwKeyword, expression] = children + if (!expression) { + throw new CompilerError( + `ThrowStatement expected expression, got ${children.length} children`, + node.from, + node.to + ) + } + + const instructions: ProgramItem[] = [] + instructions.push(...this.#compileNode(expression, input)) + instructions.push(['THROW']) return instructions } diff --git a/src/compiler/tests/exceptions.test.ts b/src/compiler/tests/exceptions.test.ts new file mode 100644 index 0000000..19e3503 --- /dev/null +++ b/src/compiler/tests/exceptions.test.ts @@ -0,0 +1,183 @@ +import { describe } from 'bun:test' +import { expect, test } from 'bun:test' + +describe('exception handling', () => { + test('try with catch - no error thrown', () => { + expect(` + try: + 42 + catch err: + 99 + end + `).toEvaluateTo(42) + }) + + test('try with catch - error thrown', () => { + expect(` + try: + throw 'something went wrong' + 99 + catch err: + err + end + `).toEvaluateTo('something went wrong') + }) + + test('try with catch - catch variable binding', () => { + expect(` + try: + throw 100 + catch my-error: + my-error + 50 + end + `).toEvaluateTo(150) + }) + + test('try with finally - no error', () => { + expect(` + x = 0 + result = try: + x = 10 + 42 + finally: + x = x + 5 + end + x + `).toEvaluateTo(15) + }) + + test('try with finally - return value from try', () => { + expect(` + x = 0 + result = try: + x = 10 + 42 + finally: + x = x + 5 + 999 + end + result + `).toEvaluateTo(42) + }) + + test('try with catch and finally - no error', () => { + expect(` + x = 0 + try: + x = 10 + 42 + catch err: + x = 999 + 0 + finally: + x = x + 5 + end + x + `).toEvaluateTo(15) + }) + + test('try with catch and finally - error thrown', () => { + expect(` + x = 0 + result = try: + x = 10 + throw 'error' + 99 + catch err: + x = 20 + err + finally: + x = x + 5 + end + x + `).toEvaluateTo(25) + }) + + test('try with catch and finally - return value from catch', () => { + expect(` + result = try: + throw 'oops' + catch err: + 'caught' + finally: + 'finally' + end + result + `).toEvaluateTo('caught') + }) + + test('throw statement with string', () => { + expect(` + try: + throw 'error message' + catch err: + err + end + `).toEvaluateTo('error message') + }) + + test('throw statement with number', () => { + expect(` + try: + throw 404 + catch err: + err + end + `).toEvaluateTo(404) + }) + + test('throw statement with dict', () => { + expect(` + try: + throw [code=500 message=failed] + catch e: + e + end + `).toEvaluateTo({ code: 500, message: 'failed' }) + }) + + test('uncaught exception fails', () => { + expect(`throw 'uncaught error'`).toFailEvaluation() + }) + + test('single-line try catch', () => { + expect(`result = try: throw 'err' catch e: 'handled' end; result`).toEvaluateTo('handled') + }) + + test('nested try blocks - inner catches', () => { + expect(` + try: + result = try: + throw 'inner error' + catch err: + err + end + result + catch outer: + 'outer' + end + `).toEvaluateTo('inner error') + }) + + test('nested try blocks - outer catches', () => { + expect(` + try: + try: + throw 'inner error' + catch err: + throw 'outer error' + end + catch outer: + outer + end + `).toEvaluateTo('outer error') + }) + + test('try as expression', () => { + expect(` + x = try: 10 catch err: 0 end + y = try: throw 'err' catch err: 20 end + x + y + `).toEvaluateTo(30) + }) +}) diff --git a/src/compiler/utils.ts b/src/compiler/utils.ts index 82b4025..96de20f 100644 --- a/src/compiler/utils.ts +++ b/src/compiler/utils.ts @@ -231,3 +231,62 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => { return { objectName, property } } + +export const getTryExprParts = (node: SyntaxNode, input: string) => { + const children = getAllChildren(node) + + // First child is always 'try' keyword, second is colon, third is TryBlock or statement + const [tryKeyword, _colon, tryBlock, ...rest] = children + + if (!tryKeyword || !tryBlock) { + throw new CompilerError( + `TryExpr expected at least 3 children, got ${children.length}`, + node.from, + node.to + ) + } + + let catchExpr: SyntaxNode | undefined + let catchVariable: string | undefined + let catchBody: SyntaxNode | undefined + let finallyExpr: SyntaxNode | undefined + let finallyBody: SyntaxNode | undefined + + rest.forEach((child) => { + if (child.type.id === terms.CatchExpr) { + catchExpr = child + const catchChildren = getAllChildren(child) + const [_catchKeyword, identifierNode, _colon, body] = catchChildren + if (!identifierNode || !body) { + throw new CompilerError( + `CatchExpr expected identifier and body, got ${catchChildren.length} children`, + child.from, + child.to + ) + } + catchVariable = input.slice(identifierNode.from, identifierNode.to) + catchBody = body + } else if (child.type.id === terms.FinallyExpr) { + finallyExpr = child + const finallyChildren = getAllChildren(child) + const [_finallyKeyword, _colon, body] = finallyChildren + if (!body) { + throw new CompilerError( + `FinallyExpr expected body, got ${finallyChildren.length} children`, + child.from, + child.to + ) + } + finallyBody = body + } + }) + + return { + tryBlock, + catchExpr, + catchVariable, + catchBody, + finallyExpr, + finallyBody, + } +} diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 635035a..297948c 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -51,6 +51,8 @@ item { consumeToTerminator { PipeExpr | ambiguousFunctionCall | + TryExpr | + ThrowStatement | IfExpr | FunctionDef | Assign | @@ -129,7 +131,35 @@ ThenBlock { } SingleLineThenBlock { - consumeToTerminator + consumeToTerminator +} + +TryExpr { + singleLineTry | multilineTry +} + +singleLineTry { + @specialize[@name=keyword] colon consumeToTerminator CatchExpr? FinallyExpr? @specialize[@name=keyword] +} + +multilineTry { + @specialize[@name=keyword] colon newlineOrSemicolon TryBlock CatchExpr? FinallyExpr? @specialize[@name=keyword] +} + +CatchExpr { + @specialize[@name=keyword] Identifier colon (newlineOrSemicolon TryBlock | consumeToTerminator) +} + +FinallyExpr { + @specialize[@name=keyword] colon (newlineOrSemicolon TryBlock | consumeToTerminator) +} + +TryBlock { + block +} + +ThrowStatement { + @specialize[@name=keyword] expression } ConditionalOp { diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index 144d69b..98bc49c 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -39,15 +39,20 @@ export const FunctionDef = 37, Params = 38, colon = 39, - keyword = 54, + keyword = 63, Underscore = 41, Array = 42, Null = 43, ConditionalOp = 44, PositionalArg = 45, - IfExpr = 47, - SingleLineThenBlock = 49, - ThenBlock = 50, - ElseIfExpr = 51, - ElseExpr = 53, - Assign = 55 + TryExpr = 47, + CatchExpr = 49, + TryBlock = 51, + FinallyExpr = 52, + ThrowStatement = 54, + IfExpr = 56, + SingleLineThenBlock = 58, + ThenBlock = 59, + ElseIfExpr = 60, + ElseExpr = 62, + Assign = 64 diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 6e19a7f..14d2ece 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -4,14 +4,14 @@ import {operatorTokenizer} from "./operatorTokenizer" import {tokenizer, specializeKeyword} from "./tokenizer" import {trackScope} from "./scopeTracker" import {highlighting} from "./highlight" -const spec_Identifier = {__proto__:null,end:80, null:86, if:96, elseif:104, else:108} +const spec_Identifier = {__proto__:null,end:80, null:86, try:96, catch:100, finally:106, throw:110, if:114, elseif:122, else:126} 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`<QQbOAN?aO>]QQO'#DlOOQ`AN?aAN?aO>bQbOAN?aO>gQbO7+%POOQ`7+%P7+%POOQ`7+%S7+%SOOQ`G24yG24yO?QQRO,5:UO?XQRO,5:UOOQ`-E7v-E7vOOQ`G24{G24{O?dQbOG24{O?iQQO,5:WOOQ`<h#i#o7^#o#p#{#p#q@`#q;'S#{;'S;=`$d<%l~#{~O#{~~@yS$QUmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUmS!fYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UmS!xQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZmS!gYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!gYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O!q~~'aO!o~U'hUmS!lQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUmS!}QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWmSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZYmShQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OWmSOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oWmShQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^WmSOt#{uw#{x!P#{!P!Q+v!Q#O#{#P;'S#{;'S;=`$d<%lO#{U+{^mSOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q#{!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wU-O^mSqQOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q0o!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wQ.PXqQOY-zZ!P-z!P!Q.l!Q!}-z!}#O/Z#O#P0Y#P;'S-z;'S;=`0i<%lO-zQ.oP!P!Q.rQ.wUqQ#Z#[.r#]#^.r#a#b.r#g#h.r#i#j.r#m#n.rQ/^VOY/ZZ#O/Z#O#P/s#P#Q-z#Q;'S/Z;'S;=`0S<%lO/ZQ/vSOY/ZZ;'S/Z;'S;=`0S<%lO/ZQ0VP;=`<%l/ZQ0]SOY-zZ;'S-z;'S;=`0i<%lO-zQ0lP;=`<%l-zU0tWmSOt#{uw#{x!P#{!P!Q1^!Q#O#{#P;'S#{;'S;=`$d<%lO#{U1ebmSqQOt#{uw#{x#O#{#P#Z#{#Z#[1^#[#]#{#]#^1^#^#a#{#a#b1^#b#g#{#g#h1^#h#i#{#i#j1^#j#m#{#m#n1^#n;'S#{;'S;=`$d<%lO#{U2r[mSOY2mYZ#{Zt2mtu/Zuw2mwx/Zx#O2m#O#P/s#P#Q,w#Q;'S2m;'S;=`3h<%lO2mU3kP;=`<%l2mU3qP;=`<%l,wU3{UmSwQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4fW!wQmSOt#{uw#{x!_#{!_!`5O!`#O#{#P;'S#{;'S;=`$d<%lO#{U5TVmSOt#{uw#{x#O#{#P#Q5j#Q;'S#{;'S;=`$d<%lO#{U5qU!vQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6YO!r~U6aU!|QmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6zUmSyQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7cYmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{U8YUtQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U8qZmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#U9d#U#o7^#o;'S#{;'S;=`$d<%lO#{U9i[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#`7^#`#a:_#a#o7^#o;'S#{;'S;=`$d<%lO#{U:d[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#g7^#g#h;Y#h#o7^#o;'S#{;'S;=`$d<%lO#{U;_[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#X7^#X#Yo[!tWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#f7^#f#g?e#g#o7^#o;'S#{;'S;=`$d<%lO#{U?j[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#i7^#i#j;Y#j#o7^#o;'S#{;'S;=`$d<%lO#{U@gU!OQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~AOO#R~", - tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!k~~", 11)], + tokenData: "AO~R|OX#{XY$jYZ%TZp#{pq$jqs#{st%ntu'Vuw#{wx'[xy'ayz'zz{#{{|(e|}#{}!O(e!O!P#{!P!Q+X!Q![)S![!]3t!]!^%T!^!}#{!}#O4_#O#P6T#P#Q6Y#Q#R#{#R#S6s#S#T#{#T#Y7^#Y#Z8l#Z#b7^#b#ch#i#o7^#o#p#{#p#q@`#q;'S#{;'S;=`$d<%l~#{~O#{~~@yS$QUmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUmS!oYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UmS#RQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZmS!pYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!pYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O!z~~'aO!x~U'hUmS!uQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUmS#WQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWmSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZYmShQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OWmSOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oWmShQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^WmSOt#{uw#{x!P#{!P!Q+v!Q#O#{#P;'S#{;'S;=`$d<%lO#{U+{^mSOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q#{!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wU-O^mSqQOY,wYZ#{Zt,wtu-zuw,wwx-zx!P,w!P!Q0o!Q!},w!}#O2m#O#P0Y#P;'S,w;'S;=`3n<%lO,wQ.PXqQOY-zZ!P-z!P!Q.l!Q!}-z!}#O/Z#O#P0Y#P;'S-z;'S;=`0i<%lO-zQ.oP!P!Q.rQ.wUqQ#Z#[.r#]#^.r#a#b.r#g#h.r#i#j.r#m#n.rQ/^VOY/ZZ#O/Z#O#P/s#P#Q-z#Q;'S/Z;'S;=`0S<%lO/ZQ/vSOY/ZZ;'S/Z;'S;=`0S<%lO/ZQ0VP;=`<%l/ZQ0]SOY-zZ;'S-z;'S;=`0i<%lO-zQ0lP;=`<%l-zU0tWmSOt#{uw#{x!P#{!P!Q1^!Q#O#{#P;'S#{;'S;=`$d<%lO#{U1ebmSqQOt#{uw#{x#O#{#P#Z#{#Z#[1^#[#]#{#]#^1^#^#a#{#a#b1^#b#g#{#g#h1^#h#i#{#i#j1^#j#m#{#m#n1^#n;'S#{;'S;=`$d<%lO#{U2r[mSOY2mYZ#{Zt2mtu/Zuw2mwx/Zx#O2m#O#P/s#P#Q,w#Q;'S2m;'S;=`3h<%lO2mU3kP;=`<%l2mU3qP;=`<%l,wU3{UmSwQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4fW#QQmSOt#{uw#{x!_#{!_!`5O!`#O#{#P;'S#{;'S;=`$d<%lO#{U5TVmSOt#{uw#{x#O#{#P#Q5j#Q;'S#{;'S;=`$d<%lO#{U5qU#PQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6YO!{~U6aU#VQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6zUmSyQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7cYmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{U8YUtQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U8qZmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#U9d#U#o7^#o;'S#{;'S;=`$d<%lO#{U9i[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#`7^#`#a:_#a#o7^#o;'S#{;'S;=`$d<%lO#{U:d[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#g7^#g#h;Y#h#o7^#o;'S#{;'S;=`$d<%lO#{U;_[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#X7^#X#Yo[!}WmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#f7^#f#g?e#g#o7^#o;'S#{;'S;=`$d<%lO#{U?j[mSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#i7^#i#j;Y#j#o7^#o;'S#{;'S;=`$d<%lO#{U@gU!OQmSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~AOO#^~", + tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!t~~", 11)], topRules: {"Program":[0,20]}, specialized: [{term: 15, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 15, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], - tokenPrec: 1135 + tokenPrec: 1415 }) diff --git a/src/parser/tests/exceptions.test.ts b/src/parser/tests/exceptions.test.ts new file mode 100644 index 0000000..aafdcf9 --- /dev/null +++ b/src/parser/tests/exceptions.test.ts @@ -0,0 +1,177 @@ +import { expect, describe, test } from 'bun:test' + +import '../shrimp.grammar' // Importing this so changes cause it to retest! + +describe('try/catch/finally/throw', () => { + test('parses try with catch', () => { + expect(`try: + risky-operation + catch err: + handle-error err + end`).toMatchTree(` + TryExpr + keyword try + colon : + TryBlock + FunctionCallOrIdentifier + Identifier risky-operation + CatchExpr + keyword catch + Identifier err + colon : + TryBlock + FunctionCall + Identifier handle-error + PositionalArg + Identifier err + keyword end + `) + }) + + test('parses try with finally', () => { + expect(`try: + do-work + finally: + cleanup + end`).toMatchTree(` + TryExpr + keyword try + colon : + TryBlock + FunctionCallOrIdentifier + Identifier do-work + FinallyExpr + keyword finally + colon : + TryBlock + FunctionCallOrIdentifier + Identifier cleanup + keyword end + `) + }) + + test('parses try with catch and finally', () => { + expect(`try: + risky-operation + catch err: + handle-error err + finally: + cleanup + end`).toMatchTree(` + TryExpr + keyword try + colon : + TryBlock + FunctionCallOrIdentifier + Identifier risky-operation + CatchExpr + keyword catch + Identifier err + colon : + TryBlock + FunctionCall + Identifier handle-error + PositionalArg + Identifier err + FinallyExpr + keyword finally + colon : + TryBlock + FunctionCallOrIdentifier + Identifier cleanup + keyword end + `) + }) + + test('parses single-line try with catch', () => { + expect('result = try: parse-number input catch err: 0 end').toMatchTree(` + Assign + AssignableIdentifier result + Eq = + TryExpr + keyword try + colon : + FunctionCall + Identifier parse-number + PositionalArg + Identifier input + CatchExpr + keyword catch + Identifier err + colon : + Number 0 + keyword end + `) + }) + + test('parses single-line try with finally', () => { + expect('try: work catch err: 0 finally: cleanup end').toMatchTree(` + TryExpr + keyword try + colon : + FunctionCallOrIdentifier + Identifier work + CatchExpr + keyword catch + Identifier err + colon : + Number 0 + FinallyExpr + keyword finally + colon : + FunctionCallOrIdentifier + Identifier cleanup + keyword end + `) + }) + + test('parses throw statement with string', () => { + expect("throw 'error message'").toMatchTree(` + ThrowStatement + keyword throw + String + StringFragment error message + `) + }) + + test('parses throw statement with identifier', () => { + expect('throw error-object').toMatchTree(` + ThrowStatement + keyword throw + Identifier error-object + `) + }) + + test('parses throw statement with dict', () => { + expect('throw [type=validation-error message=failed]').toMatchTree(` + ThrowStatement + keyword throw + Dict + NamedArg + NamedArgPrefix type= + Identifier validation-error + NamedArg + NamedArgPrefix message= + Identifier failed + `) + }) + + test('does not parse identifiers that start with try', () => { + expect('trying = try: work catch err: 0 end').toMatchTree(` + Assign + AssignableIdentifier trying + Eq = + TryExpr + keyword try + colon : + FunctionCallOrIdentifier + Identifier work + CatchExpr + keyword catch + Identifier err + colon : + Number 0 + keyword end + `) + }) +})