try/catch/throw/finally

This commit is contained in:
Chris Wanstrath 2025-10-29 14:22:57 -07:00
parent 6ca8d05c66
commit 701ca98401
7 changed files with 554 additions and 21 deletions

View File

@ -17,6 +17,7 @@ import {
getNamedArgParts, getNamedArgParts,
getPipeExprParts, getPipeExprParts,
getStringParts, getStringParts,
getTryExprParts,
} from '#compiler/utils' } from '#compiler/utils'
const DEBUG = false const DEBUG = false
@ -51,6 +52,7 @@ export class Compiler {
instructions: ProgramItem[] = [] instructions: ProgramItem[] = []
fnLabelCount = 0 fnLabelCount = 0
ifLabelCount = 0 ifLabelCount = 0
tryLabelCount = 0
bytecode: Bytecode bytecode: Bytecode
pipeCounter = 0 pipeCounter = 0
@ -317,10 +319,87 @@ export class Compiler {
} }
case terms.ThenBlock: case terms.ThenBlock:
case terms.SingleLineThenBlock: { case terms.SingleLineThenBlock:
const instructions = getAllChildren(node) case terms.TryBlock: {
.map((child) => this.#compileNode(child, input)) const children = getAllChildren(node)
.flat() 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 return instructions
} }

View File

@ -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)
})
})

View File

@ -231,3 +231,62 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => {
return { objectName, property } 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,
}
}

View File

