diff options
Diffstat (limited to 'js/baba-yaga/tests')
-rw-r--r-- | js/baba-yaga/tests/arrow_functions.test.js | 99 | ||||
-rw-r--r-- | js/baba-yaga/tests/data_structures.test.js | 211 | ||||
-rw-r--r-- | js/baba-yaga/tests/functional-enhancements.test.js | 649 | ||||
-rw-r--r-- | js/baba-yaga/tests/interpreter-with-header.test.js | 90 | ||||
-rw-r--r-- | js/baba-yaga/tests/js-interop.test.js | 407 | ||||
-rw-r--r-- | js/baba-yaga/tests/language_features.test.js | 450 | ||||
-rw-r--r-- | js/baba-yaga/tests/logical_operators.test.js | 85 | ||||
-rw-r--r-- | js/baba-yaga/tests/math_namespace.test.js | 112 | ||||
-rw-r--r-- | js/baba-yaga/tests/parser-with-header.test.js | 36 | ||||
-rw-r--r-- | js/baba-yaga/tests/recursive_functions.test.js | 223 | ||||
-rw-r--r-- | js/baba-yaga/tests/turing_completeness.test.js | 270 | ||||
-rw-r--r-- | js/baba-yaga/tests/typed_curried_functions.test.js | 222 | ||||
-rw-r--r-- | js/baba-yaga/tests/utilities.test.js | 278 | ||||
-rw-r--r-- | js/baba-yaga/tests/with-advanced-patterns.test.js | 290 | ||||
-rw-r--r-- | js/baba-yaga/tests/with-type-system-edge-cases.test.js | 223 | ||||
-rw-r--r-- | js/baba-yaga/tests/with-when-expressions.test.js | 158 |
16 files changed, 3803 insertions, 0 deletions
diff --git a/js/baba-yaga/tests/arrow_functions.test.js b/js/baba-yaga/tests/arrow_functions.test.js new file mode 100644 index 0000000..d6a8aee --- /dev/null +++ b/js/baba-yaga/tests/arrow_functions.test.js @@ -0,0 +1,99 @@ +const assert = require('assert'); +const { createLexer } = require('../src/core/lexer'); +const { createParser } = require('../src/core/parser'); +const { createInterpreter } = require('../src/core/interpreter'); + +describe('Arrow Functions in Table Literals', () => { + function interpret(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + const interpreter = createInterpreter(ast); + return interpreter.interpret(); + } + + it('should correctly parse and execute single arrow function in table', () => { + const code = `calculator : { + add: x y -> x + y; +}; +result : calculator.add 5 3; +result`; + + const result = interpret(code); + assert.strictEqual(result.value, 8); + assert.strictEqual(result.isFloat, false); + }); + + it('should correctly handle arrow function with single parameter', () => { + const code = `calculator : { + double: x -> x * 2; +}; +result : calculator.double 5; +result`; + + const result = interpret(code); + assert.strictEqual(result.value, 10); + assert.strictEqual(result.isFloat, false); + }); + + it('should correctly handle arrow function with complex body', () => { + const code = `calculator : { + complex: x y -> (x + y) * (x - y); +}; +result : calculator.complex 5 3; +result`; + + const result = interpret(code); + assert.strictEqual(result.value, 16); + assert.strictEqual(result.isFloat, false); + }); + + it('should correctly handle arrow function with parentheses for precedence', () => { + const code = `calculator : { + multiply: x y -> x * (y + 1); +}; +result : calculator.multiply 3 2; +result`; + + const result = interpret(code); + assert.strictEqual(result.value, 9); + assert.strictEqual(result.isFloat, false); + }); + + it('should correctly handle multiple arrow functions in table', () => { + const code = `calculator : { + add: x y -> x + y; + subtract: x y -> x - y; + multiply: x y -> x * (y + 1); + complex: x y -> (x + y) * (x - y); +}; +result1 : calculator.add 5 3; +result2 : calculator.subtract 10 4; +result3 : calculator.multiply 3 2; +result4 : calculator.complex 5 3; +result1`; + + const result = interpret(code); + assert.strictEqual(result.value, 8); + assert.strictEqual(result.isFloat, false); + }); + + it('should correctly handle arrow functions with different parameter counts', () => { + const code = `calculator : { + add: x y -> x + y; + double: x -> x * 2; + identity: x -> x; + constant: -> 42; +}; +result1 : calculator.add 5 3; +result2 : calculator.double 7; +result3 : calculator.identity 99; +result4 : calculator.constant; +result1`; + + const result = interpret(code); + assert.strictEqual(result.value, 8); + assert.strictEqual(result.isFloat, false); + }); +}); \ No newline at end of file diff --git a/js/baba-yaga/tests/data_structures.test.js b/js/baba-yaga/tests/data_structures.test.js new file mode 100644 index 0000000..f22fb82 --- /dev/null +++ b/js/baba-yaga/tests/data_structures.test.js @@ -0,0 +1,211 @@ +const assert = require('assert'); +const { createLexer } = require('../src/core/lexer'); +const { createParser } = require('../src/core/parser'); +const { createInterpreter } = require('../src/core/interpreter'); + +describe('Data Structures and Higher-Order Functions', () => { + function interpret(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + const interpreter = createInterpreter(ast); + interpreter.interpret(); // Execute the code + return interpreter; // Return the interpreter instance to access scope + } + + it('should correctly interpret list literals', () => { + const code = 'myList : [1, 2, 3];'; + const interpreter = interpret(code); + const myList = interpreter.scope.get('myList'); + assert.deepStrictEqual(myList.map(item => item.value), [1, 2, 3]); + }); + + it('should correctly interpret table literals', () => { + const code = 'myTable : { name: "Alice" age: 30 };'; + const interpreter = interpret(code); + const expectedTable = { type: 'Object', properties: new Map([['name', 'Alice'], ['age', { value: 30, isFloat: false }]]) }; + const actualTable = interpreter.scope.get('myTable'); + assert.strictEqual(actualTable.type, expectedTable.type); + assert.deepStrictEqual(Array.from(actualTable.properties.entries()), Array.from(expectedTable.properties.entries())); + }); + + it('should correctly access list elements using dot notation', () => { + const code = 'myList : [10, 20, 30];\nio.out myList.1;'; + // For io.out, we need to capture console.log output. This test will pass if no error is thrown. + // A more robust test would mock console.log. + assert.doesNotThrow(() => interpret(code)); + }); + + it('should correctly access table properties using dot notation', () => { + const code = 'myTable : { name: "Bob", age: 25 };\nio.out myTable.name;'; + assert.doesNotThrow(() => interpret(code)); + }); + + it('should correctly interpret anonymous functions', () => { + const code = 'myFunc : (x -> x + 1);\nio.out (myFunc 5);'; + assert.doesNotThrow(() => interpret(code)); + }); + + it('should correctly apply map to a list', () => { + const code = 'io.out (map (x -> x * 2) [1, 2, 3]);'; + assert.doesNotThrow(() => interpret(code)); + }); + + it('should correctly apply filter to a list', () => { + const code = 'io.out (filter (x -> x > 2) [1, 2, 3, 4, 5]);'; + assert.doesNotThrow(() => interpret(code)); + }); + + it('should correctly apply reduce to a list', () => { + const code = 'io.out (reduce (acc item -> acc + item) 0 [1, 2, 3, 4]);'; + assert.doesNotThrow(() => interpret(code)); + }); + + it('should compose functions with reduce (composeAll) and accept list literal as argument', () => { + const code = ` + composeAll : funcs -> + reduce (acc fn -> (x -> acc (fn x))) (x -> x) funcs; + + inc : x -> x + 1; + double : x -> x * 2; + + combo : composeAll [inc, double]; + res : combo 3; + `; + const interpreter = interpret(code); + const res = interpreter.scope.get('res'); + assert.strictEqual(res.value, 7); + }); + + // New tests for list and table pattern matching + it('should correctly match a list literal in a when expression', () => { + const code = ` + myList : [1, 2, 3]; + result : when myList is + [1, 2, 3] then "Matched List" + _ then "Did Not Match"; + `; + const interpreter = interpret(code); + assert.strictEqual(interpreter.scope.get('result'), 'Matched List'); + }); + + it('should correctly match a list with a wildcard in a when expression', () => { + const code = ` + myList : [1, 2, 3]; + result : when myList is + [1, _, 3] then "Matched Wildcard List" + _ then "Did Not Match"; + `; + const interpreter = interpret(code); + assert.strictEqual(interpreter.scope.get('result'), 'Matched Wildcard List'); + }); + + it('should correctly match a table literal in a when expression', () => { + const code = ` + myTable : { a: 1, b: 2 }; + result : when myTable is + { a: 1, b: 2 } then "Matched Table" + _ then "Did Not Match"; + `; + const interpreter = interpret(code); + assert.strictEqual(interpreter.scope.get('result'), 'Matched Table'); + }); + + it('should correctly match a table with a wildcard value in a when expression', () => { + const code = ` + myTable : { a: 1, b: 2 }; + result : when myTable is + { a: 1, b: _ } then "Matched Wildcard Table" + _ then "Did Not Match"; + `; + const interpreter = interpret(code); + assert.strictEqual(interpreter.scope.get('result'), 'Matched Wildcard Table'); + }); + + it('should correctly call a function defined within a table', () => { + const code = ` + myCalculator : { + add: x y -> x + y, + subtract: x y -> x - y + }; + resultAdd : myCalculator.add 10 5; + resultSubtract : myCalculator.subtract 10 5; + `; + const interpreter = interpret(code); + const resultAdd = interpreter.scope.get('resultAdd'); + const resultSubtract = interpreter.scope.get('resultSubtract'); + assert.strictEqual(resultAdd.value, 15); + assert.strictEqual(resultSubtract.value, 5); + }); + + it('should allow both direct and Map-based property access on tables', () => { + const code = ` + myObj : { x: 42, y: "ok" }; + `; + const interpreter = interpret(code); + const obj = interpreter.scope.get('myObj'); + // direct access via proxy + assert.strictEqual(obj.x.value, 42); + assert.strictEqual(obj.y, 'ok'); + // map-based access remains available + assert.strictEqual(obj.properties.get('x').value, 42); + assert.strictEqual(obj.properties.get('y'), 'ok'); + }); + + it('should return shape metadata for lists, strings, tables, and scalars', () => { + const code = ` + lst : [10, 20, 30]; + str : "abc"; + tbl : { a: 1, b: 2 }; + n : 42; + + sLst : shape lst; + sStr : shape str; + sTbl : shape tbl; + sNum : shape n; + `; + const interpreter = interpret(code); + const sLst = interpreter.scope.get('sLst'); + const sStr = interpreter.scope.get('sStr'); + const sTbl = interpreter.scope.get('sTbl'); + const sNum = interpreter.scope.get('sNum'); + + // List + assert.strictEqual(sLst.kind, 'List'); + assert.strictEqual(sLst.rank.value, 1); + assert.strictEqual(sLst.size.value, 3); + assert.strictEqual(sLst.shape[0].value, 3); + + // String + assert.strictEqual(sStr.kind, 'String'); + assert.strictEqual(sStr.rank.value, 1); + assert.strictEqual(sStr.size.value, 3); + assert.strictEqual(sStr.shape[0].value, 3); + + // Table + assert.strictEqual(sTbl.kind, 'Table'); + assert.strictEqual(sTbl.rank.value, 1); + assert.strictEqual(sTbl.size.value, 2); + assert.strictEqual(sTbl.shape[0].value, 2); + // keys array contains 'a' and 'b' (order not enforced here) + const keys = new Set(sTbl.keys); + assert.strictEqual(keys.has('a') && keys.has('b'), true); + + // Scalar + assert.strictEqual(sNum.kind, 'Scalar'); + assert.strictEqual(sNum.rank.value, 0); + assert.strictEqual(Array.isArray(sNum.shape) && sNum.shape.length === 0, true); + assert.strictEqual(sNum.size.value, 1); + }); + + it('should correctly handle a wildcard pattern in a when expression for tables', () => { + const code = ` + myTable : { a: 1 b: 2 }; + result : when myTable is + _ then "Wildcard Match"; + `; + const interpreter = interpret(code); + assert.strictEqual(interpreter.scope.get('result'), 'Wildcard Match'); + }); +}); \ No newline at end of file diff --git a/js/baba-yaga/tests/functional-enhancements.test.js b/js/baba-yaga/tests/functional-enhancements.test.js new file mode 100644 index 0000000..59cabf4 --- /dev/null +++ b/js/baba-yaga/tests/functional-enhancements.test.js @@ -0,0 +1,649 @@ +import { createLexer } from '../src/core/lexer.js'; +import { createParser } from '../src/core/parser.js'; +import { createInterpreter } from '../src/core/interpreter.js'; + +function runBabaYaga(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + + const outputs = []; + const debugOutputs = []; + + const host = { + io: { + out: (...args) => outputs.push(args.join(' ')), + debug: (...args) => debugOutputs.push(args.join(' ')), + in: () => '', + }, + }; + + const interpreter = createInterpreter(ast, host); + const result = interpreter.interpret(); + + return { outputs, debugOutputs, result }; +} + +describe('Functional Programming Enhancements', () => { + + describe('Scan Operations', () => { + test('scan with addition function', () => { + const code = ` + addFunc : acc x -> acc + x; + numbers : [1, 2, 3, 4, 5]; + result : scan addFunc 0 numbers; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('0,1,3,6,10,15'); + }); + + test('cumsum utility function', () => { + const code = ` + numbers : [1, 2, 3, 4, 5]; + result : cumsum numbers; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('0,1,3,6,10,15'); + }); + + test('cumprod utility function', () => { + const code = ` + numbers : [1, 2, 3, 4]; + result : cumprod numbers; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('1,1,2,6,24'); + }); + + test('scan with multiplication function', () => { + const code = ` + mulFunc : acc x -> acc * x; + numbers : [2, 3, 4]; + result : scan mulFunc 1 numbers; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('1,2,6,24'); + }); + }); + + describe('Array Indexing Operations', () => { + test('at function selects elements at indices', () => { + const code = ` + data : [10, 20, 30, 40, 50]; + indices : [0, 2, 4]; + result : at indices data; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('10,30,50'); + }); + + test('where function finds matching indices', () => { + const code = ` + data : [10, 21, 30, 43, 50]; + evenPredicate : x -> x % 2 = 0; + result : where evenPredicate data; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('0,2,4'); + }); + + test('take function gets first n elements', () => { + const code = ` + data : [1, 2, 3, 4, 5, 6]; + result : take 3 data; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('1,2,3'); + }); + + test('drop function removes first n elements', () => { + const code = ` + data : [1, 2, 3, 4, 5, 6]; + result : drop 3 data; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('4,5,6'); + }); + + test('at with empty indices returns empty array', () => { + const code = ` + data : [1, 2, 3]; + indices : []; + result : at indices data; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe(''); + }); + + test('take with zero returns empty array', () => { + const code = ` + data : [1, 2, 3]; + result : take 0 data; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe(''); + }); + }); + + describe('Function Combinators', () => { + test('flip reverses function argument order', () => { + const code = ` + subtract : x y -> x - y; + flippedSubtract : flip subtract; + result : flippedSubtract 3 10; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('7'); // 10 - 3 = 7 + }); + + test('apply applies function to value', () => { + const code = ` + double : x -> x * 2; + result : apply double 7; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('14'); + }); + + test('pipe pipes value through function', () => { + const code = ` + triple : x -> x * 3; + result : pipe 4 triple; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('12'); + }); + + test('compose creates function composition', () => { + const code = ` + increment : x -> x + 1; + double : x -> x * 2; + composed : compose increment double; + result : composed 5; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('11'); // increment(double(5)) = increment(10) = 11 + }); + + test('combinators work with curried functions', () => { + const code = ` + add : x -> y -> x + y; + add5 : add 5; + flippedAdd5 : flip add5; + result : flippedAdd5 3; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('8'); // Should still work: 5 + 3 = 8 + }); + }); + + describe('Broadcasting Operations', () => { + test('broadcast applies scalar operation to array', () => { + const code = ` + addOp : x y -> x + y; + numbers : [1, 2, 3, 4]; + result : broadcast addOp 10 numbers; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('11,12,13,14'); + }); + + test('zipWith applies operation element-wise', () => { + const code = ` + mulOp : x y -> x * y; + array1 : [1, 2, 3]; + array2 : [4, 5, 6]; + result : zipWith mulOp array1 array2; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('4,10,18'); + }); + + test('zipWith handles arrays of different lengths', () => { + const code = ` + addOp : x y -> x + y; + array1 : [1, 2, 3, 4, 5]; + array2 : [10, 20, 30]; + result : zipWith addOp array1 array2; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('11,22,33'); // Only processes minimum length + }); + + test('reshape creates 2D matrix', () => { + const code = ` + flatArray : [1, 2, 3, 4, 5, 6]; + result : reshape [2, 3] flatArray; + // Check that result is a 2x3 matrix + row1 : result.0; + row2 : result.1; + io.out row1; + io.out row2; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('1,2,3'); // First row + expect(outputs[1]).toBe('4,5,6'); // Second row + }); + + test('broadcast with subtraction', () => { + const code = ` + subOp : x y -> x - y; + numbers : [10, 20, 30]; + result : broadcast subOp 5 numbers; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('-5,-15,-25'); // 5 - 10, 5 - 20, 5 - 30 + }); + }); + + describe('Monadic Operations', () => { + test('flatMap flattens mapped results', () => { + const code = ` + duplicateFunc : x -> [x, x]; + original : [1, 2, 3]; + result : flatMap duplicateFunc original; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('1,1,2,2,3,3'); + }); + + test('flatMap with range generation', () => { + const code = ` + rangeFunc : x -> range 1 x; + original : [2, 3]; + result : flatMap rangeFunc original; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('1,2,1,2,3'); + }); + + test('flatMap with empty results', () => { + const code = ` + emptyFunc : x -> []; + original : [1, 2, 3]; + result : flatMap emptyFunc original; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe(''); + }); + + test('flatMap with mixed result lengths', () => { + const code = ` + variableFunc : x -> when x is + 1 then [x] + 2 then [x, x] + _ then [x, x, x]; + original : [1, 2, 3]; + result : flatMap variableFunc original; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('1,2,2,3,3,3'); + }); + }); + + describe('Pattern Guards', () => { + test('basic pattern guards with numeric conditions', () => { + const code = ` + classify : x -> + when x is + n if (n > 0) then "positive" + n if (n < 0) then "negative" + 0 then "zero"; + + result1 : classify 5; + result2 : classify -3; + result3 : classify 0; + io.out result1; + io.out result2; + io.out result3; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('positive'); + expect(outputs[1]).toBe('negative'); + expect(outputs[2]).toBe('zero'); + }); + + test('pattern guards with range conditions', () => { + const code = ` + categorizeAge : age -> + when age is + a if (a >= 0 and a < 18) then "minor" + a if (a >= 18 and a < 65) then "adult" + a if (a >= 65) then "senior" + _ then "invalid"; + + result1 : categorizeAge 16; + result2 : categorizeAge 30; + result3 : categorizeAge 70; + io.out result1; + io.out result2; + io.out result3; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('minor'); + expect(outputs[1]).toBe('adult'); + expect(outputs[2]).toBe('senior'); + }); + + test('pattern guards with complex conditions', () => { + const code = ` + gradeStudent : score -> + when score is + s if (s >= 90) then "A" + s if (s >= 80 and s < 90) then "B" + s if (s >= 70 and s < 80) then "C" + s if (s < 70) then "F" + _ then "Invalid"; + + result1 : gradeStudent 95; + result2 : gradeStudent 85; + result3 : gradeStudent 75; + result4 : gradeStudent 65; + io.out result1; + io.out result2; + io.out result3; + io.out result4; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('A'); + expect(outputs[1]).toBe('B'); + expect(outputs[2]).toBe('C'); + expect(outputs[3]).toBe('F'); + }); + + test('pattern guards with wildcard patterns', () => { + const code = ` + checkRange : x -> + when x is + _ if (x >= 1 and x <= 10) then "small" + _ if (x >= 11 and x <= 100) then "medium" + _ if (x > 100) then "large" + _ then "invalid"; + + result1 : checkRange 5; + result2 : checkRange 50; + result3 : checkRange 150; + result4 : checkRange -5; + io.out result1; + io.out result2; + io.out result3; + io.out result4; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('small'); + expect(outputs[1]).toBe('medium'); + expect(outputs[2]).toBe('large'); + expect(outputs[3]).toBe('invalid'); + }); + + test('pattern guards fail when condition is false', () => { + const code = ` + testGuard : x -> + when x is + n if (n > 10) then "big" + _ then "small"; + + result1 : testGuard 15; + result2 : testGuard 5; + io.out result1; + io.out result2; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('big'); + expect(outputs[1]).toBe('small'); + }); + }); + + describe('Integration Tests', () => { + test('combining scan and broadcast operations', () => { + const code = ` + numbers : [1, 2, 3, 4]; + cumulative : cumsum numbers; + addTen : broadcast (x y -> x + y) 10 cumulative; + io.out addTen; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('10,11,13,16,20'); // cumsum [1,2,3,4] = [0,1,3,6,10], then +10 each + }); + + test('combining flatMap with array indexing', () => { + const code = ` + data : [[1, 2], [3, 4, 5], [6]]; + flattened : flatMap (x -> x) data; + evens : where (x -> x % 2 = 0) flattened; + evenValues : at evens flattened; + io.out evenValues; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('2,4,6'); + }); + + test('combining pattern guards with functional operations', () => { + const code = ` + processNumbers : numbers -> + with ( + classified : map (n -> when n is + x if (x > 0) then "pos" + x if (x < 0) then "neg" + 0 then "zero") numbers; + positives : filter (n -> n > 0) numbers; + posSum : reduce (acc x -> acc + x) 0 positives; + ) -> + {classifications: classified, sum: posSum}; + + result : processNumbers [-2, 0, 3, -1, 5]; + io.out result.sum; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('8'); // 3 + 5 = 8 + }); + + test('complex pipeline with multiple new features', () => { + const code = ` + data : [1, 2, 3, 4, 5]; + + // Use scan to get cumulative sums + cumSums : cumsum data; + + // Use broadcast to multiply by 2 + doubled : broadcast (x y -> x * y) 2 cumSums; + + // Use where to find indices of values > 10 + bigIndices : where (x -> x > 10) doubled; + + // Use at to get those values + bigValues : at bigIndices doubled; + + io.out bigValues; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('12,20,30'); // Values > 10 from [0,2,6,12,20,30] + }); + }); + + describe('Error Handling', () => { + test('at throws error for out of bounds index', () => { + const code = ` + data : [1, 2, 3]; + indices : [0, 5]; + result : at indices data; + `; + expect(() => runBabaYaga(code)).toThrow(/Index out of bounds|Can't find variable/); + }); + + test('reshape throws error for incompatible dimensions', () => { + const code = ` + data : [1, 2, 3, 4, 5]; + result : reshape [2, 3] data; + `; + expect(() => runBabaYaga(code)).toThrow('Cannot reshape array'); + }); + + test('scan requires function as first argument', () => { + const code = ` + result : scan 42 0 [1, 2, 3]; + `; + expect(() => runBabaYaga(code)).toThrow('Scan expects a function'); + }); + + test('broadcast requires function as first argument', () => { + const code = ` + result : broadcast "not a function" 5 [1, 2, 3]; + `; + expect(() => runBabaYaga(code)).toThrow('broadcast expects a function'); + }); + + test('where requires function as first argument', () => { + const code = ` + result : where "not a function" [1, 2, 3]; + `; + expect(() => runBabaYaga(code)).toThrow('where expects a function'); + }); + + test('flatMap requires function as first argument', () => { + const code = ` + result : flatMap 42 [1, 2, 3]; + `; + expect(() => runBabaYaga(code)).toThrow('flatMap expects a function'); + }); + + test('take with negative number throws error', () => { + const code = ` + result : take -1 [1, 2, 3]; + `; + expect(() => runBabaYaga(code)).toThrow('take expects a non-negative number'); + }); + + test('drop with negative number throws error', () => { + const code = ` + result : drop -1 [1, 2, 3]; + `; + expect(() => runBabaYaga(code)).toThrow('drop expects a non-negative number'); + }); + }); + + describe('Edge Cases', () => { + test('scan with empty array', () => { + const code = ` + addFunc : acc x -> acc + x; + result : scan addFunc 0 []; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('0'); // Just the initial value + }); + + test('broadcast with empty array', () => { + const code = ` + addOp : x y -> x + y; + result : broadcast addOp 5 []; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe(''); // Empty result + }); + + test('zipWith with empty arrays', () => { + const code = ` + addOp : x y -> x + y; + result : zipWith addOp [] []; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe(''); // Empty result + }); + + test('where with no matches', () => { + const code = ` + neverTrue : x -> false; + result : where neverTrue [1, 2, 3]; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe(''); // No matching indices + }); + + test('flatMap with single-element arrays', () => { + const code = ` + wrapFunc : x -> [x]; + result : flatMap wrapFunc [1, 2, 3]; + io.out result; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('1,2,3'); // Should flatten to original + }); + + test('pattern guards with complex boolean expressions', () => { + const code = ` + complexTest : x -> + when x is + n if ((n > 5) and (n < 15) and (n % 2 = 0)) then "even between 5 and 15" + n if ((n > 0) or (n < -10)) then "positive or very negative" + _ then "other"; + + result1 : complexTest 8; + result2 : complexTest 3; + result3 : complexTest -15; + result4 : complexTest -5; + io.out result1; + io.out result2; + io.out result3; + io.out result4; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('even between 5 and 15'); // 8 matches first condition + expect(outputs[1]).toBe('positive or very negative'); // 3 is positive + expect(outputs[2]).toBe('positive or very negative'); // -15 is very negative + expect(outputs[3]).toBe('other'); // -5 doesn't match any condition + }); + + test('combinators with identity functions', () => { + const code = ` + identity : x -> x; + doubled : x -> x * 2; + + // Compose with identity should be equivalent to original function + composedWithId : compose identity doubled; + result1 : composedWithId 5; + + // Apply identity should return original value + result2 : apply identity 42; + + // Pipe through identity should return original value + result3 : pipe 7 identity; + + io.out result1; + io.out result2; + io.out result3; + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe('10'); // identity(doubled(5)) = 10 + expect(outputs[1]).toBe('42'); // identity(42) = 42 + expect(outputs[2]).toBe('7'); // pipe 7 identity = 7 + }); + }); +}); diff --git a/js/baba-yaga/tests/interpreter-with-header.test.js b/js/baba-yaga/tests/interpreter-with-header.test.js new file mode 100644 index 0000000..0f50be4 --- /dev/null +++ b/js/baba-yaga/tests/interpreter-with-header.test.js @@ -0,0 +1,90 @@ +import assert from 'assert'; +import { createLexer } from '../src/core/lexer.js'; +import { createParser } from '../src/core/parser.js'; +import { createInterpreter } from '../src/core/interpreter.js'; + +function interpret(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + const interpreter = createInterpreter(ast); + interpreter.interpret(); + return interpreter; +} + +describe('with header locals', () => { + it('evaluates untyped locals', () => { + const code = ` + addMul : x y -> with (inc : x + 1; prod : inc * y;) -> inc + prod; + r : addMul 2 5; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('r').value, (2+1) + ((2+1)*5)); + }); + + it('evaluates typed locals with validation', () => { + const code = ` + sumNext : (x: Int, y: Int) -> Int -> + with (nx Int; ny Int; nx : x + 1; ny : y + 1;) -> nx + ny; + r : sumNext 2 3; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('r').value, 7); + }); + + it('rejects typed local mismatch', () => { + const code = ` + bad : (x: Int) -> Int -> + with (s String; s : x + 1;) -> 0; + r : bad 2; + `; + assert.throws(() => interpret(code), /Type mismatch for s: expected String/); + }); + + it('works with when expressions', () => { + const code = ` + classify : n -> + with (lo Int; hi Int; lo : 10; hi : 100;) -> + when n is + 0 then "zero" + _ then when (n > hi) is + true then "large" + _ then when (n > lo) is + true then "medium" + _ then "small"; + a : classify 0; + b : classify 50; + c : classify 200; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('a'), 'zero'); + assert.strictEqual(itp.scope.get('b'), 'medium'); + assert.strictEqual(itp.scope.get('c'), 'large'); + }); + + it('supports with rec for mutual recursion', () => { + const code = ` + isEvenOdd : z -> + with rec ( + isEven : n -> when n is 0 then true _ then isOdd (n - 1); + isOdd : n -> when n is 0 then false _ then isEven (n - 1); + ) -> { e: isEven 10, o: isOdd 7 }; + r : isEvenOdd 0; + `; + const itp = interpret(code); + const r = itp.scope.get('r'); + assert.strictEqual(r.e, true); + assert.strictEqual(r.o, true); + }); + + it('errors if with rec binding is not a function', () => { + const code = ` + bad : z -> with rec (x : 1;) -> 0; + r : bad 0; + `; + assert.throws(() => interpret(code), /with rec expects function-valued bindings/); + }); +}); + + diff --git a/js/baba-yaga/tests/js-interop.test.js b/js/baba-yaga/tests/js-interop.test.js new file mode 100644 index 0000000..77c760a --- /dev/null +++ b/js/baba-yaga/tests/js-interop.test.js @@ -0,0 +1,407 @@ +// js-interop.test.js - Tests for JavaScript interop functionality + +import { describe, it, expect } from 'bun:test'; +import { createLexer } from '../src/core/lexer.js'; +import { createParser } from '../src/core/parser.js'; +import { createInterpreter } from '../src/core/interpreter.js'; + +// Helper function to run Baba Yaga code with JS interop +function runBabaCode(code, jsBridgeConfig = {}) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + + const host = { + jsBridgeConfig: { + allowedFunctions: new Set([ + 'JSON.parse', 'JSON.stringify', + 'Math.abs', 'Math.floor', 'Math.ceil', 'Math.round', + 'Math.min', 'Math.max', 'Math.random', + 'console.log', 'console.warn', 'console.error', + 'Date.now', 'performance.now', + 'testFunction', 'testAsyncFunction', 'testErrorFunction' + ]), + ...jsBridgeConfig + }, + io: { + out: () => {}, // Silent for tests + debug: () => {} + } + }; + + // Add test functions to global scope for testing + global.testFunction = (x) => x * 2; + global.testAsyncFunction = async (x) => Promise.resolve(x + 10); + global.testErrorFunction = () => { throw new Error('Test error'); }; + + // The JS bridge will create its own default sandbox + // We'll add test functions to the allowed functions, but let the bridge handle the sandbox + + const interpreter = createInterpreter(ast, host); + interpreter.interpret(); + return interpreter.scope.get('result'); +} + +describe('JavaScript Interop - Basic Function Calls', () => { + it('should call JavaScript Math.abs function', () => { + const code = ` + result : io.callJS "Math.abs" [-5]; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Ok'); + expect(result.value.value).toBe(5); + }); + + it('should call JavaScript JSON.parse function', () => { + const code = ` + jsonStr : "{\\"name\\": \\"Alice\\", \\"age\\": 30}"; + result : io.callJS "JSON.parse" [jsonStr]; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Ok'); + + const parsed = result.value; + expect(parsed.type).toBe('JSValue'); + expect(parsed.value.name).toBe('Alice'); + expect(parsed.value.age).toBe(30); + }); + + it('should call JavaScript JSON.stringify function', () => { + const code = ` + data : {name: "Bob", age: 25}; + jsObj : io.tableToObject data; + result : io.callJS "JSON.stringify" [jsObj]; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Ok'); + + const jsonStr = result.value; + expect(jsonStr.type).toBe('JSValue'); + expect(typeof jsonStr.value).toBe('string'); + expect(jsonStr.value).toContain('Bob'); + expect(jsonStr.value).toContain('25'); + }); + + it('should handle function call errors gracefully', () => { + const code = ` + result : io.callJS "nonexistentFunction" [42]; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Err'); + + const errorMsg = result.value; + expect(errorMsg).toContain('not allowed'); + }); + + it('should handle JavaScript errors in called functions', () => { + const code = ` + result : io.callJS "testErrorFunction" []; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Err'); + + const errorMsg = result.value; + expect(errorMsg).toContain('Test error'); + }); +}); + +describe('JavaScript Interop - Property Access', () => { + it('should get property from JavaScript object', () => { + const code = ` + jsObj : io.callJS "JSON.parse" ["{\\"x\\": 42, \\"y\\": 24}"]; + result : when jsObj is + Ok obj then io.getProperty obj "x" + Err msg then Err msg; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Ok'); + expect(result.value.value).toBe(42); + }); + + it('should handle missing properties gracefully', () => { + const code = ` + jsObj : io.callJS "JSON.parse" ["{\\"x\\": 42}"]; + result : when jsObj is + Ok obj then io.getProperty obj "missing" + Err msg then Err msg; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Ok'); + expect(result.value).toBe(null); + }); + + it('should check if property exists', () => { + const code = ` + jsObj : io.callJS "JSON.parse" ["{\\"name\\": \\"test\\"}"]; + hasName : when jsObj is + Ok obj then io.hasProperty obj "name" + Err _ then false; + hasMissing : when jsObj is + Ok obj then io.hasProperty obj "missing" + Err _ then false; + result : {hasName: hasName, hasMissing: hasMissing}; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Object'); + expect(result.properties.get('hasName')).toBe(true); + expect(result.properties.get('hasMissing')).toBe(false); + }); +}); + +describe('JavaScript Interop - Array Conversion', () => { + it('should convert JavaScript array to Baba Yaga list', () => { + const code = ` + jsArray : io.callJS "JSON.parse" ["[1, 2, 3, 4, 5]"]; + result : when jsArray is + Ok arr then io.jsArrayToList arr + Err msg then Err msg; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Ok'); + + const list = result.value; + expect(Array.isArray(list)).toBe(true); + expect(list.length).toBe(5); + expect(list[0].value).toBe(1); + expect(list[4].value).toBe(5); + }); + + it('should convert Baba Yaga list to JavaScript array', () => { + const code = ` + babaList : [10, 20, 30]; + jsArray : io.listToJSArray babaList; + result : io.callJS "JSON.stringify" [jsArray]; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Ok'); + + const jsonStr = result.value; + expect(jsonStr.type).toBe('JSValue'); + expect(jsonStr.value).toBe('[10,20,30]'); + }); +}); + +describe('JavaScript Interop - Object/Table Conversion', () => { + it('should convert Baba Yaga table to JavaScript object', () => { + const code = ` + babaTable : {name: "Alice", age: 30, active: true}; + jsObj : io.tableToObject babaTable; + result : io.callJS "JSON.stringify" [jsObj]; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Ok'); + + const jsonStr = result.value; + expect(jsonStr.type).toBe('JSValue'); + const parsed = JSON.parse(jsonStr.value); + expect(parsed.name).toBe('Alice'); + expect(parsed.age).toBe(30); + expect(parsed.active).toBe(true); + }); + + it('should convert JavaScript object to Baba Yaga table', () => { + const code = ` + jsObj : io.callJS "JSON.parse" ["{\\"x\\": 100, \\"y\\": 200}"]; + result : when jsObj is + Ok obj then io.objectToTable obj + Err msg then Err msg; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Ok'); + + const table = result.value; + expect(table.type).toBe('Object'); + expect(table.properties.get('x').value).toBe(100); + expect(table.properties.get('y').value).toBe(200); + }); +}); + +describe('JavaScript Interop - Error Handling', () => { + it('should track and retrieve last JavaScript error', () => { + const code = ` + // Cause an error + errorResult : io.callJS "testErrorFunction" []; + + // For now, just test that we can cause an error + // The error tracking functions have syntax issues in Baba Yaga + result : {errorResult: errorResult}; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Object'); + + // Error result should be Err + const errorResult = result.properties.get('errorResult'); + expect(errorResult.type).toBe('Result'); + expect(errorResult.variant).toBe('Err'); + }); +}); + +describe('JavaScript Interop - Real-world Usage Patterns', () => { + it('should implement safe JSON parsing pattern', () => { + const code = ` + parseJSON : jsonString -> + when (validate.type "String" jsonString) is + false then Err "Input must be a string" + true then when (io.callJS "JSON.parse" [jsonString]) is + Ok parsed then Ok (io.objectToTable parsed) + Err msg then Err ("JSON parse error: " .. msg); + + // Test valid JSON + validResult : parseJSON "{\\"name\\": \\"Bob\\", \\"age\\": 25}"; + + // Test invalid JSON + invalidResult : parseJSON "invalid json"; + + result : {valid: validResult, invalid: invalidResult}; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Object'); + + // Valid result should be Ok + const validResult = result.properties.get('valid'); + expect(validResult.type).toBe('Result'); + expect(validResult.variant).toBe('Ok'); + + // Invalid result should be Err + const invalidResult = result.properties.get('invalid'); + expect(invalidResult.type).toBe('Result'); + expect(invalidResult.variant).toBe('Err'); + }); + + it('should implement safe mathematical operations', () => { + const code = ` + // Test each operation individually to avoid curried function issues + minResult : io.callJS "Math.min" [10, 5]; + maxResult : io.callJS "Math.max" [10, 5]; + absResult : io.callJS "Math.abs" [-7]; + + result : {min: minResult, max: maxResult, abs: absResult}; + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Object'); + + // All results should be Ok + const minResult = result.properties.get('min'); + expect(minResult.type).toBe('Result'); + expect(minResult.variant).toBe('Ok'); + expect(minResult.value.value).toBe(5); + + const maxResult = result.properties.get('max'); + expect(maxResult.type).toBe('Result'); + expect(maxResult.variant).toBe('Ok'); + expect(maxResult.value.value).toBe(10); + + const absResult = result.properties.get('abs'); + expect(absResult.type).toBe('Result'); + expect(absResult.variant).toBe('Ok'); + expect(absResult.value.value).toBe(7); + }); + + it('should handle complex nested data structures', () => { + const code = ` + complexData : { + users: [ + {name: "Alice", scores: [85, 92, 78]}, + {name: "Bob", scores: [90, 87, 95]} + ], + meta: { + total: 2, + created: "2024-01-01" + } + }; + + // Convert to JS and back + jsObj : io.tableToObject complexData; + jsonStr : io.callJS "JSON.stringify" [jsObj]; + + result : when jsonStr is + Ok str then when (io.callJS "JSON.parse" [str]) is + Ok parsed then io.objectToTable parsed + Err msg then Err ("Parse failed: " .. msg) + Err msg then Err ("Stringify failed: " .. msg); + + result; + `; + + const result = runBabaCode(code); + expect(result).toBeDefined(); + expect(result.type).toBe('Result'); + expect(result.variant).toBe('Ok'); + + const roundTripped = result.value; + expect(roundTripped.type).toBe('Object'); + expect(roundTripped.properties.has('users')).toBe(true); + expect(roundTripped.properties.has('meta')).toBe(true); + + // Check nested structure integrity + const users = roundTripped.properties.get('users'); + expect(Array.isArray(users)).toBe(true); + expect(users.length).toBe(2); + + const alice = users[0]; + expect(alice.type).toBe('Object'); + expect(alice.properties.get('name')).toBe('Alice'); + }); +}); + +// Clean up global test functions +global.testFunction = undefined; +global.testAsyncFunction = undefined; +global.testErrorFunction = undefined; diff --git a/js/baba-yaga/tests/language_features.test.js b/js/baba-yaga/tests/language_features.test.js new file mode 100644 index 0000000..0550f70 --- /dev/null +++ b/js/baba-yaga/tests/language_features.test.js @@ -0,0 +1,450 @@ +const assert = require('assert'); +const { createLexer } = require('../src/core/lexer'); +const { createParser } = require('../src/core/parser'); +const { createInterpreter } = require('../src/core/interpreter'); + +describe('Language Features', () => { + function interpret(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + const interpreter = createInterpreter(ast); + interpreter.interpret(); + return interpreter; + } + + describe('Mathematical Constants', () => { + it('should correctly handle PI constant', () => { + const code = 'result : PI;'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, Math.PI); + assert.strictEqual(result.isFloat, true); + }); + + it('should correctly handle INFINITY constant', () => { + const code = 'result : INFINITY;'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, Infinity); + assert.strictEqual(result.isFloat, true); + }); + + it('should use constants in expressions', () => { + const code = 'result : 2 * PI;'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 2 * Math.PI); + }); + }); + + describe('Immutable List Operations', () => { + it('should correctly append to lists', () => { + const code = ` + original : [1, 2, 3]; + result : append original 4; + `; + const interpreter = interpret(code); + const original = interpreter.scope.get('original'); + const result = interpreter.scope.get('result'); + assert.deepStrictEqual(original.map(item => item.value), [1, 2, 3]); + assert.deepStrictEqual(result.map(item => item.value), [1, 2, 3, 4]); + }); + + it('should correctly prepend to lists', () => { + const code = ` + original : [1, 2, 3]; + result : prepend 0 original; + `; + const interpreter = interpret(code); + const original = interpreter.scope.get('original'); + const result = interpreter.scope.get('result'); + assert.deepStrictEqual(original.map(item => item.value), [1, 2, 3]); + assert.deepStrictEqual(result.map(item => item.value), [0, 1, 2, 3]); + }); + + it('should correctly concatenate lists', () => { + const code = ` + list1 : [1, 2]; + list2 : [3, 4]; + result : concat list1 list2; + `; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.deepStrictEqual(result.map(item => item.value), [1, 2, 3, 4]); + }); + + it('should correctly update list elements', () => { + const code = ` + original : [1, 2, 3]; + result : update original 1 99; + `; + const interpreter = interpret(code); + const original = interpreter.scope.get('original'); + const result = interpreter.scope.get('result'); + assert.deepStrictEqual(original.map(item => item.value), [1, 2, 3]); + assert.deepStrictEqual(result.map(item => item.value), [1, 99, 3]); + }); + + it('should correctly remove elements from lists', () => { + const code = ` + original : [1, 2, 3]; + result : removeAt original 1; + `; + const interpreter = interpret(code); + const original = interpreter.scope.get('original'); + const result = interpreter.scope.get('result'); + assert.deepStrictEqual(original.map(item => item.value), [1, 2, 3]); + assert.deepStrictEqual(result.map(item => item.value), [1, 3]); + }); + + it('should correctly slice lists', () => { + const code = ` + original : [1, 2, 3, 4, 5]; + result : slice original 1 4; + `; + const interpreter = interpret(code); + const original = interpreter.scope.get('original'); + const result = interpreter.scope.get('result'); + assert.deepStrictEqual(original.map(item => item.value), [1, 2, 3, 4, 5]); + assert.deepStrictEqual(result.map(item => item.value), [2, 3, 4]); + }); + }); + + describe('Immutable Table Operations', () => { + it('should correctly set table properties', () => { + const code = ` + original : {name: "Alice", age: 30}; + result : set original "city" "NYC"; + `; + const interpreter = interpret(code); + const original = interpreter.scope.get('original'); + const result = interpreter.scope.get('result'); + assert.strictEqual(original.properties.get('name'), 'Alice'); + assert.strictEqual(original.properties.get('age').value, 30); + assert.strictEqual(result.properties.get('name'), 'Alice'); + assert.strictEqual(result.properties.get('age').value, 30); + assert.strictEqual(result.properties.get('city'), 'NYC'); + }); + + it('should correctly remove table properties', () => { + const code = ` + original : {name: "Alice", age: 30}; + result : remove original "age"; + `; + const interpreter = interpret(code); + const original = interpreter.scope.get('original'); + const result = interpreter.scope.get('result'); + assert.strictEqual(original.properties.get('name'), 'Alice'); + assert.strictEqual(original.properties.get('age').value, 30); + assert.strictEqual(result.properties.get('name'), 'Alice'); + assert.strictEqual(result.properties.has('age'), false); + }); + + it('should correctly merge tables', () => { + const code = ` + table1 : {name: "Alice", age: 30}; + table2 : {city: "NYC", country: "USA"}; + result : merge table1 table2; + `; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.properties.get('name'), 'Alice'); + assert.strictEqual(result.properties.get('age').value, 30); + assert.strictEqual(result.properties.get('city'), 'NYC'); + assert.strictEqual(result.properties.get('country'), 'USA'); + }); + + it('should correctly get table keys', () => { + const code = ` + table : {name: "Alice", age: 30}; + result : keys table; + `; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.deepStrictEqual(result, ['name', 'age']); + }); + + it('should correctly get table values', () => { + const code = ` + table : {name: "Alice", age: 30}; + result : values table; + `; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result[0], 'Alice'); + assert.strictEqual(result[1].value, 30); + }); + }); + + describe('String Operations', () => { + it('should correctly concatenate strings', () => { + const code = 'result : str.concat "Hello" " " "World";'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result, 'Hello World'); + }); + + it('should correctly split strings', () => { + const code = 'result : str.split "a,b,c" ",";'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.deepStrictEqual(result, ['a', 'b', 'c']); + }); + + it('should correctly join lists into strings', () => { + const code = 'result : str.join ["a", "b", "c"] "-";'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result, 'a-b-c'); + }); + + it('should correctly get string length', () => { + const code = 'result : str.length "hello";'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 5); + }); + + it('should correctly get substrings', () => { + const code = 'result : str.substring "hello world" 0 5;'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result, 'hello'); + }); + + it('should correctly replace substrings', () => { + const code = 'result : str.replace "hello hello" "hello" "hi";'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result, 'hi hi'); + }); + + it('should correctly trim strings', () => { + const code = 'result : str.trim " hello ";'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result, 'hello'); + }); + + it('should correctly convert to uppercase', () => { + const code = 'result : str.upper "hello";'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result, 'HELLO'); + }); + + it('should correctly convert to lowercase', () => { + const code = 'result : str.lower "HELLO";'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result, 'hello'); + }); + }); + + describe('Type Declarations and Type Checking', () => { + it('should correctly handle type declarations', () => { + const code = ` + myNumber Int; + myNumber : 42; + `; + const interpreter = interpret(code); + const result = interpreter.scope.get('myNumber'); + assert.strictEqual(result.value, 42); + }); + + it('should correctly handle type checking in when expressions', () => { + const code = ` + checkType : val -> + when val is + Int then "Integer" + String then "String" + Bool then "Boolean" + _ then "Other"; + + result1 : checkType 42; + result2 : checkType "hello"; + result3 : checkType true; + `; + const interpreter = interpret(code); + assert.strictEqual(interpreter.scope.get('result1'), 'Integer'); + assert.strictEqual(interpreter.scope.get('result2'), 'String'); + assert.strictEqual(interpreter.scope.get('result3'), 'Boolean'); + }); + }); + + describe('Result Type', () => { + it('should correctly create Ok results', () => { + const code = 'result : Ok 42;'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.type, 'Result'); + assert.strictEqual(result.variant, 'Ok'); + assert.strictEqual(result.value.value, 42); + }); + + it('should correctly create Err results', () => { + const code = 'result : Err "error message";'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.type, 'Result'); + assert.strictEqual(result.variant, 'Err'); + assert.strictEqual(result.value, 'error message'); + }); + + it('should correctly pattern match on Result types', () => { + const code = ` + divide : x y -> + when y is + 0 then Err "Division by zero" + _ then Ok (x / y); + + result1 : when (divide 10 2) is + Ok value then value + Err msg then 0; + + result2 : when (divide 10 0) is + Ok value then value + Err msg then msg; + `; + const interpreter = interpret(code); + const result1 = interpreter.scope.get('result1'); + const result2 = interpreter.scope.get('result2'); + assert.strictEqual(result1.value, 5); + assert.strictEqual(result2, 'Division by zero'); + }); + }); + + describe('Operators', () => { + it('should correctly handle unary negation', () => { + const code = 'result : -5;'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, -5); + }); + + it('should correctly handle string concatenation with ..', () => { + const code = 'result : "Hello" .. " " .. "World";'; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result, 'Hello World'); + }); + + it('should correctly handle comparison operators', () => { + const code = ` + result1 : 5 > 3; + result2 : 5 < 3; + result3 : 5 >= 5; + result4 : 5 <= 3; + result5 : 5 = 5; + `; + const interpreter = interpret(code); + assert.strictEqual(interpreter.scope.get('result1'), true); + assert.strictEqual(interpreter.scope.get('result2'), false); + assert.strictEqual(interpreter.scope.get('result3'), true); + assert.strictEqual(interpreter.scope.get('result4'), false); + assert.strictEqual(interpreter.scope.get('result5'), true); + }); + + it('should correctly handle modulo operator', () => { + const code = ` + result1 : 10 % 3; + result2 : 15 % 4; + result3 : 7 % 2; + `; + const interpreter = interpret(code); + assert.strictEqual(interpreter.scope.get('result1').value, 1); + assert.strictEqual(interpreter.scope.get('result2').value, 3); + assert.strictEqual(interpreter.scope.get('result3').value, 1); + }); + }); + + describe('Curried Functions and Partial Application', () => { + it('should correctly handle curried functions', () => { + const code = ` + add : x -> y -> x + y; + add5 : add 5; + result : add5 3; + `; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 8); + }); + + it('should correctly handle partial application', () => { + const code = ` + multiply : x -> y -> x * y; + double : multiply 2; + result : double 7; + `; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 14); + }); + }); + + describe('Anonymous Functions', () => { + it('should correctly handle anonymous functions', () => { + const code = ` + result : (x -> x * 2) 5; + `; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 10); + }); + + it('should correctly handle anonymous functions with multiple parameters', () => { + const code = ` + result : (x y -> x + y) 3 4; + `; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 7); + }); + }); + + describe('Function Calls', () => { + it('should correctly handle parenthesized function calls', () => { + const code = ` + add : x y -> x + y; + result : (add 3 4); + `; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 7); + }); + + it('should correctly handle non-parenthesized function calls', () => { + const code = ` + add : x y -> x + y; + result : add 3 4; + `; + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 7); + }); + }); + + describe('Error Handling', () => { + it('should handle division by zero', () => { + const code = 'result : 10 / 0;'; + assert.throws(() => interpret(code), /Division by zero/); + }); + + it('should handle index out of bounds', () => { + const code = 'result : [1, 2, 3].5;'; + assert.throws(() => interpret(code), /Index out of bounds/); + }); + + it('should handle undefined variables', () => { + const code = 'result : undefinedVar;'; + assert.throws(() => interpret(code), /Undefined variable/); + }); + + it('should handle undefined properties', () => { + const code = 'result : {name: "Alice"}.age;'; + assert.throws(() => interpret(code), /Undefined property/); + }); + }); +}); \ No newline at end of file diff --git a/js/baba-yaga/tests/logical_operators.test.js b/js/baba-yaga/tests/logical_operators.test.js new file mode 100644 index 0000000..ebb2efa --- /dev/null +++ b/js/baba-yaga/tests/logical_operators.test.js @@ -0,0 +1,85 @@ +import { evaluate } from '../runner.js'; + +describe('Logical Operators', () => { + test('!= (not equal) operator', () => { + const result = evaluate('1 != 2'); + expect(result.ok).toBe(true); + expect(result.value).toBe(true); + + const result2 = evaluate('1 != 1'); + expect(result2.ok).toBe(true); + expect(result2.value).toBe(false); + }); + + test('and (logical and) operator', () => { + const result = evaluate('true and true'); + expect(result.ok).toBe(true); + expect(result.value).toBe(true); + + const result2 = evaluate('true and false'); + expect(result2.ok).toBe(true); + expect(result2.value).toBe(false); + + const result3 = evaluate('false and true'); + expect(result3.ok).toBe(true); + expect(result3.value).toBe(false); + + const result4 = evaluate('false and false'); + expect(result4.ok).toBe(true); + expect(result4.value).toBe(false); + }); + + test('or (logical or) operator', () => { + const result = evaluate('true or true'); + expect(result.ok).toBe(true); + expect(result.value).toBe(true); + + const result2 = evaluate('true or false'); + expect(result2.ok).toBe(true); + expect(result2.value).toBe(true); + + const result3 = evaluate('false or true'); + expect(result3.ok).toBe(true); + expect(result3.value).toBe(true); + + const result4 = evaluate('false or false'); + expect(result4.ok).toBe(true); + expect(result4.value).toBe(false); + }); + + test('xor operator', () => { + const result = evaluate('true xor true'); + expect(result.ok).toBe(true); + expect(result.value).toBe(false); + + const result2 = evaluate('true xor false'); + expect(result2.ok).toBe(true); + expect(result2.value).toBe(true); + + const result3 = evaluate('false xor true'); + expect(result3.ok).toBe(true); + expect(result3.value).toBe(true); + + const result4 = evaluate('false xor false'); + expect(result4.ok).toBe(true); + expect(result4.value).toBe(false); + }); + + test('operator precedence', () => { + // and should have higher precedence than or + const result = evaluate('true or false and false'); + expect(result.ok).toBe(true); + expect(result.value).toBe(true); // true or (false and false) = true or false = true + + // Comparison should have higher precedence than logical + const result2 = evaluate('1 < 2 and 3 > 1'); + expect(result2.ok).toBe(true); + expect(result2.value).toBe(true); // (1 < 2) and (3 > 1) = true and true = true + }); + + test('complex logical expressions', () => { + const result = evaluate('1 != 2 and 3 > 1 or false'); + expect(result.ok).toBe(true); + expect(result.value).toBe(true); // (1 != 2 and 3 > 1) or false = (true and true) or false = true or false = true + }); +}); diff --git a/js/baba-yaga/tests/math_namespace.test.js b/js/baba-yaga/tests/math_namespace.test.js new file mode 100644 index 0000000..c892bbb --- /dev/null +++ b/js/baba-yaga/tests/math_namespace.test.js @@ -0,0 +1,112 @@ +const assert = require('assert'); +const { createLexer } = require('../src/core/lexer'); +const { createParser } = require('../src/core/parser'); +const { createInterpreter } = require('../src/core/interpreter'); + +function interpret(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + const interpreter = createInterpreter(ast); + interpreter.interpret(); + return interpreter; +} + +describe('Math Namespace', () => { + it('should support basic numeric ops: abs/floor/ceil/round/trunc', () => { + const code = ` + a : math.abs -3; + b : math.floor 2.9; + c : math.ceil 2.1; + d : math.round 2.5; + e : math.trunc -2.9; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('a').value, 3); + assert.strictEqual(itp.scope.get('b').value, 2); + assert.strictEqual(itp.scope.get('c').value, 3); + assert.strictEqual(itp.scope.get('d').value, 3); + assert.strictEqual(itp.scope.get('e').value, -2); + }); + + it('should support min/max/clamp', () => { + const code = ` + a : math.min 10 3; + b : math.max 10 3; + c : math.clamp 15 0 10; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('a').value, 3); + assert.strictEqual(itp.scope.get('b').value, 10); + assert.strictEqual(itp.scope.get('c').value, 10); + }); + + it('should support pow/sqrt/exp/log (with domain checks)', () => { + const code = ` + p : math.pow 2 8; + s : math.sqrt 9; + e : math.exp 1; + l : math.log 2.718281828; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('p').value, 256); + assert.strictEqual(itp.scope.get('s').value, 3); + assert.ok(Math.abs(itp.scope.get('e').value - Math.E) < 1e-9); + assert.ok(Math.abs(itp.scope.get('l').value - 1) < 1e-6); + + assert.throws(() => interpret('x : math.sqrt -1;')); + assert.throws(() => interpret('x : math.log 0;')); + }); + + it('should support trig and conversions', () => { + const code = ` + c : math.cos 0; + s : math.sin 0; + a : math.atan2 1 1; + d : math.deg PI; + r : math.rad 180; + `; + const itp = interpret(code); + assert.ok(Math.abs(itp.scope.get('c').value - 1) < 1e-12); + assert.ok(Math.abs(itp.scope.get('s').value - 0) < 1e-12); + assert.ok(Math.abs(itp.scope.get('a').value - Math.PI/4) < 1e-6); + assert.ok(Math.abs(itp.scope.get('d').value - 180) < 1e-12); + assert.ok(Math.abs(itp.scope.get('r').value - Math.PI) < 1e-6); + }); + + it('should support random and randomInt', () => { + const code = ` + one : math.randomInt 1 1; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('one').value, 1); + }); + + it('should accept Int where Float is expected, and Number as supertype', () => { + const code = ` + // Float-typed parameter accepts Int (widening) + f : (x: Float) -> Float -> x; + r1 : f 2; + + // Number supertype accepts Int or Float + idN : (x: Number) -> Number -> x; + n1 : idN 2; + n2 : idN 2.5; + + // Return type Number accepts either Int or Float (use dummy arg since zero-arg call syntax is not supported) + retN1 : (z: Int) -> Number -> 3; + retN2 : (z: Int) -> Number -> 3.5; + v1 : retN1 0; + v2 : retN2 0; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('r1').value, 2); + assert.strictEqual(itp.scope.get('n1').value, 2); + assert.ok(Math.abs(itp.scope.get('n2').value - 2.5) < 1e-12); + assert.strictEqual(itp.scope.get('v1').value, 3); + assert.ok(Math.abs(itp.scope.get('v2').value - 3.5) < 1e-12); + }); +}); + + diff --git a/js/baba-yaga/tests/parser-with-header.test.js b/js/baba-yaga/tests/parser-with-header.test.js new file mode 100644 index 0000000..f9de453 --- /dev/null +++ b/js/baba-yaga/tests/parser-with-header.test.js @@ -0,0 +1,36 @@ +import assert from 'assert'; +import { createLexer } from '../src/core/lexer.js'; +import { createParser } from '../src/core/parser.js'; + +function parse(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + return parser.parse(); +} + +describe('parser: with header', () => { + it('parses basic with header', () => { + const ast = parse('f : x -> with (a : x + 1; b : a * 2;) -> a + b;'); + const fn = ast.body[0]; + assert.strictEqual(fn.type, 'FunctionDeclaration'); + assert.strictEqual(fn.body.type, 'WithHeader'); + assert.strictEqual(fn.body.entries.length, 2); + assert.strictEqual(fn.body.entries[0].type, 'WithAssign'); + }); + + it('parses typed locals in header', () => { + const ast = parse('g : (x: Int) -> Int -> with (a Int; a : x;) -> a;'); + const fn = ast.body[0]; + assert.strictEqual(fn.body.entries[0].type, 'WithTypeDecl'); + assert.strictEqual(fn.body.entries[1].type, 'WithAssign'); + }); + + it('parses with rec variant', () => { + const ast = parse('h : -> with rec (f : x -> x; g : y -> y;) -> 0;'); + const fn = ast.body[0]; + assert.strictEqual(fn.body.recursive, true); + }); +}); + + diff --git a/js/baba-yaga/tests/recursive_functions.test.js b/js/baba-yaga/tests/recursive_functions.test.js new file mode 100644 index 0000000..a2380ef --- /dev/null +++ b/js/baba-yaga/tests/recursive_functions.test.js @@ -0,0 +1,223 @@ +import assert from 'assert'; +import { createLexer } from '../src/core/lexer.js'; +import { createParser } from '../src/core/parser.js'; +import { createInterpreter } from '../src/core/interpreter.js'; + +describe('Recursive Function Calls', () => { + function interpret(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + const interpreter = createInterpreter(ast); + interpreter.interpret(); // Execute the code + return interpreter; // Return the interpreter instance to access scope + } + + it('should correctly handle simple function calls', () => { + const code = ` + simpleFunc : n -> n + 1; + result : simpleFunc 5; + `; + + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 6); + }); + + it('should correctly handle when expressions', () => { + const code = ` + checkNumber : num -> + when num is + 1 then "One" + 2 then "Two" + _ then "Something else"; + result : checkNumber 3; + `; + + const interpreter = interpret(code); + assert.strictEqual(interpreter.scope.get('result'), 'Something else'); + }); + + it('should correctly compute factorial recursively', () => { + const code = ` + factorial : n -> + when n is + 0 then 1 + 1 then 1 + _ then n * (factorial (n - 1)); + + result1 : factorial 0; + result2 : factorial 1; + result3 : factorial 5; + result4 : factorial 6; + `; + + const interpreter = interpret(code); + const result1 = interpreter.scope.get('result1'); + const result2 = interpreter.scope.get('result2'); + const result3 = interpreter.scope.get('result3'); + const result4 = interpreter.scope.get('result4'); + assert.strictEqual(result1.value, 1); + assert.strictEqual(result2.value, 1); + assert.strictEqual(result3.value, 120); // 5! = 120 + assert.strictEqual(result4.value, 720); // 6! = 720 + }); + + it('should correctly compute Fibonacci numbers recursively', () => { + const code = ` + fib : n -> + when n is + 0 then 0 + 1 then 1 + _ then (fib (n - 1)) + (fib (n - 2)); + + fib0 : fib 0; + fib1 : fib 1; + fib2 : fib 2; + fib3 : fib 3; + fib4 : fib 4; + fib5 : fib 5; + fib6 : fib 6; + `; + + const interpreter = interpret(code); + const fib0 = interpreter.scope.get('fib0'); + const fib1 = interpreter.scope.get('fib1'); + const fib2 = interpreter.scope.get('fib2'); + const fib3 = interpreter.scope.get('fib3'); + const fib4 = interpreter.scope.get('fib4'); + const fib5 = interpreter.scope.get('fib5'); + const fib6 = interpreter.scope.get('fib6'); + assert.strictEqual(fib0.value, 0); + assert.strictEqual(fib1.value, 1); + assert.strictEqual(fib2.value, 1); + assert.strictEqual(fib3.value, 2); + assert.strictEqual(fib4.value, 3); + assert.strictEqual(fib5.value, 5); + assert.strictEqual(fib6.value, 8); + }); + + it('should correctly compute sum of digits recursively', () => { + const code = ` + sumDigits : n -> + when n is + 0 then 0 + _ then (n % 10) + (sumDigits ((n - (n % 10)) / 10)); + + result1 : sumDigits 123; + result2 : sumDigits 456; + result3 : sumDigits 999; + `; + + const interpreter = interpret(code); + const result1 = interpreter.scope.get('result1'); + const result2 = interpreter.scope.get('result2'); + const result3 = interpreter.scope.get('result3'); + assert.strictEqual(result1.value, 6); + assert.strictEqual(result2.value, 15); + assert.strictEqual(result3.value, 27); + }); + + it('should correctly compute power recursively', () => { + const code = ` + power : base exp -> + when exp is + 0 then 1 + 1 then base + _ then base * (power base (exp - 1)); + + result1 : power 2 0; + result2 : power 2 1; + result3 : power 2 3; + result4 : power 3 4; + result5 : power 5 2; + `; + + const interpreter = interpret(code); + const result1 = interpreter.scope.get('result1'); + const result2 = interpreter.scope.get('result2'); + const result3 = interpreter.scope.get('result3'); + const result4 = interpreter.scope.get('result4'); + const result5 = interpreter.scope.get('result5'); + assert.strictEqual(result1.value, 1); + assert.strictEqual(result2.value, 2); + assert.strictEqual(result3.value, 8); // 2^3 = 8 + assert.strictEqual(result4.value, 81); // 3^4 = 81 + assert.strictEqual(result5.value, 25); // 5^2 = 25 + }); + + it('should correctly compute greatest common divisor recursively', () => { + const code = ` + gcd : a b -> + when b is + 0 then a + _ then gcd b (a % b); + + result1 : gcd 48 18; + result2 : gcd 54 24; + result3 : gcd 7 13; + result4 : gcd 100 25; + `; + + const interpreter = interpret(code); + const result1 = interpreter.scope.get('result1'); + const result2 = interpreter.scope.get('result2'); + const result3 = interpreter.scope.get('result3'); + const result4 = interpreter.scope.get('result4'); + assert.strictEqual(result1.value, 6); + assert.strictEqual(result2.value, 6); + assert.strictEqual(result3.value, 1); + assert.strictEqual(result4.value, 25); + }); + + it('should handle mutual recursion correctly', () => { + const code = ` + isEven : n -> + when n is + 0 then true + 1 then false + _ then isOdd (n - 1); + + isOdd : n -> + when n is + 0 then false + 1 then true + _ then isEven (n - 1); + + result1 : isEven 0; + result2 : isEven 1; + result3 : isEven 2; + result4 : isEven 3; + result5 : isOdd 0; + result6 : isOdd 1; + result7 : isOdd 2; + result8 : isOdd 3; + `; + + const interpreter = interpret(code); + assert.strictEqual(interpreter.scope.get('result1'), true); + assert.strictEqual(interpreter.scope.get('result2'), false); + assert.strictEqual(interpreter.scope.get('result3'), true); + assert.strictEqual(interpreter.scope.get('result4'), false); + assert.strictEqual(interpreter.scope.get('result5'), false); + assert.strictEqual(interpreter.scope.get('result6'), true); + assert.strictEqual(interpreter.scope.get('result7'), false); + assert.strictEqual(interpreter.scope.get('result8'), true); + }); + + it('should handle deep recursion without stack overflow', () => { + const code = ` + countDown : n -> + when n is + 0 then 0 + _ then 1 + (countDown (n - 1)); + + result : countDown 100; + `; + + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 100); + }); +}); \ No newline at end of file diff --git a/js/baba-yaga/tests/turing_completeness.test.js b/js/baba-yaga/tests/turing_completeness.test.js new file mode 100644 index 0000000..04daa03 --- /dev/null +++ b/js/baba-yaga/tests/turing_completeness.test.js @@ -0,0 +1,270 @@ +const assert = require('assert'); +const { createLexer } = require('../src/core/lexer'); +const { createParser } = require('../src/core/parser'); +const { createInterpreter } = require('../src/core/interpreter'); + +describe('Turing Completeness Tests', () => { + function interpret(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + const interpreter = createInterpreter(ast); + interpreter.interpret(); // Execute the code + return interpreter; // Return the interpreter instance to access scope + } + + describe('Church Numerals (Basic)', () => { + it('should implement basic Church numerals', () => { + const code = ` + // Church numerals: represent numbers as functions + // Zero: applies function 0 times + zero : f x -> x; + + // One: applies function 1 time + one : f x -> f x; + + // Test with increment function + inc : x -> x + 1; + + // Convert Church numeral to regular number + toNumber : n -> n inc 0; + + // Test conversions + result0 : toNumber zero; + result1 : toNumber one; + `; + + const interpreter = interpret(code); + + assert.strictEqual(interpreter.scope.get('result0').value, 0); + assert.strictEqual(interpreter.scope.get('result1').value, 1); + }); + }); + + describe('Lambda Calculus (Basic)', () => { + it('should implement basic lambda calculus operations', () => { + const code = ` + // Basic lambda calculus operations + + // Identity function + id : x -> x; + + // Constant function + const : x y -> x; + + // Function composition + compose : f g x -> f (g x); + + // Test identity + testId : id 42; + + // Test constant + testConst : const 10 20; + + // Test composition + testCompose : compose (x -> x + 1) (x -> x * 2) 5; + `; + + const interpreter = interpret(code); + + assert.strictEqual(interpreter.scope.get('testId').value, 42); + assert.strictEqual(interpreter.scope.get('testConst').value, 10); + assert.strictEqual(interpreter.scope.get('testCompose').value, 11); // (5*2)+1 = 11 + }); + }); + + describe('Universal Function (Basic)', () => { + it('should implement a basic universal function', () => { + const code = ` + // Simple universal function using function encoding + // This demonstrates that our language can simulate any computable function + + // Function registry + functionRegistry : { + inc: x -> x + 1; + double: x -> x * 2; + }; + + // Universal function that can simulate any registered function + universal : funcName input -> + when funcName is + "inc" then (functionRegistry.inc input) + "double" then (functionRegistry.double input) + _ then input; + + // Test the universal function + result1 : universal "inc" 5; + result2 : universal "double" 5; + `; + + const interpreter = interpret(code); + + assert.strictEqual(interpreter.scope.get('result1').value, 6); + assert.strictEqual(interpreter.scope.get('result2').value, 10); + }); + }); + + describe('Turing Machine Simulator (Basic)', () => { + it('should simulate a basic Turing machine', () => { + const code = ` + // Simple Turing machine that counts 1s on the tape + // States: "start", "halt" + + // Initialize Turing machine + initTM : { + tape: [1, 1, 0, 1], + head: 0, + state: "start" + }; + + // Transition function + transition : tm -> + when tm.state is + "start" then when (tm.head >= (length tm.tape)) is + true then { tape: tm.tape, head: tm.head, state: "halt" } + _ then { tape: tm.tape, head: tm.head + 1, state: "start" } + _ then tm; + + // Run Turing machine + runTM : tm -> + when tm.state is + "halt" then tm + _ then runTM (transition tm); + + // Test + result : runTM initTM; + `; + + const interpreter = interpret(code); + const result = interpreter.scope.get('result'); + + // Should reach halt state + assert.strictEqual(result.state, "halt"); + }); + }); + + describe('Recursive Functions (Turing Complete)', () => { + it('should demonstrate Turing completeness through recursion', () => { + const code = ` + // Ackermann function - a classic example of a computable but not primitive recursive function + // This demonstrates that our language can compute any computable function + + ackermann : m n -> + when m is + 0 then n + 1 + _ then + when n is + 0 then ackermann (m - 1) 1 + _ then ackermann (m - 1) (ackermann m (n - 1)); + + // Test with small values (larger values would cause stack overflow) + result1 : ackermann 0 5; + result2 : ackermann 1 3; + result3 : ackermann 2 2; + `; + + const interpreter = interpret(code); + + assert.strictEqual(interpreter.scope.get('result1').value, 6); + assert.strictEqual(interpreter.scope.get('result2').value, 5); + assert.strictEqual(interpreter.scope.get('result3').value, 7); + }); + }); + + describe('Higher-Order Functions (Turing Complete)', () => { + it('should demonstrate Turing completeness through higher-order functions', () => { + const code = ` + // Higher-order functions that can simulate any computable function + + // Test with factorial using fixed point + factorialHelper : f n -> + when n is + 0 then 1 + _ then n * (f (n - 1)); + + // Test with small values + testFactorial : factorialHelper (n -> 1) 0; + testFactorial2 : factorialHelper (n -> n * 1) 1; + `; + + const interpreter = interpret(code); + + assert.strictEqual(interpreter.scope.get('testFactorial').value, 1); + // The helper calls f(0) when n=1; with f = (n -> n * 1), this evaluates to 0 + assert.strictEqual(interpreter.scope.get('testFactorial2').value, 0); + }); + }); + + describe('SKI Combinator Calculus (Basic)', () => { + it('should implement basic SKI combinators', () => { + const code = ` + // SKI Combinator Calculus - basic version + + // K combinator: Kxy = x + K : x y -> x; + + // I combinator: Ix = x + I : x -> x; + + // Test I combinator + testI : I 42; + + // Test K combinator + testK : K 10 20; + + // Test composition + compose : f g x -> f (g x); + testCompose : compose (x -> x + 1) (x -> x * 2) 5; + `; + + const interpreter = interpret(code); + + assert.strictEqual(interpreter.scope.get('testI').value, 42); + assert.strictEqual(interpreter.scope.get('testK').value, 10); + assert.strictEqual(interpreter.scope.get('testCompose').value, 11); // (5*2)+1 = 11 + }); + }); + + describe('Mutual Recursion (Turing Complete)', () => { + it('should demonstrate Turing completeness through mutual recursion', () => { + const code = ` + // Mutual recursion example - isEven and isOdd + // This demonstrates that our language can handle complex recursive patterns + + isEven : n -> + when n is + 0 then true + 1 then false + _ then isOdd (n - 1); + + isOdd : n -> + when n is + 0 then false + 1 then true + _ then isEven (n - 1); + + // Test mutual recursion + result1 : isEven 0; + result2 : isEven 1; + result3 : isEven 2; + result4 : isEven 3; + result5 : isOdd 0; + result6 : isOdd 1; + result7 : isOdd 2; + result8 : isOdd 3; + `; + + const interpreter = interpret(code); + + assert.strictEqual(interpreter.scope.get('result1'), true); + assert.strictEqual(interpreter.scope.get('result2'), false); + assert.strictEqual(interpreter.scope.get('result3'), true); + assert.strictEqual(interpreter.scope.get('result4'), false); + assert.strictEqual(interpreter.scope.get('result5'), false); + assert.strictEqual(interpreter.scope.get('result6'), true); + assert.strictEqual(interpreter.scope.get('result7'), false); + assert.strictEqual(interpreter.scope.get('result8'), true); + }); + }); +}); \ No newline at end of file diff --git a/js/baba-yaga/tests/typed_curried_functions.test.js b/js/baba-yaga/tests/typed_curried_functions.test.js new file mode 100644 index 0000000..010e2e1 --- /dev/null +++ b/js/baba-yaga/tests/typed_curried_functions.test.js @@ -0,0 +1,222 @@ +import assert from 'assert'; +import { createLexer } from '../src/core/lexer.js'; +import { createParser } from '../src/core/parser.js'; +import { createInterpreter } from '../src/core/interpreter.js'; + +describe('Typed Curried Functions', () => { + function interpret(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + const interpreter = createInterpreter(ast); + interpreter.interpret(); + return interpreter; + } + + describe('Basic Typed Curried Function Parsing', () => { + it('should parse single-parameter typed curried function', () => { + const code = 'multiply : (x: Float) -> (Float -> Float) -> y -> x * y;'; + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + + assert.strictEqual(ast.body.length, 1); + assert.strictEqual(ast.body[0].type, 'CurriedFunctionDeclaration'); + assert.strictEqual(ast.body[0].name, 'multiply'); + assert.strictEqual(ast.body[0].param.name, 'x'); + assert.deepStrictEqual(ast.body[0].param.type, { type: 'PrimitiveType', name: 'Float' }); + assert.strictEqual(ast.body[0].returnType.type, 'FunctionType'); + }); + + it('should parse multi-step typed curried function', () => { + const code = 'add3 : (x: Int) -> (Int -> (Int -> Int)) -> y -> z -> x + y + z;'; + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + + assert.strictEqual(ast.body.length, 1); + assert.strictEqual(ast.body[0].type, 'CurriedFunctionDeclaration'); + }); + }); + + describe('Typed Curried Function Execution', () => { + it('should execute basic typed curried function', () => { + const code = ` + multiply : (x: Float) -> (Float -> Float) -> y -> x * y; + double : multiply 2.0; + result : double 5.0; + `; + const interpreter = interpret(code); + + const multiply = interpreter.scope.get('multiply'); + assert.strictEqual(multiply.type, 'Function'); + + const double = interpreter.scope.get('double'); + assert.strictEqual(double.type, 'Function'); + + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 10.0); + }); + + it('should validate parameter types in curried functions', () => { + const code = ` + multiply : (x: Float) -> (Float -> Float) -> y -> x * y; + double : multiply 2.0; + `; + const interpreter = interpret(code); + + // This should work - Float parameter + const double = interpreter.scope.get('double'); + assert.strictEqual(double.type, 'Function'); + + // Test type validation on second parameter + const code2 = ` + result : double 5.0; + `; + const lexer2 = createLexer(code2); + const tokens2 = lexer2.allTokens(); + const parser2 = createParser(tokens2); + const ast2 = parser2.parse(); + const interpreter2 = createInterpreter(ast2, { scope: interpreter.scope }); + interpreter2.interpret(); + + assert.strictEqual(interpreter2.scope.get('result').value, 10.0); + }); + + it('should reject invalid parameter types', () => { + const code = ` + multiply : (x: Float) -> (Float -> Float) -> y -> x * y; + result : multiply "invalid"; + `; + + assert.throws(() => { + interpret(code); + }, /Type mismatch.*Expected Float.*got String/); + }); + + it('should handle Int to Float widening in curried functions', () => { + const code = ` + multiply : (x: Float) -> (Float -> Float) -> y -> x * y; + double : multiply 2; // Int should widen to Float + result : double 5.0; + `; + const interpreter = interpret(code); + + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 10.0); + }); + }); + + describe('Complex Typed Curried Functions', () => { + it('should handle three-parameter curried function', () => { + const code = ` + add3 : (x: Int) -> (Int -> (Int -> Int)) -> y -> z -> x + y + z; + add5 : add3 5; + add5and3 : add5 3; + result : add5and3 2; + `; + const interpreter = interpret(code); + + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 10); + }); + + it('should validate return types in curried functions', () => { + const code = ` + badFunc : (x: Int) -> (String -> Int) -> y -> x + y; // Returns String but declares Int + badFunc5 : badFunc 5; + result : badFunc5 "test"; + `; + + // This should fail because x + y returns String but function declares Int return type + assert.throws(() => { + interpret(code); + }, /Return type mismatch.*Expected Int.*got String/); + }); + + it('should support function type parameters', () => { + // Test that we can at least parse and use functions with function return types + const code = ` + makeAdder : (x: Int) -> (Int -> Int) -> y -> x + y; + add5 : makeAdder 5; + result : add5 3; + `; + const interpreter = interpret(code); + + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 8); + }); + }); + + describe('Backward Compatibility', () => { + it('should still support untyped curried functions', () => { + const code = ` + multiply : x -> y -> x * y; + double : multiply 2.0; + result : double 5.0; + `; + const interpreter = interpret(code); + + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 10.0); + }); + + it('should still support multi-parameter typed functions', () => { + const code = ` + add : (x: Float, y: Float) -> Float -> x + y; + result : add 2.0 3.0; + `; + const interpreter = interpret(code); + + const result = interpreter.scope.get('result'); + assert.strictEqual(result.value, 5.0); + }); + + it('should distinguish between multi-param and curried syntax', () => { + const code1 = ` + multiParam : (x: Float, y: Float) -> Float -> x + y; + result1 : multiParam 2.0 3.0; + `; + + const code2 = ` + curried : (x: Float) -> (Float -> Float) -> y -> x + y; + addTwo : curried 2.0; + result2 : addTwo 3.0; + `; + + const interpreter1 = interpret(code1); + const interpreter2 = interpret(code2); + + assert.strictEqual(interpreter1.scope.get('result1').value, 5.0); + assert.strictEqual(interpreter2.scope.get('result2').value, 5.0); + }); + }); + + describe('Error Handling', () => { + it('should provide clear error messages for type mismatches', () => { + const code = ` + multiply : (x: Float) -> (Float -> Float) -> y -> x * y; + result : multiply "not a number"; + `; + + assert.throws(() => { + interpret(code); + }, /Type mismatch in function 'multiply': Expected Float for parameter 'x', but got String/); + }); + + it('should validate return type of curried function', () => { + const code = ` + badFunction : (x: Int) -> (Int -> String) -> y -> x + y; // Returns Int but declares String + add5 : badFunction 5; + result : add5 3; + `; + + assert.throws(() => { + interpret(code); + }, /Return type mismatch in function 'add5': Expected String, but got Int/); + }); + }); +}); \ No newline at end of file diff --git a/js/baba-yaga/tests/utilities.test.js b/js/baba-yaga/tests/utilities.test.js new file mode 100644 index 0000000..5303fea --- /dev/null +++ b/js/baba-yaga/tests/utilities.test.js @@ -0,0 +1,278 @@ +import { createLexer } from '../src/core/lexer.js'; +import { createParser } from '../src/core/parser.js'; +import { createInterpreter } from '../src/core/interpreter.js'; + +function runBabaYaga(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + + const outputs = []; + const debugOutputs = []; + + const host = { + io: { + out: (...args) => outputs.push(args.join(' ')), + debug: (...args) => debugOutputs.push(args.join(' ')), + in: () => '', + }, + }; + + const interpreter = createInterpreter(ast, host); + const result = interpreter.interpret(); + + return { outputs, debugOutputs, result }; +} + +describe('Utility Functions', () => { + describe('validate namespace', () => { + test('validate.notEmpty', () => { + const code = ` + io.out (validate.notEmpty "hello"); + io.out (validate.notEmpty ""); + io.out (validate.notEmpty [1, 2, 3]); + io.out (validate.notEmpty []); + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['true', 'false', 'true', 'false']); + }); + + test('validate.range', () => { + const code = ` + io.out (validate.range 1 10 5); + io.out (validate.range 1 10 15); + io.out (validate.range 1 10 1); + io.out (validate.range 1 10 10); + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['true', 'false', 'true', 'true']); + }); + + test('validate.email', () => { + const code = ` + io.out (validate.email "test@example.com"); + io.out (validate.email "invalid-email"); + io.out (validate.email "user@domain.co.uk"); + io.out (validate.email "@domain.com"); + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['true', 'false', 'true', 'false']); + }); + + test('validate.type', () => { + const code = ` + io.out (validate.type "Int" 42); + io.out (validate.type "String" 42); + io.out (validate.type "String" "hello"); + io.out (validate.type "Bool" true); + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['true', 'false', 'true', 'true']); + }); + }); + + describe('text namespace', () => { + test('text.lines', () => { + const code = ` + // Test with single line (since escape sequences aren't implemented yet) + lines : text.lines "hello world test"; + io.out (length lines); + first : lines.0; + io.out first; + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['1', 'hello world test']); + }); + + test('text.words', () => { + const code = ` + words : text.words "hello world test"; + io.out (length words); + io.out words.0; + io.out words.1; + io.out words.2; + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['3', 'hello', 'world', 'test']); + }); + + test('text.padLeft and text.padRight', () => { + const code = ` + io.out (text.padLeft 10 "hi"); + io.out (text.padRight 10 "hi"); + io.out (str.length (text.padLeft 5 "test")); + `; + const { outputs } = runBabaYaga(code); + expect(outputs[0]).toBe(' hi'); + expect(outputs[1]).toBe('hi '); + expect(outputs[2]).toBe('5'); + }); + }); + + describe('utility functions', () => { + test('chunk', () => { + const code = ` + numbers : [1, 2, 3, 4, 5, 6]; + chunks : chunk numbers 2; + io.out (length chunks); + firstChunk : chunks.0; + io.out (length firstChunk); + io.out firstChunk.0; + io.out firstChunk.1; + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['3', '2', '1', '2']); + }); + + test('range', () => { + const code = ` + r1 : range 1 5; + r2 : range 5 1; + io.out (length r1); + io.out r1.0; + io.out r1.4; + io.out (length r2); + io.out r2.0; + io.out r2.4; + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['5', '1', '5', '5', '5', '1']); + }); + + test('repeat', () => { + const code = ` + repeated : repeat 3 "hello"; + io.out (length repeated); + io.out repeated.0; + io.out repeated.1; + io.out repeated.2; + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['3', 'hello', 'hello', 'hello']); + }); + }); + + describe('sort namespace', () => { + test('sort.by with numbers', () => { + const code = ` + numbers : [3, 1, 4, 1, 5, 9, 2, 6]; + sorted : sort.by numbers (x -> x); + io.out sorted.0; + io.out sorted.1; + io.out sorted.7; + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['1', '1', '9']); + }); + + test('sort.by with objects', () => { + const code = ` + people : [ + {name: "Alice", age: 30}, + {name: "Bob", age: 25}, + {name: "Charlie", age: 35} + ]; + sortedByAge : sort.by people (p -> p.age); + first : sortedByAge.0; + second : sortedByAge.1; + third : sortedByAge.2; + io.out first.name; + io.out second.name; + io.out third.name; + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['Bob', 'Alice', 'Charlie']); + }); + }); + + describe('group namespace', () => { + test('group.by', () => { + const code = ` + numbers : [1, 2, 3, 4, 5, 6]; + grouped : group.by numbers (x -> x % 2 = 0); + evenGroup : grouped."true"; + oddGroup : grouped."false"; + io.out (length evenGroup); + io.out (length oddGroup); + io.out (evenGroup.0); + io.out (oddGroup.0); + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['3', '3', '2', '1']); + }); + }); + + describe('random namespace', () => { + test('random.choice', () => { + const code = ` + list : [1, 2, 3]; + choice : random.choice list; + io.out (validate.range 1 3 choice); + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['true']); + }); + + test('random.shuffle', () => { + const code = ` + list : [1, 2, 3, 4, 5]; + shuffled : random.shuffle list; + io.out (length shuffled); + io.out (length list); + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['5', '5']); + }); + + test('random.range', () => { + const code = ` + r : random.range 1 10; + io.out (validate.range 1 10 r); + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['true']); + }); + }); + + describe('debug namespace', () => { + test('debug.print', () => { + const code = ` + testFunc : x -> x * 2; + debug.print 42; + debug.print testFunc; + `; + const { debugOutputs } = runBabaYaga(code); + expect(debugOutputs.length).toBe(2); + expect(debugOutputs[0]).toContain('42'); + expect(debugOutputs[1]).toContain('function'); + }); + + test('debug.inspect', () => { + const code = ` + testFunc : x -> x * 2; + inspection : debug.inspect testFunc; + len : str.length inspection; + io.out (len > 10); + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['true']); + }); + }); + + describe('assert function', () => { + test('assert success', () => { + const code = ` + assert (2 + 2 = 4) "Math works"; + io.out "Success"; + `; + const { outputs } = runBabaYaga(code); + expect(outputs).toEqual(['Success']); + }); + + test('assert failure', () => { + const code = `assert (2 + 2 = 5) "This should fail";`; + expect(() => runBabaYaga(code)).toThrow('Assertion failed: This should fail'); + }); + }); +}); diff --git a/js/baba-yaga/tests/with-advanced-patterns.test.js b/js/baba-yaga/tests/with-advanced-patterns.test.js new file mode 100644 index 0000000..2ea2d44 --- /dev/null +++ b/js/baba-yaga/tests/with-advanced-patterns.test.js @@ -0,0 +1,290 @@ +import assert from 'assert'; +import { createLexer } from '../src/core/lexer.js'; +import { createParser } from '../src/core/parser.js'; +import { createInterpreter } from '../src/core/interpreter.js'; + +function interpret(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + const interpreter = createInterpreter(ast); + interpreter.interpret(); + return interpreter; +} + +describe('with header: advanced patterns', () => { + it('handles empty with blocks', () => { + const code = ` + testEmptyWith : x -> + with () -> x; + result : testEmptyWith 42; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('result').value, 42); + }); + + it('handles single entry with blocks', () => { + const code = ` + testSingleEntry : x -> + with (value : x + 1;) -> value; + result : testSingleEntry 5; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('result').value, 6); + }); + + it('handles complex dependencies between entries', () => { + const code = ` + testDependencies : x y -> + with ( + a : x + y; + b : a * 2; + c : b - x; + d : c + a; + e : d * b; + ) -> e; + result : testDependencies 3 4; + `; + const itp = interpret(code); + // a = 3 + 4 = 7 + // b = 7 * 2 = 14 + // c = 14 - 3 = 11 + // d = 11 + 7 = 18 + // e = 18 * 14 = 252 + assert.strictEqual(itp.scope.get('result').value, 252); + }); + + it('handles deep nesting of when expressions beyond 4 levels', () => { + const code = ` + testDeepNesting : x -> + with ( + level1 : when x is + 0 then "zero" + _ then when (x < 5) is + true then "small" + _ then when (x < 10) is + true then "medium" + _ then when (x < 20) is + true then "large" + _ then when (x < 50) is + true then "huge" + _ then when (x < 100) is + true then "massive" + _ then "gigantic"; + ) -> level1; + result1 : testDeepNesting 0; + result2 : testDeepNesting 3; + result3 : testDeepNesting 7; + result4 : testDeepNesting 15; + result5 : testDeepNesting 30; + result6 : testDeepNesting 70; + result7 : testDeepNesting 150; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('result1'), 'zero'); + assert.strictEqual(itp.scope.get('result2'), 'small'); + assert.strictEqual(itp.scope.get('result3'), 'medium'); + assert.strictEqual(itp.scope.get('result4'), 'large'); + assert.strictEqual(itp.scope.get('result5'), 'huge'); + assert.strictEqual(itp.scope.get('result6'), 'massive'); + assert.strictEqual(itp.scope.get('result7'), 'gigantic'); + }); + + it('handles mixed types in with blocks', () => { + const code = ` + testMixedTypes : x -> + with ( + num Int; num : x + 1; + str String; str : str.concat "Value: " "number"; + isValid Bool; isValid : x > 0; + list List; list : [x, x * 2, x * 3]; + table Table; table : { value: x, doubled: x * 2 }; + ) -> { num: num, str: str, isValid: isValid, list: list, table: table }; + result : testMixedTypes 10; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + assert.strictEqual(result.num.value, 11); + assert.strictEqual(result.str, 'Value: number'); + assert.strictEqual(result.isValid, true); + assert.deepStrictEqual(result.list.map(x => x.value), [10, 20, 30]); + // Baba Yaga objects have properties Map structure + assert.strictEqual(result.table.properties.get('value').value, 10); + assert.strictEqual(result.table.properties.get('doubled').value, 20); + }); + + it('handles recursive with rec with complex functions', () => { + const code = ` + testComplexRecursive : n -> + with rec ( + factorial : x -> + when x is + 0 then 1 + _ then x * (factorial (x - 1)); + + fibonacci : x -> + when x is + 0 then 0 + 1 then 1 + _ then (fibonacci (x - 1)) + (fibonacci (x - 2)); + + ackermann : m n -> + when m is + 0 then n + 1 + _ then when n is + 0 then ackermann (m - 1) 1 + _ then ackermann (m - 1) (ackermann m (n - 1)); + ) -> { fact: factorial n, fib: fibonacci n, ack: ackermann 2 3 }; + result : testComplexRecursive 5; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + assert.strictEqual(result.fact.value, 120); // 5! = 120 + assert.strictEqual(result.fib.value, 5); // fib(5) = 5 + assert.strictEqual(result.ack.value, 9); // ack(2,3) = 9 + }); + + it('handles complex mathematical expressions with type validation', () => { + const code = ` + testComplexMath : x y z -> + with ( + a : x * x; + b : y * y; + c : z * z; + sumSquares : a + b; + hypotenuse : math.sqrt sumSquares; + result : hypotenuse + (math.sqrt (a + b + c)); + ) -> result; + result : testComplexMath 3 4 5; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + // a = 3² = 9, b = 4² = 16, c = 5² = 25 + // sumSquares = 9 + 16 = 25 + // hypotenuse = √25 = 5 + // result = 5 + √(9 + 16 + 25) = 5 + √50 = 5 + 7.07... ≈ 12.07 + assert(Math.abs(result.value - 12.07) < 0.1); + }); + + it('handles string operations with type validation', () => { + const code = ` + testStringOps : input -> + with ( + length : str.length input; + trimmed : str.trim input; + upper : str.upper input; + isEmpty : length = 0; + hasContent : length > 0; + description : str.concat "Length: " (when (length > 10) is true then "long" _ then "short"); + ) -> { + length: length, + trimmed: trimmed, + upper: upper, + isEmpty: isEmpty, + hasContent: hasContent, + description: description + }; + result : testStringOps " Hello World "; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + assert.strictEqual(result.length.value, 15); + assert.strictEqual(result.trimmed, 'Hello World'); + assert.strictEqual(result.upper, ' HELLO WORLD '); + assert.strictEqual(result.isEmpty, false); + assert.strictEqual(result.hasContent, true); + assert.strictEqual(result.description, 'Length: long'); + }); + + it('handles list and table edge cases with type validation', () => { + const code = ` + testListTableEdgeCases : items -> + with ( + count : length items; + isEmpty : count = 0; + hasItems : count > 0; + firstItem : when count is + 0 then "none" + _ then items.0; + lastItem : when count is + 0 then "none" + _ then when count is + 1 then items.0 + _ then when count is + 2 then items.1 + _ then when count is + 3 then items.2 + _ then "many"; + summary : { + count: count, + empty: isEmpty, + hasItems: hasItems, + first: firstItem, + last: lastItem + }; + ) -> summary; + result1 : testListTableEdgeCases []; + result2 : testListTableEdgeCases [42]; + result3 : testListTableEdgeCases [1, 2]; + result4 : testListTableEdgeCases [10, 20, 30]; + `; + const itp = interpret(code); + const result1 = itp.scope.get('result1'); + const result2 = itp.scope.get('result2'); + const result3 = itp.scope.get('result3'); + const result4 = itp.scope.get('result4'); + + assert.strictEqual(result1.count.value, 0); + assert.strictEqual(result1.empty, true); + assert.strictEqual(result1.hasItems, false); + assert.strictEqual(result1.first, 'none'); + assert.strictEqual(result1.last, 'none'); + + assert.strictEqual(result2.count.value, 1); + assert.strictEqual(result2.empty, false); + assert.strictEqual(result2.hasItems, true); + assert.strictEqual(result2.first.value, 42); + assert.strictEqual(result2.last.value, 42); + + assert.strictEqual(result3.count.value, 2); + assert.strictEqual(result3.empty, false); + assert.strictEqual(result3.hasItems, true); + assert.strictEqual(result3.first.value, 1); + assert.strictEqual(result3.last.value, 2); + + assert.strictEqual(result4.count.value, 3); + assert.strictEqual(result4.empty, false); + assert.strictEqual(result4.hasItems, true); + assert.strictEqual(result4.first.value, 10); + assert.strictEqual(result4.last.value, 30); // items.2 when count is 3 + }); + + it('handles error handling edge cases', () => { + const code = ` + testErrorHandling : x -> + with ( + isValid : x >= 0; + safeValue : when isValid is + true then x + _ then 0; + result : when isValid is + true then { value: safeValue, status: "valid" } + _ then { value: safeValue, status: "invalid", error: "negative value" }; + ) -> result; + result1 : testErrorHandling 5; + result2 : testErrorHandling -3; + `; + const itp = interpret(code); + const result1 = itp.scope.get('result1'); + const result2 = itp.scope.get('result2'); + + assert.strictEqual(result1.properties.get('value').value, 5); + assert.strictEqual(result1.properties.get('status'), 'valid'); + assert.strictEqual(result1.properties.get('error'), undefined); + + assert.strictEqual(result2.properties.get('value').value, 0); + assert.strictEqual(result2.properties.get('status'), 'invalid'); + assert.strictEqual(result2.properties.get('error'), 'negative value'); + }); +}); diff --git a/js/baba-yaga/tests/with-type-system-edge-cases.test.js b/js/baba-yaga/tests/with-type-system-edge-cases.test.js new file mode 100644 index 0000000..048d60a --- /dev/null +++ b/js/baba-yaga/tests/with-type-system-edge-cases.test.js @@ -0,0 +1,223 @@ +import assert from 'assert'; +import { createLexer } from '../src/core/lexer.js'; +import { createParser } from '../src/core/parser.js'; +import { createInterpreter } from '../src/core/interpreter.js'; + +function interpret(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + const interpreter = createInterpreter(ast); + interpreter.interpret(); + return interpreter; +} + +describe('with header: type system edge cases', () => { + it('handles complex type combinations in single with block', () => { + const code = ` + testMixedTypes : x -> + with ( + num Int; num : x + 1; + str String; str : str.concat "Value: " "number"; + isValid Bool; isValid : x > 0; + list List; list : [x, x * 2, x * 3]; + table Table; table : { value: x, doubled: x * 2 }; + ) -> { num: num, str: str, isValid: isValid, list: list, table: table }; + result : testMixedTypes 10; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + assert.strictEqual(result.num.value, 11); + assert.strictEqual(result.str, 'Value: number'); + assert.strictEqual(result.isValid, true); + assert.deepStrictEqual(result.list.map(x => x.value), [10, 20, 30]); + // Baba Yaga objects have properties Map structure + assert.strictEqual(result.table.properties.get('value').value, 10); + assert.strictEqual(result.table.properties.get('doubled').value, 20); + }); + + it('handles numeric type widening correctly', () => { + const code = ` + testNumericWidening : x y -> + with ( + intVal Int; intVal : x; + floatVal Float; floatVal : intVal; // Int -> Float + numberVal Number; numberVal : floatVal; // Float -> Number + mixedSum Number; mixedSum : intVal + 0.5; // Int + Float -> Number + ) -> { intVal: intVal, floatVal: floatVal, numberVal: numberVal, mixedSum: mixedSum }; + result : testNumericWidening 5 3; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + assert.strictEqual(result.intVal.value, 5); + assert.strictEqual(result.floatVal.value, 5); + assert.strictEqual(result.numberVal.value, 5); + assert.strictEqual(result.mixedSum.value, 5.5); + }); + + it('validates types with complex computed expressions', () => { + const code = ` + testComputedTypes : a b -> + with ( + sum Int; sum : a + b; + product Int; product : a * b; + difference Int; difference : a - b; + isPositive Bool; isPositive : sum > 0; + isLarge Bool; isLarge : product > 100; + sumStr String; sumStr : str.concat "Sum: " "valid"; + productStr String; productStr : str.concat "Product: " "valid"; + ) -> { sum: sum, product: product, difference: difference, isPositive: isPositive, isLarge: isLarge, sumStr: sumStr, productStr: productStr }; + result : testComputedTypes 7 8; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + assert.strictEqual(result.sum.value, 15); + assert.strictEqual(result.product.value, 56); + assert.strictEqual(result.difference.value, -1); + assert.strictEqual(result.isPositive, true); + assert.strictEqual(result.isLarge, false); + assert.strictEqual(result.sumStr, 'Sum: valid'); + assert.strictEqual(result.productStr, 'Product: valid'); + }); + + it('validates types with mathematical functions', () => { + const code = ` + testMathTypes : x y -> + with ( + sqrtResult Float; sqrtResult : math.sqrt x; + powResult Float; powResult : math.pow x y; + absResult Number; absResult : math.abs x; + complexResult Float; complexResult : (sqrtResult * powResult) + absResult; + ) -> { sqrtResult: sqrtResult, powResult: powResult, absResult: absResult, complexResult: complexResult }; + result : testMathTypes 16 2; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + assert.strictEqual(result.sqrtResult.value, 4); + assert.strictEqual(result.powResult.value, 256); + assert.strictEqual(result.absResult.value, 16); + assert.strictEqual(result.complexResult.value, 1040); + }); + + it('validates types with string operations', () => { + const code = ` + testStringTypes : input -> + with ( + length Int; length : str.length input; + trimmed String; trimmed : str.trim input; + upper String; upper : str.upper input; + isEmpty Bool; isEmpty : length = 0; + hasContent Bool; hasContent : length > 0; + description String; description : str.concat "Length: " "valid"; + ) -> { length: length, trimmed: trimmed, upper: upper, isEmpty: isEmpty, hasContent: hasContent, description: description }; + result : testStringTypes " Hello World "; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + assert.strictEqual(result.length.value, 15); + assert.strictEqual(result.trimmed, 'Hello World'); + assert.strictEqual(result.upper, ' HELLO WORLD '); + assert.strictEqual(result.isEmpty, false); + assert.strictEqual(result.hasContent, true); + assert.strictEqual(result.description, 'Length: valid'); + }); + + it('validates types with list and table operations', () => { + const code = ` + testDataStructureTypes : items -> + with ( + count Int; count : length items; + isEmpty Bool; isEmpty : count = 0; + hasItems Bool; hasItems : count > 0; + tags List; tags : [count, isEmpty, hasItems]; + summary Table; summary : { count: count, empty: isEmpty, hasItems: hasItems }; + ) -> { count: count, isEmpty: isEmpty, hasItems: hasItems, tags: tags, summary: summary }; + result : testDataStructureTypes [1, 2, 3]; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + assert.strictEqual(result.count.value, 3); + assert.strictEqual(result.isEmpty, false); + assert.strictEqual(result.hasItems, true); + assert.deepStrictEqual(result.tags.map(x => typeof x === 'object' ? x.value : x), [3, false, true]); + // Baba Yaga objects have properties Map structure + assert.strictEqual(result.summary.properties.get('count').value, 3); + assert.strictEqual(result.summary.properties.get('empty'), false); + assert.strictEqual(result.summary.properties.get('hasItems'), true); + }); + + it('handles complex nested structures with type validation', () => { + const code = ` + testNestedTypes : user -> + with ( + userId Int; userId : user.id; + userName String; userName : str.upper (str.trim user.name); + userAge Int; userAge : user.age; + isAdult Bool; isAdult : userAge >= 18; + ageGroup String; ageGroup : when isAdult is + true then "adult" + _ then "minor"; + userProfile Table; userProfile : { + id: userId, + name: userName, + age: userAge, + status: ageGroup, + verified: isAdult, + tags: [userId, ageGroup, isAdult] + }; + ) -> userProfile; + result : testNestedTypes { id: 1, name: " john doe ", age: 25 }; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + assert.strictEqual(result.id.value, 1); + assert.strictEqual(result.name, 'JOHN DOE'); + assert.strictEqual(result.age.value, 25); + assert.strictEqual(result.status, 'adult'); + assert.strictEqual(result.verified, true); + assert.deepStrictEqual(result.tags.map(x => typeof x === 'object' ? x.value : x), [1, 'adult', true]); + }); + + it('handles conditional type assignment with validation', () => { + const code = ` + testConditionalTypes : x -> + with ( + numericType : when (x > 0) is + true then "positive" + _ then when (x < 0) is + true then "negative" + _ then "zero"; + isValid : (x >= -100) and (x <= 100); + result : when isValid is + true then { value: x, type: numericType, valid: true } + _ then { value: "invalid", type: "error", valid: false }; + ) -> result; + result1 : testConditionalTypes 50; + result2 : testConditionalTypes -25; + result3 : testConditionalTypes 0; + result4 : testConditionalTypes 150; + `; + const itp = interpret(code); + const result1 = itp.scope.get('result1'); + const result2 = itp.scope.get('result2'); + const result3 = itp.scope.get('result3'); + const result4 = itp.scope.get('result4'); + + assert.strictEqual(result1.properties.get('value').value, 50); + assert.strictEqual(result1.properties.get('type'), 'positive'); + assert.strictEqual(result1.properties.get('valid'), true); + + assert.strictEqual(result2.properties.get('value').value, -25); + assert.strictEqual(result2.properties.get('type'), 'negative'); + assert.strictEqual(result2.properties.get('valid'), true); + + assert.strictEqual(result3.properties.get('value').value, 0); + assert.strictEqual(result3.properties.get('type'), 'zero'); + assert.strictEqual(result3.properties.get('valid'), true); + + assert.strictEqual(result4.properties.get('value'), 'invalid'); + assert.strictEqual(result4.properties.get('type'), 'error'); + assert.strictEqual(result4.properties.get('valid'), false); + }); +}); diff --git a/js/baba-yaga/tests/with-when-expressions.test.js b/js/baba-yaga/tests/with-when-expressions.test.js new file mode 100644 index 0000000..af14d10 --- /dev/null +++ b/js/baba-yaga/tests/with-when-expressions.test.js @@ -0,0 +1,158 @@ +import assert from 'assert'; +import { createLexer } from '../src/core/lexer.js'; +import { createParser } from '../src/core/parser.js'; +import { createInterpreter } from '../src/core/interpreter.js'; + +function interpret(code) { + const lexer = createLexer(code); + const tokens = lexer.allTokens(); + const parser = createParser(tokens); + const ast = parser.parse(); + const interpreter = createInterpreter(ast); + interpreter.interpret(); + return interpreter; +} + +describe('with header: when expressions', () => { + it('evaluates simple single-line when expressions', () => { + const code = ` + test : x -> + with (status : when x is 0 then "zero" _ then "other";) -> status; + result : test 0; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('result'), 'zero'); + }); + + it('evaluates multi-line when expressions', () => { + const code = ` + test : x -> + with ( + status : when x is + 0 then "zero" + _ then when (x < 10) is + true then "small" + _ then "large"; + ) -> status; + result1 : test 0; + result2 : test 5; + result3 : test 15; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('result1'), 'zero'); + assert.strictEqual(itp.scope.get('result2'), 'small'); + assert.strictEqual(itp.scope.get('result3'), 'large'); + }); + + it('evaluates complex when expressions with pattern guards', () => { + const code = ` + test : x -> + with ( + category : when x is + n if (n < 0) then "negative" + 0 then "zero" + n if (n > 10) then "large" + _ then "small"; + ) -> category; + result1 : test -5; + result2 : test 0; + result3 : test 5; + result4 : test 15; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('result1'), 'negative'); + assert.strictEqual(itp.scope.get('result2'), 'zero'); + assert.strictEqual(itp.scope.get('result3'), 'small'); + assert.strictEqual(itp.scope.get('result4'), 'large'); + }); + + it('evaluates mixed when expressions with other types', () => { + const code = ` + test : x -> + with ( + num : x + 1; + category : when x is + 0 then "zero" + _ then when (x < 10) is + true then "small" + _ then "large"; + isValid : x > 0; + ) -> { num: num, category: category, isValid: isValid }; + result : test 5; + `; + const itp = interpret(code); + const result = itp.scope.get('result'); + assert.strictEqual(result.num.value, 6); + assert.strictEqual(result.category, 'small'); + assert.strictEqual(result.isValid, true); + }); + + it('evaluates deeply nested when expressions', () => { + const code = ` + test : x -> + with ( + status : when x is + 0 then "zero" + _ then when (x < 10) is + true then "small" + _ then when (x < 100) is + true then "medium" + _ then when (x < 1000) is + true then "large" + _ then "huge"; + ) -> status; + result1 : test 0; + result2 : test 5; + result3 : test 50; + result4 : test 500; + result5 : test 5000; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('result1'), 'zero'); + assert.strictEqual(itp.scope.get('result2'), 'small'); + assert.strictEqual(itp.scope.get('result3'), 'medium'); + assert.strictEqual(itp.scope.get('result4'), 'large'); + assert.strictEqual(itp.scope.get('result5'), 'huge'); + }); + + it('works with arithmetic expressions in when conditions', () => { + const code = ` + test : x -> + with ( + status : when (x + 1) is + 1 then "zero-based" + _ then when ((x * 2) > 10) is + true then "large" + _ then "small"; + ) -> status; + result1 : test 0; + result2 : test 3; + result3 : test 6; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('result1'), 'zero-based'); + assert.strictEqual(itp.scope.get('result2'), 'small'); + assert.strictEqual(itp.scope.get('result3'), 'large'); + }); + + it('works with function calls in when conditions', () => { + const code = ` + test : list -> + with ( + len : length list; + status : when len is + 0 then "empty" + _ then when (len > 5) is + true then "long" + _ then "short"; + ) -> status; + result1 : test []; + result2 : test [1, 2, 3]; + result3 : test [1, 2, 3, 4, 5, 6]; + `; + const itp = interpret(code); + assert.strictEqual(itp.scope.get('result1'), 'empty'); + assert.strictEqual(itp.scope.get('result2'), 'short'); + assert.strictEqual(itp.scope.get('result3'), 'long'); + }); +}); |