From 701ca98401e04bc8310f247b0b1035de28a02418 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 29 Oct 2025 14:22:57 -0700 Subject: [PATCH 1/2] 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 + `) + }) +}) From 9bc514a78211514fe862e2ddac94949c514a6523 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 29 Oct 2025 14:38:23 -0700 Subject: [PATCH 2/2] implicit function-level try blocks --- src/compiler/compiler.ts | 134 ++++++++++++++++---------- src/compiler/tests/exceptions.test.ts | 128 ++++++++++++++++++++++++ src/compiler/utils.ts | 50 +++++++++- src/parser/shrimp.grammar | 8 +- src/parser/shrimp.terms.ts | 20 ++-- src/parser/shrimp.ts | 14 +-- src/parser/tests/exceptions.test.ts | 107 +++++++++++++++++++- 7 files changed, 380 insertions(+), 81 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index fb3dd9e..4e35747 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -255,7 +255,10 @@ export class Compiler { } case terms.FunctionDef: { - const { paramNames, bodyNodes } = getFunctionDefParts(node, input) + const { paramNames, bodyNodes, catchVariable, catchBody, finallyBody } = getFunctionDefParts( + node, + input + ) const instructions: ProgramItem[] = [] const functionLabel: Label = `.func_${this.fnLabelCount++}` const afterLabel: Label = `.after_${functionLabel}` @@ -263,9 +266,27 @@ export class Compiler { instructions.push(['JUMP', afterLabel]) instructions.push([`${functionLabel}:`]) - bodyNodes.forEach((bodyNode) => { - instructions.push(...this.#compileNode(bodyNode, input)) - }) + + const compileFunctionBody = () => { + const bodyInstructions: ProgramItem[] = [] + bodyNodes.forEach((bodyNode, index) => { + bodyInstructions.push(...this.#compileNode(bodyNode, input)) + if (index < bodyNodes.length - 1) { + bodyInstructions.push(['POP']) + } + }) + return bodyInstructions + } + + if (catchVariable || finallyBody) { + // If function has catch or finally, wrap body in try/catch/finally + instructions.push( + ...this.#compileTryCatchFinally(compileFunctionBody, catchVariable, catchBody, finallyBody, input) + ) + } else { + instructions.push(...compileFunctionBody()) + } + instructions.push(['RETURN']) instructions.push([`${afterLabel}:`]) @@ -338,60 +359,21 @@ export class Compiler { 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 + return this.#compileTryCatchFinally( + () => this.#compileNode(tryBlock, input), + catchVariable, + catchBody, + finallyBody, + input + ) } - case terms.ThrowStatement: { + case terms.Throw: { const children = getAllChildren(node) const [_throwKeyword, expression] = children if (!expression) { throw new CompilerError( - `ThrowStatement expected expression, got ${children.length} children`, + `Throw expected expression, got ${children.length} children`, node.from, node.to ) @@ -606,4 +588,52 @@ export class Compiler { ) } } + + #compileTryCatchFinally( + compileTryBody: () => ProgramItem[], + catchVariable: string | undefined, + catchBody: SyntaxNode | undefined, + finallyBody: SyntaxNode | undefined, + input: string + ): ProgramItem[] { + 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]) + instructions.push(...compileTryBody()) + instructions.push(['POP_TRY']) + instructions.push(['JUMP', finallyBody ? finallyLabel : endLabel]) + + // catch block + instructions.push([`${catchLabel}:`]) + if (catchBody && catchVariable) { + 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 + } } diff --git a/src/compiler/tests/exceptions.test.ts b/src/compiler/tests/exceptions.test.ts index 19e3503..9a71012 100644 --- a/src/compiler/tests/exceptions.test.ts +++ b/src/compiler/tests/exceptions.test.ts @@ -181,3 +181,131 @@ describe('exception handling', () => { `).toEvaluateTo(30) }) }) + +describe('function-level exception handling', () => { + test('function with catch - no error', () => { + expect(` + read-file = do path: + path + catch e: + 'default' + end + + read-file test.txt + `).toEvaluateTo('test.txt') + }) + + test('function with catch - error thrown', () => { + expect(` + read-file = do path: + throw 'file not found' + catch e: + 'default' + end + + read-file test.txt + `).toEvaluateTo('default') + }) + + test('function with catch - error variable binding', () => { + expect(` + safe-call = do: + throw 'operation failed' + catch err: + err + end + + safe-call + `).toEvaluateTo('operation failed') + }) + + test('function with finally - always runs', () => { + expect(` + counter = 0 + increment-task = do: + result = 42 + result + finally: + counter = counter + 1 + end + + x = increment-task + y = increment-task + counter + `).toEvaluateTo(2) + }) + + test('function with finally - return value from body', () => { + expect(` + get-value = do: + 100 + finally: + 999 + end + + get-value + `).toEvaluateTo(100) + }) + + test('function with catch and finally', () => { + expect(` + cleanup-count = 0 + safe-op = do should-fail: + if should-fail: + throw 'failed' + end + 'success' + catch e: + 'caught' + finally: + cleanup-count = cleanup-count + 1 + end + + result1 = safe-op false + result2 = safe-op true + cleanup-count + `).toEvaluateTo(2) + }) + + test('function with catch and finally - catch return value', () => { + expect(` + safe-fail = do: + throw 'always fails' + catch e: + 'error handled' + finally: + noop = 1 + end + + safe-fail + `).toEvaluateTo('error handled') + }) + + test('function without catch/finally still works', () => { + expect(` + regular = do x: + x + 10 + end + + regular 5 + `).toEvaluateTo(15) + }) + + test('nested functions with catch', () => { + expect(` + inner = do: + throw 'inner error' + catch e: + 'inner caught' + end + + outer = do: + inner + catch e: + 'outer caught' + end + + outer + `).toEvaluateTo('inner caught') + }) +}) diff --git a/src/compiler/utils.ts b/src/compiler/utils.ts index 96de20f..7fd9cf5 100644 --- a/src/compiler/utils.ts +++ b/src/compiler/utils.ts @@ -59,11 +59,11 @@ export const getAssignmentParts = (node: SyntaxNode) => { export const getFunctionDefParts = (node: SyntaxNode, input: string) => { const children = getAllChildren(node) - const [fnKeyword, paramsNode, colon, ...bodyNodes] = children + const [fnKeyword, paramsNode, colon, ...rest] = children - if (!fnKeyword || !paramsNode || !colon || !bodyNodes) { + if (!fnKeyword || !paramsNode || !colon || !rest) { throw new CompilerError( - `FunctionDef expected 5 children, got ${children.length}`, + `FunctionDef expected at least 4 children, got ${children.length}`, node.from, node.to ) @@ -80,8 +80,48 @@ export const getFunctionDefParts = (node: SyntaxNode, input: string) => { return input.slice(param.from, param.to) }) - const bodyWithoutEnd = bodyNodes.slice(0, -1) - return { paramNames, bodyNodes: bodyWithoutEnd } + // Separate body nodes from catch/finally/end + const bodyNodes: SyntaxNode[] = [] + let catchExpr: SyntaxNode | undefined + let catchVariable: string | undefined + let catchBody: SyntaxNode | undefined + let finallyExpr: SyntaxNode | undefined + let finallyBody: SyntaxNode | undefined + + for (const child of rest) { + 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 + } else if (child.type.name === 'keyword' && input.slice(child.from, child.to) === 'end') { + // Skip the end keyword + } else { + bodyNodes.push(child) + } + } + + return { paramNames, bodyNodes, catchVariable, catchBody, finallyBody } } export const getFunctionCallParts = (node: SyntaxNode, input: string) => { diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 297948c..455a9b2 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -52,7 +52,7 @@ consumeToTerminator { PipeExpr | ambiguousFunctionCall | TryExpr | - ThrowStatement | + Throw | IfExpr | FunctionDef | Assign | @@ -99,11 +99,11 @@ FunctionDef { } singleLineFunctionDef { - Do Params colon consumeToTerminator @specialize[@name=keyword] + Do Params colon consumeToTerminator CatchExpr? FinallyExpr? @specialize[@name=keyword] } multilineFunctionDef { - Do Params colon newlineOrSemicolon block @specialize[@name=keyword] + Do Params colon newlineOrSemicolon block CatchExpr? FinallyExpr? @specialize[@name=keyword] } IfExpr { @@ -158,7 +158,7 @@ TryBlock { block } -ThrowStatement { +Throw { @specialize[@name=keyword] expression } diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index 98bc49c..8a80f73 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -39,17 +39,17 @@ export const FunctionDef = 37, Params = 38, colon = 39, + CatchExpr = 40, keyword = 63, - Underscore = 41, - Array = 42, - Null = 43, - ConditionalOp = 44, - PositionalArg = 45, - TryExpr = 47, - CatchExpr = 49, - TryBlock = 51, - FinallyExpr = 52, - ThrowStatement = 54, + TryBlock = 42, + FinallyExpr = 43, + Underscore = 46, + Array = 47, + Null = 48, + ConditionalOp = 49, + PositionalArg = 50, + TryExpr = 52, + Throw = 54, IfExpr = 56, SingleLineThenBlock = 58, ThenBlock = 59, diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 14d2ece..f1f7a33 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -4,13 +4,13 @@ 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, try:96, catch:100, finally:106, throw:110, if:114, elseif:122, else:126} +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: "7^QYQbOOO#tQcO'#CvO$qOSO'#CxO%PQbO'#E`OOQ`'#DR'#DROOQa'#DO'#DOO&SQbO'#DWO'XQcO'#ETOOQa'#ET'#ETO)cQcO'#ESO*bQRO'#CwO+WQcO'#EOO+hQcO'#EOO+rQbO'#CuO,jOpO'#CsOOQ`'#EP'#EPO,oQbO'#EOO,vQQO'#EfOOQ`'#D]'#D]O,{QbO'#DdO,{QbO'#EhOOQ`'#Df'#DfO-pQRO'#DnOOQ`'#EO'#EOO-uQQO'#D}OOQ`'#D}'#D}OOQ`'#Do'#DoQYQbOOO-}QbO'#DPOOQa'#ES'#ESOOQ`'#DZ'#DZOOQ`'#Ee'#EeOOQ`'#Dv'#DvO.XQbO,59^O.rQbO'#CzO.zQWO'#C{OOOO'#EV'#EVOOOO'#Dp'#DpO/`OSO,59dOOQa,59d,59dOOQ`'#Dr'#DrO/nQbO'#DSO/vQQO,5:zOOQ`'#Dq'#DqO/{QbO,59rO0SQQO,59jOOQa,59r,59rO0_QbO,59rO,{QbO,59cO,{QbO,59cO,{QbO,59cO,{QbO,59tO,{QbO,59tO,{QbO,59tO0iQRO,59aO0pQRO,59aO1RQRO,59aO0|QQO,59aO1^QQO,59aO1fObO,59_O1qQbO'#DwO1|QbO,59]O2eQbO,5;QOOQ`,5:O,5:OO2xQRO,5;SO3PQRO,5;SO3[QbO,5:YOOQ`,5:i,5:iOOQ`-E7m-E7mOOQ`,59k,59kOOQ`-E7t-E7tOOOO,59f,59fOOOO,59g,59gOOOO-E7n-E7nOOQa1G/O1G/OOOQ`-E7p-E7pO3lQbO1G0fOOQ`-E7o-E7oO4PQQO1G/UOOQa1G/^1G/^O4[QbO1G/^OOQO'#Dt'#DtO4PQQO1G/UOOQa1G/U1G/UOOQ`'#Du'#DuO4[QbO1G/^OOQa1G.}1G.}O5TQcO1G.}O5_QcO1G.}O5iQcO1G.}OOQa1G/`1G/`O7XQcO1G/`O7`QcO1G/`O7gQcO1G/`OOQa1G.{1G.{OOQa1G.y1G.yO!aQbO'#CvO&ZQbO'#CrOOQ`,5:c,5:cOOQ`-E7u-E7uO7nQbO1G0lO7yQbO1G0mO8gQbO1G0nOOQ`1G/t1G/tO8zQbO7+&QO9PQbO7+&RO9gQQO7+$pOOQa7+$p7+$pO9rQbO7+$xOOQa7+$x7+$xOOQO-E7r-E7rOOQ`-E7s-E7sO9|QbO'#D_O:RQQO'#DbOOQ`7+&W7+&WO:WQbO7+&WO:]QbO7+&WOOQ`'#Ds'#DsO:eQQO'#DsO:jQbO'#EbOOQ`'#Da'#DaO;^QbO7+&XOOQ`'#Dh'#DhO;iQbO7+&YO;nQbO7+&ZOOQ`<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`<bQbO1G/_OOQ`1G/_1G/_OOQ`AN?^AN?^OOQ`AN?_AN?_O>xQbOAN?_O,{QbO'#DjOOQ`'#Dx'#DxO>}QbOAN?aO?YQQO'#DlOOQ`AN?aAN?aO?_QbOAN?aOOQ`G24rG24rOOQ`G24tG24tO?dQbOG24tO?iQbO7+$vOOQ`7+$v7+$vOOQ`7+$y7+$yOOQ`G24yG24yO@SQRO,5:UO@ZQRO,5:UOOQ`-E7v-E7vOOQ`G24{G24{O@fQbOG24{O@kQQO,5:WOOQ`LD*`LD*`OOQ`<bQbO1G/rO;^QbO7+%[OOQ`7+%^7+%^OOQ`<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!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#^~", + 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#{U6zUmS!OQOt#{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!TQmSOt#{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: 1415 + tokenPrec: 1463 }) diff --git a/src/parser/tests/exceptions.test.ts b/src/parser/tests/exceptions.test.ts index aafdcf9..039a279 100644 --- a/src/parser/tests/exceptions.test.ts +++ b/src/parser/tests/exceptions.test.ts @@ -127,7 +127,7 @@ describe('try/catch/finally/throw', () => { test('parses throw statement with string', () => { expect("throw 'error message'").toMatchTree(` - ThrowStatement + Throw keyword throw String StringFragment error message @@ -136,7 +136,7 @@ describe('try/catch/finally/throw', () => { test('parses throw statement with identifier', () => { expect('throw error-object').toMatchTree(` - ThrowStatement + Throw keyword throw Identifier error-object `) @@ -144,7 +144,7 @@ describe('try/catch/finally/throw', () => { test('parses throw statement with dict', () => { expect('throw [type=validation-error message=failed]').toMatchTree(` - ThrowStatement + Throw keyword throw Dict NamedArg @@ -175,3 +175,104 @@ describe('try/catch/finally/throw', () => { `) }) }) + +describe('function-level exception handling', () => { + test('parses function with catch', () => { + expect(`read-file = do path: + read-data path + catch e: + empty-string + end`).toMatchTree(` + Assign + AssignableIdentifier read-file + Eq = + FunctionDef + Do do + Params + Identifier path + colon : + FunctionCall + Identifier read-data + PositionalArg + Identifier path + CatchExpr + keyword catch + Identifier e + colon : + TryBlock + FunctionCallOrIdentifier + Identifier empty-string + keyword end + `) + }) + + test('parses function with finally', () => { + expect(`cleanup-task = do x: + do-work x + finally: + close-resources + end`).toMatchTree(` + Assign + AssignableIdentifier cleanup-task + Eq = + FunctionDef + Do do + Params + Identifier x + colon : + FunctionCall + Identifier do-work + PositionalArg + Identifier x + FinallyExpr + keyword finally + colon : + TryBlock + FunctionCallOrIdentifier + Identifier close-resources + keyword end + `) + }) + + test('parses function with catch and finally', () => { + expect(`safe-operation = do x: + risky-work x + catch err: + log err + default-value + finally: + cleanup + end`).toMatchTree(` + Assign + AssignableIdentifier safe-operation + Eq = + FunctionDef + Do do + Params + Identifier x + colon : + FunctionCall + Identifier risky-work + PositionalArg + Identifier x + CatchExpr + keyword catch + Identifier err + colon : + TryBlock + FunctionCall + Identifier log + PositionalArg + Identifier err + FunctionCallOrIdentifier + Identifier default-value + FinallyExpr + keyword finally + colon : + TryBlock + FunctionCallOrIdentifier + Identifier cleanup + keyword end + `) + }) +})