From adcb956fa294564a8ab6ec42c678089f489f3c51 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 | 13 + src/parser/shrimp.terms.ts | 21 +- src/parser/shrimp.ts | 18 +- src/parser/tests/function-blocks.test.ts | 292 +++++++++++++++++++++ 6 files changed, 506 insertions(+), 19 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 efcff49..1e95bb0 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -405,6 +405,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(``) + } + + 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}`) + } + + 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 ea98833..7c76484 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -50,6 +50,7 @@ item { consumeToTerminator { PipeExpr | + FunctionCallWithBlock | ambiguousFunctionCall | TryExpr | Throw | @@ -70,6 +71,18 @@ pipeOperand { FunctionCall | FunctionCallOrIdentifier } +FunctionCallWithBlock { + singleLineFunctionCallWithBlock | multiLineFunctionCallWithBlock +} + +singleLineFunctionCallWithBlock { + ambiguousFunctionCall colon consumeToTerminator CatchExpr? FinallyExpr? @specialize[@name=keyword] +} + +multiLineFunctionCallWithBlock { + ambiguousFunctionCall colon newlineOrSemicolon block CatchExpr? FinallyExpr? @specialize[@name=keyword] +} + FunctionCallOrIdentifier { DotGet | Identifier } diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index ab4011b..c1b2108 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -45,7 +45,7 @@ export const Params = 43, colon = 44, CatchExpr = 45, - keyword = 68, + keyword = 69, TryBlock = 47, FinallyExpr = 48, Underscore = 51, @@ -53,12 +53,13 @@ export const Null = 53, ConditionalOp = 54, PositionalArg = 55, - TryExpr = 57, - Throw = 59, - IfExpr = 61, - SingleLineThenBlock = 63, - ThenBlock = 64, - ElseIfExpr = 65, - ElseExpr = 67, - CompoundAssign = 69, - Assign = 70 + FunctionCallWithBlock = 57, + TryExpr = 58, + Throw = 60, + IfExpr = 62, + SingleLineThenBlock = 64, + ThenBlock = 65, + ElseIfExpr = 66, + ElseExpr = 68, + CompoundAssign = 70, + Assign = 71 diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 6762943..688d44b 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,catch:92, finally:98, end:100, null:106, try:116, throw:120, if:124, elseif:132, else:136} +const spec_Identifier = {__proto__:null,catch:92, finally:98, end:100, null:106, try:118, throw:122, if:126, elseif:134, else:138} export const parser = LRParser.deserialize({ version: 14, - states: "9[QYQbOOO#tQcO'#C{O$qOSO'#C}O%PQbO'#EfOOQ`'#DW'#DWOOQa'#DT'#DTO&SQbO'#DbO'eQcO'#EZOOQa'#EZ'#EZO(hQcO'#EZO)jQcO'#EYO)}QRO'#C|O+ZQcO'#EUO+kQcO'#EUO+uQbO'#CzO,mOpO'#CxOOQ`'#EV'#EVO,rQbO'#EUO,yQQO'#ElOOQ`'#Dg'#DgO-OQbO'#DiO-OQbO'#EnOOQ`'#Dk'#DkO-sQRO'#DsOOQ`'#EU'#EUO.XQQO'#ETOOQ`'#ET'#ETOOQ`'#Du'#DuQYQbOOO.aQbO'#DUOOQa'#EY'#EYOOQ`'#De'#DeOOQ`'#Ek'#EkOOQ`'#D|'#D|O.kQbO,59cO/_QbO'#DPO/gQWO'#DQOOOO'#E]'#E]OOOO'#Dv'#DvO/{OSO,59iOOQa,59i,59iOOQ`'#Dx'#DxO0ZQbO'#DXO0cQQO,5;QOOQ`'#Dw'#DwO0hQbO,59|O0oQQO,59oOOQa,59|,59|O0zQbO,59|O1UQbO,5:`O-OQbO,59hO-OQbO,59hO-OQbO,59hO-OQbO,5:OO-OQbO,5:OO-OQbO,5:OO1fQRO,59fO1mQRO,59fO2OQRO,59fO1yQQO,59fO2ZQQO,59fO2cObO,59dO2nQbO'#D}O2yQbO,59bO3bQbO,5;WO3uQcO,5:TO4kQcO,5:TO4{QcO,5:TO5qQRO,5;YO5xQRO,5;YO1UQbO,5:_OOQ`,5:o,5:oOOQ`-E7s-E7sOOQ`,59p,59pOOQ`-E7z-E7zOOOO,59k,59kOOOO,59l,59lOOOO-E7t-E7tOOQa1G/T1G/TOOQ`-E7v-E7vO6TQbO1G0lOOQ`-E7u-E7uO6hQQO1G/ZOOQa1G/h1G/hO6sQbO1G/hOOQO'#Dz'#DzO6hQQO1G/ZOOQa1G/Z1G/ZOOQ`'#D{'#D{O6sQbO1G/hOOQ`1G/z1G/zOOQa1G/S1G/SO7lQcO1G/SO7vQcO1G/SO8QQcO1G/SOOQa1G/j1G/jO9pQcO1G/jO9wQcO1G/jO:OQcO1G/jOOQa1G/Q1G/QOOQa1G/O1G/OO!aQbO'#C{O:VQbO'#CwOOQ`,5:i,5:iOOQ`-E7{-E7{O:dQbO1G0rO:oQbO1G0sO;]QbO1G0tOOQ`1G/y1G/yO;pQbO7+&WO:oQbO7+&YO;{QQO7+$uOOQa7+$u7+$uOSQbO7+&aOOQ`<pQbO<uQbO<}QbO<SQbO7+%aOOQ`7+%c7+%cOOQ`<kQbO7+&`OOQ`7+&a7+&aO>vQbO7+&aO>{QbO7+&aOOQ`'#D]'#D]O?TQbO7+&bOOQ`'#Dn'#DnO?`QbO7+&cO?eQbO7+&dOOQ`<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!uYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UrS#XQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZrS!vYOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mS!vYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O#Q~~'aO#O~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|QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6{W#WQrSOt#{uw#{x!_#{!_!`7e!`#O#{#P;'S#{;'S;=`$d<%lO#{U7jVrSOt#{uw#{x#O#{#P#Q8P#Q;'S#{;'S;=`$d<%lO#{U8WU#VQrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~8oO#R~U8vU#]QrSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U9aUrS!TQOt#{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[#SWrSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{^?u[#UWrSOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{^@r^#TWrSOt#{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!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|QOt#{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!TQOt#{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#j (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 20, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], - tokenPrec: 1591 + tokenPrec: 1677 }) diff --git a/src/parser/tests/function-blocks.test.ts b/src/parser/tests/function-blocks.test.ts new file mode 100644 index 0000000..fb47157 --- /dev/null +++ b/src/parser/tests/function-blocks.test.ts @@ -0,0 +1,292 @@ +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 : + 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 : + 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 : + 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 : + 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 : + 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 : + 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 : + 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 : + 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 : + 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 : + 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 : + 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