@ -51,6 +51,8 @@ item {
consumeToTerminator { consumeToTerminator {
PipeExpr | PipeExpr |
ambiguousFunctionCall | ambiguousFunctionCall |
TryExpr |
ThrowStatement |
IfExpr | IfExpr |
FunctionDef | FunctionDef |
Assign | Assign |
@ -132,6 +134,34 @@ SingleLineThenBlock {
consumeToTerminator consumeToTerminator
} }
TryExpr {
singleLineTry | multilineTry
}
singleLineTry {
@specialize[@name=keyword]<Identifier, "try"> colon consumeToTerminator CatchExpr? FinallyExpr? @specialize[@name=keyword]<Identifier, "end">
}
multilineTry {
@specialize[@name=keyword]<Identifier, "try"> colon newlineOrSemicolon TryBlock CatchExpr? FinallyExpr? @specialize[@name=keyword]<Identifier, "end">
}
CatchExpr {
@specialize[@name=keyword]<Identifier, "catch"> Identifier colon (newlineOrSemicolon TryBlock | consumeToTerminator)
}
FinallyExpr {
@specialize[@name=keyword]<Identifier, "finally"> colon (newlineOrSemicolon TryBlock | consumeToTerminator)
}
TryBlock {
block
}
ThrowStatement {
@specialize[@name=keyword]<Identifier, "throw"> expression
}
ConditionalOp { ConditionalOp {
expression !comparison EqEq expression | expression !comparison EqEq expression |
expression !comparison Neq expression | expression !comparison Neq expression |

View File

@ -39,15 +39,20 @@ export const
FunctionDef = 37, FunctionDef = 37,
Params = 38, Params = 38,
colon = 39, colon = 39,
keyword = 54, keyword = 63,
Underscore = 41, Underscore = 41,
Array = 42, Array = 42,
Null = 43, Null = 43,
ConditionalOp = 44, ConditionalOp = 44,
PositionalArg = 45, PositionalArg = 45,
IfExpr = 47, TryExpr = 47,
SingleLineThenBlock = 49, CatchExpr = 49,
ThenBlock = 50, TryBlock = 51,
ElseIfExpr = 51, FinallyExpr = 52,
ElseExpr = 53, ThrowStatement = 54,
Assign = 55 IfExpr = 56,
SingleLineThenBlock = 58,
ThenBlock = 59,
ElseIfExpr = 60,
ElseExpr = 62,
Assign = 64

View File

@ -4,14 +4,14 @@ import {operatorTokenizer} from "./operatorTokenizer"
import {tokenizer, specializeKeyword} from "./tokenizer" import {tokenizer, specializeKeyword} from "./tokenizer"
import {trackScope} from "./scopeTracker" import {trackScope} from "./scopeTracker"
import {highlighting} from "./highlight" 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({ export const parser = LRParser.deserialize({
version: 14, 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`<<Ic<<IcOOQ`'#Dj'#DjO7jQQO'#DjO7oQbO'#EXO8VQbO<<IdOOQa<<H[<<H[OOQa<<Hd<<HdOOQ`<<Ii<<IiOOQ`'#D`'#D`O8[QbO<<IjOOQ`,5:U,5:UOOQ`-E7h-E7hOOQ`AN?OAN?OO,bQbO'#DaOOQ`'#Do'#DoO8gQbOAN?UO8rQQO'#DcOOQ`AN?UAN?UO8wQbOAN?UO8|QRO,59{O9TQRO,59{OOQ`-E7m-E7mOOQ`G24pG24pO9`QbOG24pO9eQQO,59}O9jQQO1G/gOOQ`LD*[LD*[O6WQbO1G/iO7SQbO7+%ROOQ`7+%T7+%TOOQ`<<Hm<<Hm", 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`<<Il<<IlO<[QbO<<ImOOQa<<H[<<H[OOQa<<Hd<<HdO<aQQO,59yO<fQbO,59|OOQ`<<Ir<<IrO<yQbO<<IrOOQ`,5:_,5:_OOQ`-E7q-E7qOOQ`<<Is<<IsO=OQbO<<IsO=TQbO<<IsOOQ`<<It<<ItOOQ`'#Di'#DiO=]QbO<<IuOOQ`AN?XAN?XO=hQbO1G/eO9PQbO1G/hOOQ`1G/h1G/hOOQ`AN?^AN?^OOQ`AN?_AN?_O={QbOAN?_O,{QbO'#DjOOQ`'#Dx'#DxO>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`<<Hk<<HkO?nQQO1G/pOOQ`LD*gLD*gO9PQbO1G/rO;nQbO7+%[OOQ`7+%^7+%^OOQ`<<Hv<<Hv",
stateData: "9r~O!fOS!gOS~O_PO`cOaWOb^OcROhWOpWOqWO{WO!QaO!l]O!oQO!vTO!wUO!xfO~O_jOaWOb^OcROhWOpWOqWOtiOykO{WO!l]O!oQO!vTO!wUO!OjX!xjX#RjX!}jXxjX~OP!mXQ!mXR!mXS!mXT!mXU!mXW!mXX!mXY!mXZ!mX[!mX]!mX^!mX~P!ZOmqO!otO!qoO!rpO~O_uOwvP~O_jOaWOb^OhWOpWOqWOtiO{WO!l]O!oQO!vTO!wUO!xxO~O!|{O~P${O_jOaWOb^OcROhWOpWOqWOtiOykO{WO!l]O!oQO!vTO!wUO~OP!nXQ!nXR!nXS!nXT!nXU!nXW!nXX!nXY!nXZ!nX[!nX]!nX^!nX!x!nX#R!nX!}!nXx!nX~P%}OP!mXQ!mXR!mXS!mXT!mXU!mXW!mXX!mXY!mXZ!mX[!mX]!mX^!mX~O!x!iX#R!iXx!iX~P(UOT!ROU!SOW!QOX!QOY!QOZ!QO[!QO]!QO~OP!OOQ!OOR!POS!PO^}O~P)^OP!OOQ!OOR!POS!PO!x!iX#R!iXx!iX~OT!ROU!SO!x!iX#R!iXx!iX~O_POaWOb^OcROhWOpWOqWO{WO!l]O!oQO!vTO!wUO~O!k!YO~O!O!ZO!x!iX#R!iXx!iX~O_jOaWOb^OhWOpWOqWO{WO!l]O!oQO!vTO!wUO~OV!_O~O!x!`O#R!`O~OcROy!bO~P,bO!Ofa!xfa#Rfa!}faxfa~P%}O_!dO!l]O~O!o!eO!q!eO!r!eO!s!eO!t!eO!u!eO~OmqO!o!gO!qoO!rpO~O_uOwvX~Ow!iO~O!|!lO~P${OtiO!x!nO!|!pO~O!x!qO!|!lO~P,bO!}!{O~P(UOP!OOQ!OOR!POS!PO!}!{O~OT!ROU!SO!}!{O~O!O!ZO!}!{O~O_!|Oh!|O!l]O~O_!}Ob^O!l]O~O!O!ZO!xea#Rea!}eaxea~Ow#RO~P)^OT!ROU!SOw#RO~O`cO!QaO~P+VO`cO!QaO!x#UO~P+VOtiO!x!nO!|#WO~O!x!qO!|#YO~P,bO^}ORkiSki!xki#Rki!}kixki~OPkiQki~P3OOP!OOQ!OO~P3OOP!OOQ!OORkiSki!xki#Rki!}kixki~OW!QOX!QOY!QOZ!QO[!QO]!QOT|i!x|i#R|i!}|iw|ix|i~OU!SO~P4gOU!SO~P4yOU|i~P4gO`cO!QaO!x#_O~P+VOx#`O~O`cO!QaO!x#aOx!{P~P+VOtiO!x!nO!|#eO~O!x!qO!|#fO~P,bOx#gO~O`cO!QaO!x#aOx!{P!U!{P!W!{P~P+VO!x#jO~O`cO!QaO!x#aOx!{X!U!{X!W!{X~P+VOx#lO~Ox#qO!U#mO!W#pO~Ox#vO!U#mO!W#pO~Ow#xO~Ox#vO~Ow#yO~P)^OT!ROU!SOw#yO~Ox#zO~O!x#{O~O!x#|O~Ohq~", stateData: "?v~O!oOS!pOS~O_PO`fOaWOb^OcROhWOpWOqWO{WO!QaO!XcO!ZdO!u]O!xQO#PTO#QUO#RiO~O_mOaWOb^OcROhWOpWOqWOtlOynO{WO!u]O!xQO#PTO#QUO!OjX#RjX#^jX#WjXxjX!SjX!VjX~OP!vXQ!vXR!vXS!vXT!vXU!vXW!vXX!vXY!vXZ!vX[!vX]!vX^!vX~P!aOmtO!xwO!zrO!{sO~O_xOwvP~O_mOaWOb^OhWOpWOqWOtlO{WO!u]O!xQO#PTO#QUO#R{O~O#V!OO~P%XO_mOaWOb^OcROhWOpWOqWOtlOynO{WO!u]O!xQO#PTO#QUO~OP!wXQ!wXR!wXS!wXT!wXU!wXW!wXX!wXY!wXZ!wX[!wX]!wX^!wX#R!wX#^!wX#W!wXx!wX!S!wX!V!wX~P&ZOP!vXQ!vXR!vXS!vXT!vXU!vXW!vXX!vXY!vXZ!vX[!vX]!vX^!vX~O#R!rX#^!rXx!rX!S!rX!V!rX~P(hOT!UOU!VOW!TOX!TOY!TOZ!TO[!TO]!TO~OP!ROQ!ROR!SOS!SO^!QO~P)vO#R!rX#^!rXx!rX!S!rX!V!rX~OP!ROQ!ROR!SOS!SO~P*uOT!UOU!VO~P*uO_POaWOb^OcROhWOpWOqWO{WO!u]O!xQO#PTO#QUO~O!t!]O~O!O!^O~P*uOw!`O~O_mOaWOb^OhWOpWOqWO{WO!u]O!xQO#PTO#QUO~OV!dO~O#R!eO#^!eO~OcROy!gO~P,{O!Ofa#Rfa#^fa#Wfaxfa!Sfa!Vfa~P&ZO_!iO!u]O~O!x!jO!z!jO!{!jO!|!jO!}!jO#O!jO~OmtO!x!lO!zrO!{sO~O_xOwvX~Ow!nO~O#V!qO~P%XOtlO#R!sO#V!uO~O#R!vO#V!qO~P,{O#W#QO~P(hOP!ROQ!ROR!SOS!SO#W#QO~OT!UOU!VO#W#QO~O!O!^O#W#QO~O_#ROh#RO!u]O~O_#SOb^O!u]O~O!O!^O#Rea#^ea#Weaxea!Sea!Vea~O`fO!QaO!XcO!ZdO#R#XO~P+rOw#YO~P)vOT!UOU!VOw#YO~O`fO!QaO!XcO!ZdO~P+rO`fO!QaO!XcO!ZdO#R#]O~P+rOtlO#R!sO#V#_O~O#R!vO#V#aO~P,{O^!QORkiSki#Rki#^ki#Wkixki!Ski!Vki~OPkiQki~P4fOP!ROQ!RO~P4fOP!ROQ!RORkiSki#Rki#^ki#Wkixki!Ski!Vki~OW!TOX!TOY!TOZ!TO[!TO]!TOT|i#R|i#^|i#W|iw|ix|i!S|i!V|i~OU!VO~P6ZOU!VO~P6mOU|i~P6ZOx#fO!S#dO!V#eO~O`fO!QaO!XcO!ZdO#R#iOx#UP!S#UP!V#UP~P+rO`fO!QaO!XcO!ZdO#R#pO~P+rOx#qO~O`fO!QaO!XcO!ZdO#R#iOx#UP~P+rOtlO#R!sO#V#sO~O#R!vO#V#tO~P,{O_#uO~Ow#vO~Ox#wO~Ox#wO!V#eO~O#R#yO~O`fO!QaO!XcO!ZdO#R#iOx#UX!S#UX!V#UX!_#UX!a#UX~P+rOx#{O!S#dO!V#eO~Ox$OO~O`fO!QaO!XcO!ZdO#R#iOx#UP!_#UP!a#UP~P+rOx$RO~Ow$SO~O`fO!QaO!XcO!ZdO#R$TO~P+rOx$VO~Ox$WO~Ox$WO!V#eO~Ox$^O!_$YO!a$]O~O`fO!QaO!XcO!ZdO#R$`O~P+rOx$cO~Ox$gO!_$YO!a$]O~Ow$iO~Ox$gO~O`fO!QaO!XcO!ZdO#R#iOx#UP!V#UP~P+rOw$kO~P)vOT!UOU!VOw$kO~Ox$lO~O#R$mO~O#R$nO~Ohq~",
goto: ".}#RPPPPPPPPPPPPPPPPPPPPP#S#c#qP$i#c%g%|P&o&oPP%|&sP'W'qPPP%|P't(aP(hP(t(w)QP)UP(h)[)b)h)n)t)}*X*c*l*sPPPP*y*}+cPP+u-SP-yPPPPPPPP-}-}.bPP.j.q.qddOh!_!i#R#U#_#c#{#|R!W]i_O]h!Z!_!i#R#U#_#c#{#|fPO]h!_!i#R#U#_#c#{#|xjPUVainy|}!O!P!Q!R!S!m!r!}#O#X#mR!}!ZfVO]h!_!i#R#U#_#c#{#|xWPUVainy|}!O!P!Q!R!S!m!r!}#O#X#mQ!doQ!|!YR#O!ZdZOh!_!i#R#U#_#c#{#|Q!U]Q!s!OR!v!P!aWOPUV]ahiny|}!O!P!Q!R!S!_!i!m!r!}#O#R#U#X#_#c#m#{#|TqQsYlPVn!}#OQzUQ!kyX!nz!k!o#VddOh!_!i#R#U#_#c#{#|YkPVn!}#OQ!W]R!biRwRd[Oh!_!i#R#U#_#c#{#|Q!V]Q!^aQ!w!SQ!y!RR#t#mZlPVn!}#OedOh!_!i#R#U#_#c#{#|R#^#RQ#i#_Q#}#{R$O#|T#n#i#oQ#r#iR#w#oQhOR!ahQsQR!fsQyUR!jyQvRR!hvW#c#U#_#{#|R#k#cQ!ozQ#V!kT#Z!o#VQ!r|Q#X!mT#[!r#XWnPV!}#OR!cnS![`!XR#Q![Q#o#iR#u#oTgOhSeOhQ#S!_Q#T!iQ#]#RZ#b#U#_#c#{#|d`Oh!_!i#R#U#_#c#{#|Q!X]R#P!ZfYO]h!_!i#R#U#_#c#{#|YkPVn!}#OQ|UQ!]aQ!biQ!myW!q|!m!r#XQ!s}Q!t!OQ!u!PQ!w!QQ!x!RQ!z!SR#s#mdXOh!_!i#R#U#_#c#{#|xjPUVainy|}!O!P!Q!R!S!m!r!}#O#X#mR!T]TrQssSOPV]hin!_!i!}#O#R#U#_#c#{#|Q#d#UV#h#_#{#|ZmPVn!}#OebOh!_!i#R#U#_#c#{#|", goto: "2l#^PPPPPPPPPPPPPPPPPPPPP#_#t$YP%X#t&^&yP's'sPP&y'wP([({PPP&yP)O)qP)xP*[P*b*kP)xP)xP*w*z+TP+XP)x+_+e+k+q+w,T,_,i,r,yPPPP-P-T-uPP._/uP0sPPPPPPPP0w0w1bPP1o1v1v2Y2YpgOk!`!d!n#X#Y#]#k#p#v$S$T$`$m$nR!Z]u_O]k!^!`!d!n#X#Y#]#k#p#v$S$T$`$m$nrPO]k!`!d!n#X#Y#]#k#p#v$S$T$`$m$nzmPUVcdlq|!P!Q!R!S!T!U!V!r!w#S#T#`$YR#S!^rVO]k!`!d!n#X#Y#]#k#p#v$S$T$`$m$nzWPUVcdlq|!P!Q!R!S!T!U!V!r!w#S#T#`$YQ!irQ#R!]R#T!^pZOk!`!d!n#X#Y#]#k#p#v$S$T$`$m$nQ!X]Q!x!RR!{!S!oWOPUV]cdklq|!P!Q!R!S!T!U!V!`!d!n!r!w#S#T#X#Y#]#`#k#p#v$S$T$Y$`$m$nTtQvYoPVq#S#TQ}UQ!p|X!s}!p!t#^pgOk!`!d!n#X#Y#]#k#p#v$S$T$`$m$nYnPVq#S#TQ!Z]R!glRzRp[Ok!`!d!n#X#Y#]#k#p#v$S$T$`$m$nQ!Y]Q!cdQ!|!VQ#O!UR$e$YZoPVq#S#TqgOk!`!d!n#X#Y#]#k#p#v$S$T$`$m$nQ#h#WR#}#mQ#m#XQ$b$TR$j$`Q#g#WQ#x#hQ#|#mR$X#}R#o#YQ$Q#pQ$o$mR$p$nT$Z$Q$[Q$_$QR$h$[QkOR!fkQvQR!kvQ|UR!o|QyRR!my^#k#X#]#p$T$`$m$nR#z#kQ!t}Q#^!pT#b!t#^Q!w!PQ#`!rT#c!w#`WqPV#S#TR!hqS!_`![R#V!_Q$[$QR$f$[TjOkShOkQ#W!`Q#Z!dQ#[!n`#j#X#]#k#p$T$`$m$nQ#n#YQ$U#vR$a$Sp`Ok!`!d!n#X#Y#]#k#p#v$S$T$`$m$nQ![]R#U!^rYO]k!`!d!n#X#Y#]#k#p#v$S$T$`$m$nYnPVq#S#TQ!PUQ!acQ!bdQ!glQ!r|W!v!P!r!w#`Q!x!QQ!y!RQ!z!SQ!|!TQ!}!UQ#P!VR$d$YpXOk!`!d!n#X#Y#]#k#p#v$S$T$`$m$nzmPUVcdlq|!P!Q!R!S!T!U!V!r!w#S#T#`$YR!W]TuQv!PSOPV]klq!`!d!n#S#T#X#Y#]#k#p#v$S$T$`$m$nU#l#X$T$`Q#r#]V$P#p$m$nZpPVq#S#TqbOk!`!d!n#X#Y#]#k#p#v$S$T$`$m$nqeOk!`!d!n#X#Y#]#k#p#v$S$T$`$m$n",
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Dict NamedArg NamedArgPrefix FunctionDef Params colon keyword Underscore Array Null ConditionalOp PositionalArg operator IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign", nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo Identifier AssignableIdentifier Word IdentifierBeforeDot Do Program PipeExpr FunctionCall DotGet Number ParenExpr FunctionCallOrIdentifier BinOp String StringFragment Interpolation EscapeSeq Boolean Regex Dict NamedArg NamedArgPrefix FunctionDef Params colon keyword Underscore Array Null ConditionalOp PositionalArg operator TryExpr keyword CatchExpr keyword TryBlock FinallyExpr keyword ThrowStatement keyword IfExpr keyword SingleLineThenBlock ThenBlock ElseIfExpr keyword ElseExpr keyword Assign",
maxTerm: 95, maxTerm: 106,
context: trackScope, context: trackScope,
nodeProps: [ nodeProps: [
["closedBy", 39,"end"] ["closedBy", 39,"end"]
@ -19,9 +19,9 @@ export const parser = LRParser.deserialize({
propSources: [highlighting], propSources: [highlighting],
skippedNodes: [0], skippedNodes: [0],
repeatNodeCount: 10, repeatNodeCount: 10,
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#c<z#c#f7^#f#g=q#g#h7^#h#i>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#Y<T#Y#o7^#o;'S#{;'S;=`$d<%lO#{U<[YpQmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=RY!sWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=xY!uWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^>o[!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~", 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#c<z#c#f7^#f#g=q#g#h7^#h#i>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#Y<T#Y#o7^#o;'S#{;'S;=`$d<%lO#{U<[YpQmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=RY!|WmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^=xY#OWmSOt#{uw#{x!_#{!_!`8R!`#O#{#P#T#{#T#o7^#o;'S#{;'S;=`$d<%lO#{^>o[!}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!k~~", 11)], tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO!t~~", 11)],
topRules: {"Program":[0,20]}, 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}], 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
}) })

View File

@ -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
`)
})
})