From a6261fce7f365ef01b09226dc36f19392ed00e21 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 29 Oct 2025 14:38:23 -0700 Subject: [PATCH] implicit function-level try blocks --- src/compiler/compiler.ts | 135 ++++++++++++++++---------- 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, 381 insertions(+), 81 deletions(-) diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index fb3dd9e..b0003d5 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,28 @@ 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 function has catch or finally, wrap body in try/catch/finally + if (catchVariable || finallyBody) { + instructions.push( + ...this.#compileTryCatchFinally(compileFunctionBody, catchVariable, catchBody, finallyBody, input) + ) + } else { + // Normal function without catch/finally + instructions.push(...compileFunctionBody()) + } + instructions.push(['RETURN']) instructions.push([`${afterLabel}:`]) @@ -338,60 +360,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 +589,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 + `) + }) +})