diff options
Diffstat (limited to 'js/scripting-lang/lang.js')
-rw-r--r-- | js/scripting-lang/lang.js | 3763 |
1 files changed, 1600 insertions, 2163 deletions
diff --git a/js/scripting-lang/lang.js b/js/scripting-lang/lang.js index 97d2595..d3bc0b5 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -1,1908 +1,1443 @@ -// The goal here is less to make anything useful...or even something that works, but to learn what parts an interpreted languages needs to have to function. +// Cross-platform scripting language implementation +// Supports Node.js, Bun, and browser environments -// Define the types of tokens -const TokenType = { - NUMBER: 'NUMBER', - PLUS: 'PLUS', - MINUS: 'MINUS', - MULTIPLY: 'MULTIPLY', - DIVIDE: 'DIVIDE', - IDENTIFIER: 'IDENTIFIER', - ASSIGNMENT: 'ASSIGNMENT', - ARROW: 'ARROW', - CASE: 'CASE', - OF: 'OF', - WILDCARD: 'WILDCARD', - FUNCTION: 'FUNCTION', - LEFT_PAREN: 'LEFT_PAREN', - RIGHT_PAREN: 'RIGHT_PAREN', - LEFT_BRACE: 'LEFT_BRACE', - RIGHT_BRACE: 'RIGHT_BRACE', - SEMICOLON: 'SEMICOLON', - IO_IN: 'IO_IN', - IO_OUT: 'IO_OUT', - IO_ASSERT: 'IO_ASSERT', - EQUALS: 'EQUALS', - FUNCTION_REF: 'FUNCTION_REF', - STRING: 'STRING', - // New arithmetic operators - MODULO: 'MODULO', - POWER: 'POWER', - // New comparison operators - LESS_THAN: 'LESS_THAN', - GREATER_THAN: 'GREATER_THAN', - LESS_EQUAL: 'LESS_EQUAL', - GREATER_EQUAL: 'GREATER_EQUAL', - NOT_EQUAL: 'NOT_EQUAL', - // New logical operators - AND: 'AND', - OR: 'OR', - XOR: 'XOR', - NOT: 'NOT', -}; - -// Lexer -function lexer(input) { - debugLog('Starting lexer with input', input); - const tokens = []; - let current = 0; - - while (current < input.length) { - let char = input[current]; - - if (/\d/.test(char)) { - let value = ''; - while (/\d/.test(char)) { - value += char; - char = input[++current]; - } - tokens.push({ - type: TokenType.NUMBER, - value - }); - continue; - } - - if (char === '+') { - tokens.push({ - type: TokenType.PLUS - }); - current++; - continue; - } - - if (input.slice(current, current + 2) === '->') { - tokens.push({ - type: TokenType.ARROW - }); - current += 2; - continue; - } - - if (char === '-') { - tokens.push({ - type: TokenType.MINUS - }); - current++; - continue; - } - - if (char === '*') { - tokens.push({ - type: TokenType.MULTIPLY - }); - current++; - continue; - } - - if (char === '/') { - tokens.push({ - type: TokenType.DIVIDE - }); - current++; - continue; - } +import { lexer, TokenType } from './lexer.js'; +import { parser } from './parser.js'; - if (char === '%') { - tokens.push({ - type: TokenType.MODULO - }); - current++; - continue; - } - - if (char === '^') { - tokens.push({ - type: TokenType.POWER - }); - current++; - continue; - } - - if (char === '<') { - if (input.slice(current, current + 2) === '<=') { - tokens.push({ - type: TokenType.LESS_EQUAL - }); - current += 2; - } else { - tokens.push({ - type: TokenType.LESS_THAN - }); - current++; - } - continue; +/** + * Initializes the standard library in the provided scope. + * + * @param {Object} scope - The global scope object to inject functions into + * @description Injects higher-order functions and combinator functions into the interpreter's global scope. + * These functions provide functional programming utilities and implement the combinator foundation + * that eliminates parsing ambiguity by translating all operations to function calls. + * + * The standard library includes: + * - Higher-order functions (map, compose, pipe, apply, filter, reduce, fold, curry) + * - Arithmetic combinators (add, subtract, multiply, divide, modulo, power, negate) + * - Comparison combinators (equals, notEquals, lessThan, greaterThan, lessEqual, greaterEqual) + * - Logical combinators (logicalAnd, logicalOr, logicalXor, logicalNot) + * - Enhanced combinators (identity, constant, flip, on, both, either) + * + * This approach ensures that user code can access these functions as if they were built-in, + * without special syntax or reserved keywords. The combinator foundation allows the parser + * to translate all operators to function calls, eliminating ambiguity while preserving syntax. + * + * Functions are written to check argument types at runtime since the language is dynamically + * typed and does not enforce arity or types at parse time. + */ +function initializeStandardLibrary(scope) { + /** + * Map: Apply a function to a value + * @param {Function} f - Function to apply + * @param {*} x - Value to apply function to + * @returns {*} Result of applying f to x + * @throws {Error} When first argument is not a function + */ + scope.map = function(f, x) { + if (typeof f === 'function') { + return f(x); + } else { + throw new Error('map: first argument must be a function'); } - - if (char === '>') { - if (input.slice(current, current + 2) === '>=') { - tokens.push({ - type: TokenType.GREATER_EQUAL - }); - current += 2; + }; + + /** + * Compose: Compose two functions (f ∘ g)(x) = f(g(x)) + * @param {Function} f - Outer function + * @param {Function} g - Inner function + * @param {*} [x] - Optional argument to apply composed function to + * @returns {Function|*} Either a composed function or the result of applying it + * @throws {Error} When first two arguments are not functions + */ + scope.compose = function(f, g, x) { + if (typeof f === 'function' && typeof g === 'function') { + if (arguments.length === 3) { + return f(g(x)); } else { - tokens.push({ - type: TokenType.GREATER_THAN - }); - current++; + return function(x) { + return f(g(x)); + }; } - continue; - } - - if (input.slice(current, current + 2) === '!=') { - tokens.push({ - type: TokenType.NOT_EQUAL - }); - current += 2; - continue; - } - - // Check for keywords before general identifiers - if (input.slice(current, current + 4) === 'case') { - tokens.push({ - type: TokenType.CASE - }); - current += 4; - continue; - } - - if (input.slice(current, current + 2) === 'of') { - tokens.push({ - type: TokenType.OF - }); - current += 2; - continue; - } - - // Check for logical keywords - if (input.slice(current, current + 3) === 'and') { - tokens.push({ - type: TokenType.AND - }); - current += 3; - continue; - } - - if (input.slice(current, current + 2) === 'or') { - tokens.push({ - type: TokenType.OR - }); - current += 2; - continue; + } else { + throw new Error('compose: first two arguments must be functions'); } - - if (input.slice(current, current + 3) === 'xor') { - tokens.push({ - type: TokenType.XOR - }); - current += 3; - continue; + }; + + /** + * Curry: Apply a function to arguments (simplified currying) + * @param {Function} f - Function to curry + * @param {*} x - First argument + * @param {*} y - Second argument + * @returns {*} Result of applying f to x and y + * @throws {Error} When first argument is not a function + */ + scope.curry = function(f, x, y) { + if (typeof f === 'function') { + return f(x, y); + } else { + throw new Error('curry: first argument must be a function'); } - - if (input.slice(current, current + 3) === 'not') { - tokens.push({ - type: TokenType.NOT - }); - current += 3; - continue; + }; + + /** + * Apply: Apply a function to an argument (explicit function application) + * @param {Function} f - Function to apply + * @param {*} x - Argument to apply function to + * @returns {*} Result of applying f to x + * @throws {Error} When first argument is not a function + */ + scope.apply = function(f, x) { + if (typeof f === 'function') { + return f(x); + } else { + throw new Error('apply: first argument must be a function'); } - - if (/[a-z]/i.test(char)) { - let value = ''; - while (/[a-z0-9]/i.test(char)) { - value += char; - char = input[++current]; + }; + + /** + * Pipe: Compose functions in left-to-right order (opposite of compose) + * @param {Function} f - First function + * @param {Function} g - Second function + * @param {*} [x] - Optional argument to apply piped function to + * @returns {Function|*} Either a piped function or the result of applying it + * @throws {Error} When first two arguments are not functions + */ + scope.pipe = function(f, g, x) { + if (typeof f === 'function' && typeof g === 'function') { + if (arguments.length === 3) { + return g(f(x)); + } else { + return function(x) { + return g(f(x)); + }; } - tokens.push({ - type: TokenType.IDENTIFIER, - value - }); - continue; - } - - if (char === ':') { - tokens.push({ - type: TokenType.ASSIGNMENT - }); - current++; - continue; - } - - if (input.slice(current, current + 2) === '==') { - tokens.push({ - type: TokenType.EQUALS - }); - current += 2; - continue; - } - - if (char === '@') { - tokens.push({ - type: TokenType.FUNCTION_REF - }); - current++; - continue; - } - - if (char === '=') { - tokens.push({ - type: TokenType.EQUALS - }); - current++; - continue; - } - - - - if (char === '_') { - tokens.push({ - type: TokenType.WILDCARD - }); - current++; - continue; - } - - if (char === '(') { - tokens.push({ - type: TokenType.LEFT_PAREN - }); - current++; - continue; - } - - if (char === ')') { - tokens.push({ - type: TokenType.RIGHT_PAREN - }); - current++; - continue; - } - - if (char === '{') { - tokens.push({ - type: TokenType.LEFT_BRACE - }); - current++; - continue; - } - - if (char === '}') { - tokens.push({ - type: TokenType.RIGHT_BRACE - }); - current++; - continue; - } - - if (input.slice(current, current + 8) === 'function') { - tokens.push({ - type: TokenType.FUNCTION - }); - current += 8; - continue; - } - - // Check for IO tokens - if (input.slice(current, current + 4) === '..in') { - tokens.push({ - type: TokenType.IO_IN - }); - current += 4; - continue; + } else { + throw new Error('pipe: first two arguments must be functions'); } - - if (input.slice(current, current + 5) === '..out') { - tokens.push({ - type: TokenType.IO_OUT - }); - current += 5; - continue; + }; + + /** + * Filter: Filter a value based on a predicate + * @param {Function} p - Predicate function + * @param {*} x - Value to test + * @returns {*|0} The value if predicate is true, 0 otherwise + * @throws {Error} When first argument is not a function + */ + scope.filter = function(p, x) { + if (typeof p === 'function') { + return p(x) ? x : 0; + } else { + throw new Error('filter: first argument must be a function'); } - - if (input.slice(current, current + 8) === '..assert') { - tokens.push({ - type: TokenType.IO_ASSERT - }); - current += 8; - continue; + }; + + /** + * Reduce: Reduce two values using a binary function + * @param {Function} f - Binary function + * @param {*} init - Initial value + * @param {*} x - Second value + * @returns {*} Result of applying f to init and x + * @throws {Error} When first argument is not a function + */ + scope.reduce = function(f, init, x) { + if (typeof f === 'function') { + return f(init, x); + } else { + throw new Error('reduce: first argument must be a function'); } - - // Handle string literals - if (char === '"') { - let value = ''; - current++; // Skip opening quote - while (current < input.length && input[current] !== '"') { - value += input[current]; - current++; - } - if (current < input.length && input[current] === '"') { - current++; // Skip closing quote - tokens.push({ - type: TokenType.STRING, - value - }); - continue; - } else { - throw new Error('Unterminated string literal'); - } + }; + + /** + * Fold: Same as reduce, but more explicit about the folding direction + * @param {Function} f - Binary function + * @param {*} init - Initial value + * @param {*} x - Second value + * @returns {*} Result of applying f to init and x + * @throws {Error} When first argument is not a function + */ + scope.fold = function(f, init, x) { + if (typeof f === 'function') { + return f(init, x); + } else { + throw new Error('fold: first argument must be a function'); } - - if (char === ';') { - tokens.push({ type: TokenType.SEMICOLON }); - current++; - continue; + }; + + // ===== ARITHMETIC COMBINATORS ===== + + /** + * Add: Add two numbers + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Sum of x and y + */ + scope.add = function(x, y) { + return x + y; + }; + + /** + * Subtract: Subtract second number from first + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Difference of x and y + */ + scope.subtract = function(x, y) { + return x - y; + }; + + /** + * Multiply: Multiply two numbers + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Product of x and y + */ + scope.multiply = function(x, y) { + return x * y; + }; + + /** + * Divide: Divide first number by second + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Quotient of x and y + * @throws {Error} When second argument is zero + */ + scope.divide = function(x, y) { + if (y === 0) { + throw new Error('Division by zero'); + } + return x / y; + }; + + /** + * Modulo: Get remainder of division + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Remainder of x divided by y + */ + scope.modulo = function(x, y) { + return x % y; + }; + + /** + * Power: Raise first number to power of second + * @param {number} x - Base number + * @param {number} y - Exponent + * @returns {number} x raised to the power of y + */ + scope.power = function(x, y) { + return Math.pow(x, y); + }; + + /** + * Negate: Negate a number + * @param {number} x - Number to negate + * @returns {number} Negated value of x + */ + scope.negate = function(x) { + return -x; + }; + + // ===== COMPARISON COMBINATORS ===== + + /** + * Equals: Check if two values are equal + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x equals y + */ + scope.equals = function(x, y) { + return x === y; + }; + + /** + * NotEquals: Check if two values are not equal + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x does not equal y + */ + scope.notEquals = function(x, y) { + return x !== y; + }; + + /** + * LessThan: Check if first value is less than second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x < y + */ + scope.lessThan = function(x, y) { + return x < y; + }; + + /** + * GreaterThan: Check if first value is greater than second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x > y + */ + scope.greaterThan = function(x, y) { + return x > y; + }; + + /** + * LessEqual: Check if first value is less than or equal to second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x <= y + */ + scope.lessEqual = function(x, y) { + return x <= y; + }; + + /** + * GreaterEqual: Check if first value is greater than or equal to second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x >= y + */ + scope.greaterEqual = function(x, y) { + return x >= y; + }; + + // ===== LOGICAL COMBINATORS ===== + + /** + * LogicalAnd: Logical AND of two values + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if both x and y are truthy + */ + scope.logicalAnd = function(x, y) { + return !!(x && y); + }; + + /** + * LogicalOr: Logical OR of two values + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if either x or y is truthy + */ + scope.logicalOr = function(x, y) { + return !!(x || y); + }; + + /** + * LogicalXor: Logical XOR of two values + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if exactly one of x or y is truthy + */ + scope.logicalXor = function(x, y) { + return !!((x && !y) || (!x && y)); + }; + + /** + * LogicalNot: Logical NOT of a value + * @param {*} x - Value to negate + * @returns {boolean} True if x is falsy, false if x is truthy + */ + scope.logicalNot = function(x) { + return !x; + }; + + // ===== ASSIGNMENT COMBINATOR ===== + + /** + * Assign: Assign a value to a variable name + * @param {string} name - Variable name + * @param {*} value - Value to assign + * @returns {*} The assigned value + * @throws {Error} When trying to reassign an immutable variable + * @note This function needs access to the global scope, so it will be + * set up during interpreter initialization + */ + // Note: assign will be set up in the interpreter with access to globalScope + + // ===== ENHANCED HIGHER-ORDER COMBINATORS ===== + + /** + * Identity: Return the input unchanged + * @param {*} x - Any value + * @returns {*} The same value + */ + scope.identity = function(x) { + return x; + }; + + /** + * Constant: Create a function that always returns the same value + * @param {*} x - Value to return + * @param {*} [y] - Optional second argument (ignored) + * @returns {*} The value x, or a function if only one argument provided + */ + scope.constant = function(x, y) { + if (arguments.length === 2) { + return x; + } else { + return function(y) { + return x; + }; } - - if (/[a-z]/i.test(char)) { - let value = ''; - while (/[a-z0-9]/i.test(char)) { - value += char; - char = input[++current]; - } - tokens.push({ - type: TokenType.IDENTIFIER, - value - }); - continue; + }; + + /** + * Flip: Flip the order of arguments for a binary function + * @param {Function} f - Binary function + * @param {*} [x] - Optional first argument + * @param {*} [y] - Optional second argument + * @returns {Function|*} Function with flipped argument order, or result if arguments provided + */ + scope.flip = function(f, x, y) { + if (arguments.length === 3) { + return f(y, x); + } else { + return function(x, y) { + return f(y, x); + }; } - - current++; - } - - return tokens; + }; + + /** + * On: Apply a function to the results of another function + * @param {Function} f - Outer function + * @param {Function} g - Inner function + * @returns {Function} Function that applies f to the results of g + */ + scope.on = function(f, g) { + return function(x, y) { + return f(g(x), g(y)); + }; + }; + + /** + * Both: Check if both predicates are true + * @param {Function} f - First predicate + * @param {Function} g - Second predicate + * @returns {Function} Function that returns true if both predicates are true + */ + scope.both = function(f, g) { + return function(x) { + return f(x) && g(x); + }; + }; + + /** + * Either: Check if either predicate is true + * @param {Function} f - First predicate + * @param {Function} g - Second predicate + * @returns {Function} Function that returns true if either predicate is true + */ + scope.either = function(f, g) { + return function(x) { + return f(x) || g(x); + }; + }; } -// Parser -function parser(tokens) { - debugLog('Starting parser with tokens', tokens); +/** + * Interpreter: Walks the AST and evaluates each node. + * + * @param {Object} ast - Abstract Syntax Tree to evaluate + * @returns {*} The result of evaluating the AST, or a Promise for async operations + * @throws {Error} For evaluation errors like division by zero, undefined variables, etc. + * + * @description Evaluates an AST by walking through each node and performing the + * corresponding operations. Manages scope, handles function calls, and supports + * both synchronous and asynchronous operations. + * + * The interpreter implements a combinator-based architecture where all operations + * are translated to function calls to standard library combinators. This eliminates + * parsing ambiguity while preserving the original syntax. The parser generates + * FunctionCall nodes for operators (e.g., x + y becomes add(x, y)), and the + * interpreter executes these calls using the combinator functions in the global scope. + * + * The interpreter uses a global scope for variable storage and function definitions. + * Each function call creates a new scope (using prototypal inheritance) to implement + * lexical scoping. Immutability is enforced by preventing reassignment in the + * global scope. + * + * The interpreter is split into three functions: evalNode (global), + * localEvalNodeWithScope (for function bodies), and localEvalNode (for internal + * recursion). This separation allows for correct scope handling and easier debugging. + * + * Recursive function support is implemented using a forward declaration pattern: + * a placeholder function is created in the global scope before evaluation, allowing + * the function body to reference itself during evaluation. + */ +function interpreter(ast) { + const globalScope = {}; + initializeStandardLibrary(globalScope); - + // Debug: Check if combinators are available + if (process.env.DEBUG) { + console.log('[DEBUG] Available functions in global scope:', Object.keys(globalScope)); + console.log('[DEBUG] add function exists:', typeof globalScope.add === 'function'); + console.log('[DEBUG] subtract function exists:', typeof globalScope.subtract === 'function'); + } - let current = 0; - - function walk() { - if (current >= tokens.length) { - return null; // Return null when there are no more tokens - } - - let token = tokens[current]; - - // Helper function to detect ambiguous nested function calls - function detectAmbiguousFunctionCalls() { - // Look ahead to see if we have a pattern like: func1 func2 arg1 func3 arg2 - // This indicates ambiguous nested function calls - let tempCurrent = current; - let functionCalls = []; - - while (tempCurrent < tokens.length && tokens[tempCurrent].type !== TokenType.SEMICOLON) { - if (tokens[tempCurrent].type === TokenType.IDENTIFIER) { - // Check if this identifier is followed by arguments (function call) - if (tempCurrent + 1 < tokens.length && - tokens[tempCurrent + 1].type !== TokenType.ASSIGNMENT && - tokens[tempCurrent + 1].type !== TokenType.SEMICOLON && - (tokens[tempCurrent + 1].type === TokenType.NUMBER || - tokens[tempCurrent + 1].type === TokenType.IDENTIFIER || - tokens[tempCurrent + 1].type === TokenType.LEFT_PAREN)) { + // Reset call stack tracker at the start of interpretation + callStackTracker.reset(); + + /** + * Evaluates AST nodes in the global scope. + * + * @param {Object} node - AST node to evaluate + * @returns {*} The result of evaluating the node + * @throws {Error} For evaluation errors + * + * @description Main evaluation function that handles all node types in the + * global scope context. This function processes the core language constructs + * and delegates to combinator functions for all operations. + * + * The function implements the forward declaration pattern for recursive functions: + * when a function assignment is detected, a placeholder is created in the global + * scope before evaluation, allowing the function body to reference itself. + */ + function evalNode(node) { + callStackTracker.push('evalNode', node?.type || 'unknown'); + + try { + if (!node) { + return undefined; + } + switch (node.type) { + case 'NumberLiteral': + return parseFloat(node.value); + case 'StringLiteral': + return node.value; + case 'BooleanLiteral': + return node.value; + case 'PlusExpression': + return evalNode(node.left) + evalNode(node.right); + case 'MinusExpression': + return evalNode(node.left) - evalNode(node.right); + case 'MultiplyExpression': + return evalNode(node.left) * evalNode(node.right); + case 'DivideExpression': + const divisor = evalNode(node.right); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return evalNode(node.left) / evalNode(node.right); + case 'ModuloExpression': + return evalNode(node.left) % evalNode(node.right); + case 'PowerExpression': + return Math.pow(evalNode(node.left), evalNode(node.right)); + case 'EqualsExpression': + return evalNode(node.left) === evalNode(node.right); + case 'LessThanExpression': + return evalNode(node.left) < evalNode(node.right); + case 'GreaterThanExpression': + return evalNode(node.left) > evalNode(node.right); + case 'LessEqualExpression': + return evalNode(node.left) <= evalNode(node.right); + case 'GreaterEqualExpression': + return evalNode(node.left) >= evalNode(node.right); + case 'NotEqualExpression': + return evalNode(node.left) !== evalNode(node.right); + case 'AndExpression': + return !!(evalNode(node.left) && evalNode(node.right)); + case 'OrExpression': + return !!(evalNode(node.left) || evalNode(node.right)); + case 'XorExpression': + const leftVal = evalNode(node.left); + const rightVal = evalNode(node.right); + return !!((leftVal && !rightVal) || (!leftVal && rightVal)); + case 'NotExpression': + return !evalNode(node.operand); + case 'UnaryMinusExpression': + return -evalNode(node.operand); + case 'TableLiteral': + const table = {}; + let arrayIndex = 1; + + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = evalNode(entry.value); + arrayIndex++; + } else { + // Key-value entry: {name: "Alice", age: 30} + let key; + if (entry.key.type === 'Identifier') { + // Convert identifier keys to strings + key = entry.key.value; + } else { + // For other key types (numbers, strings), evaluate normally + key = evalNode(entry.key); + } + const value = evalNode(entry.value); + table[key] = value; + } + } + + return table; + case 'TableAccess': + const tableValue = evalNode(node.table); + let keyValue; + + // Handle different key types + if (node.key.type === 'Identifier') { + // For dot notation, use the identifier name as the key + keyValue = node.key.value; + } else { + // For bracket notation, evaluate the key expression + keyValue = evalNode(node.key); + } + + if (typeof tableValue !== 'object' || tableValue === null) { + throw new Error('Cannot access property of non-table value'); + } + + if (tableValue[keyValue] === undefined) { + throw new Error(`Key '${keyValue}' not found in table`); + } + + return tableValue[keyValue]; + case 'AssignmentExpression': + // Prevent reassignment of standard library functions + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.name} is not yet fully defined`); + }; + + // Store the placeholder in global scope + globalScope[node.name] = placeholder; + + // Now evaluate the function definition with access to the placeholder + const actualFunction = evalNode(node.value); + + // Replace the placeholder with the actual function + globalScope[node.name] = actualFunction; + return; + } + + const value = evalNode(node.value); + globalScope[node.name] = value; + return; + case 'Assignment': + // Prevent reassignment of standard library functions + if (globalScope.hasOwnProperty(node.identifier)) { + throw new Error(`Cannot reassign immutable variable: ${node.identifier}`); + } + + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.identifier} is not yet fully defined`); + }; + + // Store the placeholder in global scope + globalScope[node.identifier] = placeholder; - functionCalls.push(tokens[tempCurrent].value); + // Now evaluate the function definition with access to the placeholder + const actualFunction = evalNode(node.value); - // Skip the function name and its arguments - tempCurrent++; // Skip function name - while (tempCurrent < tokens.length && - tokens[tempCurrent].type !== TokenType.SEMICOLON && - tokens[tempCurrent].type !== TokenType.RIGHT_PAREN) { - tempCurrent++; + // Replace the placeholder with the actual function + globalScope[node.identifier] = actualFunction; + return; + } + + const assignmentValue = evalNode(node.value); + globalScope[node.identifier] = assignmentValue; + return; + case 'Identifier': + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined) { + throw new Error(`Variable ${node.value} is not defined`); + } + return identifierValue; + case 'FunctionDeclaration': + // For anonymous functions, the name comes from the assignment + // The function itself doesn't have a name, so we just return + // The assignment will handle storing it in the global scope + return function(...args) { + callStackTracker.push('FunctionCall', node.params.join(',')); + try { + let localScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + localScope[node.params[i]] = args[i]; + } + return localEvalNodeWithScope(node.body, localScope); + } finally { + callStackTracker.pop(); + } + }; + case 'FunctionDefinition': + // Create a function from the function definition + return function(...args) { + callStackTracker.push('FunctionCall', node.parameters.join(',')); + try { + let localScope = Object.create(globalScope); + for (let i = 0; i < node.parameters.length; i++) { + localScope[node.parameters[i]] = args[i]; + } + return localEvalNodeWithScope(node.body, localScope); + } finally { + callStackTracker.pop(); + } + }; + case 'FunctionCall': + let funcToCall; + if (typeof node.name === 'string') { + // Regular function call with string name + funcToCall = globalScope[node.name]; + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: looking up function '${node.name}' in globalScope, found:`, typeof funcToCall); + } + } else if (node.name.type === 'Identifier') { + // Function call with identifier + funcToCall = globalScope[node.name.value]; + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: looking up function '${node.name.value}' in globalScope, found:`, typeof funcToCall); } } else { - tempCurrent++; + // Function call from expression (e.g., parenthesized function, higher-order) + funcToCall = evalNode(node.name); + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: evaluated function expression, found:`, typeof funcToCall); + } } - } else { - tempCurrent++; - } - } - - // If we have more than 2 function calls in sequence, it's ambiguous - if (functionCalls.length > 2) { - throw new Error(`Ambiguous nested function calls detected: ${functionCalls.join(' ')}. Use parentheses to explicitly group function calls.`); - } - } - - // Helper function to parse function calls with proper precedence - function parseFunctionCall() { - if (token.type !== TokenType.IDENTIFIER) { - return null; - } - - // Check if this is a function call (identifier followed by arguments) - if (tokens[current + 1] && - tokens[current + 1].type !== TokenType.ASSIGNMENT && - tokens[current + 1].type !== TokenType.PLUS && - tokens[current + 1].type !== TokenType.MINUS && - tokens[current + 1].type !== TokenType.MULTIPLY && - tokens[current + 1].type !== TokenType.DIVIDE && - tokens[current + 1].type !== TokenType.MODULO && - tokens[current + 1].type !== TokenType.POWER && - tokens[current + 1].type !== TokenType.EQUALS && - tokens[current + 1].type !== TokenType.LESS_THAN && - tokens[current + 1].type !== TokenType.GREATER_THAN && - tokens[current + 1].type !== TokenType.LESS_EQUAL && - tokens[current + 1].type !== TokenType.GREATER_EQUAL && - tokens[current + 1].type !== TokenType.NOT_EQUAL && - tokens[current + 1].type !== TokenType.AND && - tokens[current + 1].type !== TokenType.OR && - tokens[current + 1].type !== TokenType.XOR && - tokens[current + 1].type !== TokenType.NOT && - tokens[current + 1].type !== TokenType.SEMICOLON && - (tokens[current + 1].type === TokenType.NUMBER || - tokens[current + 1].type === TokenType.IDENTIFIER || - tokens[current + 1].type === TokenType.LEFT_PAREN)) { - - - - const funcName = token.value; - current++; // Skip function name - const args = []; - - // Collect arguments until we hit a semicolon, closing parenthesis, or end - while (current < tokens.length && - tokens[current].type !== TokenType.SEMICOLON && - tokens[current].type !== TokenType.RIGHT_PAREN) { - // Handle function references (@functionName) - if (tokens[current] && tokens[current].type === TokenType.FUNCTION_REF) { - current++; // Skip @ - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - args.push({ - type: 'FunctionReference', - name: tokens[current].value, - }); - current++; - } else { - throw new Error('Expected function name after @'); + if (funcToCall instanceof Function) { + let args = node.args.map(evalNode); + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: calling function with args:`, args); } - } else { - // Parse arguments with proper precedence - // For nested function calls, we need to parse them as complete function calls - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER && - tokens[current + 1] && tokens[current + 1].type !== TokenType.ASSIGNMENT && - tokens[current + 1].type !== TokenType.PLUS && - tokens[current + 1].type !== TokenType.MINUS && - tokens[current + 1].type !== TokenType.MULTIPLY && - tokens[current + 1].type !== TokenType.DIVIDE && - tokens[current + 1].type !== TokenType.MODULO && - tokens[current + 1].type !== TokenType.POWER && - tokens[current + 1].type !== TokenType.EQUALS && - tokens[current + 1].type !== TokenType.LESS_THAN && - tokens[current + 1].type !== TokenType.GREATER_THAN && - tokens[current + 1].type !== TokenType.LESS_EQUAL && - tokens[current + 1].type !== TokenType.GREATER_EQUAL && - tokens[current + 1].type !== TokenType.NOT_EQUAL && - tokens[current + 1].type !== TokenType.AND && - tokens[current + 1].type !== TokenType.OR && - tokens[current + 1].type !== TokenType.XOR && - tokens[current + 1].type !== TokenType.NOT && - tokens[current + 1].type !== TokenType.SEMICOLON && - (tokens[current + 1].type === TokenType.NUMBER || - tokens[current + 1].type === TokenType.IDENTIFIER || - tokens[current + 1].type === TokenType.LEFT_PAREN)) { - - // Check for ambiguous nested function calls - // Look for pattern: func1 func2 arg1 func3 arg2 - let tempCurrent = current; - let functionNames = []; - - while (tempCurrent < tokens.length && - tokens[tempCurrent].type !== TokenType.SEMICOLON && - tokens[tempCurrent].type !== TokenType.RIGHT_PAREN) { - if (tokens[tempCurrent].type === TokenType.IDENTIFIER && - tempCurrent + 1 < tokens.length && - tokens[tempCurrent + 1].type !== TokenType.ASSIGNMENT && - tokens[tempCurrent + 1].type !== TokenType.PLUS && - tokens[tempCurrent + 1].type !== TokenType.MINUS && - tokens[tempCurrent + 1].type !== TokenType.MULTIPLY && - tokens[tempCurrent + 1].type !== TokenType.DIVIDE && - tokens[tempCurrent + 1].type !== TokenType.MODULO && - tokens[tempCurrent + 1].type !== TokenType.POWER && - tokens[tempCurrent + 1].type !== TokenType.EQUALS && - tokens[tempCurrent + 1].type !== TokenType.LESS_THAN && - tokens[tempCurrent + 1].type !== TokenType.GREATER_THAN && - tokens[tempCurrent + 1].type !== TokenType.LESS_EQUAL && - tokens[tempCurrent + 1].type !== TokenType.GREATER_EQUAL && - tokens[tempCurrent + 1].type !== TokenType.NOT_EQUAL && - tokens[tempCurrent + 1].type !== TokenType.AND && - tokens[tempCurrent + 1].type !== TokenType.OR && - tokens[tempCurrent + 1].type !== TokenType.XOR && - tokens[tempCurrent + 1].type !== TokenType.NOT && - tokens[tempCurrent + 1].type !== TokenType.SEMICOLON && - (tokens[tempCurrent + 1].type === TokenType.NUMBER || - tokens[tempCurrent + 1].type === TokenType.IDENTIFIER || - tokens[tempCurrent + 1].type === TokenType.LEFT_PAREN)) { - functionNames.push(tokens[tempCurrent].value); + return funcToCall(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'WhenExpression': + // Handle both single values and arrays of values + const whenValues = Array.isArray(node.value) + ? node.value.map(evalNode) + : [evalNode(node.value)]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: whenValues =`, whenValues); + } + + for (const caseItem of node.cases) { + // Handle both single patterns and arrays of patterns + const patterns = caseItem.pattern.map(evalNode); + + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: patterns =`, patterns); + } + + // Check if patterns match the values + let matches = true; + if (whenValues.length !== patterns.length) { + matches = false; + } else { + for (let i = 0; i < whenValues.length; i++) { + const value = whenValues[i]; + const pattern = patterns[i]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: comparing value ${value} with pattern ${pattern}`); } - tempCurrent++; - } - - if (functionNames.length > 2) { - throw new Error(`Ambiguous nested function calls detected: ${functionNames.join(' ')}. Use parentheses to explicitly group function calls.`); - } - - // This is a nested function call, parse it as a complete function call - const nestedFuncName = tokens[current].value; - current++; // Skip function name - const nestedArgs = []; - - // Parse nested function arguments - while (current < tokens.length && - tokens[current].type !== TokenType.SEMICOLON && - tokens[current].type !== TokenType.RIGHT_PAREN) { - const nestedArg = walk(); - if (nestedArg) { - nestedArgs.push(nestedArg); + + if (pattern === true) { // Wildcard pattern + // Wildcard always matches + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: wildcard matches`); + } + continue; + } else if (value !== pattern) { + matches = false; + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: pattern does not match`); + } + break; + } else { + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: pattern matches`); + } } } - - args.push({ - type: 'FunctionCall', - name: nestedFuncName, - args: nestedArgs, - }); - } else { - // Use walk() for other types of arguments - const arg = walk(); - if (arg) { - args.push(arg); + } + + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: case matches = ${matches}`); + } + + if (matches) { + const results = caseItem.result.map(evalNode); + if (results.length === 1) { + return results[0]; } + return results.join(' '); } } - } - - return { - type: 'FunctionCall', - name: funcName, - args, - }; - } - - return null; - } - - // Handle arithmetic expressions that start with a number followed by an operator - if (token.type === TokenType.NUMBER && tokens[current + 1] && ( - tokens[current + 1].type === TokenType.PLUS || - tokens[current + 1].type === TokenType.MINUS || - tokens[current + 1].type === TokenType.MULTIPLY || - tokens[current + 1].type === TokenType.DIVIDE || - tokens[current + 1].type === TokenType.MODULO || - tokens[current + 1].type === TokenType.POWER - )) { - debugLog('Parsing arithmetic expression starting with number', { - current: token, - next: tokens[current + 1] - }); - const left = { - type: 'NumberLiteral', - value: token.value, - }; - current++; // Skip the number - const operator = tokens[current].type; - current++; // Skip the operator - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - [TokenType.MODULO]: 'ModuloExpression', - [TokenType.POWER]: 'PowerExpression', - }; - - return { - type: expressionTypes[operator], - left, - right, - }; - } - - if (token.type === TokenType.NUMBER) { - current++; - return { - type: 'NumberLiteral', - value: token.value, - }; - } - - if (token.type === TokenType.STRING) { - current++; - return { - type: 'StringLiteral', - value: token.value, - }; - } - - if (token.type === TokenType.WILDCARD) { - current++; - return { - type: 'WildcardPattern', - }; - } - - if (token.type === TokenType.IO_IN) { - current++; - return { - type: 'IOInExpression', - }; - } - - if (token.type === TokenType.IO_OUT) { - current++; - const value = walk(); - return { - type: 'IOOutExpression', - value, - }; - } - - if (token.type === TokenType.FUNCTION_REF) { - current++; // Skip the @ token - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - const functionName = tokens[current].value; - current++; // Skip the function name - return { - type: 'FunctionReference', - name: functionName, - }; - } else { - throw new Error('Expected function name after @'); - } - } - - if (token.type === TokenType.IO_ASSERT) { - current++; - - // Parse a comparison expression (e.g., x = 5, y > 3, z != 0) - const left = walk(); - - // Expect a comparison operator - if (tokens[current] && ( - tokens[current].type === TokenType.EQUALS || - tokens[current].type === TokenType.LESS_THAN || - tokens[current].type === TokenType.GREATER_THAN || - tokens[current].type === TokenType.LESS_EQUAL || - tokens[current].type === TokenType.GREATER_EQUAL || - tokens[current].type === TokenType.NOT_EQUAL - )) { - const operator = tokens[current].type; - current++; // Skip the operator - const right = walk(); - - return { - type: 'IOAssertExpression', - left, - operator, - right, - }; - } else { - throw new Error('Expected comparison operator (=, <, >, <=, >=, !=) in assertion'); - } - } - - - - if (token.type === TokenType.PLUS) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'PlusExpression', - left, - right, - }; - } - - if (token.type === TokenType.MINUS) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'MinusExpression', - left, - right, - }; - } - - if (token.type === TokenType.MULTIPLY) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'MultiplyExpression', - left, - right, - }; - } - - if (token.type === TokenType.DIVIDE) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'DivideExpression', - left, - right, - }; - } - - if (token.type === TokenType.MODULO) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'ModuloExpression', - left, - right, - }; - } - - if (token.type === TokenType.POWER) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'PowerExpression', - left, - right, - }; - } - - // Comparison operators - if (token.type === TokenType.EQUALS) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'EqualsExpression', - left, - right, - }; - } - - if (token.type === TokenType.LESS_THAN) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'LessThanExpression', - left, - right, - }; - } - - if (token.type === TokenType.GREATER_THAN) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'GreaterThanExpression', - left, - right, - }; - } - - if (token.type === TokenType.LESS_EQUAL) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'LessEqualExpression', - left, - right, - }; - } - - if (token.type === TokenType.GREATER_EQUAL) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'GreaterEqualExpression', - left, - right, - }; - } - - if (token.type === TokenType.NOT_EQUAL) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'NotEqualExpression', - left, - right, - }; - } - - // Logical operators - if (token.type === TokenType.AND) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'AndExpression', - left, - right, - }; - } - - if (token.type === TokenType.OR) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'OrExpression', - left, - right, - }; - } - - if (token.type === TokenType.XOR) { - current++; - const left = walk(); - const right = walk(); - return { - type: 'XorExpression', - left, - right, - }; - } - - if (token.type === TokenType.NOT) { - current++; - const operand = walk(); - return { - type: 'NotExpression', - operand, - }; - } - - if (token.type === TokenType.LEFT_PAREN) { - debugLog('Parsing left parenthesis'); - current++; // Skip opening parenthesis - const expression = walk(); - - // Expect closing parenthesis - if (tokens[current] && tokens[current].type === TokenType.RIGHT_PAREN) { - debugLog('Found right parenthesis'); - current++; // Skip closing parenthesis - - // Check if there's an arithmetic operator after the parentheses - if (tokens[current] && ( - tokens[current].type === TokenType.PLUS || - tokens[current].type === TokenType.MINUS || - tokens[current].type === TokenType.MULTIPLY || - tokens[current].type === TokenType.DIVIDE || - tokens[current].type === TokenType.MODULO || - tokens[current].type === TokenType.POWER || - tokens[current].type === TokenType.EQUALS || - tokens[current].type === TokenType.LESS_THAN || - tokens[current].type === TokenType.GREATER_THAN || - tokens[current].type === TokenType.LESS_EQUAL || - tokens[current].type === TokenType.GREATER_EQUAL || - tokens[current].type === TokenType.NOT_EQUAL || - tokens[current].type === TokenType.AND || - tokens[current].type === TokenType.OR || - tokens[current].type === TokenType.XOR || - tokens[current].type === TokenType.NOT - )) { - // This is an arithmetic expression with parentheses as left operand - const operator = tokens[current].type; - current++; // Skip the operator - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - [TokenType.MODULO]: 'ModuloExpression', - [TokenType.POWER]: 'PowerExpression', - [TokenType.EQUALS]: 'EqualsExpression', - [TokenType.LESS_THAN]: 'LessThanExpression', - [TokenType.GREATER_THAN]: 'GreaterThanExpression', - [TokenType.LESS_EQUAL]: 'LessEqualExpression', - [TokenType.GREATER_EQUAL]: 'GreaterEqualExpression', - [TokenType.NOT_EQUAL]: 'NotEqualExpression', - [TokenType.AND]: 'AndExpression', - [TokenType.OR]: 'OrExpression', - [TokenType.XOR]: 'XorExpression', - [TokenType.NOT]: 'NotExpression', - }; + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); - return { - type: expressionTypes[operator], - left: expression, - right, - }; - } - - // If not followed by an arithmetic operator, just return the parenthesized expression - // This handles cases like function arguments: add (3 + 2) (4 + 1) - return expression; - } else { - debugLog('Expected right parenthesis but found', tokens[current]); - throw new Error('Expected closing parenthesis'); + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const outputValue = evalNode(node.value); + console.log(outputValue); + return outputValue; + case 'IOAssertExpression': + const assertionValue = evalNode(node.value); + if (!assertionValue) { + throw new Error('Assertion failed'); + } + return assertionValue; + case 'FunctionReference': + const functionValue = globalScope[node.name]; + if (functionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof functionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return functionValue; + case 'ArrowExpression': + // Arrow expressions are function bodies that should be evaluated + return evalNode(node.body); + default: + throw new Error(`Unknown node type: ${node.type}`); } + } finally { + callStackTracker.pop(); } + } - if (token.type === TokenType.ASSIGNMENT) { - current++; // Skip the assignment token - return walk(); // Continue parsing - } - - if (token.type === TokenType.IDENTIFIER) { - // First, try to parse as a function call with proper precedence - const functionCall = parseFunctionCall(); - if (functionCall) { - return functionCall; + /** + * Evaluates AST nodes in a local scope with access to parent scope. + * + * @param {Object} node - AST node to evaluate + * @param {Object} scope - Local scope object (prototypally inherits from global) + * @returns {*} The result of evaluating the node + * @throws {Error} For evaluation errors + * + * @description Used for evaluating function bodies and other expressions + * that need access to both local and global variables. This function implements + * lexical scoping by creating a local scope that prototypally inherits from + * the global scope, allowing access to both local parameters and global functions. + * + * The function handles the same node types as evalNode but uses the local scope + * for variable lookups. It also implements the forward declaration pattern for + * recursive functions, ensuring that function definitions can reference themselves + * during evaluation. + * + * This separation of global and local evaluation allows for proper scope management + * and prevents variable name conflicts between function parameters and global variables. + */ + const localEvalNodeWithScope = (node, scope) => { + callStackTracker.push('localEvalNodeWithScope', node?.type || 'unknown'); + + try { + if (!node) { + return undefined; } - - // Check if this is followed by an assignment (variable or function) - if (tokens[current + 1] && tokens[current + 1].type === TokenType.ASSIGNMENT) { - const name = token.value; - - // Check if this is a function declaration (has parameters and arrow) - // Look ahead to see if there are parameters followed by arrow - let params = []; - let isFunction = false; - let tempCurrent = current + 2; // Skip identifier and assignment - - // Collect parameters until we hit an arrow - while (tempCurrent < tokens.length && tokens[tempCurrent].type === TokenType.IDENTIFIER) { - params.push(tokens[tempCurrent].value); - tempCurrent++; - } - - // Check if next token is arrow - if (tempCurrent < tokens.length && tokens[tempCurrent].type === TokenType.ARROW) { - isFunction = true; - } - - if (isFunction) { - // This is a function declaration - // Advance current to where tempCurrent left off - current = tempCurrent + 1; // Skip the arrow token + switch (node.type) { + case 'NumberLiteral': + return parseFloat(node.value); + case 'StringLiteral': + return node.value; + case 'BooleanLiteral': + return node.value; + case 'PlusExpression': + return localEvalNodeWithScope(node.left, scope) + localEvalNodeWithScope(node.right, scope); + case 'MinusExpression': + return localEvalNodeWithScope(node.left, scope) - localEvalNodeWithScope(node.right, scope); + case 'MultiplyExpression': + return localEvalNodeWithScope(node.left, scope) * localEvalNodeWithScope(node.right, scope); + case 'DivideExpression': + const divisor = localEvalNodeWithScope(node.right, scope); + if (divisor === 0) { + throw new Error('Division by zero'); + } + return localEvalNodeWithScope(node.left, scope) / localEvalNodeWithScope(node.right, scope); + case 'ModuloExpression': + return localEvalNodeWithScope(node.left, scope) % localEvalNodeWithScope(node.right, scope); + case 'PowerExpression': + return Math.pow(localEvalNodeWithScope(node.left, scope), localEvalNodeWithScope(node.right, scope)); + case 'EqualsExpression': + return localEvalNodeWithScope(node.left, scope) === localEvalNodeWithScope(node.right, scope); + case 'LessThanExpression': + return localEvalNodeWithScope(node.left, scope) < localEvalNodeWithScope(node.right, scope); + case 'GreaterThanExpression': + return localEvalNodeWithScope(node.left, scope) > localEvalNodeWithScope(node.right, scope); + case 'LessEqualExpression': + return localEvalNodeWithScope(node.left, scope) <= localEvalNodeWithScope(node.right, scope); + case 'GreaterEqualExpression': + return localEvalNodeWithScope(node.left, scope) >= localEvalNodeWithScope(node.right, scope); + case 'NotEqualExpression': + return localEvalNodeWithScope(node.left, scope) !== localEvalNodeWithScope(node.right, scope); + case 'AndExpression': + return !!(localEvalNodeWithScope(node.left, scope) && localEvalNodeWithScope(node.right, scope)); + case 'OrExpression': + return !!(localEvalNodeWithScope(node.left, scope) || localEvalNodeWithScope(node.right, scope)); + case 'XorExpression': + const leftVal = localEvalNodeWithScope(node.left, scope); + const rightVal = localEvalNodeWithScope(node.right, scope); + return !!((leftVal && !rightVal) || (!leftVal && rightVal)); + case 'NotExpression': + return !localEvalNodeWithScope(node.operand, scope); + case 'UnaryMinusExpression': + return -localEvalNodeWithScope(node.operand, scope); + case 'TableLiteral': + const table = {}; + let arrayIndex = 1; - // Check if the body is a case expression - if (tokens[current] && tokens[current].type === TokenType.CASE) { - current++; // Skip 'case' - - // Parse the value being matched (e.g., "x y") - const value = []; - while (current < tokens.length && - tokens[current].type !== TokenType.OF) { - if (tokens[current].type === TokenType.IDENTIFIER) { - value.push({ - type: 'Identifier', - value: tokens[current].value, - }); - } else if (tokens[current].type === TokenType.NUMBER) { - value.push({ - type: 'NumberLiteral', - value: tokens[current].value, - }); + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = localEvalNodeWithScope(entry.value, scope); + arrayIndex++; + } else { + // Key-value entry: {name: "Alice", age: 30} + let key; + if (entry.key.type === 'Identifier') { + // Convert identifier keys to strings + key = entry.key.value; + } else { + // For other key types (numbers, strings), evaluate normally + key = localEvalNodeWithScope(entry.key, scope); } - current++; + const value = localEvalNodeWithScope(entry.value, scope); + table[key] = value; } + } + + return table; + case 'TableAccess': + const tableValue = localEvalNodeWithScope(node.table, scope); + let keyValue; + + // Handle different key types + if (node.key.type === 'Identifier') { + // For dot notation, use the identifier name as the key + keyValue = node.key.value; + } else { + // For bracket notation, evaluate the key expression + keyValue = localEvalNodeWithScope(node.key, scope); + } + + if (typeof tableValue !== 'object' || tableValue === null) { + throw new Error('Cannot access property of non-table value'); + } + + if (tableValue[keyValue] === undefined) { + throw new Error(`Key '${keyValue}' not found in table`); + } + + return tableValue[keyValue]; + case 'AssignmentExpression': + // Prevent reassignment of standard library functions + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.name} is not yet fully defined`); + }; - // Expect 'of' after the value - if (tokens[current] && tokens[current].type === TokenType.OF) { - current++; // Skip 'of' - } else { - throw new Error('Expected "of" after case value'); - } + // Store the placeholder in global scope + globalScope[node.name] = placeholder; - const cases = []; + // Now evaluate the function definition with access to the placeholder + const actualFunction = localEvalNodeWithScope(node.value, scope); - // Parse cases until we hit a semicolon or end - while (current < tokens.length) { - // Check if we've reached the end of the case expression - if (tokens[current] && tokens[current].type === TokenType.SEMICOLON) { - debugLog('Found semicolon at start of loop, ending case parsing'); - current++; - break; // Exit the case parsing loop - } - - debugLog('Case parsing loop', { - current: tokens[current], - currentType: tokens[current]?.type - }); - // Parse pattern (e.g., "0 0" or "0 _" or "_ 0" or "_ _") - debugLog('Parsing pattern', { - current: tokens[current], - currentType: tokens[current]?.type - }); - const pattern = []; - while (current < tokens.length && - tokens[current].type !== TokenType.ASSIGNMENT && - tokens[current].type !== TokenType.SEMICOLON) { - if (tokens[current].type === TokenType.NUMBER) { - pattern.push({ - type: 'NumberLiteral', - value: tokens[current].value, - }); - } else if (tokens[current].type === TokenType.WILDCARD) { - pattern.push({ - type: 'WildcardPattern', - }); - } else if (tokens[current].type === TokenType.IDENTIFIER) { - pattern.push({ - type: 'Identifier', - value: tokens[current].value, - }); - } - current++; - } - - // Expect ':' after pattern - debugLog('Looking for : after pattern', { - current: tokens[current], - currentType: tokens[current]?.type - }); - if (tokens[current] && tokens[current].type === TokenType.ASSIGNMENT) { - current++; // Skip ':' - debugLog('Found : after pattern'); - } else { - throw new Error('Expected ":" after pattern'); - } - - // Parse result using walk() to handle complex expressions - debugLog('Starting case result parsing', { - current: tokens[current], - currentType: tokens[current]?.type - }); - const result = []; - - // Check if we're at the start of the next pattern - if ((tokens[current].type === TokenType.NUMBER || - tokens[current].type === TokenType.WILDCARD) && - tokens[current + 1] && tokens[current + 1].type === TokenType.ASSIGNMENT) { - debugLog('Found start of next pattern, stopping case result parsing'); - } else { - // Parse one expression for the case result - debugLog('Parsing case result node', { - current: tokens[current], - currentType: tokens[current]?.type - }); - const resultNode = walk(); - if (resultNode) { - result.push(resultNode); - debugLog('Added result node', resultNode); - } - - // Check if we've reached the end of the case expression - if (tokens[current] && tokens[current].type === TokenType.SEMICOLON) { - debugLog('Found semicolon after result, ending case parsing'); - current++; - break; // Exit the case parsing loop - } + // Replace the placeholder with the actual function + globalScope[node.name] = actualFunction; + return; + } + + globalScope[node.name] = localEvalNodeWithScope(node.value, scope); + return; + case 'Identifier': + // First check local scope, then global scope + if (scope && scope.hasOwnProperty(node.value)) { + return scope[node.value]; + } + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined && node.value) { + return node.value; + } + return identifierValue; + case 'FunctionDeclaration': + // For anonymous functions, the name comes from the assignment + // The function itself doesn't have a name, so we just return + // The assignment will handle storing it in the global scope + return function(...args) { + callStackTracker.push('FunctionCall', node.params.join(',')); + try { + let nestedScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + nestedScope[node.params[i]] = args[i]; } - - cases.push({ pattern, result }); - - // Check if we've reached the end of the case expression - if (tokens[current] && tokens[current].type === TokenType.SEMICOLON) { - debugLog('Found semicolon, ending case parsing'); - current++; - break; // Exit the case parsing loop + return localEvalNodeWithScope(node.body, nestedScope); + } finally { + callStackTracker.pop(); + } + }; + case 'FunctionDefinition': + // Create a function from the function definition + return function(...args) { + callStackTracker.push('FunctionCall', node.parameters.join(',')); + try { + let nestedScope = Object.create(globalScope); + for (let i = 0; i < node.parameters.length; i++) { + nestedScope[node.parameters[i]] = args[i]; } + return localEvalNodeWithScope(node.body, nestedScope); + } finally { + callStackTracker.pop(); } - - const body = { - type: 'CaseExpression', - value, - cases, - }; - - return { - type: 'FunctionDeclaration', - name, - params, - body, - }; + }; + case 'FunctionCall': + let localFunc; + if (typeof node.name === 'string') { + // Regular function call with string name + localFunc = globalScope[node.name]; + } else if (node.name.type === 'Identifier') { + // Function call with identifier + localFunc = globalScope[node.name.value]; } else { - // Parse function body directly to avoid function call detection issues - let body; - if (tokens[current] && ( - tokens[current].type === TokenType.PLUS || - tokens[current].type === TokenType.MINUS || - tokens[current].type === TokenType.MULTIPLY || - tokens[current].type === TokenType.DIVIDE - )) { - // Arithmetic expression - const operator = tokens[current].type; - current++; // Skip operator - const left = walk(); - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - }; - - body = { - type: expressionTypes[operator], - left, - right, - }; - } else { - // Fallback to walk() for other cases - debugLog('Using walk() fallback for function body'); - body = walk(); - } - - return { - type: 'FunctionDeclaration', - name, - params, - body, - }; + // Function call from expression (e.g., parenthesized function, higher-order) + localFunc = localEvalNodeWithScope(node.name, scope); } - } else { - // This is a variable assignment - // Advance current to skip identifier and assignment token - current += 2; // Skip identifier and assignment - debugLog('Assignment parsing', { - current: tokens[current], - next: tokens[current + 1], - currentType: tokens[current]?.type, - nextType: tokens[current + 1]?.type - }); + if (localFunc instanceof Function) { + let args = node.args.map(arg => localEvalNodeWithScope(arg, scope)); + return localFunc(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'WhenExpression': + // Handle both single values and arrays of values + const whenValues = Array.isArray(node.value) + ? node.value.map(val => localEvalNodeWithScope(val, scope)) + : [localEvalNodeWithScope(node.value, scope)]; - // Parse the value directly without calling walk() - let value; + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: whenValues =`, whenValues); + } - // Check if the value is a function call - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER && - tokens[current + 1] && tokens[current + 1].type !== TokenType.ASSIGNMENT && - tokens[current + 1].type !== TokenType.SEMICOLON && - tokens[current + 1].type !== TokenType.ARROW && - (tokens[current + 1].type === TokenType.NUMBER || - tokens[current + 1].type === TokenType.IDENTIFIER || - tokens[current + 1].type === TokenType.FUNCTION_REF || - tokens[current + 1].type === TokenType.LEFT_PAREN)) { - // This is a function call as the value - const funcName = tokens[current].value; - current++; // Skip function name - const args = []; + for (const caseItem of node.cases) { + // Handle both single patterns and arrays of patterns + const patterns = caseItem.pattern.map(pat => localEvalNodeWithScope(pat, scope)); - // Collect arguments until we hit a semicolon, closing parenthesis, or end - while (current < tokens.length && - tokens[current].type !== TokenType.SEMICOLON && - tokens[current].type !== TokenType.RIGHT_PAREN) { - // Handle function references (@functionName) - if (tokens[current] && tokens[current].type === TokenType.FUNCTION_REF) { - current++; // Skip @ - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - args.push({ - type: 'FunctionReference', - name: tokens[current].value, - }); - current++; - } else { - throw new Error('Expected function name after @'); + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: patterns =`, patterns); + } + + // Check if patterns match the values + let matches = true; + if (whenValues.length !== patterns.length) { + matches = false; + } else { + for (let i = 0; i < whenValues.length; i++) { + const value = whenValues[i]; + const pattern = patterns[i]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: comparing value ${value} with pattern ${pattern}`); } - } else { - // Use walk() to parse complex arguments (including arithmetic expressions) - const arg = walk(); - if (arg) { - args.push(arg); + + if (pattern === true) { // Wildcard pattern + // Wildcard always matches + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: wildcard matches`); + } + continue; + } else if (value !== pattern) { + matches = false; + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern does not match`); + } + break; + } else { + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern matches`); + } } } } - value = { - type: 'FunctionCall', - name: funcName, - args, - }; - - debugLog('Function call parsed in assignment', { name: funcName, args }); - } else if (tokens[current] && tokens[current].type === TokenType.NUMBER) { - // Check if this is the start of an arithmetic expression - if (tokens[current + 1] && ( - tokens[current + 1].type === TokenType.PLUS || - tokens[current + 1].type === TokenType.MINUS || - tokens[current + 1].type === TokenType.MULTIPLY || - tokens[current + 1].type === TokenType.DIVIDE - )) { - // This is an arithmetic expression, use walk() to parse it - debugLog('Parsing arithmetic expression in assignment'); - debugLog('Current token before walk()', tokens[current]); - value = walk(); - debugLog('Arithmetic expression parsed', value); - } else { - // Simple number value - value = { - type: 'NumberLiteral', - value: tokens[current].value, - }; - current++; + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: case matches = ${matches}`); } - } else if (tokens[current] && tokens[current].type === TokenType.STRING) { - // Simple string value - value = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; - } else if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - // Check if this is the start of an infix arithmetic expression - if (tokens[current + 1] && ( - tokens[current + 1].type === TokenType.PLUS || - tokens[current + 1].type === TokenType.MINUS || - tokens[current + 1].type === TokenType.MULTIPLY || - tokens[current + 1].type === TokenType.DIVIDE - )) { - // This is an infix arithmetic expression - const left = { - type: 'Identifier', - value: tokens[current].value, - }; - current++; // Skip left operand - const operator = tokens[current].type; - current++; // Skip operator - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - }; - - value = { - type: expressionTypes[operator], - left, - right, - }; - } else { - // Simple identifier value - value = { - type: 'Identifier', - value: tokens[current].value, - }; - current++; - } - } else if (tokens[current] && ( - tokens[current].type === TokenType.PLUS || - tokens[current].type === TokenType.MINUS || - tokens[current].type === TokenType.MULTIPLY || - tokens[current].type === TokenType.DIVIDE - )) { - // Arithmetic expression value (prefix notation) - const operator = tokens[current].type; - current++; // Skip operator - const left = walk(); - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - }; - value = { - type: expressionTypes[operator], - left, - right, - }; - } else { - // Fallback to walk() for other cases - debugLog('Using walk() fallback for assignment value', { current: tokens[current] }); - value = walk(); + if (matches) { + const results = caseItem.result.map(res => localEvalNodeWithScope(res, scope)); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); + } } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); - return { - type: 'AssignmentExpression', - name, - value, - }; - } - } - - - - // Check if this is followed by an operator - if (tokens[current + 1] && ( - tokens[current + 1].type === TokenType.PLUS || - tokens[current + 1].type === TokenType.MINUS || - tokens[current + 1].type === TokenType.MULTIPLY || - tokens[current + 1].type === TokenType.DIVIDE - )) { - const left = { - type: 'Identifier', - value: token.value, - }; - const operator = tokens[current + 1].type; - current += 2; // Skip identifier and operator - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - }; - - return { - type: expressionTypes[operator], - left, - right, - }; - } - - // Check if this is followed by an operator - if (tokens[current + 1] && ( - tokens[current + 1].type === TokenType.PLUS || - tokens[current + 1].type === TokenType.MINUS || - tokens[current + 1].type === TokenType.MULTIPLY || - tokens[current + 1].type === TokenType.DIVIDE - )) { - const left = { - type: 'Identifier', - value: token.value, - }; - const operator = tokens[current + 1].type; - current += 2; // Skip identifier and operator - const right = walk(); - - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - }; - - return { - type: expressionTypes[operator], - left, - right, - }; - } - - current++; - return { - type: 'Identifier', - value: token.value, - }; - } - - - - // Pattern matching: value : pattern1 : result1 pattern2 : result2 ... - if (token.type === TokenType.CASE) { - current++; // Skip 'case' - - // Parse the value being matched (wrap in array for interpreter compatibility) - const valueNode = walk(); - const value = [valueNode]; - - // Expect 'of' - if (tokens[current].type !== TokenType.OF) { - throw new Error('Expected "of" after "case"'); - } - current++; // Skip 'of' - - const cases = []; - - // Parse cases until we hit a semicolon or end - while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { - const patternNode = walk(); - const resultNode = walk(); - cases.push({ - pattern: [patternNode], - result: [resultNode] - }); - } - - return { - type: 'CaseExpression', - value, - cases, - }; - } - - if (token.type === TokenType.IF) { - current++; - let node = { - type: 'IfExpression', - test: walk(), - consequent: walk(), - alternate: tokens[current].type === TokenType.ELSE ? (current++, walk()) : null, - }; - return node; - } - - if (token.type === TokenType.FUNCTION) { - current++; - let node = { - type: 'FunctionDeclaration', - name: tokens[current++].value, - params: [], - body: [], - }; - while (tokens[current].type !== TokenType.RIGHT_PAREN) { - node.params.push(tokens[current++].value); - } - current++; // Skip right paren - while (tokens[current].type !== TokenType.RIGHT_BRACE) { - node.body.push(walk()); - } - current++; // Skip right brace - return node; - } - - if (token.type === TokenType.IDENTIFIER && tokens[current + 1].type === TokenType.LEFT_PAREN) { - current++; - let node = { - type: 'FunctionCall', - name: token.value, - args: [], - }; - current++; // Skip left paren - while (tokens[current].type !== TokenType.RIGHT_PAREN) { - node.args.push(walk()); + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const localOutputValue = localEvalNodeWithScope(node.value, scope); + console.log(localOutputValue); + return localOutputValue; + case 'IOAssertExpression': + const localAssertionValue = localEvalNodeWithScope(node.value, scope); + if (!localAssertionValue) { + throw new Error('Assertion failed'); + } + return localAssertionValue; + case 'FunctionReference': + const localFunctionValue = globalScope[node.name]; + if (localFunctionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof localFunctionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return localFunctionValue; + case 'ArrowExpression': + // Arrow expressions are function bodies that should be evaluated + return localEvalNodeWithScope(node.body, scope); + default: + throw new Error(`Unknown node type: ${node.type}`); } - current++; // Skip right paren - return node; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.SEMICOLON) { - current++; - return null; - } - - throw new TypeError(token.type); - } - - let ast = { - type: 'Program', - body: [], }; - while (current < tokens.length) { - const node = walk(); - if (node !== null && node !== undefined) { - ast.body.push(node); - } - } - - return ast; -} - -// Interpreter -function interpreter(ast) { - debugLog('Starting interpreter with AST', ast); - let globalScope = {}; - - function evalNode(node) { - if (!node) { - return undefined; - } - switch (node.type) { - case 'NumberLiteral': - return parseInt(node.value); - case 'StringLiteral': - return node.value; - case 'PlusExpression': - return evalNode(node.left) + evalNode(node.right); - case 'MinusExpression': - return evalNode(node.left) - evalNode(node.right); - case 'MultiplyExpression': - return evalNode(node.left) * evalNode(node.right); - case 'DivideExpression': - const divisor = evalNode(node.right); - if (divisor === 0) { - throw new Error('Division by zero'); - } - return evalNode(node.left) / evalNode(node.right); - case 'ModuloExpression': - return evalNode(node.left) % evalNode(node.right); - case 'PowerExpression': - return Math.pow(evalNode(node.left), evalNode(node.right)); - case 'EqualsExpression': - return evalNode(node.left) === evalNode(node.right); - case 'LessThanExpression': - return evalNode(node.left) < evalNode(node.right); - case 'GreaterThanExpression': - return evalNode(node.left) > evalNode(node.right); - case 'LessEqualExpression': - return evalNode(node.left) <= evalNode(node.right); - case 'GreaterEqualExpression': - return evalNode(node.left) >= evalNode(node.right); - case 'NotEqualExpression': - return evalNode(node.left) !== evalNode(node.right); - case 'AndExpression': - return evalNode(node.left) && evalNode(node.right); - case 'OrExpression': - return evalNode(node.left) || evalNode(node.right); - case 'XorExpression': - const leftVal = evalNode(node.left); - const rightVal = evalNode(node.right); - return (leftVal && !rightVal) || (!leftVal && rightVal); - case 'NotExpression': - return !evalNode(node.operand); - case 'AssignmentExpression': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - const value = evalNode(node.value); - globalScope[node.name] = value; - return; - case 'Identifier': - const identifierValue = globalScope[node.value]; - if (identifierValue === undefined) { - throw new Error(`Variable ${node.value} is not defined`); - } - return identifierValue; - case 'FunctionReference': - const functionValue = globalScope[node.name]; - if (functionValue === undefined) { - throw new Error(`Function ${node.name} is not defined`); - } - if (typeof functionValue !== 'function') { - throw new Error(`${node.name} is not a function`); - } - return functionValue; - case 'IfExpression': - return evalNode(node.test) ? evalNode(node.consequent) : node.alternate ? evalNode(node.alternate) : undefined; - case 'FunctionDeclaration': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - globalScope[node.name] = function(...args) { - let localScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - localScope[node.params[i]] = args[i]; + /** + * Evaluates AST nodes in the global scope (internal recursion helper). + * + * @param {Object} node - AST node to evaluate + * @returns {*} The result of evaluating the node + * @throws {Error} For evaluation errors + * + * @description Internal helper function for recursive evaluation that + * always uses the global scope. This function is used to avoid circular + * dependencies and infinite recursion when evaluating nested expressions + * that need access to the global scope. + * + * This function duplicates the logic of evalNode but without the scope + * parameter, ensuring that all variable lookups go through the global scope. + * It's primarily used for evaluating function bodies and other expressions + * that need to be isolated from local scope contexts. + * + * The function also implements the forward declaration pattern for recursive + * functions, maintaining consistency with the other evaluation functions. + */ + const localEvalNode = (node) => { + callStackTracker.push('localEvalNode', node?.type || 'unknown'); + + try { + if (!node) { + return undefined; + } + switch (node.type) { + case 'NumberLiteral': + return parseFloat(node.value); + case 'StringLiteral': + return node.value; + case 'BooleanLiteral': + return node.value; + case 'PlusExpression': + return localEvalNode(node.left) + localEvalNode(node.right); + case 'MinusExpression': + return localEvalNode(node.left) - localEvalNode(node.right); + case 'MultiplyExpression': + return localEvalNode(node.left) * localEvalNode(node.right); + case 'DivideExpression': + const divisor = localEvalNode(node.right); + if (divisor === 0) { + throw new Error('Division by zero'); } - // Create a new evalNode function that uses localScope - const localEvalNode = (node) => { - if (!node) { - return undefined; + return localEvalNode(node.left) / localEvalNode(node.right); + case 'ModuloExpression': + return localEvalNode(node.left) % localEvalNode(node.right); + case 'PowerExpression': + return Math.pow(localEvalNode(node.left), localEvalNode(node.right)); + case 'EqualsExpression': + return localEvalNode(node.left) === localEvalNode(node.right); + case 'LessThanExpression': + return localEvalNode(node.left) < localEvalNode(node.right); + case 'GreaterThanExpression': + return localEvalNode(node.left) > localEvalNode(node.right); + case 'LessEqualExpression': + return localEvalNode(node.left) <= localEvalNode(node.right); + case 'GreaterEqualExpression': + return localEvalNode(node.left) >= localEvalNode(node.right); + case 'NotEqualExpression': + return localEvalNode(node.left) !== localEvalNode(node.right); + case 'AndExpression': + return !!(localEvalNode(node.left) && localEvalNode(node.right)); + case 'OrExpression': + return !!(localEvalNode(node.left) || localEvalNode(node.right)); + case 'XorExpression': + const leftVal = localEvalNode(node.left); + const rightVal = localEvalNode(node.right); + return !!((leftVal && !rightVal) || (!leftVal && rightVal)); + case 'NotExpression': + return !localEvalNode(node.operand); + case 'UnaryMinusExpression': + return -localEvalNode(node.operand); + case 'TableLiteral': + const table = {}; + let arrayIndex = 1; + + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = localEvalNode(entry.value); + arrayIndex++; + } else { + // Key-value entry: {name: "Alice", age: 30} + let key; + if (entry.key.type === 'Identifier') { + // Convert identifier keys to strings + key = entry.key.value; + } else { + // For other key types (numbers, strings), evaluate normally + key = localEvalNode(entry.key); + } + const value = localEvalNode(entry.value); + table[key] = value; } - switch (node.type) { - case 'NumberLiteral': - return parseInt(node.value); - case 'StringLiteral': - return node.value; - case 'PlusExpression': - return localEvalNode(node.left) + localEvalNode(node.right); - case 'MinusExpression': - return localEvalNode(node.left) - localEvalNode(node.right); - case 'MultiplyExpression': - return localEvalNode(node.left) * localEvalNode(node.right); - case 'DivideExpression': - const divisor = localEvalNode(node.right); - if (divisor === 0) { - throw new Error('Division by zero'); - } - return localEvalNode(node.left) / localEvalNode(node.right); - case 'ModuloExpression': - return localEvalNode(node.left) % localEvalNode(node.right); - case 'PowerExpression': - return Math.pow(localEvalNode(node.left), localEvalNode(node.right)); - case 'EqualsExpression': - return localEvalNode(node.left) === localEvalNode(node.right); - case 'LessThanExpression': - return localEvalNode(node.left) < localEvalNode(node.right); - case 'GreaterThanExpression': - return localEvalNode(node.left) > localEvalNode(node.right); - case 'LessEqualExpression': - return localEvalNode(node.left) <= localEvalNode(node.right); - case 'GreaterEqualExpression': - return localEvalNode(node.left) >= localEvalNode(node.right); - case 'NotEqualExpression': - return localEvalNode(node.left) !== localEvalNode(node.right); - case 'AndExpression': - return localEvalNode(node.left) && localEvalNode(node.right); - case 'OrExpression': - return localEvalNode(node.left) || localEvalNode(node.right); - case 'XorExpression': - const leftVal = localEvalNode(node.left); - const rightVal = localEvalNode(node.right); - return (leftVal && !rightVal) || (!leftVal && rightVal); - case 'NotExpression': - return !localEvalNode(node.operand); - case 'AssignmentExpression': - if (localScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - localScope[node.name] = localEvalNode(node.value); - return; - case 'Identifier': - const identifierValue = localScope[node.value]; - if (identifierValue === undefined && node.value) { - return node.value; // Return as string literal if not found in scope - } - return identifierValue; - case 'FunctionReference': - const localFunctionValue = localScope[node.name]; - if (localFunctionValue === undefined) { - throw new Error(`Function ${node.name} is not defined`); - } - if (typeof localFunctionValue !== 'function') { - throw new Error(`${node.name} is not a function`); - } - return localFunctionValue; - case 'IfExpression': - return localEvalNode(node.test) ? localEvalNode(node.consequent) : node.alternate ? localEvalNode(node.alternate) : undefined; - case 'FunctionDeclaration': - if (localScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - localScope[node.name] = function(...args) { - let nestedScope = Object.create(localScope); - for (let i = 0; i < node.params.length; i++) { - nestedScope[node.params[i]] = args[i]; - } - return localEvalNode(node.body); - }; - return; - case 'FunctionCall': - if (localScope[node.name] instanceof Function) { - let args = node.args.map(localEvalNode); - return localScope[node.name](...args); - } - throw new Error(`Function ${node.name} is not defined`); - case 'CaseExpression': - // Evaluate the values being matched (e.g., [x, y]) - const values = node.value.map(localEvalNode); - - for (const caseItem of node.cases) { - // Evaluate the pattern (e.g., [0, 0] or [0, _]) - const pattern = caseItem.pattern.map(localEvalNode); + } + + return table; + case 'TableAccess': + const tableValue = localEvalNode(node.table); + let keyValue; + + // Handle different key types + if (node.key.type === 'Identifier') { + // For dot notation, use the identifier name as the key + keyValue = node.key.value; + } else { + // For bracket notation, evaluate the key expression + keyValue = localEvalNode(node.key); + } + + if (typeof tableValue !== 'object' || tableValue === null) { + throw new Error('Cannot access property of non-table value'); + } + + if (tableValue[keyValue] === undefined) { + throw new Error(`Key '${keyValue}' not found in table`); + } + + return tableValue[keyValue]; + case 'AssignmentExpression': + // Prevent reassignment of standard library functions + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } - // Check if pattern matches values - let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.name} is not yet fully defined`); + }; - // Wildcard always matches - if (patternValue === true) continue; + // Store the placeholder in global scope + globalScope[node.name] = placeholder; - // Otherwise, values must be equal - if (value !== patternValue) { - matches = false; - break; - } + // Now evaluate the function definition with access to the placeholder + const actualFunction = localEvalNode(node.value); + + // Replace the placeholder with the actual function + globalScope[node.name] = actualFunction; + return; } - if (matches) { - // Evaluate all results - const results = caseItem.result.map(localEvalNode); - // If there's only one result, return it directly (preserves number type) - if (results.length === 1) { - return results[0]; - } - // Otherwise join multiple results as string - return results.join(' '); + globalScope[node.name] = localEvalNode(node.value); + return; + case 'Identifier': + const identifierValue = globalScope[node.value]; + if (identifierValue === undefined && node.value) { + return node.value; } - } - throw new Error('No matching pattern found'); - case 'WildcardPattern': - return true; // Wildcard always matches - case 'IOInExpression': - // Read from stdin - const readline = require('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - return new Promise((resolve) => { - rl.question('', (input) => { - rl.close(); - const num = parseInt(input); - resolve(isNaN(num) ? input : num); - }); - }); - case 'IOOutExpression': - // Output to stdout - const localOutputValue = localEvalNode(node.value); - console.log(localOutputValue); - return localOutputValue; - case 'IOAssertExpression': - // Assert that left operator right is true - const localLeftValue = localEvalNode(node.left); - const localRightValue = localEvalNode(node.right); - - let localAssertionPassed = false; - switch (node.operator) { - case TokenType.EQUALS: - localAssertionPassed = localLeftValue === localRightValue; - break; - case TokenType.LESS_THAN: - localAssertionPassed = localLeftValue < localRightValue; - break; - case TokenType.GREATER_THAN: - localAssertionPassed = localLeftValue > localRightValue; - break; - case TokenType.LESS_EQUAL: - localAssertionPassed = localLeftValue <= localRightValue; - break; - case TokenType.GREATER_EQUAL: - localAssertionPassed = localLeftValue >= localRightValue; - break; - case TokenType.NOT_EQUAL: - localAssertionPassed = localLeftValue !== localRightValue; - break; - default: - throw new Error(`Unsupported comparison operator in assertion: ${node.operator}`); + return identifierValue; + case 'FunctionDeclaration': + // For anonymous functions, the name comes from the assignment + // The function itself doesn't have a name, so we just return + // The assignment will handle storing it in the global scope + return function(...args) { + callStackTracker.push('FunctionCall', node.params.join(',')); + try { + let nestedScope = Object.create(globalScope); + for (let i = 0; i < node.params.length; i++) { + nestedScope[node.params[i]] = args[i]; } - - if (!localAssertionPassed) { - const operatorSymbol = { - [TokenType.EQUALS]: '=', - [TokenType.LESS_THAN]: '<', - [TokenType.GREATER_THAN]: '>', - [TokenType.LESS_EQUAL]: '<=', - [TokenType.GREATER_EQUAL]: '>=', - [TokenType.NOT_EQUAL]: '!=' - }[node.operator]; - throw new Error(`Assertion failed: ${JSON.stringify(localLeftValue)} ${operatorSymbol} ${JSON.stringify(localRightValue)}`); + return localEvalNodeWithScope(node.body, nestedScope); + } finally { + callStackTracker.pop(); + } + }; + case 'FunctionDefinition': + // Create a function from the function definition + return function(...args) { + callStackTracker.push('FunctionCall', node.parameters.join(',')); + try { + let nestedScope = Object.create(globalScope); + for (let i = 0; i < node.parameters.length; i++) { + nestedScope[node.parameters[i]] = args[i]; } - return localLeftValue; - default: - throw new Error(`Unknown node type: ${node.type}`); + return localEvalNodeWithScope(node.body, nestedScope); + } finally { + callStackTracker.pop(); + } + }; + case 'FunctionCall': + let localFunc; + if (typeof node.name === 'string') { + // Regular function call with string name + localFunc = globalScope[node.name]; + } else if (node.name.type === 'Identifier') { + // Function call with identifier + localFunc = globalScope[node.name.value]; + } else { + // Function call from expression (e.g., parenthesized function, higher-order) + localFunc = localEvalNode(node.name); } - }; - return localEvalNode(node.body); - }; - return; - case 'FunctionCall': - if (globalScope[node.name] instanceof Function) { - let args = node.args.map(evalNode); - return globalScope[node.name](...args); - } - throw new Error(`Function ${node.name} is not defined`); - case 'CaseExpression': - debugLog('CaseExpression evaluation', { value: node.value, cases: node.cases }); - // Evaluate the values being matched (e.g., [x, y]) - const values = node.value.map(evalNode); - - for (const caseItem of node.cases) { - // Evaluate the pattern (e.g., [0, 0] or [0, _]) - const pattern = caseItem.pattern.map(evalNode); - // Check if pattern matches values - let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; - - // Wildcard always matches - if (patternValue === true) continue; + if (localFunc instanceof Function) { + let args = node.args.map(localEvalNode); + return localFunc(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'WhenExpression': + // Handle both single values and arrays of values + const whenValues = Array.isArray(node.value) + ? node.value.map(localEvalNode) + : [localEvalNode(node.value)]; + + for (const caseItem of node.cases) { + // Handle both single patterns and arrays of patterns + const patterns = caseItem.pattern.map(localEvalNode); - // Otherwise, values must be equal - if (value !== patternValue) { + // Check if patterns match the values + let matches = true; + if (whenValues.length !== patterns.length) { matches = false; - break; + } else { + for (let i = 0; i < whenValues.length; i++) { + const value = whenValues[i]; + const pattern = patterns[i]; + + if (pattern === true) { // Wildcard pattern + // Wildcard always matches + continue; + } else if (value !== pattern) { + matches = false; + break; + } + } + } + + if (matches) { + const results = caseItem.result.map(localEvalNode); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); } } + throw new Error('No matching pattern found'); + case 'WildcardPattern': + return true; + case 'IOInExpression': + const readline = require('readline'); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); - if (matches) { - // Evaluate all results - const results = caseItem.result.map(evalNode); - // If there's only one result, return it directly (preserves number type) - if (results.length === 1) { - return results[0]; - } - // Otherwise join multiple results as string - return results.join(' '); + return new Promise((resolve) => { + rl.question('', (input) => { + rl.close(); + const num = parseInt(input); + resolve(isNaN(num) ? input : num); + }); + }); + case 'IOOutExpression': + const localOutputValue = localEvalNode(node.value); + console.log(localOutputValue); + return localOutputValue; + case 'IOAssertExpression': + const localAssertionValue = localEvalNode(node.value); + if (!localAssertionValue) { + throw new Error('Assertion failed'); } - } - throw new Error('No matching pattern found'); - case 'WildcardPattern': - return true; // Wildcard always matches - case 'IOInExpression': - // Read from stdin - const readline = require('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - return new Promise((resolve) => { - rl.question('', (input) => { - rl.close(); - const num = parseInt(input); - resolve(isNaN(num) ? input : num); - }); - }); - case 'IOOutExpression': - // Output to stdout - const outputValue = evalNode(node.value); - console.log(outputValue); - return outputValue; - case 'IOAssertExpression': - // Assert that left operator right is true - const leftValue = evalNode(node.left); - const rightValue = evalNode(node.right); - - let assertionPassed = false; - switch (node.operator) { - case TokenType.EQUALS: - assertionPassed = leftValue === rightValue; - break; - case TokenType.LESS_THAN: - assertionPassed = leftValue < rightValue; - break; - case TokenType.GREATER_THAN: - assertionPassed = leftValue > rightValue; - break; - case TokenType.LESS_EQUAL: - assertionPassed = leftValue <= rightValue; - break; - case TokenType.GREATER_EQUAL: - assertionPassed = leftValue >= rightValue; - break; - case TokenType.NOT_EQUAL: - assertionPassed = leftValue !== rightValue; - break; - default: - throw new Error(`Unsupported comparison operator in assertion: ${node.operator}`); - } - - if (!assertionPassed) { - const operatorSymbol = { - [TokenType.EQUALS]: '=', - [TokenType.LESS_THAN]: '<', - [TokenType.GREATER_THAN]: '>', - [TokenType.LESS_EQUAL]: '<=', - [TokenType.GREATER_EQUAL]: '>=', - [TokenType.NOT_EQUAL]: '!=' - }[node.operator]; - throw new Error(`Assertion failed: ${JSON.stringify(leftValue)} ${operatorSymbol} ${JSON.stringify(rightValue)}`); - } - return leftValue; - default: - throw new Error(`Unknown node type: ${node.type}`); + return localAssertionValue; + case 'FunctionReference': + const localFunctionValue = globalScope[node.name]; + if (localFunctionValue === undefined) { + throw new Error(`Function ${node.name} is not defined`); + } + if (typeof localFunctionValue !== 'function') { + throw new Error(`${node.name} is not a function`); + } + return localFunctionValue; + case 'ArrowExpression': + // Arrow expressions are function bodies that should be evaluated + return localEvalNode(node.body); + default: + throw new Error(`Unknown node type: ${node.type}`); + } + } finally { + callStackTracker.pop(); } - } + }; let lastResult; for (let node of ast.body) { - if (node) { // Skip undefined nodes + if (node) { lastResult = evalNode(node); } } - // Handle async results (from interactive IO operations) if (lastResult instanceof Promise) { return lastResult.then(result => { return result; @@ -1912,393 +1447,295 @@ function interpreter(ast) { return lastResult; } -// Debug configuration -const DEBUG = process.env.DEBUG === 'true' || process.argv.includes('--debug'); - +/** + * Debug logging utility function. + * + * @param {string} message - Debug message to log + * @param {*} [data=null] - Optional data to log with the message + * + * @description Logs debug messages to console when DEBUG environment variable is set. + * Provides verbose output during development while remaining silent in production. + * + * Debug functions are gated by the DEBUG environment variable, allowing for + * verbose output during development and silent operation in production. This + * approach makes it easy to trace execution and diagnose issues without + * cluttering normal output. + */ function debugLog(message, data = null) { - if (DEBUG) { + if (process.env.DEBUG) { + console.log(`[DEBUG] ${message}`); if (data) { - console.log(`[DEBUG] ${message}:`, JSON.stringify(data, null, 2)); - } else { - console.log(`[DEBUG] ${message}`); + console.log(data); } } } +/** + * Debug error logging utility function. + * + * @param {string} message - Debug error message to log + * @param {Error} [error=null] - Optional error object to log + * + * @description Logs debug error messages to console when DEBUG environment variable is set. + * Provides verbose error output during development while remaining silent in production. + * + * Debug functions are gated by the DEBUG environment variable, allowing for + * verbose output during development and silent operation in production. This + * approach makes it easy to trace execution and diagnose issues without + * cluttering normal output. + */ function debugError(message, error = null) { - if (DEBUG) { + if (process.env.DEBUG) { console.error(`[DEBUG ERROR] ${message}`); if (error) { - console.error(`[DEBUG ERROR] Details:`, error); + console.error(error); } } } -// Main execution logic -const fs = require('fs'); -const readline = require('readline'); - /** - * Execute a script file + * Call stack tracking for debugging recursion issues. + * + * @description Tracks function calls to help identify infinite recursion + * and deep call stacks that cause stack overflow errors. This is essential + * for debugging the interpreter's recursive evaluation of AST nodes. + * + * The tracker maintains a stack of function calls with timestamps and context + * information, counts function calls to identify hot paths, and detects + * potential infinite recursion by monitoring stack depth. + * + * This tool is particularly important for the combinator-based architecture + * where function calls are the primary execution mechanism, and complex + * nested expressions can lead to deep call stacks. */ -function executeFile(filePath) { - try { - debugLog(`Executing file: ${filePath}`); - - const input = fs.readFileSync(filePath, 'utf-8'); - debugLog('Input content', input); +const callStackTracker = { + stack: [], + maxDepth: 0, + callCounts: new Map(), + + /** + * Push a function call onto the stack + * @param {string} functionName - Name of the function being called + * @param {string} context - Context where the call is happening + */ + push: function(functionName, context = '') { + const callInfo = { functionName, context, timestamp: Date.now() }; + this.stack.push(callInfo); - const tokens = lexer(input); - debugLog('Tokens generated', tokens); + // Track maximum depth + if (this.stack.length > this.maxDepth) { + this.maxDepth = this.stack.length; + } - const ast = parser(tokens); - debugLog('AST generated', ast); + // Count function calls + const key = `${functionName}${context ? `:${context}` : ''}`; + this.callCounts.set(key, (this.callCounts.get(key) || 0) + 1); - const result = interpreter(ast); - debugLog('Interpreter result', result); + // Check for potential infinite recursion + if (this.stack.length > 1000) { + console.error('=== POTENTIAL INFINITE RECURSION DETECTED ==='); + console.error('Call stack depth:', this.stack.length); + console.error('Function call counts:', Object.fromEntries(this.callCounts)); + console.error('Recent call stack:'); + this.stack.slice(-10).forEach((call, i) => { + console.error(` ${this.stack.length - 10 + i}: ${call.functionName}${call.context ? ` (${call.context})` : ''}`); + }); + throw new Error(`Potential infinite recursion detected. Call stack depth: ${this.stack.length}`); + } - // Only print result if it's not undefined - if (result !== undefined) { - console.log(result); + if (process.env.DEBUG && this.stack.length % 100 === 0) { + console.log(`[DEBUG] Call stack depth: ${this.stack.length}, Max: ${this.maxDepth}`); } + }, + + /** + * Pop a function call from the stack + */ + pop: function() { + return this.stack.pop(); + }, + + /** + * Get current stack depth + */ + getDepth: function() { + return this.stack.length; + }, + + /** + * Get call statistics + */ + getStats: function() { + return { + currentDepth: this.stack.length, + maxDepth: this.maxDepth, + callCounts: Object.fromEntries(this.callCounts) + }; + }, + + /** + * Reset the tracker + */ + reset: function() { + this.stack = []; + this.maxDepth = 0; + this.callCounts.clear(); + } +}; + +/** + * Cross-platform file I/O utility + * + * @param {string} filePath - Path to the file to read + * @returns {Promise<string>} File contents as a string + * @throws {Error} For file reading errors + * + * @description Handles file reading across different platforms (Node.js, Bun, browser) + * with appropriate fallbacks for each environment. This function is essential for + * the language's file execution model where scripts are loaded from .txt files. + * + * The function prioritizes ES modules compatibility by using dynamic import, + * but falls back to require for older Node.js versions. Browser environments + * are not supported for file I/O operations. + * + * This cross-platform approach ensures the language can run in various JavaScript + * environments while maintaining consistent behavior. + */ +async function readFile(filePath) { + // Check if we're in a browser environment + if (typeof window !== 'undefined') { + // Browser environment - would need to implement file input or fetch + throw new Error('File I/O not supported in browser environment'); + } + + // Node.js or Bun environment + try { + // Try dynamic import for ES modules compatibility + const fs = await import('fs'); + return fs.readFileSync(filePath, 'utf8'); } catch (error) { - debugError('Execution failed', error); - console.error(`Error executing file: ${error.message}`); - process.exit(1); + // Fallback to require for older Node.js versions + const fs = require('fs'); + return fs.readFileSync(filePath, 'utf8'); } } /** - * Start REPL mode + * Reads a file, tokenizes, parses, and interprets it. + * + * @param {string} filePath - Path to the file to execute + * @returns {Promise<*>} The result of executing the file + * @throws {Error} For file reading, parsing, or execution errors + * + * @description Main entry point for file execution. Handles the complete language + * pipeline: file reading, lexical analysis, parsing, and interpretation. + * + * This function orchestrates the entire language execution process: + * 1. Reads the source file using cross-platform I/O utilities + * 2. Tokenizes the source code using the lexer + * 3. Parses tokens into an AST using the combinator-based parser + * 4. Interprets the AST using the combinator-based interpreter + * + * The function provides comprehensive error handling and debug output at each + * stage for transparency and troubleshooting. It also manages the call stack + * tracker to provide execution statistics and detect potential issues. + * + * Supports both synchronous and asynchronous execution, with proper + * error handling and process exit codes. */ -function startREPL() { - console.log('Scripting Language REPL'); - console.log('Type expressions to evaluate. End with semicolon (;) to execute.'); - console.log('Type "exit" to quit.'); - console.log(''); - - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - prompt: '> ' - }); - - // Create a persistent global scope for the REPL - let globalScope = {}; - let currentInput = ''; - - rl.prompt(); - - rl.on('line', (line) => { - const trimmedLine = line.trim(); - - if (trimmedLine === 'exit' || trimmedLine === 'quit') { - rl.close(); - return; +async function executeFile(filePath) { + try { + // Validate file extension + if (!filePath.endsWith('.txt')) { + throw new Error('Only .txt files are supported'); } - // Add the line to current input - currentInput += line + '\n'; + const input = await readFile(filePath); + + debugLog('Input:', input); + + const tokens = lexer(input); + debugLog('Tokens:', tokens); + + const ast = parser(tokens); + debugLog('AST:', JSON.stringify(ast, null, 2)); + + const result = interpreter(ast); - // Check if the input ends with a semicolon - if (currentInput.trim().endsWith(';')) { - try { - const tokens = lexer(currentInput); - const ast = parser(tokens); - - // Create a custom interpreter that uses the persistent scope - const result = interpreterWithScope(ast, globalScope); - - // Only print result if it's not undefined - if (result !== undefined) { - console.log(result); + if (result instanceof Promise) { + result.then(finalResult => { + if (finalResult !== undefined) { + console.log(finalResult); } - } catch (error) { - console.error(`Error: ${error.message}`); + // Print call stack statistics after execution + const stats = callStackTracker.getStats(); + console.log('\n=== CALL STACK STATISTICS ==='); + console.log('Maximum call stack depth:', stats.maxDepth); + console.log('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); + }).catch(error => { + console.error(`Error executing file: ${error.message}`); + // Print call stack statistics on error + const stats = callStackTracker.getStats(); + console.error('\n=== CALL STACK STATISTICS ON ERROR ==='); + console.error('Maximum call stack depth:', stats.maxDepth); + console.error('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); + process.exit(1); + }); + } else { + if (result !== undefined) { + console.log(result); } - - // Reset current input for next expression - currentInput = ''; + // Print call stack statistics after execution + const stats = callStackTracker.getStats(); + console.log('\n=== CALL STACK STATISTICS ==='); + console.log('Maximum call stack depth:', stats.maxDepth); + console.log('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); } - - rl.prompt(); - }); - - rl.on('close', () => { - console.log('Goodbye!'); - process.exit(0); - }); + } catch (error) { + console.error(`Error executing file: ${error.message}`); + // Print call stack statistics on error + const stats = callStackTracker.getStats(); + console.error('\n=== CALL STACK STATISTICS ON ERROR ==='); + console.error('Maximum call stack depth:', stats.maxDepth); + console.error('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); + process.exit(1); + } } /** - * Interpreter that uses a provided global scope + * CLI argument handling and program entry point. + * + * @description Processes command line arguments and executes the specified file. + * Provides helpful error messages for incorrect usage. + * + * The language is designed for file execution only (no REPL), so the CLI + * enforces this usage and provides helpful error messages for incorrect invocation. + * The function validates that exactly one file path is provided and that the + * file has the correct .txt extension. + * + * Exits with appropriate error codes for different failure scenarios. */ -function interpreterWithScope(ast, globalScope) { - function evalNode(node) { - if (!node) { - return undefined; - } - switch (node.type) { - case 'NumberLiteral': - return parseInt(node.value); - case 'StringLiteral': - return node.value; - case 'PlusExpression': - return evalNode(node.left) + evalNode(node.right); - case 'MinusExpression': - return evalNode(node.left) - evalNode(node.right); - case 'MultiplyExpression': - return evalNode(node.left) * evalNode(node.right); - case 'DivideExpression': - const divisor = evalNode(node.right); - if (divisor === 0) { - throw new Error('Division by zero'); - } - return evalNode(node.left) / evalNode(node.right); - case 'AssignmentExpression': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - const value = evalNode(node.value); - globalScope[node.name] = value; - return; - case 'Identifier': - const identifierValue = globalScope[node.value]; - if (identifierValue === undefined) { - throw new Error(`Variable ${node.value} is not defined`); - } - return identifierValue; - case 'FunctionDeclaration': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - globalScope[node.name] = function(...args) { - let localScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - localScope[node.params[i]] = args[i]; - } - return localEvalNode(node.body); - }; - return; - case 'FunctionCall': - if (globalScope[node.name] instanceof Function) { - let args = node.args.map(evalNode); - return globalScope[node.name](...args); - } - throw new Error(`Function ${node.name} is not defined`); - case 'CaseExpression': - const values = node.value.map(evalNode); - - for (const caseItem of node.cases) { - const pattern = caseItem.pattern.map(evalNode); - - let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; - - if (patternValue === true) continue; - - if (value !== patternValue) { - matches = false; - break; - } - } - - if (matches) { - const results = caseItem.result.map(evalNode); - if (results.length === 1) { - return results[0]; - } - return results.join(' '); - } - } - throw new Error('No matching pattern found'); - case 'WildcardPattern': - return true; - case 'IOInExpression': - const readline = require('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - return new Promise((resolve) => { - rl.question('', (input) => { - rl.close(); - const num = parseInt(input); - resolve(isNaN(num) ? input : num); - }); - }); - case 'IOOutExpression': - const outputValue = evalNode(node.value); - console.log(outputValue); - return outputValue; - default: - throw new Error(`Unknown node type: ${node.type}`); - } - } +async function main() { + const args = process.argv.slice(2); - const localEvalNode = (node) => { - if (!node) { - return undefined; - } - switch (node.type) { - case 'NumberLiteral': - return parseInt(node.value); - case 'StringLiteral': - return node.value; - case 'PlusExpression': - return localEvalNode(node.left) + localEvalNode(node.right); - case 'MinusExpression': - return localEvalNode(node.left) - localEvalNode(node.right); - case 'MultiplyExpression': - return localEvalNode(node.left) * localEvalNode(node.right); - case 'DivideExpression': - const divisor = localEvalNode(node.right); - if (divisor === 0) { - throw new Error('Division by zero'); - } - return localEvalNode(node.left) / localEvalNode(node.right); - case 'ModuloExpression': - return localEvalNode(node.left) % localEvalNode(node.right); - case 'PowerExpression': - return Math.pow(localEvalNode(node.left), localEvalNode(node.right)); - case 'EqualsExpression': - return localEvalNode(node.left) === localEvalNode(node.right); - case 'LessThanExpression': - return localEvalNode(node.left) < localEvalNode(node.right); - case 'GreaterThanExpression': - return localEvalNode(node.left) > localEvalNode(node.right); - case 'LessEqualExpression': - return localEvalNode(node.left) <= localEvalNode(node.right); - case 'GreaterEqualExpression': - return localEvalNode(node.left) >= localEvalNode(node.right); - case 'NotEqualExpression': - return localEvalNode(node.left) !== localEvalNode(node.right); - case 'AndExpression': - return localEvalNode(node.left) && localEvalNode(node.right); - case 'OrExpression': - return localEvalNode(node.left) || localEvalNode(node.right); - case 'XorExpression': - const leftVal = localEvalNode(node.left); - const rightVal = localEvalNode(node.right); - return (leftVal && !rightVal) || (!leftVal && rightVal); - case 'NotExpression': - return !localEvalNode(node.operand); - case 'AssignmentExpression': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - globalScope[node.name] = localEvalNode(node.value); - return; - case 'Identifier': - const identifierValue = globalScope[node.value]; - if (identifierValue === undefined && node.value) { - return node.value; - } - return identifierValue; - case 'FunctionDeclaration': - if (globalScope.hasOwnProperty(node.name)) { - throw new Error(`Cannot reassign immutable variable: ${node.name}`); - } - globalScope[node.name] = function(...args) { - let nestedScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - nestedScope[node.params[i]] = args[i]; - } - return localEvalNode(node.body); - }; - return; - case 'FunctionCall': - if (globalScope[node.name] instanceof Function) { - let args = node.args.map(localEvalNode); - return globalScope[node.name](...args); - } - throw new Error(`Function ${node.name} is not defined`); - case 'CaseExpression': - const values = node.value.map(localEvalNode); - - for (const caseItem of node.cases) { - const pattern = caseItem.pattern.map(localEvalNode); - - let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; - - if (patternValue === true) continue; - - if (value !== patternValue) { - matches = false; - break; - } - } - - if (matches) { - const results = caseItem.result.map(localEvalNode); - if (results.length === 1) { - return results[0]; - } - return results.join(' '); - } - } - throw new Error('No matching pattern found'); - case 'WildcardPattern': - return true; - case 'IOInExpression': - const readline = require('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - - return new Promise((resolve) => { - rl.question('', (input) => { - rl.close(); - const num = parseInt(input); - resolve(isNaN(num) ? input : num); - }); - }); - case 'IOOutExpression': - const localOutputValue = localEvalNode(node.value); - console.log(localOutputValue); - return localOutputValue; - default: - throw new Error(`Unknown node type: ${node.type}`); - } - }; - - let lastResult; - for (let node of ast.body) { - if (node) { - lastResult = evalNode(node); - } - } - - if (lastResult instanceof Promise) { - return lastResult.then(result => { - return result; - }); + if (args.length === 0) { + console.error('Usage: node lang.js <file>'); + console.error(' Provide a file path to execute'); + process.exit(1); + } else if (args.length === 1) { + // Execute the file + const filePath = args[0]; + await executeFile(filePath); + } else { + // Too many arguments + console.error('Usage: node lang.js <file>'); + console.error(' Provide exactly one file path to execute'); + process.exit(1); } - - return lastResult; } -// Check command line arguments -const args = process.argv.slice(2); - -if (args.length === 0) { - // No arguments - start REPL mode - startREPL(); -} else if (args.length === 1) { - // One argument - execute the file - const filePath = args[0]; - executeFile(filePath); -} else { - // Too many arguments - console.error('Usage: node lang.js [file]'); - console.error(' If no file is provided, starts REPL mode'); - console.error(' If file is provided, executes the file'); +// Start the program +main().catch(error => { + console.error('Fatal error:', error.message); process.exit(1); -} \ No newline at end of file +}); \ No newline at end of file |