import { expect, describe, test } from 'bun:test' import '../shrimp.grammar' // Importing this so changes cause it to retest! describe('null', () => { test('parses null', () => { expect('null').toMatchTree(`Null null`) }) test('parses null in assignments', () => { expect('a = null').toMatchTree(` Assign AssignableIdentifier a Eq = Null null`) }) test('does not parse null in identifier', () => { expect('null-jk = 5').toMatchTree(` Assign AssignableIdentifier null-jk Eq = Number 5`) }) }) describe('Identifier', () => { test('parses identifiers with emojis and dashes', () => { expect('moo-😊-34').toMatchTree(` FunctionCallOrIdentifier Identifier moo-😊-34`) }) test('parses mathematical unicode symbols like πœ‹ as identifiers', () => { expect('πœ‹').toMatchTree(` FunctionCallOrIdentifier Identifier πœ‹`) }) }) describe('Unicode Symbol Support', () => { describe('Emoji (currently supported)', () => { test('Basic Emoticons (U+1F600-U+1F64F)', () => { expect('πŸ˜€').toMatchTree(` FunctionCallOrIdentifier Identifier πŸ˜€`) expect('😊-counter').toMatchTree(` FunctionCallOrIdentifier Identifier 😊-counter`) }) test('Miscellaneous Symbols and Pictographs (U+1F300-U+1F5FF)', () => { expect('🌍').toMatchTree(` FunctionCallOrIdentifier Identifier 🌍`) expect('πŸ”₯-handler').toMatchTree(` FunctionCallOrIdentifier Identifier πŸ”₯-handler`) }) test('Transport and Map Symbols (U+1F680-U+1F6FF)', () => { expect('πŸš€').toMatchTree(` FunctionCallOrIdentifier Identifier πŸš€`) expect('πŸš€-launch').toMatchTree(` FunctionCallOrIdentifier Identifier πŸš€-launch`) }) test('Regional Indicator Symbols / Flags (U+1F1E6-U+1F1FF)', () => { // Note: Flags are typically two regional indicators combined expect('πŸ‡Ί').toMatchTree(` FunctionCallOrIdentifier Identifier πŸ‡Ί`) }) test('Supplemental Symbols and Pictographs (U+1F900-U+1F9FF)', () => { expect('πŸ€–').toMatchTree(` FunctionCallOrIdentifier Identifier πŸ€–`) expect('πŸ¦€-lang').toMatchTree(` FunctionCallOrIdentifier Identifier πŸ¦€-lang`) }) test('Dingbats (U+2700-U+27BF)', () => { expect('βœ‚').toMatchTree(` FunctionCallOrIdentifier Identifier βœ‚`) expect('✨-magic').toMatchTree(` FunctionCallOrIdentifier Identifier ✨-magic`) }) test('Miscellaneous Symbols (U+2600-U+26FF)', () => { expect('⚑').toMatchTree(` FunctionCallOrIdentifier Identifier ⚑`) expect('β˜€-bright').toMatchTree(` FunctionCallOrIdentifier Identifier β˜€-bright`) }) }) describe('Greek Letters (not currently supported)', () => { test('Greek lowercase alpha Ξ± (U+03B1)', () => { expect('Ξ±').toMatchTree(` FunctionCallOrIdentifier Identifier Ξ±`) }) test('Greek lowercase beta Ξ² (U+03B2)', () => { expect('Ξ²').toMatchTree(` FunctionCallOrIdentifier Identifier Ξ²`) }) test('Greek lowercase lambda Ξ» (U+03BB)', () => { expect('Ξ»').toMatchTree(` FunctionCallOrIdentifier Identifier Ξ»`) }) test('Greek lowercase pi Ο€ (U+03C0)', () => { // Note: This is different from mathematical pi πœ‹ expect('Ο€').toMatchTree(` FunctionCallOrIdentifier Identifier Ο€`) }) }) describe('Mathematical Alphanumeric Symbols (not currently supported)', () => { test('Mathematical italic small pi πœ‹ (U+1D70B)', () => { expect('πœ‹').toMatchTree(` FunctionCallOrIdentifier Identifier πœ‹`) }) test('Mathematical bold small x 𝐱 (U+1D431)', () => { expect('𝐱').toMatchTree(` FunctionCallOrIdentifier Identifier 𝐱`) }) test('Mathematical script capital F 𝓕 (U+1D4D5)', () => { expect('𝓕').toMatchTree(` FunctionCallOrIdentifier Identifier 𝓕`) }) }) describe('Mathematical Operators (not currently supported)', () => { test('Infinity symbol ∞ (U+221E)', () => { expect('∞').toMatchTree(` FunctionCallOrIdentifier Identifier ∞`) }) test('Sum symbol βˆ‘ (U+2211)', () => { expect('βˆ‘').toMatchTree(` FunctionCallOrIdentifier Identifier βˆ‘`) }) test('Integral symbol ∫ (U+222B)', () => { expect('∫').toMatchTree(` FunctionCallOrIdentifier Identifier ∫`) }) }) describe('Superscripts and Subscripts (not currently supported)', () => { test('Superscript two Β² (U+00B2)', () => { expect('xΒ²').toMatchTree(` FunctionCallOrIdentifier Identifier xΒ²`) }) test('Subscript two β‚‚ (U+2082)', () => { expect('hβ‚‚o').toMatchTree(` FunctionCallOrIdentifier Identifier hβ‚‚o`) }) }) describe('Arrows (not currently supported)', () => { test('Rightward arrow β†’ (U+2192)', () => { expect('β†’').toMatchTree(` FunctionCallOrIdentifier Identifier β†’`) }) test('Leftward arrow ← (U+2190)', () => { expect('←').toMatchTree(` FunctionCallOrIdentifier Identifier ←`) }) test('Double rightward arrow β‡’ (U+21D2)', () => { expect('β‡’').toMatchTree(` FunctionCallOrIdentifier Identifier β‡’`) }) }) describe('CJK Symbols (not currently supported)', () => { test('Hiragana あ (U+3042)', () => { expect('あ').toMatchTree(` FunctionCallOrIdentifier Identifier あ`) }) test('Katakana γ‚« (U+30AB)', () => { expect('γ‚«').toMatchTree(` FunctionCallOrIdentifier Identifier γ‚«`) }) test('CJK Unified Ideograph δΈ­ (U+4E2D)', () => { expect('δΈ­').toMatchTree(` FunctionCallOrIdentifier Identifier δΈ­`) }) }) }) describe('Parentheses', () => { test('allows binOps with parentheses correctly', () => { expect('(2 + 3)').toMatchTree(` ParenExpr BinOp Number 2 Plus + 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 Gt > Identifier b`) expect('(a and b)').toMatchTree(` ParenExpr ConditionalOp Identifier a And and Identifier b`) }) test('allows parens in function calls', () => { expect('echo (3 + 3)').toMatchTree(` FunctionCall Identifier echo PositionalArg ParenExpr BinOp Number 3 Plus + Number 3`) }) test('a word can be contained in parens', () => { expect('(basename ./cool)').toMatchTree(` ParenExpr FunctionCall Identifier basename PositionalArg Word ./cool `) }) test('a word start with an operator', () => { const operators = ['*', '/', '+', '-', 'and', 'or', '=', '!=', '>=', '<=', '>', '<'] for (const operator of operators) { expect(`find ${operator}cool*`).toMatchTree(` FunctionCall Identifier find PositionalArg Word ${operator}cool* `) } }) test('a word can look like a binop', () => { expect('find cool*wow').toMatchTree(` FunctionCall Identifier find PositionalArg Word cool*wow `) }) test('nested parentheses', () => { expect('(2 + (1 * 4))').toMatchTree(` ParenExpr BinOp Number 2 Plus + ParenExpr BinOp Number 1 Star * Number 4`) }) test('Function in parentheses', () => { expect('4 + (echo 3)').toMatchTree(` BinOp Number 4 Plus + ParenExpr FunctionCall Identifier echo PositionalArg Number 3`) }) }) describe('BinOp', () => { test('addition tests', () => { expect('2 + 3').toMatchTree(` BinOp Number 2 Plus + Number 3 `) }) test('subtraction tests', () => { expect('5 - 2').toMatchTree(` BinOp Number 5 Minus - Number 2 `) }) test('multiplication tests', () => { expect('4 * 3').toMatchTree(` BinOp Number 4 Star * Number 3 `) }) test('division tests', () => { expect('8 / 2').toMatchTree(` BinOp Number 8 Slash / Number 2 `) }) test('mixed operations with precedence', () => { expect('2 + 3 * 4 - 5 / 1').toMatchTree(` BinOp BinOp Number 2 Plus + BinOp Number 3 Star * Number 4 Minus - BinOp Number 5 Slash / Number 1 `) }) }) describe('ambiguity', () => { test('parses ambiguous expressions correctly', () => { expect('a + -3').toMatchTree(` BinOp Identifier a Plus + Number -3 `) }) test('parses ambiguous expressions correctly', () => { expect('a-var + a-thing').toMatchTree(` BinOp Identifier a-var Plus + Identifier a-thing `) }) }) describe('newlines', () => { test('parses multiple statements separated by newlines', () => { expect(`x = 5 y = 2`).toMatchTree(` Assign AssignableIdentifier x Eq = Number 5 Assign AssignableIdentifier y Eq = Number 2`) }) test('parses statements separated by semicolons', () => { expect(`x = 5; y = 2`).toMatchTree(` Assign AssignableIdentifier x Eq = Number 5 Assign AssignableIdentifier y Eq = Number 2`) }) test('parses statement with word and a semicolon', () => { expect(`a = hello; 2`).toMatchTree(` Assign AssignableIdentifier a Eq = FunctionCallOrIdentifier Identifier hello Number 2`) }) }) describe('Assign', () => { test('parses simple assignment', () => { expect('x = 5').toMatchTree(` Assign AssignableIdentifier x Eq = Number 5`) }) test('parses assignment with addition', () => { expect('x = 5 + 3').toMatchTree(` Assign AssignableIdentifier x Eq = BinOp Number 5 Plus + Number 3`) }) test('parses assignment with functions', () => { expect('add = do a b: a + b end').toMatchTree(` Assign AssignableIdentifier add Eq = FunctionDef Do do Params Identifier a Identifier b colon : BinOp Identifier a Plus + Identifier b keyword end`) }) }) describe('DotGet whitespace sensitivity', () => { test('no whitespace - DotGet works when identifier in scope', () => { expect('basename = 5; basename.prop').toMatchTree(` Assign AssignableIdentifier basename Eq = Number 5 FunctionCallOrIdentifier DotGet IdentifierBeforeDot basename Identifier prop`) }) test('space before dot - NOT DotGet, parses as division', () => { expect('basename = 5; basename / prop').toMatchTree(` Assign AssignableIdentifier basename Eq = Number 5 BinOp Identifier basename Slash / Identifier prop`) }) test('dot followed by slash is Word, not DotGet', () => { expect('basename ./cool').toMatchTree(` FunctionCall Identifier basename PositionalArg Word ./cool`) }) test('identifier not in scope with dot becomes Word', () => { expect('readme.txt').toMatchTree(`Word readme.txt`) }) }) describe('Comments', () => { test('are barely there', () => { expect(`x = 5 # one banana\ny = 2 # two bananas`).toMatchTree(` Assign AssignableIdentifier x Eq = Number 5 Assign AssignableIdentifier y Eq = Number 2`) expect('# some comment\nbasename = 5 # very astute\n basename / prop\n# good info').toMatchTree(` Assign AssignableIdentifier basename Eq = Number 5 BinOp Identifier basename Slash / Identifier prop`) }) })