diff --git a/README.md b/README.md index 17c6f73..e25581d 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Go to http://localhost:3000 to try out the playground. tail log.txt lines=50 name = "Shrimp" - greet = fn person: echo "Hello" person + greet = do person: echo "Hello" person result = tail log.txt lines=10 @@ -33,7 +33,7 @@ Go to http://localhost:3000 to try out the playground. ## Architecture **parser/** - Lezer grammar and tokenizers that parse Shrimp code into syntax trees -**editor/** - CodeMirror integration with syntax highlighting and language support +**editor/** - CodeMirror integration with syntax highlighting and language support **compiler/** - Transforms syntax trees into ReefVM bytecode for execution The flow: Shrimp source → parser (CST) → compiler (bytecode) → ReefVM (execution) diff --git a/bin/shrimp b/bin/shrimp index 49cd7f3..5706ab9 100755 --- a/bin/shrimp +++ b/bin/shrimp @@ -2,9 +2,11 @@ import { Compiler } from '../src/compiler/compiler' import { colors, globals } from '../src/prelude' +import { parser } from '../src/parser/shrimp' +import { treeToString } from '../src/utils/tree' import { VM, fromValue, bytecodeToString } from 'reefvm' import { readFileSync, writeFileSync, mkdirSync } from 'fs' -import { randomUUID } from "crypto" +import { randomUUID } from 'crypto' import { spawn } from 'child_process' import { join } from 'path' @@ -32,6 +34,17 @@ async function compileFile(filePath: string) { } } +async function parseFile(filePath: string) { + try { + const code = readFileSync(filePath, 'utf-8') + const tree = parser.parse(code) + return treeToString(tree, code) + } catch (error: any) { + console.error(`${colors.red}Error:${colors.reset} ${error.message}`) + process.exit(1) + } +} + function showHelp() { console.log(`${colors.bright}${colors.magenta}🦐 Shrimp${colors.reset} is a scripting language in a shell. @@ -39,6 +52,7 @@ ${colors.bright}Usage:${colors.reset} shrimp [...args] ${colors.bright}Commands:${colors.reset} ${colors.cyan}run ${colors.yellow}./my-file.sh${colors.reset} Execute a file with Shrimp + ${colors.cyan}parse ${colors.yellow}./my-file.sh${colors.reset} Print parse tree for Shrimp file ${colors.cyan}bytecode ${colors.yellow}./my-file.sh${colors.reset} Print bytecode for Shrimp file ${colors.cyan}eval ${colors.yellow}'some code'${colors.reset} Evaluate a line of Shrimp code ${colors.cyan}repl${colors.reset} Start REPL @@ -102,6 +116,16 @@ async function main() { return } + if (['parse', '-parse', '--parse', '-p'].includes(command)) { + const file = args[1] + if (!file) { + console.log(`${colors.bright}usage: shrimp parse ${colors.reset}`) + process.exit(1) + } + console.log(await parseFile(file)) + return + } + if (['run', '-run', '--run', '-r'].includes(command)) { const file = args[1] if (!file) { diff --git a/bun.lock b/bun.lock index afb8aaa..559b57e 100644 --- a/bun.lock +++ b/bun.lock @@ -62,7 +62,7 @@ "hono": ["hono@4.9.8", "", {}, "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg=="], - "reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#c69b172c78853756ec8acba5bc33d93eb6a571c6", { "peerDependencies": { "typescript": "^5" } }, "c69b172c78853756ec8acba5bc33d93eb6a571c6"], + "reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#0f39e9401eb7a0a7c906e150127f9829458a79b6", { "peerDependencies": { "typescript": "^5" } }, "0f39e9401eb7a0a7c906e150127f9829458a79b6"], "style-mod": ["style-mod@4.1.2", "", {}, "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="], diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index efcff49..be939cf 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -54,6 +54,7 @@ export class Compiler { fnLabelCount = 0 ifLabelCount = 0 tryLabelCount = 0 + loopLabelCount = 0 bytecode: Bytecode pipeCounter = 0 @@ -388,9 +389,7 @@ export class Compiler { return instructions } - case terms.ThenBlock: - case terms.SingleLineThenBlock: - case terms.TryBlock: { + case terms.Block: { const children = getAllChildren(node) const instructions: ProgramItem[] = [] @@ -405,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) @@ -629,6 +673,24 @@ export class Compiler { return instructions } + case terms.WhileExpr: { + const [_while, test, _colon, block] = getAllChildren(node) + const instructions: ProgramItem[] = [] + + this.loopLabelCount++ + const startLoop = `.loop_${this.loopLabelCount}:` + const endLoop = `.end_loop_${this.loopLabelCount}:` + + instructions.push([`${startLoop}:`]) + instructions.push(...this.#compileNode(test!, input)) + instructions.push(['JUMP_IF_FALSE', endLoop]) + instructions.push(...this.#compileNode(block!, input)) + instructions.push(['JUMP', startLoop]) + instructions.push([`${endLoop}:`]) + + return instructions + } + default: throw new CompilerError( `Compiler doesn't know how to handle a "${node.type.name}" node.`, diff --git a/src/compiler/tests/compiler.test.ts b/src/compiler/tests/compiler.test.ts index 9418f0b..743f738 100644 --- a/src/compiler/tests/compiler.test.ts +++ b/src/compiler/tests/compiler.test.ts @@ -154,18 +154,18 @@ describe('compiler', () => { end`).toEvaluateTo('white') }) - test('if elseif', () => { + test('if else if', () => { expect(`if false: boromir - elseif true: + else if true: frodo end`).toEvaluateTo('frodo') }) - test('if elseif else', () => { + test('if else if else', () => { expect(`if false: destroyed - elseif true: + else if true: fire else: darkness @@ -173,9 +173,9 @@ describe('compiler', () => { expect(`if false: king - elseif false: + else if false: elf - elseif true: + else if true: dwarf else: scattered diff --git a/src/compiler/tests/function-blocks.test.ts b/src/compiler/tests/function-blocks.test.ts new file mode 100644 index 0000000..41bf65d --- /dev/null +++ b/src/compiler/tests/function-blocks.test.ts @@ -0,0 +1,55 @@ +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]) + }) +}) diff --git a/src/compiler/tests/ribbit.test.ts b/src/compiler/tests/ribbit.test.ts new file mode 100644 index 0000000..e2bb6c2 --- /dev/null +++ b/src/compiler/tests/ribbit.test.ts @@ -0,0 +1,115 @@ +import { expect, describe, test, beforeEach } from 'bun:test' + +const buffer: string[] = [] + +const ribbitGlobals = { + ribbit: async (cb: Function) => { + await cb() + return buffer.join("\n") + }, + tag: async (tagFn: Function, atDefaults = {}) => { + return (atNamed = {}, ...args: any[]) => tagFn(Object.assign({}, atDefaults, atNamed), ...args) + }, + head: (atNamed: {}, ...args: any[]) => tag('head', atNamed, ...args), + title: (atNamed: {}, ...args: any[]) => tag('title', atNamed, ...args), + meta: (atNamed: {}, ...args: any[]) => tag('meta', atNamed, ...args), + 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), + ul: (atNamed: {}, ...args: any[]) => tag('ul', atNamed, ...args), + li: (atNamed: {}, ...args: any[]) => tag('li', atNamed, ...args), + nospace: () => NOSPACE_TOKEN, + echo: (...args: any[]) => console.log(...args) +} + +function raw(fn: Function) { (fn as any).raw = true } + +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 === TAG_TOKEN ? buffer.pop() : a) + .reverse().join(' ') + .replaceAll(` ${NOSPACE_TOKEN} `, '') + + if (SELF_CLOSING.includes(tagName)) + buffer.push(`<${tagName}${space}${attrs.join(' ')} />`) + else + 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) + return TAG_TOKEN + } +} + +const NOSPACE_TOKEN = '!!ribbit-nospace!!' +const TAG_TOKEN = '!!ribbit-tag!!' +const SELF_CLOSING = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"] + +describe('ribbit', () => { + beforeEach(() => buffer.length = 0) + + test('head tag', () => { + expect(` +ribbit: + head: + title What up + meta charset=UTF-8 + meta name=viewport content='width=device-width, initial-scale=1, viewport-fit=cover' + end +end + `).toEvaluateTo(` +What up + + +`, ribbitGlobals) + }) + + test('custom tags', () => { + expect(` +list = tag ul class=list +ribbit: + list: + li border-bottom='1px solid black' one + li two + li three + end +end`).toEvaluateTo(``, ribbitGlobals) + }) + + test('inline expressions', () => { + 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.

