import { describe } from 'bun:test' import { expect, test } from 'bun:test' describe('compiler', () => { test('number literal', () => { expect('42').toEvaluateTo(42) }) test('negative number', () => { expect('-5').toEvaluateTo(-5) }) test('string literal', () => { expect(`'hello'`).toEvaluateTo('hello') }) test('boolean true', () => { expect('true').toEvaluateTo(true) }) test('boolean false', () => { expect('false').toEvaluateTo(false) }) test('addition', () => { expect('2 + 3').toEvaluateTo(5) }) test('subtraction', () => { expect('10 - 4').toEvaluateTo(6) }) test('multiplication', () => { expect('3 * 4').toEvaluateTo(12) }) test('division', () => { expect('15 / 3').toEvaluateTo(5) }) test('modulo', () => { expect('44 % 2').toEvaluateTo(0) expect('44 % 3').toEvaluateTo(2) expect('3 % 4').toEvaluateTo(3) }) test('assign number', () => { expect('x = 5').toEvaluateTo(5) }) test('emoji assignment to number', () => { expect('💎 = 5; 💎').toEvaluateTo(5) }) test('unbound identifier', () => { expect('a = hello; a').toEvaluateTo('hello') }) test('assign string', () => { expect(`name = 'Alice'; name`).toEvaluateTo('Alice') }) test('assign expression', () => { expect('sum = 2 + 3; sum').toEvaluateTo(5) }) test('array destructuring with two variables', () => { expect('[ a b ] = [ 1 2 3 4 ]; a').toEvaluateTo(1) expect('[ a b ] = [ 1 2 3 4 ]; b').toEvaluateTo(2) }) test('array destructuring with one variable', () => { expect('[ x ] = [ 42 ]; x').toEvaluateTo(42) }) test('array destructuring with missing elements assigns null', () => { expect('[ a b c ] = [ 1 2 ]; c').toEvaluateTo(null) }) test('array destructuring returns the original array', () => { expect('[ a b ] = [ 1 2 3 4 ]').toEvaluateTo([1, 2, 3, 4]) }) test('array destructuring with emoji identifiers', () => { expect('[ 🚀 💎 ] = [ 1 2 ]; 🚀').toEvaluateTo(1) expect('[ 🚀 💎 ] = [ 1 2 ]; 💎').toEvaluateTo(2) }) test('parentheses', () => { expect('(2 + 3) * 4').toEvaluateTo(20) }) test('function', () => { expect(`do a b: a + b end`).toEvaluateTo(Function) }) test('function call', () => { expect(`add = do a b: a + b end; add 2 9`).toEvaluateTo(11) }) test('function call with named args', () => { expect(`minus = do a b: a - b end; minus b=2 a=9`).toEvaluateTo(7) }) test('function call with named and positional args', () => { expect(`minus = do a b: a - b end; minus b=2 9`).toEvaluateTo(7) expect(`minus = do a b: a - b end; minus 90 b=20`).toEvaluateTo(70) expect(`minus = do a b: a - b end; minus a=900 200`).toEvaluateTo(700) expect(`minus = do a b: a - b end; minus 2000 a=9000`).toEvaluateTo(7000) }) test('function call with no args', () => { expect(`bloop = do: 'bleep' end; bloop`).toEvaluateTo('bleep') expect(`bloop = [ go=do: 'bleep' end ]; bloop.go`).toEvaluateTo('bleep') expect(`bloop = [ go=do: 'bleep' end ]; abc = do x: x end; abc (bloop.go)`).toEvaluateTo('bleep') expect(`num = ((math.random) * 10 + 1) | math.floor; num >= 1 and num <= 10 `).toEvaluateTo(true) }) test('function call with if statement and multiple expressions', () => { expect(` abc = do: if false: echo nope end true end abc `).toEvaluateTo(true) }) test('simple conditionals', () => { expect(`(3 < 6)`).toEvaluateTo(true) expect(`(10 > 20)`).toEvaluateTo(false) expect(`(4 <= 9)`).toEvaluateTo(true) expect(`(15 >= 20)`).toEvaluateTo(false) expect(`(7 == 7)`).toEvaluateTo(true) expect(`(5 != 5)`).toEvaluateTo(false) expect(`('shave' and 'haircut')`).toEvaluateTo('haircut') expect(`(false and witness)`).toEvaluateTo(false) expect(`('pride' or 'prejudice')`).toEvaluateTo('pride') expect(`(false or false)`).toEvaluateTo(false) }) test('if', () => { expect(`if 3 < 9: shire end`).toEvaluateTo('shire') }) test('if else', () => { expect(`if false: grey else: white end`).toEvaluateTo('white') }) test('if else if', () => { expect(`if false: boromir else if true: frodo end`).toEvaluateTo('frodo') }) test('if else if else', () => { expect(`if false: destroyed else if true: fire else: darkness end`).toEvaluateTo('fire') expect(`if false: king else if false: elf else if true: dwarf else: scattered end`).toEvaluateTo('dwarf') }) test('single line if', () => { expect(`if 3 < 9: shire end`).toEvaluateTo('shire') }) test('if statement with function definition (bytecode labels)', () => { expect(` if false: abc = do x: x end else: nope end `).toEvaluateTo('nope') }) }) describe('errors', () => { test('syntax error', () => { expect('2 + ').toFailEvaluation() }) }) describe('multiline tests', () => { test('multiline function', () => { expect(` add = do a b: result = a + b result end add 3 4 `).toEvaluateTo(7) }) }) describe('string interpolation', () => { test('string with variable interpolation', () => { expect(`name = 'Alice'; 'hello $name'`).toEvaluateTo('hello Alice') }) test('string with expression interpolation', () => { expect(`'sum is $(2 + 3)'`).toEvaluateTo('sum is 5') }) test('string with multiple interpolations', () => { expect(`a = 10; b = 20; '$a + $b = $(a + b)'`).toEvaluateTo('10 + 20 = 30') }) test('string with escape sequences', () => { expect(`'line1\\nline2'`).toEvaluateTo('line1\nline2') expect(`'tab\\there'`).toEvaluateTo('tab\there') expect(`'back\\\\slash'`).toEvaluateTo('back\\slash') }) test('string with escaped dollar sign', () => { expect(`'price is \\$10'`).toEvaluateTo('price is $10') }) test('string with mixed interpolation and escapes', () => { expect(`x = 5; 'value: $x\\ntotal: $(x * 2)'`).toEvaluateTo('value: 5\ntotal: 10') }) test('interpolation with unbound identifier', () => { expect(`'greeting: $hello'`).toEvaluateTo('greeting: hello') }) test('nested expression interpolation', () => { expect(`a = 3; b = 4; 'result: $(a * (b + 1))'`).toEvaluateTo('result: 15') }) }) describe('Regex', () => { test('simple regex', () => { expect('//hello//').toEvaluateTo(/hello/) }) test('regex with flags', () => { expect('//[a-z]+//gi').toEvaluateTo(/[a-z]+/gi) }) test('regex in assignment', () => { expect('pattern = //\\d+//; pattern').toEvaluateTo(/\d+/) }) test('invalid regex pattern', () => { expect('//[unclosed//').toEvaluateTo('//[unclosed//') }) }) describe('native functions', () => { test('print function', () => { const add = (x: number, y: number) => x + y expect(`add 5 9`).toEvaluateTo(14, { add }) }) }) describe('dot get', () => { const array = (...items: any) => items const dict = (atNamed: any) => atNamed test('access array element', () => { expect(`arr = array 'a' 'b' 'c'; arr.1`).toEvaluateTo('b', { array }) }) test('access dict element', () => { expect(`dict = dict a=1 b=2; dict.a`).toEvaluateTo(1, { dict }) }) test('use parens expr with dot-get', () => { expect(`a = 1; arr = array 'a' 'b' 'c'; arr.(1 + a)`).toEvaluateTo('c', { array }) }) test('chained dot get: two levels', () => { expect(`obj = [inner=[value=42]]; obj.inner.value`).toEvaluateTo(42) }) test('chained dot get: three levels', () => { expect(`obj = [a=[b=[c=123]]]; obj.a.b.c`).toEvaluateTo(123) }) test('chained dot get: four levels', () => { expect(`obj = [w=[x=[y=[z='deep']]]]; obj.w.x.y.z`).toEvaluateTo('deep') }) test('chained dot get with numeric index', () => { expect(`obj = [items=[1 2 3]]; obj.items.0`).toEvaluateTo(1) }) test('chained dot get in expression', () => { expect(`config = [server=[port=3000]]; config.server.port + 1`).toEvaluateTo(3001) }) test('chained dot get as function argument', () => { const double = (x: number) => x * 2 expect(`obj = [val=[num=21]]; double obj.val.num`).toEvaluateTo(42, { double }) }) test('chained dot get in binary operation', () => { expect(`a = [x=[y=10]]; b = [x=[y=20]]; a.x.y + b.x.y`).toEvaluateTo(30) }) test('chained dot get with parens at end', () => { expect(`idx = 1; obj = [items=[10 20 30]]; obj.items.(idx)`).toEvaluateTo(20) }) test('mixed chained and simple dot get', () => { expect(`obj = [a=1 b=[c=2]]; obj.a + obj.b.c`).toEvaluateTo(3) }) }) describe('default params', () => { test('function with single default parameter', () => { expect('add1 = do x=1: x + 1 end; add1').toEvaluateTo(2) expect('add1 = do x=1: x + 1 end; add1 5').toEvaluateTo(6) }) test('function with multiple default parameters', () => { expect(`weird = do x='something' y=true: [x y] end; weird`).toEvaluateTo(['something', true]) }) test('function with mixed parameters', () => { expect('multiply = do x y=5: x * y end; multiply 5').toEvaluateTo(25) expect('multiply = do x y=5: x * y end; multiply 5 2').toEvaluateTo(10) }) test('null triggers default value', () => { expect('test = do n=true: n end; test').toEvaluateTo(true) expect('test = do n=true: n end; test false').toEvaluateTo(false) expect('test = do n=true: n end; test null').toEvaluateTo(true) }) test('null triggers default for named parameters', () => { expect("greet = do name='World': name end; greet name=null").toEvaluateTo('World') expect("greet = do name='World': name end; greet name='Bob'").toEvaluateTo('Bob') }) test('null triggers default with multiple parameters', () => { expect('calc = do x=10 y=20: x + y end; calc null 5').toEvaluateTo(15) expect('calc = do x=10 y=20: x + y end; calc 3 null').toEvaluateTo(23) expect('calc = do x=10 y=20: x + y end; calc null null').toEvaluateTo(30) }) test.skip('array default', () => { expect('abc = do alpha=[a b c]: alpha end; abc').toEvaluateTo(['a', 'b', 'c']) expect('abc = do alpha=[a b c]: alpha end; abc [x y z]').toEvaluateTo(['x', 'y', 'z']) }) test.skip('dict default', () => { expect('make-person = do person=[name=Bob age=60]: person end; make-person').toEvaluateTo({ name: 'Bob', age: 60, }) expect( 'make-person = do person=[name=Bob age=60]: person end; make-person [name=Jon age=21]' ).toEvaluateTo({ name: 'Jon', age: 21 }) }) }) describe('Nullish coalescing operator (??)', () => { test('returns left side when not null', () => { expect('5 ?? 10').toEvaluateTo(5) }) test('returns right side when left is null', () => { expect('null ?? 10').toEvaluateTo(10) }) test('returns left side when left is false', () => { expect('false ?? 10').toEvaluateTo(false) }) test('returns left side when left is 0', () => { expect('0 ?? 10').toEvaluateTo(0) }) test('returns left side when left is empty string', () => { expect(`'' ?? 'default'`).toEvaluateTo('') }) test('chains left to right', () => { expect('null ?? null ?? 42').toEvaluateTo(42) expect('null ?? 10 ?? 20').toEvaluateTo(10) }) test('short-circuits evaluation', () => { const throwError = () => { throw new Error('Should not evaluate') } expect('5 ?? throw-error').toEvaluateTo(5, { 'throw-error': throwError }) }) test('works with variables', () => { expect('x = null; x ?? 5').toEvaluateTo(5) expect('y = 3; y ?? 5').toEvaluateTo(3) }) test('works with function calls', () => { const getValue = () => null const getDefault = () => 42 // Note: identifiers without parentheses refer to the function, not call it // Use explicit call syntax to invoke the function expect('(get-value) ?? (get-default)').toEvaluateTo(42, { 'get-value': getValue, 'get-default': getDefault }) }) }) describe('Nullish coalescing assignment (??=)', () => { test('assigns when variable is null', () => { expect('x = null; x ??= 5; x').toEvaluateTo(5) }) test('does not assign when variable is not null', () => { expect('x = 3; x ??= 10; x').toEvaluateTo(3) }) test('does not assign when variable is false', () => { expect('x = false; x ??= true; x').toEvaluateTo(false) }) test('does not assign when variable is 0', () => { expect('x = 0; x ??= 100; x').toEvaluateTo(0) }) test('does not assign when variable is empty string', () => { expect(`x = ''; x ??= 'default'; x`).toEvaluateTo('') }) test('returns the final value', () => { expect('x = null; x ??= 5').toEvaluateTo(5) expect('y = 3; y ??= 10').toEvaluateTo(3) }) test('short-circuits evaluation when not null', () => { const throwError = () => { throw new Error('Should not evaluate') } expect('x = 5; x ??= throw-error; x').toEvaluateTo(5, { 'throw-error': throwError }) }) test('works with expressions', () => { expect('x = null; x ??= 2 + 3; x').toEvaluateTo(5) }) test('works with function calls', () => { const getDefault = () => 42 expect('x = null; x ??= (get-default); x').toEvaluateTo(42, { 'get-default': getDefault }) }) test('throws when variable is undefined', () => { expect(() => expect('undefined-var ??= 5').toEvaluateTo(null)).toThrow() }) }) describe('Compound assignment operators', () => { test('+=', () => { expect('x = 5; x += 3; x').toEvaluateTo(8) }) test('-=', () => { expect('x = 10; x -= 4; x').toEvaluateTo(6) }) test('*=', () => { expect('x = 3; x *= 4; x').toEvaluateTo(12) }) test('/=', () => { expect('x = 20; x /= 5; x').toEvaluateTo(4) }) test('%=', () => { expect('x = 10; x %= 3; x').toEvaluateTo(1) }) }) describe('import', () => { test('imports single dict', () => { expect(`import str; starts-with? abc a`).toEvaluateTo(true) }) test('imports multiple dicts', () => { expect(`import str math list; map [1 2 3] do x: x * 2 end`).toEvaluateTo([2, 4, 6]) }) test('imports non-prelude dicts', () => { expect(` abc = [a=true b=yes c=si] import abc abc.b `).toEvaluateTo('yes') }) test('can specify imports', () => { expect(`import str only=ends-with?; ref ends-with? | function?`).toEvaluateTo(true) expect(`import str only=ends-with?; ref starts-with? | function?`).toEvaluateTo(false) expect(` abc = [a=true b=yes c=si] import abc only=[a c] [a c] `).toEvaluateTo([true, 'si']) }) })