From 7bcd582dc68e3daf698559123d42d7b7bbac21db Mon Sep 17 00:00:00 2001
From: Chris Wanstrath
Date: Sat, 1 Nov 2025 16:31:18 -0700
Subject: [PATCH] what have i done
---
src/compiler/compiler.ts | 45 +++
src/compiler/tests/function-blocks.test.ts | 136 +++++++++
src/parser/shrimp.grammar | 5 +
src/parser/shrimp.terms.ts | 17 +-
src/parser/shrimp.ts | 18 +-
src/parser/tests/function-blocks.test.ts | 303 +++++++++++++++++++++
6 files changed, 507 insertions(+), 17 deletions(-)
create mode 100644 src/compiler/tests/function-blocks.test.ts
create mode 100644 src/parser/tests/function-blocks.test.ts
diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts
index 86cb600..be939cf 100644
--- a/src/compiler/compiler.ts
+++ b/src/compiler/compiler.ts
@@ -404,6 +404,51 @@ export class Compiler {
return instructions
}
+ case terms.FunctionCallWithBlock: {
+ const [fn, _colon, ...block] = getAllChildren(node)
+ let instructions: ProgramItem[] = []
+
+ const fnLabel: Label = `.func_${this.fnLabelCount++}`
+ const afterLabel: Label = `.after_${fnLabel}`
+
+ instructions.push(['JUMP', afterLabel])
+ instructions.push([`${fnLabel}:`])
+ instructions.push(
+ ...block.filter(x => x.type.name !== 'keyword')
+ .map(x => this.#compileNode(x!, input))
+ .flat()
+ )
+ instructions.push(['RETURN'])
+ instructions.push([`${afterLabel}:`])
+
+ if (fn?.type.id === terms.FunctionCallOrIdentifier) {
+ instructions.push(['LOAD', input.slice(fn!.from, fn!.to)])
+ instructions.push(['MAKE_FUNCTION', [], fnLabel])
+ instructions.push(['PUSH', 1])
+ instructions.push(['PUSH', 0])
+ instructions.push(['CALL'])
+ } else if (fn?.type.id === terms.FunctionCall) {
+ let body = this.#compileNode(fn!, input)
+ const namedArgCount = (body[body.length - 2]![1] as number) * 2
+ const startSlice = body.length - namedArgCount - 3
+
+ body = [
+ ...body.slice(0, startSlice),
+ ['MAKE_FUNCTION', [], fnLabel],
+ ...body.slice(startSlice)
+ ]
+
+ // @ts-ignore
+ body[body.length - 3]![1] += 1
+ instructions.push(...body)
+
+ } else {
+ throw new Error(`FunctionCallWithBlock: Expected FunctionCallOrIdentifier or FunctionCall`)
+ }
+
+ return instructions
+ }
+
case terms.TryExpr: {
const { tryBlock, catchVariable, catchBody, finallyBody } = getTryExprParts(node, input)
diff --git a/src/compiler/tests/function-blocks.test.ts b/src/compiler/tests/function-blocks.test.ts
new file mode 100644
index 0000000..056affe
--- /dev/null
+++ b/src/compiler/tests/function-blocks.test.ts
@@ -0,0 +1,136 @@
+import { expect, describe, test } from 'bun:test'
+
+describe('single line function blocks', () => {
+ test('work with no args', () => {
+ expect(`trap = do x: x end; trap: true end`).toEvaluateTo(true)
+ })
+
+ test('work with one arg', () => {
+ expect(`trap = do x y: [ x (y) ] end; trap EXIT: true end`).toEvaluateTo(['EXIT', true])
+ })
+
+ test('work with named args', () => {
+ expect(`attach = do signal fn: [ signal (fn) ] end; attach signal='exit': true end`).toEvaluateTo(['exit', true])
+ })
+
+
+ test('work with dot-get', () => {
+ expect(`signals = [trap=do x y: [x (y)] end]; signals.trap 'EXIT': true end`).toEvaluateTo(['EXIT', true])
+ })
+})
+
+describe('multi line function blocks', () => {
+ test('work with no args', () => {
+ expect(`
+trap = do x: x end
+trap:
+ true
+end`).toEvaluateTo(true)
+ })
+
+ test('work with one arg', () => {
+ expect(`
+trap = do x y: [ x (y) ] end
+trap EXIT:
+ true
+end`).toEvaluateTo(['EXIT', true])
+ })
+
+ test('work with named args', () => {
+ expect(`
+attach = do signal fn: [ signal (fn) ] end
+attach signal='exit':
+ true
+end`).toEvaluateTo(['exit', true])
+ })
+
+
+ test('work with dot-get', () => {
+ expect(`
+signals = [trap=do x y: [x (y)] end]
+signals.trap 'EXIT':
+ true
+end`).toEvaluateTo(['EXIT', true])
+ })
+})
+
+describe('ribbit', () => {
+ test('head tag', () => {
+ expect(`
+ head:
+ title What up
+ meta charSet=UTF-8
+ meta name=viewport content='width=device-width, initial-scale=1, viewport-fit=cover'
+ end`).toEvaluateTo(`head`, {
+ head: () => 'head'
+ })
+ })
+
+ test('li', () => {
+ expect(`
+ list:
+ li border-bottom='1px solid black' one
+ li two
+ li three
+ end`).toEvaluateTo(`list`, {
+ list: () => 'list'
+ })
+ })
+
+ test('inline expressions', () => {
+ const buffer: string[] = []
+
+ const tagBlock = async (tagName: string, props = {}, fn: Function) => {
+ const attrs = Object.entries(props).map(([key, value]) => `${key}="${value}"`)
+ const space = attrs.length ? ' ' : ''
+
+ buffer.push(`<${tagName}${space}${attrs.join(' ')}>`)
+ await fn()
+ buffer.push(`${tagName}>`)
+ }
+
+ const tagCall = (tagName: string, atNamed: {}, ...args: any[]) => {
+ const attrs = Object.entries(atNamed).map(([key, value]) => `${key}="${value}"`)
+ const space = attrs.length ? ' ' : ''
+ const children = args
+ .reverse()
+ .map(a => a === null ? buffer.pop() : a)
+ .reverse().join(' ')
+ .replaceAll(' !!ribbit-nospace!! ', '')
+
+ buffer.push(`<${tagName}${space}${attrs.join(' ')}>${children}${tagName}>`)
+ }
+
+ const tag = async (tagName: string, atNamed: {}, ...args: any[]) => {
+ if (typeof args[0] === 'function')
+ await tagBlock(tagName, atNamed, args[0])
+ else
+ tagCall(tagName, atNamed, ...args)
+ }
+
+ expect(`
+ ribbit:
+ p class=container:
+ h1 class=bright style='font-family: helvetica' Heya
+ h2 man that is (b wild) (nospace) !
+ p Double the fun.
+ end
+ end`).toEvaluateTo(
+ `
+
Heya
+man that is wild!
+Double the fun.
+
`, {
+ ribbit: async (cb: Function) => {
+ await cb()
+ return buffer.join("\n")
+ },
+ p: (atNamed: {}, ...args: any[]) => tag('p', atNamed, ...args),
+ h1: (atNamed: {}, ...args: any[]) => tag('h1', atNamed, ...args),
+ h2: (atNamed: {}, ...args: any[]) => tag('h2', atNamed, ...args),
+ b: (atNamed: {}, ...args: any[]) => tag('b', atNamed, ...args),
+ nospace: () => '!!ribbit-nospace!!',
+ join: (...args: string[]) => args.join(''),
+ })
+ })
+})
\ No newline at end of file
diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar
index 4ed609a..01f95b6 100644
--- a/src/parser/shrimp.grammar
+++ b/src/parser/shrimp.grammar
@@ -59,6 +59,7 @@ item {
consumeToTerminator {
PipeExpr |
WhileExpr |
+ FunctionCallWithBlock |
ambiguousFunctionCall |
TryExpr |
Throw |
@@ -87,6 +88,10 @@ Block {
consumeToTerminator | newlineOrSemicolon block
}
+FunctionCallWithBlock {
+ ambiguousFunctionCall colon Block CatchExpr? FinallyExpr? end
+}
+
FunctionCallOrIdentifier {
DotGet | Identifier
}
diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts
index 498ae00..05f3d5a 100644
--- a/src/parser/shrimp.terms.ts
+++ b/src/parser/shrimp.terms.ts
@@ -47,7 +47,7 @@ export const
Null = 45,
colon = 46,
CatchExpr = 47,
- keyword = 67,
+ keyword = 68,
Block = 49,
FinallyExpr = 50,
Underscore = 53,
@@ -55,10 +55,11 @@ export const
ConditionalOp = 55,
PositionalArg = 56,
WhileExpr = 58,
- TryExpr = 60,
- Throw = 62,
- IfExpr = 64,
- ElseIfExpr = 66,
- ElseExpr = 68,
- CompoundAssign = 69,
- Assign = 70
+ FunctionCallWithBlock = 60,
+ TryExpr = 61,
+ Throw = 63,
+ IfExpr = 65,
+ ElseIfExpr = 67,
+ ElseExpr = 69,
+ CompoundAssign = 70,
+ Assign = 71
diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts
index c25351b..5afc5d8 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,null:90, catch:96, finally:102, end:104, while:118, try:122, throw:126, if:130, else:134}
+const spec_Identifier = {__proto__:null,null:90, catch:96, finally:102, end:104, while:118, try:124, throw:128, if:132, else:136}
export const parser = LRParser.deserialize({
version: 14,
- states: "7|QYQbOOO#zQcO'#C{O$wOSO'#C}OOQa'#DT'#DTO&QQbO'#DdO'fQcO'#E[OOQa'#E['#E[O(iQcO'#E[O)kQcO'#EZO*RQRO'#C|O+bQcO'#EVO+rQcO'#EVO+|QbO'#CzO,tOpO'#CxOOQ`'#EW'#EWO,yQbO'#EVO-QQRO'#DsOOQ`'#EV'#EVO-fQQO'#EUOOQ`'#EU'#EUOOQ`'#Du'#DuQYQbOOO-nQbO'#DWO-yQbO'#DhO.nQQO'#DjO-yQbO'#DlO-yQbO'#DnO.sQbO'#DUOOQa'#EZ'#EZOOQ`'#Df'#DfOOQ`'#Ej'#EjOOQ`'#D}'#D}O.}QbO,59cO/tQbO'#DPO/|QWO'#DQOOOO'#E^'#E^OOOO'#Dv'#DvO0bOSO,59iOOQa,59i,59iOOQ`'#Dw'#DwO0pQbO,5:OO0wQQO,59oOOQa,5:O,5:OO1SQbO,5:OO1^QbO,5:`O-yQbO,59hO-yQbO,59hO-yQbO,59hO-yQbO,5:PO-yQbO,5:PO-yQbO,5:PO1qQRO,59fO1xQRO,59fO2ZQRO,59fO2UQQO,59fO2fQQO,59fO2nObO,59dO2yQbO'#EOO3UQbO,59bO1^QbO,5:_OOQ`,5:p,5:pOOQ`-E7s-E7sOOQ`'#Dx'#DxO3pQbO'#DXO3{QbO'#DYOOQO'#Dy'#DyO3sQQO'#DXO4ZQQO,59rO4zQRO,5:SO5RQRO,5:SO5^QbO,5:UO5tQcO,5:WO6pQcO,5:WO7QQcO,5:WO7[QRO,5:YO7cQRO,5:YOOQ`,59p,59pOOQ`-E7{-E7{OOOO,59k,59kOOOO,59l,59lOOOO-E7t-E7tOOQa1G/T1G/TOOQ`-E7u-E7uO7nQQO1G/ZOOQa1G/j1G/jO7yQbO1G/jOOQO'#D{'#D{O7nQQO1G/ZOOQa1G/Z1G/ZOOQ`'#D|'#D|O7yQbO1G/jOOQ`1G/z1G/zOOQa1G/S1G/SO8uQcO1G/SO9PQcO1G/SO9ZQcO1G/SOOQa1G/k1G/kO;PQcO1G/kO;WQcO1G/kO;_QcO1G/kOOQa1G/Q1G/QOOQa1G/O1G/OO!dQbO'#C{O;fQbO'#CwOOQ`,5:j,5:jOOQ`-E7|-E7|OOQ`1G/y1G/yOOQ`-E7v-E7vO;sQQO,59sOOQO,59t,59tOOQO-E7w-E7wO;{QbO1G/^O5^QbO1G/nOOQ`'#D_'#D_OSQbO7+$xO>sQbO7+%YOOQ`'#Dz'#DzO>xQQO'#DzO>}QbO'#EgOOQ`,59y,59yO?qQbO'#D]O?vQQO'#D`OOQ`7+%[7+%[O?{QbO7+%[O@QQbO7+%[O@YQbO7+%`OOQa<OAN>OOAkQbOAN>OOApQbOAN>OO5^QbO1G/cOOQ`1G/f1G/fOOQ`AN>bAN>bOOQ`-E7}-E7}OOQ`AN>fAN>fOAxQbOAN>fO-yQbO,5:[O5^QbO,5:^OOQ`G23jG23jOA}QbOG23jOOQ`7+$}7+$}PAaQbO'#DpOOQ`G24QG24QOBSQRO1G/vOBZQRO1G/vOOQ`1G/x1G/xOOQ`LD)ULD)UO5^QbO7+%bOOQ`<RQbO7+%UOOQa7+%U7+%UOOQO-E7z-E7zOOQ`-E7{-E7{OOQ`'#D{'#D{O>]QQO'#D{O>bQbO'#EhOOQ`,59y,59yO?UQbO'#D]O?ZQQO'#D`OOQ`7+%[7+%[O?`QbO7+%[O?eQbO7+%[O?mQbO7+$xO?xQbO7+$xO@iQbO7+%YOOQ`7+%]7+%]O@nQbO7+%]O@sQbO7+%]O@{QbO7+%aOOQa<bAN>bOOQ`AN>OAN>OOBcQbOAN>OOBhQbOAN>OOOQ`AN>cAN>cOOQ`-E8O-E8OOOQ`AN>gAN>gOBpQbOAN>gO.PQbO,5:]O3yQbO,5:_OOQ`7+$}7+$}OOQ`G23jG23jOBuQbOG23jPBXQbO'#DqOOQ`G24RG24ROBzQRO1G/wOCRQRO1G/wOOQ`1G/y1G/yOOQ`LD)ULD)UO3yQbO7+%cOOQ`<q#c#f,Y#f#g?n#g#h,Y#h#i@k#i#o,Y#o#p#{#p#qBo#q;'S#{;'S;=`$d<%l~#{~O#{~~CYS$QUrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUrS!vYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UrS#YQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZrS!wYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!wYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O#R~~'aO#P~U'hUrS!|QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUrS#]QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWrSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZYrSmQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OWrSOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oWrSmQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^^rSOt#{uw#{x}#{}!O,Y!O!Q#{!Q![)S![!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{U,_[rSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{U-[UyQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U-sWrSOt#{uw#{x!P#{!P!Q.]!Q#O#{#P;'S#{;'S;=`$d<%lO#{U.b^rSOY/^YZ#{Zt/^tu0auw/^wx0ax!P/^!P!Q#{!Q!}/^!}#O5S#O#P2o#P;'S/^;'S;=`6T<%lO/^U/e^rSvQOY/^YZ#{Zt/^tu0auw/^wx0ax!P/^!P!Q3U!Q!}/^!}#O5S#O#P2o#P;'S/^;'S;=`6T<%lO/^Q0fXvQOY0aZ!P0a!P!Q1R!Q!}0a!}#O1p#O#P2o#P;'S0a;'S;=`3O<%lO0aQ1UP!P!Q1XQ1^UvQ#Z#[1X#]#^1X#a#b1X#g#h1X#i#j1X#m#n1XQ1sVOY1pZ#O1p#O#P2Y#P#Q0a#Q;'S1p;'S;=`2i<%lO1pQ2]SOY1pZ;'S1p;'S;=`2i<%lO1pQ2lP;=`<%l1pQ2rSOY0aZ;'S0a;'S;=`3O<%lO0aQ3RP;=`<%l0aU3ZWrSOt#{uw#{x!P#{!P!Q3s!Q#O#{#P;'S#{;'S;=`$d<%lO#{U3zbrSvQOt#{uw#{x#O#{#P#Z#{#Z#[3s#[#]#{#]#^3s#^#a#{#a#b3s#b#g#{#g#h3s#h#i#{#i#j3s#j#m#{#m#n3s#n;'S#{;'S;=`$d<%lO#{U5X[rSOY5SYZ#{Zt5Stu1puw5Swx1px#O5S#O#P2Y#P#Q/^#Q;'S5S;'S;=`5}<%lO5SU6QP;=`<%l5SU6WP;=`<%l/^U6bUrS!OQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6{W#XQrSOt#{uw#{x!_#{!_!`7e!`#O#{#P;'S#{;'S;=`$d<%lO#{U7jVrSOt#{uw#{x#O#{#P#Q8P#Q;'S#{;'S;=`$d<%lO#{U8WU#WQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~8oO#S~U8vU#[QrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U9aUrS!VQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U9x]rSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#U:q#U#o,Y#o;'S#{;'S;=`$d<%lO#{U:v^rSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#`,Y#`#a;r#a#o,Y#o;'S#{;'S;=`$d<%lO#{U;w^rSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#g,Y#g#hx[#TWrSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{^?u[#VWrSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{^@r^#UWrSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#f,Y#f#gAn#g#o,Y#o;'S#{;'S;=`$d<%lO#{UAs^rSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#i,Y#i#jq#c#f,Y#f#g?n#g#h,Y#h#i@k#i#o,Y#o#p#{#p#qBo#q;'S#{;'S;=`$d<%l~#{~O#{~~CYS$QUrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUrS!wYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UrS#ZQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZrS!xYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!xYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O#S~~'aO#Q~U'hUrS!}QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUrS#^QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWrSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZYrSmQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OWrSOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oWrSmQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^^rSOt#{uw#{x}#{}!O,Y!O!Q#{!Q![)S![!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{U,_[rSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{U-[UyQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U-sWrSOt#{uw#{x!P#{!P!Q.]!Q#O#{#P;'S#{;'S;=`$d<%lO#{U.b^rSOY/^YZ#{Zt/^tu0auw/^wx0ax!P/^!P!Q#{!Q!}/^!}#O5S#O#P2o#P;'S/^;'S;=`6T<%lO/^U/e^rSvQOY/^YZ#{Zt/^tu0auw/^wx0ax!P/^!P!Q3U!Q!}/^!}#O5S#O#P2o#P;'S/^;'S;=`6T<%lO/^Q0fXvQOY0aZ!P0a!P!Q1R!Q!}0a!}#O1p#O#P2o#P;'S0a;'S;=`3O<%lO0aQ1UP!P!Q1XQ1^UvQ#Z#[1X#]#^1X#a#b1X#g#h1X#i#j1X#m#n1XQ1sVOY1pZ#O1p#O#P2Y#P#Q0a#Q;'S1p;'S;=`2i<%lO1pQ2]SOY1pZ;'S1p;'S;=`2i<%lO1pQ2lP;=`<%l1pQ2rSOY0aZ;'S0a;'S;=`3O<%lO0aQ3RP;=`<%l0aU3ZWrSOt#{uw#{x!P#{!P!Q3s!Q#O#{#P;'S#{;'S;=`$d<%lO#{U3zbrSvQOt#{uw#{x#O#{#P#Z#{#Z#[3s#[#]#{#]#^3s#^#a#{#a#b3s#b#g#{#g#h3s#h#i#{#i#j3s#j#m#{#m#n3s#n;'S#{;'S;=`$d<%lO#{U5X[rSOY5SYZ#{Zt5Stu1puw5Swx1px#O5S#O#P2Y#P#Q/^#Q;'S5S;'S;=`5}<%lO5SU6QP;=`<%l5SU6WP;=`<%l/^U6bUrS!OQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6{W#YQrSOt#{uw#{x!_#{!_!`7e!`#O#{#P;'S#{;'S;=`$d<%lO#{U7jVrSOt#{uw#{x#O#{#P#Q8P#Q;'S#{;'S;=`$d<%lO#{U8WU#XQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~8oO#T~U8vU#]QrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U9aUrS!VQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U9x]rSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#U:q#U#o,Y#o;'S#{;'S;=`$d<%lO#{U:v^rSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#`,Y#`#a;r#a#o,Y#o;'S#{;'S;=`$d<%lO#{U;w^rSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#g,Y#g#hx[#UWrSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{^?u[#WWrSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{^@r^#VWrSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#f,Y#f#gAn#g#o,Y#o;'S#{;'S;=`$d<%lO#{UAs^rSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#i,Y#i#j (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 20, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
- tokenPrec: 1540
+ tokenPrec: 1578
})
diff --git a/src/parser/tests/function-blocks.test.ts b/src/parser/tests/function-blocks.test.ts
new file mode 100644
index 0000000..80805a9
--- /dev/null
+++ b/src/parser/tests/function-blocks.test.ts
@@ -0,0 +1,303 @@
+import { expect, describe, test } from 'bun:test'
+
+import '../shrimp.grammar' // Importing this so changes cause it to retest!
+
+describe('single line function blocks', () => {
+ test('work with no args', () => {
+ expect(`trap: echo bye bye end`).toMatchTree(`
+ FunctionCallWithBlock
+ FunctionCallOrIdentifier
+ Identifier trap
+ colon :
+ Block
+ FunctionCall
+ Identifier echo
+ PositionalArg
+ Identifier bye
+ PositionalArg
+ Identifier bye
+ keyword end`
+ )
+ })
+
+ test('work with one arg', () => {
+ expect(`trap EXIT: echo bye bye end`).toMatchTree(`
+ FunctionCallWithBlock
+ FunctionCall
+ Identifier trap
+ PositionalArg
+ Word EXIT
+ colon :
+ Block
+ FunctionCall
+ Identifier echo
+ PositionalArg
+ Identifier bye
+ PositionalArg
+ Identifier bye
+ keyword end`
+ )
+ })
+
+ test('work with named args', () => {
+ expect(`attach signal='exit': echo bye bye end`).toMatchTree(`
+ FunctionCallWithBlock
+ FunctionCall
+ Identifier attach
+ NamedArg
+ NamedArgPrefix signal=
+ String
+ StringFragment exit
+ colon :
+ Block
+ FunctionCall
+ Identifier echo
+ PositionalArg
+ Identifier bye
+ PositionalArg
+ Identifier bye
+ keyword end`
+ )
+ })
+
+
+ test('work with dot-get', () => {
+ expect(`signals = [=]; signals.trap 'EXIT': echo bye bye end`).toMatchTree(`
+ Assign
+ AssignableIdentifier signals
+ Eq =
+ Dict [=]
+ FunctionCallWithBlock
+ FunctionCall
+ DotGet
+ IdentifierBeforeDot signals
+ Identifier trap
+ PositionalArg
+ String
+ StringFragment EXIT
+ colon :
+ Block
+ FunctionCall
+ Identifier echo
+ PositionalArg
+ Identifier bye
+ PositionalArg
+ Identifier bye
+ keyword end`
+ )
+ })
+})
+
+describe('multi line function blocks', () => {
+ test('work with no args', () => {
+ expect(`
+trap:
+ echo bye bye
+end
+`).toMatchTree(`
+ FunctionCallWithBlock
+ FunctionCallOrIdentifier
+ Identifier trap
+ colon :
+ Block
+ FunctionCall
+ Identifier echo
+ PositionalArg
+ Identifier bye
+ PositionalArg
+ Identifier bye
+ keyword end`
+ )
+ })
+
+ test('work with one arg', () => {
+ expect(`
+trap EXIT:
+ echo bye bye
+end`).toMatchTree(`
+ FunctionCallWithBlock
+ FunctionCall
+ Identifier trap
+ PositionalArg
+ Word EXIT
+ colon :
+ Block
+ FunctionCall
+ Identifier echo
+ PositionalArg
+ Identifier bye
+ PositionalArg
+ Identifier bye
+ keyword end`
+ )
+ })
+
+ test('work with named args', () => {
+ expect(`
+attach signal='exit' code=1:
+ echo bye bye
+end`).toMatchTree(`
+ FunctionCallWithBlock
+ FunctionCall
+ Identifier attach
+ NamedArg
+ NamedArgPrefix signal=
+ String
+ StringFragment exit
+ NamedArg
+ NamedArgPrefix code=
+ Number 1
+ colon :
+ Block
+ FunctionCall
+ Identifier echo
+ PositionalArg
+ Identifier bye
+ PositionalArg
+ Identifier bye
+ keyword end`
+ )
+ })
+
+
+ test('work with dot-get', () => {
+ expect(`
+signals = [=]
+signals.trap 'EXIT':
+ echo bye bye
+end`).toMatchTree(`
+ Assign
+ AssignableIdentifier signals
+ Eq =
+ Dict [=]
+ FunctionCallWithBlock
+ FunctionCall
+ DotGet
+ IdentifierBeforeDot signals
+ Identifier trap
+ PositionalArg
+ String
+ StringFragment EXIT
+ colon :
+ Block
+ FunctionCall
+ Identifier echo
+ PositionalArg
+ Identifier bye
+ PositionalArg
+ Identifier bye
+ keyword end`
+ )
+ })
+})
+
+describe('ribbit', () => {
+ test('head tag', () => {
+ expect(`
+head:
+ title What up
+ meta charSet=UTF-8
+ meta name='viewport' content='width=device-width, initial-scale=1, viewport-fit=cover'
+end`).toMatchTree(`
+ FunctionCallWithBlock
+ FunctionCallOrIdentifier
+ Identifier head
+ colon :
+ Block
+ FunctionCall
+ Identifier title
+ PositionalArg
+ Word What
+ PositionalArg
+ Identifier up
+ FunctionCall
+ Identifier meta
+ PositionalArg
+ Word charSet=UTF-8
+ FunctionCall
+ Identifier meta
+ NamedArg
+ NamedArgPrefix name=
+ String
+ StringFragment viewport
+ NamedArg
+ NamedArgPrefix content=
+ String
+ StringFragment width=device-width, initial-scale=1, viewport-fit=cover
+ keyword end
+ `)
+ })
+
+ test('li', () => {
+ expect(`
+list:
+ li border-bottom='1px solid black' one
+ li two
+ li three
+end`).toMatchTree(`
+ FunctionCallWithBlock
+ FunctionCallOrIdentifier
+ Identifier list
+ colon :
+ Block
+ FunctionCall
+ Identifier li
+ NamedArg
+ NamedArgPrefix border-bottom=
+ String
+ StringFragment 1px solid black
+ PositionalArg
+ Identifier one
+ FunctionCall
+ Identifier li
+ PositionalArg
+ Identifier two
+ FunctionCall
+ Identifier li
+ PositionalArg
+ Identifier three
+ keyword end`)
+ })
+
+ test('inline expressions', () => {
+ expect(`
+p:
+ h1 class=bright style='font-family: helvetica' Heya
+ h2 man that is (b wild)!
+end`)
+ .toMatchTree(`
+ FunctionCallWithBlock
+ FunctionCallOrIdentifier
+ Identifier p
+ colon :
+ Block
+ FunctionCall
+ Identifier h1
+ NamedArg
+ NamedArgPrefix class=
+ Identifier bright
+ NamedArg
+ NamedArgPrefix style=
+ String
+ StringFragment font-family: helvetica
+ PositionalArg
+ Word Heya
+ FunctionCall
+ Identifier h2
+ PositionalArg
+ Identifier man
+ PositionalArg
+ Identifier that
+ PositionalArg
+ Identifier is
+ PositionalArg
+ ParenExpr
+ FunctionCall
+ Identifier b
+ PositionalArg
+ Identifier wild
+ PositionalArg
+ Word !
+ keyword end`)
+ })
+})
\ No newline at end of file