+

`, ribbitGlobals) + }) +}) \ No newline at end of file diff --git a/src/compiler/tests/while.test.ts b/src/compiler/tests/while.test.ts new file mode 100644 index 0000000..c3afdb9 --- /dev/null +++ b/src/compiler/tests/while.test.ts @@ -0,0 +1,48 @@ +import { describe } from 'bun:test' +import { expect, test } from 'bun:test' + +describe('while', () => { + test('basic variable', () => { + expect(` + a = true + b = '' + while a: + a = false + b = done + end + b`) + .toEvaluateTo('done') + }) + + test('basic expression', () => { + expect(` + a = 0 + while a < 10: + a += 1 + end + a`) + .toEvaluateTo(10) + }) + + test('compound expression', () => { + expect(` + a = 1 + b = 0 + while a > 0 and b < 100: + b += 1 + end + b`) + .toEvaluateTo(100) + }) + + test('returns value', () => { + expect(` + a = 0 + ret = while a < 10: + a += 1 + done + end + ret`) + .toEvaluateTo('done') + }) +}) \ No newline at end of file diff --git a/src/compiler/utils.ts b/src/compiler/utils.ts index 6151563..ed0dfcc 100644 --- a/src/compiler/utils.ts +++ b/src/compiler/utils.ts @@ -210,7 +210,7 @@ export const getIfExprParts = (node: SyntaxNode, input: string) => { } elseThenBlock = parts.at(-1) } else if (child.type.id === terms.ElseIfExpr) { - const [_keyword, conditional, _colon, thenBlock] = parts + const [_else, _if, conditional, _colon, thenBlock] = parts if (!conditional || !thenBlock) { const names = parts.map((p) => p.type.name).join(', ') const message = `ElseIfExpr expected conditional and thenBlock, got ${names}` @@ -309,7 +309,7 @@ export const getDotGetParts = (node: SyntaxNode, input: string) => { 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 + // First child is always 'try' keyword, second is colon, third is Block const [tryKeyword, _colon, tryBlock, ...rest] = children if (!tryKeyword || !tryBlock) { diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index a3daaf4..01f95b6 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -25,9 +25,18 @@ Underscore { "_" } Regex { "//" (![/\\\n[] | "\\" ![\n] | "[" (![\n\\\]] | "\\" ![\n])* "]")+ ("//" $[gimsuy]*)? } // Stolen from the lezer JavaScript grammar "|"[@name=operator] - } +end { @specialize[@name=keyword] } +while { @specialize[@name=keyword] } +if { @specialize[@name=keyword] } +else { @specialize[@name=keyword] } +try { @specialize[@name=keyword] } +catch { @specialize[@name=keyword] } +finally { @specialize[@name=keyword] } +throw { @specialize[@name=keyword] } +null { @specialize[@name=Null] } + @external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot } @external specialize {Identifier} specializeKeyword from "./tokenizer" { Do } @@ -47,9 +56,10 @@ item { newlineOrSemicolon // allow blank lines } - consumeToTerminator { PipeExpr | + WhileExpr | + FunctionCallWithBlock | ambiguousFunctionCall | TryExpr | Throw | @@ -70,6 +80,18 @@ pipeOperand { FunctionCall | FunctionCallOrIdentifier } +WhileExpr { + while (ConditionalOp | expression) colon Block end +} + +Block { + consumeToTerminator | newlineOrSemicolon block +} + +FunctionCallWithBlock { + ambiguousFunctionCall colon Block CatchExpr? FinallyExpr? end +} + FunctionCallOrIdentifier { DotGet | Identifier } @@ -86,7 +108,6 @@ arg { PositionalArg | NamedArg } - PositionalArg { expression | FunctionDef | Underscore } @@ -96,71 +117,35 @@ NamedArg { } FunctionDef { - singleLineFunctionDef | multilineFunctionDef -} - -singleLineFunctionDef { - Do Params colon consumeToTerminator CatchExpr? FinallyExpr? @specialize[@name=keyword] -} - -multilineFunctionDef { - Do Params colon newlineOrSemicolon block CatchExpr? FinallyExpr? @specialize[@name=keyword] + Do Params colon (consumeToTerminator | newlineOrSemicolon block) CatchExpr? FinallyExpr? end } IfExpr { - singleLineIf | multilineIf -} - -singleLineIf { - @specialize[@name=keyword] (ConditionalOp | expression) colon SingleLineThenBlock @specialize[@name=keyword] -} - -multilineIf { - @specialize[@name=keyword] (ConditionalOp | expression) colon newlineOrSemicolon ThenBlock ElseIfExpr* ElseExpr? @specialize[@name=keyword] + if (ConditionalOp | expression) colon Block ElseIfExpr* ElseExpr? end } ElseIfExpr { - @specialize[@name=keyword] (ConditionalOp | expression) colon newlineOrSemicolon ThenBlock + else if (ConditionalOp | expression) colon Block } ElseExpr { - @specialize[@name=keyword] colon newlineOrSemicolon ThenBlock -} - -ThenBlock { - block -} - -SingleLineThenBlock { - consumeToTerminator + else colon Block } 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] + try colon Block CatchExpr? FinallyExpr? end } CatchExpr { - @specialize[@name=keyword] Identifier colon (newlineOrSemicolon TryBlock | consumeToTerminator) + catch Identifier colon Block } FinallyExpr { - @specialize[@name=keyword] colon (newlineOrSemicolon TryBlock | consumeToTerminator) -} - -TryBlock { - block + finally colon Block } Throw { - @specialize[@name=keyword] (BinOp | ConditionalOp | expression) + throw (BinOp | ConditionalOp | expression) } ConditionalOp { @@ -179,7 +164,7 @@ Params { } NamedParam { - NamedArgPrefix (String | Number | Boolean | @specialize[@name=Null]) + NamedArgPrefix (String | Number | Boolean | null) } Assign { @@ -217,7 +202,6 @@ expression { } String { "'" stringContent* "'" } - } stringContent { @@ -253,7 +237,7 @@ Array { // to go through ambiguousFunctionCall (which is what we want semantically). // Yes, it is annoying and I gave up trying to use GLR to fix it. expressionWithoutIdentifier { - ParenExpr | Word | String | Number | Boolean | Regex | Dict | Array | @specialize[@name=Null] + ParenExpr | Word | String | Number | Boolean | Regex | Dict | Array | null } block { diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index 07b57b4..05f3d5a 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -47,19 +47,19 @@ export const Null = 45, colon = 46, CatchExpr = 47, - keyword = 69, - TryBlock = 49, + keyword = 68, + Block = 49, FinallyExpr = 50, Underscore = 53, Array = 54, ConditionalOp = 55, PositionalArg = 56, - TryExpr = 58, - Throw = 60, - IfExpr = 62, - SingleLineThenBlock = 64, - ThenBlock = 65, - ElseIfExpr = 66, - ElseExpr = 68, + WhileExpr = 58, + 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 0877626..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, try:118, throw:122, if:126, elseif:134, else:138} +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: ":QQYQbOOO#tQcO'#C{O$qOSO'#C}O%PQbO'#EhOOQ`'#DW'#DWOOQa'#DT'#DTO&VQbO'#DdO'hQcO'#E]OOQa'#E]'#E]O(kQcO'#E]O)mQcO'#E[O*QQRO'#C|O+^QcO'#EWO+nQcO'#EWO+xQbO'#CzO,pOpO'#CxOOQ`'#EX'#EXO,uQbO'#EWO,|QQO'#EnOOQ`'#Dh'#DhO-RQbO'#DjO-RQbO'#EpOOQ`'#Dl'#DlO-vQRO'#DtOOQ`'#EW'#EWO.[QQO'#EVOOQ`'#EV'#EVOOQ`'#Dv'#DvQYQbOOO.dQbO'#DUOOQa'#E['#E[OOQ`'#Df'#DfOOQ`'#Em'#EmOOQ`'#EO'#EOO.nQbO,59cO/bQbO'#DPO/jQWO'#DQOOOO'#E_'#E_OOOO'#Dw'#DwO0OOSO,59iOOQa,59i,59iOOQ`'#Dy'#DyO0^QbO'#DXO0iQbO'#DYOOQO'#Dz'#DzO0aQQO'#DXO0wQQO,5;SOOQ`'#Dx'#DxO0|QbO,5:OO1TQQO,59oOOQa,5:O,5:OO1`QbO,5:OO1jQbO,5:aO-RQbO,59hO-RQbO,59hO-RQbO,59hO-RQbO,5:PO-RQbO,5:PO-RQbO,5:PO1zQRO,59fO2RQRO,59fO2dQRO,59fO2_QQO,59fO2oQQO,59fO2wObO,59dO3SQbO'#EPO3_QbO,59bO3vQbO,5;YO4ZQcO,5:UO5PQcO,5:UO5aQcO,5:UO6VQRO,5;[O6^QRO,5;[O1jQbO,5:`OOQ`,5:q,5:qOOQ`-E7t-E7tOOQ`,59p,59pOOQ`-E7|-E7|OOOO,59k,59kOOOO,59l,59lOOOO-E7u-E7uOOQa1G/T1G/TOOQ`-E7w-E7wO6iQQO,59sOOQO,59t,59tOOQO-E7x-E7xO6qQbO1G0nOOQ`-E7v-E7vO7UQQO1G/ZOOQa1G/j1G/jO7aQbO1G/jOOQO'#D|'#D|O7UQQO1G/ZOOQa1G/Z1G/ZOOQ`'#D}'#D}O7aQbO1G/jOOQ`1G/{1G/{OOQa1G/S1G/SO8YQcO1G/SO8dQcO1G/SO8nQcO1G/SOOQa1G/k1G/kO:^QcO1G/kO:eQcO1G/kO:lQcO1G/kOOQa1G/Q1G/QOOQa1G/O1G/OO!aQbO'#C{O:sQbO'#CwOOQ`,5:k,5:kOOQ`-E7}-E7}O;QQbO1G0tO;]QbO1G0uO;yQbO1G0vOOQ`1G/z1G/zO<^QbO7+&YO;]QbO7+&[O`QbO7+&aOOQ`'#Dn'#DnO>kQbO7+&bO>pQbO7+&cOOQ`<pQbO7+%bOOQ`7+%d7+%dOOQ`<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!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#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: 1619 + tokenPrec: 1578 }) diff --git a/src/parser/tests/basics.test.ts b/src/parser/tests/basics.test.ts index d890f91..b4092a9 100644 --- a/src/parser/tests/basics.test.ts +++ b/src/parser/tests/basics.test.ts @@ -752,7 +752,7 @@ Assign EqEq == Number 5 colon : - ThenBlock + Block Boolean true keyword end keyword end @@ -794,7 +794,7 @@ Assign EqEq == Number 5 colon : - ThenBlock + Block Boolean true keyword end keyword end diff --git a/src/parser/tests/control-flow.test.ts b/src/parser/tests/control-flow.test.ts index 70efddb..af0f704 100644 --- a/src/parser/tests/control-flow.test.ts +++ b/src/parser/tests/control-flow.test.ts @@ -2,7 +2,7 @@ import { expect, describe, test } from 'bun:test' import '../shrimp.grammar' // Importing this so changes cause it to retest! -describe('if/elseif/else', () => { +describe('if/else if/else', () => { test('parses single line if', () => { expect(`if y == 1: 'cool' end`).toMatchTree(` IfExpr @@ -12,7 +12,7 @@ describe('if/elseif/else', () => { EqEq == Number 1 colon : - SingleLineThenBlock + Block String StringFragment cool keyword end @@ -26,7 +26,7 @@ describe('if/elseif/else', () => { keyword if Identifier x colon : - SingleLineThenBlock + Block Number 2 keyword end `) @@ -44,7 +44,7 @@ describe('if/elseif/else', () => { Lt < Number 9 colon : - ThenBlock + Block FunctionCallOrIdentifier Identifier yes keyword end @@ -61,78 +61,81 @@ describe('if/elseif/else', () => { keyword if Identifier with-else colon : - ThenBlock + Block FunctionCallOrIdentifier Identifier x ElseExpr keyword else colon : - ThenBlock + Block FunctionCallOrIdentifier Identifier y keyword end `) }) - test('parses multiline if with elseif', () => { - expect(`if with-elseif: + test('parses multiline if with else if', () => { + expect(`if with-else-if: x - elseif another-condition: + else if another-condition: y end`).toMatchTree(` IfExpr keyword if - Identifier with-elseif + Identifier with-else-if colon : - ThenBlock + Block FunctionCallOrIdentifier Identifier x ElseIfExpr - keyword elseif + keyword else + keyword if Identifier another-condition colon : - ThenBlock + Block FunctionCallOrIdentifier Identifier y keyword end `) }) - test('parses multiline if with multiple elseif and else', () => { - expect(`if with-elseif-else: + test('parses multiline if with multiple else if and else', () => { + expect(`if with-else-if-else: x - elseif another-condition: + else if another-condition: y - elseif yet-another-condition: + else if yet-another-condition: z else: oh-no end`).toMatchTree(` IfExpr keyword if - Identifier with-elseif-else + Identifier with-else-if-else colon : - ThenBlock + Block FunctionCallOrIdentifier Identifier x ElseIfExpr - keyword elseif + keyword else + keyword if Identifier another-condition colon : - ThenBlock + Block FunctionCallOrIdentifier Identifier y ElseIfExpr - keyword elseif + keyword else + keyword if Identifier yet-another-condition colon : - ThenBlock + Block FunctionCallOrIdentifier Identifier z ElseExpr keyword else colon : - ThenBlock + Block FunctionCallOrIdentifier Identifier oh-no keyword end @@ -148,9 +151,124 @@ describe('if/elseif/else', () => { keyword if Boolean true colon : - SingleLineThenBlock + Block Number 2 keyword end `) }) }) + +describe('while', () => { + test('infinite loop', () => { + expect(`while true: true end`).toMatchTree(` + WhileExpr + keyword while + Boolean true + colon : + Block + Boolean true + keyword end`) + }) + + test('basic expression', () => { + expect(`while a > 0: true end`).toMatchTree(` + WhileExpr + keyword while + ConditionalOp + Identifier a + Gt > + Number 0 + colon : + Block + Boolean true + keyword end`) + }) + + + test('compound expression', () => { + expect(`while a > 0 and b < 100 and c < 1000: true end`).toMatchTree(` + WhileExpr + keyword while + ConditionalOp + ConditionalOp + ConditionalOp + Identifier a + Gt > + Number 0 + And and + ConditionalOp + Identifier b + Lt < + Number 100 + And and + ConditionalOp + Identifier c + Lt < + Number 1000 + colon : + Block + Boolean true + keyword end`) + }) + + test('multiline infinite loop', () => { + expect(` + while true: + true + end`).toMatchTree(` + WhileExpr + keyword while + Boolean true + colon : + Block + Boolean true + keyword end`) + }) + + test('multiline basic expression', () => { + expect(` + while a > 0: + true + end`).toMatchTree(` + WhileExpr + keyword while + ConditionalOp + Identifier a + Gt > + Number 0 + colon : + Block + Boolean true + keyword end`) + }) + + + test('multiline compound expression', () => { + expect(` + while a > 0 and b < 100 and c < 1000: + true + end`).toMatchTree(` + WhileExpr + keyword while + ConditionalOp + ConditionalOp + ConditionalOp + Identifier a + Gt > + Number 0 + And and + ConditionalOp + Identifier b + Lt < + Number 100 + And and + ConditionalOp + Identifier c + Lt < + Number 1000 + colon : + Block + Boolean true + keyword end`) + }) +}) \ No newline at end of file diff --git a/src/parser/tests/exceptions.test.ts b/src/parser/tests/exceptions.test.ts index 039a279..e89c80e 100644 --- a/src/parser/tests/exceptions.test.ts +++ b/src/parser/tests/exceptions.test.ts @@ -12,14 +12,14 @@ describe('try/catch/finally/throw', () => { TryExpr keyword try colon : - TryBlock + Block FunctionCallOrIdentifier Identifier risky-operation CatchExpr keyword catch Identifier err colon : - TryBlock + Block FunctionCall Identifier handle-error PositionalArg @@ -37,13 +37,13 @@ describe('try/catch/finally/throw', () => { TryExpr keyword try colon : - TryBlock + Block FunctionCallOrIdentifier Identifier do-work FinallyExpr keyword finally colon : - TryBlock + Block FunctionCallOrIdentifier Identifier cleanup keyword end @@ -61,14 +61,14 @@ describe('try/catch/finally/throw', () => { TryExpr keyword try colon : - TryBlock + Block FunctionCallOrIdentifier Identifier risky-operation CatchExpr keyword catch Identifier err colon : - TryBlock + Block FunctionCall Identifier handle-error PositionalArg @@ -76,7 +76,7 @@ describe('try/catch/finally/throw', () => { FinallyExpr keyword finally colon : - TryBlock + Block FunctionCallOrIdentifier Identifier cleanup keyword end @@ -91,15 +91,17 @@ describe('try/catch/finally/throw', () => { TryExpr keyword try colon : - FunctionCall - Identifier parse-number - PositionalArg - Identifier input + Block + FunctionCall + Identifier parse-number + PositionalArg + Identifier input CatchExpr keyword catch Identifier err colon : - Number 0 + Block + Number 0 keyword end `) }) @@ -109,18 +111,21 @@ describe('try/catch/finally/throw', () => { TryExpr keyword try colon : - FunctionCallOrIdentifier - Identifier work + Block + FunctionCallOrIdentifier + Identifier work CatchExpr keyword catch Identifier err colon : - Number 0 + Block + Number 0 FinallyExpr keyword finally colon : - FunctionCallOrIdentifier - Identifier cleanup + Block + FunctionCallOrIdentifier + Identifier cleanup keyword end `) }) @@ -164,13 +169,15 @@ describe('try/catch/finally/throw', () => { TryExpr keyword try colon : - FunctionCallOrIdentifier - Identifier work + Block + FunctionCallOrIdentifier + Identifier work CatchExpr keyword catch Identifier err colon : - Number 0 + Block + Number 0 keyword end `) }) @@ -199,7 +206,7 @@ describe('function-level exception handling', () => { keyword catch Identifier e colon : - TryBlock + Block FunctionCallOrIdentifier Identifier empty-string keyword end @@ -227,7 +234,7 @@ describe('function-level exception handling', () => { FinallyExpr keyword finally colon : - TryBlock + Block FunctionCallOrIdentifier Identifier close-resources keyword end @@ -259,7 +266,7 @@ describe('function-level exception handling', () => { keyword catch Identifier err colon : - TryBlock + Block FunctionCall Identifier log PositionalArg @@ -269,7 +276,7 @@ describe('function-level exception handling', () => { FinallyExpr keyword finally colon : - TryBlock + Block FunctionCallOrIdentifier Identifier cleanup keyword end 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