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 082abab..96a0b60 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
@@ -406,9 +407,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[] = []
@@ -423,6 +422,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)
@@ -647,6 +691,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 74cf248..ad62392 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(`${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 === 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}${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)
+ 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/highlight.ts b/src/parser/highlight.ts
index 63fe0e0..9363e5c 100644
--- a/src/parser/highlight.ts
+++ b/src/parser/highlight.ts
@@ -5,6 +5,7 @@ export const highlighting = styleTags({
Number: tags.number,
String: tags.string,
Boolean: tags.bool,
+ Do: tags.keyword,
keyword: tags.keyword,
end: tags.keyword,
':': tags.keyword,
@@ -15,4 +16,5 @@ export const highlighting = styleTags({
Command: tags.function(tags.variableName),
'Params/Identifier': tags.definition(tags.variableName),
Paren: tags.paren,
+ Comment: tags.comment,
})
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