From 57711c4e8924f5d1bfb5f1446b5b1197e7f8a1da Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Tue, 14 Oct 2025 16:45:45 -0700 Subject: [PATCH] wip --- packages/ReefVM | 2 +- src/parser/tests/basics.test.ts | 270 ++++++++++++++++++++++++++ src/parser/tests/control-flow.test.ts | 139 +++++++++++++ src/parser/tests/functions.test.ts | 137 +++++++++++++ src/parser/tests/multiline.test.ts | 74 +++++++ src/parser/tests/pipes.test.ts | 87 +++++++++ src/parser/tests/strings.test.ts | 94 +++++++++ 7 files changed, 802 insertions(+), 1 deletion(-) create mode 100644 src/parser/tests/basics.test.ts create mode 100644 src/parser/tests/control-flow.test.ts create mode 100644 src/parser/tests/functions.test.ts create mode 100644 src/parser/tests/multiline.test.ts create mode 100644 src/parser/tests/pipes.test.ts create mode 100644 src/parser/tests/strings.test.ts diff --git a/packages/ReefVM b/packages/ReefVM index 82e7b18..0844e99 160000 --- a/packages/ReefVM +++ b/packages/ReefVM @@ -1 +1 @@ -Subproject commit 82e7b181ec1b0a2df4d76ca529b4736c9e56383b +Subproject commit 0844e99d2d04fb9ba0999f25248a17430bdc5ee6 diff --git a/src/parser/tests/basics.test.ts b/src/parser/tests/basics.test.ts new file mode 100644 index 0000000..cdbba7c --- /dev/null +++ b/src/parser/tests/basics.test.ts @@ -0,0 +1,270 @@ +import { expect, describe, test } from 'bun:test' + +import '../shrimp.grammar' // Importing this so changes cause it to retest! + +describe('Identifier', () => { + test('parses identifiers with emojis and dashes', () => { + expect('moo-😊-34').toMatchTree(` + FunctionCallOrIdentifier + Identifier moo-😊-34`) + }) +}) + +describe('Parentheses', () => { + test('allows binOps with parentheses correctly', () => { + expect('(2 + 3)').toMatchTree(` + ParenExpr + BinOp + Number 2 + operator + + Number 3`) + }) + + test('allows numbers, strings, and booleans with parentheses correctly', () => { + expect('(42)').toMatchTree(` + ParenExpr + Number 42`) + + expect("('hello')").toMatchTree(` + ParenExpr + String + StringFragment hello`) + + expect('(true)').toMatchTree(` + ParenExpr + Boolean true`) + + expect('(false)').toMatchTree(` + ParenExpr + Boolean false`) + }) + + test('allows function calls in parens', () => { + expect('(echo 3)').toMatchTree(` + ParenExpr + FunctionCall + Identifier echo + PositionalArg + Number 3`) + + expect('(echo)').toMatchTree(` + ParenExpr + FunctionCallOrIdentifier + Identifier echo`) + }) + + test('allows conditionals in parens', () => { + expect('(a > b)').toMatchTree(` + ParenExpr + ConditionalOp + Identifier a + operator > + Identifier b`) + + expect('(a and b)').toMatchTree(` + ParenExpr + ConditionalOp + Identifier a + operator and + Identifier b`) + }) + + test('allows parens in function calls', () => { + expect('echo (3 + 3)').toMatchTree(` + FunctionCall + Identifier echo + PositionalArg + ParenExpr + BinOp + Number 3 + operator + + Number 3`) + }) + + test('a word can be contained in parens', () => { + expect('(basename ./cool)').toMatchTree(` + ParenExpr + FunctionCall + Identifier basename + PositionalArg + Word ./cool + `) + }) + + test('nested parentheses', () => { + expect('(2 + (1 * 4))').toMatchTree(` + ParenExpr + BinOp + Number 2 + operator + + ParenExpr + BinOp + Number 1 + operator * + Number 4`) + }) + + test('Function in parentheses', () => { + expect('4 + (echo 3)').toMatchTree(` + BinOp + Number 4 + operator + + ParenExpr + FunctionCall + Identifier echo + PositionalArg + Number 3`) + }) +}) + +describe('BinOp', () => { + test('addition tests', () => { + expect('2 + 3').toMatchTree(` + BinOp + Number 2 + operator + + Number 3 + `) + }) + + test('subtraction tests', () => { + expect('5 - 2').toMatchTree(` + BinOp + Number 5 + operator - + Number 2 + `) + }) + + test('multiplication tests', () => { + expect('4 * 3').toMatchTree(` + BinOp + Number 4 + operator * + Number 3 + `) + }) + + test('division tests', () => { + expect('8 / 2').toMatchTree(` + BinOp + Number 8 + operator / + Number 2 + `) + }) + + test('mixed operations with precedence', () => { + expect('2 + 3 * 4 - 5 / 1').toMatchTree(` + BinOp + BinOp + Number 2 + operator + + BinOp + Number 3 + operator * + Number 4 + operator - + BinOp + Number 5 + operator / + Number 1 + `) + }) +}) + +describe('ambiguity', () => { + test('parses ambiguous expressions correctly', () => { + expect('a + -3').toMatchTree(` + BinOp + Identifier a + operator + + Number -3 + `) + }) + + test('parses ambiguous expressions correctly', () => { + expect('a-var + a-thing').toMatchTree(` + BinOp + Identifier a-var + operator + + Identifier a-thing + `) + }) +}) + +describe('newlines', () => { + test('parses multiple statements separated by newlines', () => { + expect(`x = 5 +y = 2`).toMatchTree(` + Assign + Identifier x + operator = + Number 5 + Assign + Identifier y + operator = + Number 2`) + }) + + test('parses statements separated by semicolons', () => { + expect(`x = 5; y = 2`).toMatchTree(` + Assign + Identifier x + operator = + Number 5 + Assign + Identifier y + operator = + Number 2`) + }) + + test('parses statement with word and a semicolon', () => { + expect(`a = hello; 2`).toMatchTree(` + Assign + Identifier a + operator = + FunctionCallOrIdentifier + Identifier hello + Number 2`) + }) +}) + +describe('Assign', () => { + test('parses simple assignment', () => { + expect('x = 5').toMatchTree(` + Assign + Identifier x + operator = + Number 5`) + }) + + test('parses assignment with addition', () => { + expect('x = 5 + 3').toMatchTree(` + Assign + Identifier x + operator = + BinOp + Number 5 + operator + + Number 3`) + }) + + test('parses assignment with functions', () => { + expect('add = fn a b: a + b end').toMatchTree(` + Assign + Identifier add + operator = + FunctionDef + keyword fn + Params + Identifier a + Identifier b + colon : + BinOp + Identifier a + operator + + Identifier b + end end`) + }) +}) diff --git a/src/parser/tests/control-flow.test.ts b/src/parser/tests/control-flow.test.ts new file mode 100644 index 0000000..250e0b8 --- /dev/null +++ b/src/parser/tests/control-flow.test.ts @@ -0,0 +1,139 @@ +import { expect, describe, test } from 'bun:test' + +import '../shrimp.grammar' // Importing this so changes cause it to retest! + +describe('if/elsif/else', () => { + test('parses single line if', () => { + expect(`if y = 1: 'cool'`).toMatchTree(` + IfExpr + keyword if + ConditionalOp + Identifier y + operator = + Number 1 + colon : + ThenBlock + String + StringFragment cool + `) + + expect('a = if x: 2').toMatchTree(` + Assign + Identifier a + operator = + IfExpr + keyword if + Identifier x + colon : + ThenBlock + Number 2 + `) + }) + + test('parses multiline if', () => { + expect(` + if x < 9: + yes + end`).toMatchTree(` + IfExpr + keyword if + ConditionalOp + Identifier x + operator < + Number 9 + colon : + ThenBlock + FunctionCallOrIdentifier + Identifier yes + end end + `) + }) + + test('parses multiline if with else', () => { + expect(`if with-else: + x + else: + y + end`).toMatchTree(` + IfExpr + keyword if + Identifier with-else + colon : + ThenBlock + FunctionCallOrIdentifier + Identifier x + ElseExpr + keyword else + colon : + ThenBlock + FunctionCallOrIdentifier + Identifier y + end end + `) + }) + + test('parses multiline if with elsif', () => { + expect(`if with-elsif: + x + elsif another-condition: + y + end`).toMatchTree(` + IfExpr + keyword if + Identifier with-elsif + colon : + ThenBlock + FunctionCallOrIdentifier + Identifier x + ElsifExpr + keyword elsif + Identifier another-condition + colon : + ThenBlock + FunctionCallOrIdentifier + Identifier y + end end + `) + }) + + test('parses multiline if with multiple elsif and else', () => { + expect(`if with-elsif-else: + x + elsif another-condition: + y + elsif yet-another-condition: + z + else: + oh-no + end`).toMatchTree(` + IfExpr + keyword if + Identifier with-elsif-else + colon : + ThenBlock + FunctionCallOrIdentifier + Identifier x + ElsifExpr + keyword elsif + Identifier another-condition + colon : + ThenBlock + FunctionCallOrIdentifier + Identifier y + ElsifExpr + keyword elsif + Identifier yet-another-condition + colon : + ThenBlock + FunctionCallOrIdentifier + Identifier z + ElseExpr + keyword else + colon : + ThenBlock + FunctionCallOrIdentifier + Identifier oh-no + end end + `) + }) +}) diff --git a/src/parser/tests/functions.test.ts b/src/parser/tests/functions.test.ts new file mode 100644 index 0000000..80e4436 --- /dev/null +++ b/src/parser/tests/functions.test.ts @@ -0,0 +1,137 @@ +import { expect, describe, test } from 'bun:test' +import { afterEach } from 'bun:test' +import { resetCommandSource, setCommandSource } from '#editor/commands' +import { beforeEach } from 'bun:test' + +import '../shrimp.grammar' // Importing this so changes cause it to retest! + +describe('calling functions', () => { + beforeEach(() => { + setCommandSource(() => [ + { + command: 'echo', + args: [{ name: 'path', type: 'string' }], + execute: (p: any) => p, + }, + ]) + }) + + afterEach(() => { + resetCommandSource() + }) + + test('call with no args', () => { + expect('tail').toMatchTree(` + FunctionCallOrIdentifier + Identifier tail + `) + }) + + test('call with arg', () => { + expect('tail path').toMatchTree(` + FunctionCall + Identifier tail + PositionalArg + Identifier path + `) + }) + + test('call with arg and named arg', () => { + expect('tail path lines=30').toMatchTree(` + FunctionCall + Identifier tail + PositionalArg + Identifier path + NamedArg + NamedArgPrefix lines= + Number 30 + `) + }) + + test('command with arg that is also a command', () => { + expect('tail tail').toMatchTree(` + FunctionCall + Identifier tail + PositionalArg + Identifier tail + `) + + expect('tai').toMatchTree(` + FunctionCallOrIdentifier + Identifier tai + `) + }) + + test('Incomplete namedArg', () => { + expect('tail lines=').toMatchTree(` + FunctionCall + Identifier tail + NamedArg + NamedArgPrefix lines= + ⚠ + ⚠ `) + }) +}) + +describe('Fn', () => { + test('parses function no parameters', () => { + expect('fn: 1 end').toMatchTree(` + FunctionDef + keyword fn + Params + colon : + Number 1 + end end`) + }) + + test('parses function with single parameter', () => { + expect('fn x: x + 1 end').toMatchTree(` + FunctionDef + keyword fn + Params + Identifier x + colon : + BinOp + Identifier x + operator + + Number 1 + end end`) + }) + + test('parses function with multiple parameters', () => { + expect('fn x y: x * y end').toMatchTree(` + FunctionDef + keyword fn + Params + Identifier x + Identifier y + colon : + BinOp + Identifier x + operator * + Identifier y + end end`) + }) + + test('parses multiline function with multiple statements', () => { + expect(`fn x y: + x * y + x + 9 +end`).toMatchTree(` + FunctionDef + keyword fn + Params + Identifier x + Identifier y + colon : + BinOp + Identifier x + operator * + Identifier y + BinOp + Identifier x + operator + + Number 9 + end end`) + }) +}) diff --git a/src/parser/tests/multiline.test.ts b/src/parser/tests/multiline.test.ts new file mode 100644 index 0000000..11993e9 --- /dev/null +++ b/src/parser/tests/multiline.test.ts @@ -0,0 +1,74 @@ +import { expect, describe, test } from 'bun:test' + +import '../shrimp.grammar' // Importing this so changes cause it to retest! + +describe('multiline', () => { + test('parses multiline strings', () => { + expect(`'first'\n'second'`).toMatchTree(` + String + StringFragment first + String + StringFragment second`) + }) + + test('parses multiline functions', () => { + expect(` + add = fn a b: + result = a + b + result + end + + add 3 4 + `).toMatchTree(` + Assign + Identifier add + operator = + FunctionDef + keyword fn + Params + Identifier a + Identifier b + colon : + Assign + Identifier result + operator = + BinOp + Identifier a + operator + + Identifier b + FunctionCallOrIdentifier + Identifier result + + end end + FunctionCall + Identifier add + PositionalArg + Number 3 + PositionalArg + Number 4`) + }) + + test('ignores leading and trailing whitespace in expected tree', () => { + expect(` + 3 + + + fn x y: + x +end + +`).toMatchTree(` + Number 3 + + FunctionDef + keyword fn + Params + Identifier x + Identifier y + colon : + FunctionCallOrIdentifier + Identifier x + end end + `) + }) +}) diff --git a/src/parser/tests/pipes.test.ts b/src/parser/tests/pipes.test.ts new file mode 100644 index 0000000..25eb829 --- /dev/null +++ b/src/parser/tests/pipes.test.ts @@ -0,0 +1,87 @@ +import { expect, describe, test } from 'bun:test' + +import '../shrimp.grammar' // Importing this so changes cause it to retest! + +describe('pipe expressions', () => { + test('simple pipe expression', () => { + expect('echo hello | grep h').toMatchTree(` + PipeExpr + FunctionCall + Identifier echo + PositionalArg + Identifier hello + operator | + FunctionCall + Identifier grep + PositionalArg + Identifier h + `) + }) + + test('multi-stage pipe chain', () => { + expect('find files | filter active | sort').toMatchTree(` + PipeExpr + FunctionCall + Identifier find + PositionalArg + Identifier files + operator | + FunctionCall + Identifier filter + PositionalArg + Identifier active + operator | + FunctionCallOrIdentifier + Identifier sort + `) + }) + + test('pipe with identifier', () => { + expect('get-value | process').toMatchTree(` + PipeExpr + FunctionCallOrIdentifier + Identifier get-value + operator | + FunctionCallOrIdentifier + Identifier process + `) + }) + + test('pipe expression in assignment', () => { + expect('result = echo hello | grep h').toMatchTree(` + Assign + Identifier result + operator = + PipeExpr + FunctionCall + Identifier echo + PositionalArg + Identifier hello + operator | + FunctionCall + Identifier grep + PositionalArg + Identifier h + `) + }) + + test('pipe with inline function', () => { + expect('items | each fn x: x end').toMatchTree(` + PipeExpr + FunctionCallOrIdentifier + Identifier items + operator | + FunctionCall + Identifier each + PositionalArg + FunctionDef + keyword fn + Params + Identifier x + colon : + FunctionCallOrIdentifier + Identifier x + end end + `) + }) +}) diff --git a/src/parser/tests/strings.test.ts b/src/parser/tests/strings.test.ts new file mode 100644 index 0000000..c7ba69a --- /dev/null +++ b/src/parser/tests/strings.test.ts @@ -0,0 +1,94 @@ +import { expect, describe, test } from 'bun:test' + +import '../shrimp.grammar' // Importing this so changes cause it to retest! + +describe('string interpolation', () => { + test('string with variable interpolation', () => { + expect("'hello $name'").toMatchTree(` + String + StringFragment ${'hello '} + Interpolation + Identifier name + `) + }) + + test('string with expression interpolation', () => { + expect("'sum is $(a + b)'").toMatchTree(` + String + StringFragment ${'sum is '} + Interpolation + BinOp + Identifier a + operator + + Identifier b + `) + }) +}) + +describe('string escape sequences', () => { + test('escaped dollar sign', () => { + expect("'price is \\$10'").toMatchTree(` + String + StringFragment ${'price is '} + StringEscape \\$ + StringFragment 10 + `) + }) + + test('escaped single quote', () => { + expect("'it\\'s working'").toMatchTree(` + String + StringFragment ${'it'} + StringEscape \\' + StringFragment ${'s working'} + `) + }) + + test('escaped backslash', () => { + expect("'path\\\\file'").toMatchTree(` + String + StringFragment path + StringEscape \\\\ + StringFragment file + `) + }) + + test('escaped newline', () => { + expect("'line1\\nline2'").toMatchTree(` + String + StringFragment line1 + StringEscape \\n + StringFragment line2 + `) + }) + + test('escaped tab', () => { + expect("'col1\\tcol2'").toMatchTree(` + String + StringFragment col1 + StringEscape \\t + StringFragment col2 + `) + }) + + test('escaped carriage return', () => { + expect("'text\\rmore'").toMatchTree(` + String + StringFragment text + StringEscape \\r + StringFragment more + `) + }) + + test('multiple escape sequences', () => { + expect("'\\$10\\nTotal: \\$20'").toMatchTree(` + String + StringEscape \\$ + StringFragment 10 + StringEscape \\n + StringFragment ${'Total: '} + StringEscape \\$ + StringFragment 20 + `) + }) +})