681 lines
15 KiB
TypeScript
681 lines
15 KiB
TypeScript
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 𝜋`)
|
||
})
|
||
|
||
test('parses identifiers with queries', () => {
|
||
expect('even? 20').toMatchTree(`
|
||
FunctionCall
|
||
Identifier even?
|
||
PositionalArg
|
||
Number 20`)
|
||
|
||
expect('even?').toMatchTree(`
|
||
FunctionCallOrIdentifier
|
||
Identifier even?`)
|
||
})
|
||
|
||
})
|
||
|
||
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('modulo tests', () => {
|
||
expect('4 % 3').toMatchTree(`
|
||
BinOp
|
||
Number 4
|
||
Modulo %
|
||
Number 3
|
||
`)
|
||
})
|
||
|
||
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`)
|
||
})
|
||
})
|
||
|
||
describe('Conditional ops', () => {
|
||
test('or can be chained', () => {
|
||
expect(`
|
||
is-positive = do x:
|
||
if x == 3 or x == 4 or x == 5:
|
||
true
|
||
end
|
||
end
|
||
`).toMatchTree(`
|
||
Assign
|
||
AssignableIdentifier is-positive
|
||
Eq =
|
||
FunctionDef
|
||
Do do
|
||
Params
|
||
Identifier x
|
||
colon :
|
||
IfExpr
|
||
keyword if
|
||
ConditionalOp
|
||
ConditionalOp
|
||
ConditionalOp
|
||
Identifier x
|
||
EqEq ==
|
||
Number 3
|
||
Or or
|
||
ConditionalOp
|
||
Identifier x
|
||
EqEq ==
|
||
Number 4
|
||
Or or
|
||
ConditionalOp
|
||
Identifier x
|
||
EqEq ==
|
||
Number 5
|
||
colon :
|
||
ThenBlock
|
||
Boolean true
|
||
keyword end
|
||
keyword end
|
||
`)
|
||
})
|
||
|
||
test('and can be chained', () => {
|
||
expect(`
|
||
is-positive = do x:
|
||
if x == 3 and x == 4 and x == 5:
|
||
true
|
||
end
|
||
end
|
||
`).toMatchTree(`
|
||
Assign
|
||
AssignableIdentifier is-positive
|
||
Eq =
|
||
FunctionDef
|
||
Do do
|
||
Params
|
||
Identifier x
|
||
colon :
|
||
IfExpr
|
||
keyword if
|
||
ConditionalOp
|
||
ConditionalOp
|
||
ConditionalOp
|
||
Identifier x
|
||
EqEq ==
|
||
Number 3
|
||
And and
|
||
ConditionalOp
|
||
Identifier x
|
||
EqEq ==
|
||
Number 4
|
||
And and
|
||
ConditionalOp
|
||
Identifier x
|
||
EqEq ==
|
||
Number 5
|
||
colon :
|
||
ThenBlock
|
||
Boolean true
|
||
keyword end
|
||
keyword end
|
||
`)
|
||
})
|
||
}) |