diff options
Diffstat (limited to 'js/scripting-lang/lang.js')
-rw-r--r-- | js/scripting-lang/lang.js | 5089 |
1 files changed, 2606 insertions, 2483 deletions
diff --git a/js/scripting-lang/lang.js b/js/scripting-lang/lang.js index 0184c8f..1a0d77e 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -1,10 +1,29 @@ -// 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. - -// Initialize standard library functions +/** + * Initializes the standard library in the provided scope. + * + * @param {Object} scope - The global scope object to inject functions into + * + * @description Injects higher-order functions into the interpreter's global scope. + * These functions provide functional programming utilities like map, compose, pipe, etc. + * + * @why Injecting the standard library directly into the scope ensures that user code + * can access these functions as if they were built-in, without special syntax or + * reserved keywords. This approach also allows for easy extension and testing, as + * the library is just a set of regular functions in the scope chain. + * + * @how Each function is added as a property of the scope object. 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 each element + /** + * 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) { - // Handle function references by calling them if they're functions if (typeof f === 'function') { return f(x); } else { @@ -12,17 +31,36 @@ function initializeStandardLibrary(scope) { } }; - // Compose: Compose two functions (f ∘ g)(x) = f(g(x)) + /** + * 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') { - return f(g(x)); + if (arguments.length === 3) { + return f(g(x)); + } else { + return function(x) { + return f(g(x)); + }; + } } else { throw new Error('compose: first two arguments must be functions'); } }; - // Curry: Convert a function that takes multiple arguments into a series of functions - // Since our language already uses curried functions by default, this is mostly for explicit currying + /** + * 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); @@ -31,7 +69,13 @@ function initializeStandardLibrary(scope) { } }; - // Apply: Apply a function to an argument (same as function call, but more explicit) + /** + * 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); @@ -40,18 +84,35 @@ function initializeStandardLibrary(scope) { } }; - // Pipe: Compose functions in left-to-right order (opposite of compose) - // pipe f g x = g f x + /** + * 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') { - return g(f(x)); + if (arguments.length === 3) { + return g(f(x)); + } else { + return function(x) { + return g(f(x)); + }; + } } else { throw new Error('pipe: first two arguments must be functions'); } }; - // Filter: Filter based on a predicate - // For now, we'll implement it as a higher-order function + /** + * 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; @@ -60,8 +121,14 @@ function initializeStandardLibrary(scope) { } }; - // Reduce: Reduce to a single value using a binary function - // For now, we'll implement it as a higher-order function + /** + * 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); @@ -70,7 +137,14 @@ function initializeStandardLibrary(scope) { } }; - // Fold: Same as reduce, but more explicit about the folding direction + /** + * 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); @@ -80,7 +154,17 @@ function initializeStandardLibrary(scope) { }; } -// Define the types of tokens +/** + * TokenType enumeration for all supported token types. + * + * @type {Object.<string, string>} + * + * @description A flat object mapping token names to their string representations. + * This approach allows for fast string comparisons and easy extensibility. + * + * @why Using a flat object avoids the need for import/export or enum boilerplate, + * and makes it easy to add new token types as the language evolves. + */ const TokenType = { NUMBER: 'NUMBER', PLUS: 'PLUS', @@ -98,2289 +182,2548 @@ const TokenType = { RIGHT_PAREN: 'RIGHT_PAREN', LEFT_BRACE: 'LEFT_BRACE', RIGHT_BRACE: 'RIGHT_BRACE', + LEFT_BRACKET: 'LEFT_BRACKET', + RIGHT_BRACKET: 'RIGHT_BRACKET', SEMICOLON: 'SEMICOLON', - IO_IN: 'IO_IN', - IO_OUT: 'IO_OUT', - IO_ASSERT: 'IO_ASSERT', - EQUALS: 'EQUALS', - FUNCTION_REF: 'FUNCTION_REF', + COMMA: 'COMMA', + DOT: 'DOT', STRING: 'STRING', - // New arithmetic operators - MODULO: 'MODULO', - POWER: 'POWER', - // New comparison operators + TRUE: 'TRUE', + FALSE: 'FALSE', + AND: 'AND', + OR: 'OR', + XOR: 'XOR', + NOT: 'NOT', + EQUALS: 'EQUALS', 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', - // Comments - COMMENT: 'COMMENT', - // Table syntax - COLON: 'COLON', - COMMA: 'COMMA', - LEFT_BRACKET: 'LEFT_BRACKET', - RIGHT_BRACKET: 'RIGHT_BRACKET', - DOT: 'DOT', - // Boolean literals - TRUE: 'TRUE', - FALSE: 'FALSE', + MODULO: 'MODULO', + POWER: 'POWER', + IO_IN: 'IO_IN', + IO_OUT: 'IO_OUT', + IO_ASSERT: 'IO_ASSERT', + FUNCTION_REF: 'FUNCTION_REF' }; -// Lexer +/** + * Lexer: Converts source code to tokens. + * + * @param {string} input - Source code to tokenize + * @returns {Array.<Object>} Array of token objects with type and value properties + * @throws {Error} For unterminated strings or unexpected characters + * + * @description Performs lexical analysis by converting source code into a stream of tokens. + * Handles whitespace, nested comments, numbers (integers and decimals), strings, + * identifiers/keywords, and both single- and multi-character operators. + * + * @how Uses a single pass with a while loop and manual character inspection. + * Each character is examined to determine the appropriate token type, with + * special handling for multi-character tokens and nested constructs. + * + * @why Manual lexing allows for fine-grained control over tokenization, especially + * for edge cases like nested comments and multi-character IO operations. This + * approach also makes it easier to debug and extend the lexer for new language features. + * + * @note IO operations (..in, ..out, ..assert) are recognized as multi-character + * tokens to avoid ambiguity with the dot operator. Decimal numbers are parsed + * as a single token to support floating point arithmetic. + */ function lexer(input) { - debugLog('Starting lexer with input', input); - const tokens = []; let current = 0; - + const tokens = []; + while (current < input.length) { let char = input[current]; - - // Handle comments /* ... */ - if (input.slice(current, current + 2) === '/*') { - let commentContent = ''; - let commentDepth = 1; // Track nested comment depth + + // Skip whitespace + if (/\s/.test(char)) { + current++; + continue; + } + + // Handle nested comments: /* ... */ with support for /* /* ... */ */ + if (char === '/' && input[current + 1] === '*') { + let commentDepth = 1; current += 2; // Skip /* - // Find the closing */, handling nested comments while (current < input.length && commentDepth > 0) { - if (input.slice(current, current + 2) === '/*') { + if (input[current] === '/' && input[current + 1] === '*') { commentDepth++; - commentContent += input[current]; - commentContent += input[current + 1]; current += 2; - } else if (input.slice(current, current + 2) === '*/') { + } else if (input[current] === '*' && input[current + 1] === '/') { commentDepth--; - if (commentDepth > 0) { - commentContent += input[current]; - commentContent += input[current + 1]; - } current += 2; } else { - commentContent += input[current]; current++; } } - - if (commentDepth === 0) { - // Don't add comment tokens to the output - they're ignored - continue; - } else { - throw new Error('Unterminated comment: missing */'); - } - } - - 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; } - - 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 - }); + + // Parse numbers (integers and decimals) + if (/[0-9]/.test(char)) { + let value = ''; + while (current < input.length && /[0-9]/.test(input[current])) { + value += input[current]; current++; } - continue; - } - - if (char === '>') { - if (input.slice(current, current + 2) === '>=') { + + // Check for decimal point + if (current < input.length && input[current] === '.') { + value += input[current]; + current++; + + // Parse decimal part + while (current < input.length && /[0-9]/.test(input[current])) { + value += input[current]; + current++; + } + tokens.push({ - type: TokenType.GREATER_EQUAL + type: TokenType.NUMBER, + value: parseFloat(value) }); - current += 2; } else { tokens.push({ - type: TokenType.GREATER_THAN + type: TokenType.NUMBER, + value: parseInt(value) }); - current++; - } - 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; - } - - if (input.slice(current, current + 3) === 'xor') { - tokens.push({ - type: TokenType.XOR - }); - current += 3; - continue; - } - - if (input.slice(current, current + 3) === 'not') { - tokens.push({ - type: TokenType.NOT - }); - current += 3; - continue; - } - - - - // Check for boolean literals (must come before identifier parsing) - if (input.slice(current, current + 4) === 'true') { - tokens.push({ - type: TokenType.TRUE - }); - current += 4; - continue; - } - - if (input.slice(current, current + 5) === 'false') { - tokens.push({ - type: TokenType.FALSE - }); - current += 5; - continue; - } - - 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; - } - - if (char === ':') { - // Check if this is a table key-value separator (inside braces) - // For now, we'll use ASSIGNMENT for both and handle the distinction in the parser - 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 (char === ',') { - tokens.push({ - type: TokenType.COMMA - }); - current++; - continue; - } - - if (char === '[') { - tokens.push({ - type: TokenType.LEFT_BRACKET - }); - current++; - continue; - } - - if (char === ']') { - tokens.push({ - type: TokenType.RIGHT_BRACKET - }); - current++; - continue; - } - - // Check for IO tokens (must come before individual dot handling) - if (input.slice(current, current + 4) === '..in') { - tokens.push({ - type: TokenType.IO_IN - }); - current += 4; - continue; - } - - if (input.slice(current, current + 5) === '..out') { - tokens.push({ - type: TokenType.IO_OUT - }); - current += 5; - continue; - } - - if (input.slice(current, current + 8) === '..assert') { - tokens.push({ - type: TokenType.IO_ASSERT - }); - current += 8; - continue; - } - - if (char === '.') { - tokens.push({ - type: TokenType.DOT - }); - current++; - continue; - } - - if (input.slice(current, current + 8) === 'function') { - tokens.push({ - type: TokenType.FUNCTION - }); - current += 8; - continue; - } - - // Handle string literals + + // Parse 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] === '"') { + + if (current < input.length) { current++; // Skip closing quote tokens.push({ type: TokenType.STRING, - value + value: value }); - continue; } else { - throw new Error('Unterminated string literal'); + throw new Error('Unterminated string'); } + continue; } - - if (char === ';') { - tokens.push({ type: TokenType.SEMICOLON }); - current++; + + // Parse identifiers and keywords + if (/[a-zA-Z_]/.test(char)) { + let value = ''; + while (current < input.length && /[a-zA-Z0-9_]/.test(input[current])) { + value += input[current]; + current++; + } + + // Check for keywords + switch (value) { + case 'case': + tokens.push({ type: TokenType.CASE }); + break; + case 'of': + tokens.push({ type: TokenType.OF }); + break; + case 'function': + tokens.push({ type: TokenType.FUNCTION }); + break; + case 'true': + tokens.push({ type: TokenType.TRUE }); + break; + case 'false': + tokens.push({ type: TokenType.FALSE }); + break; + case 'and': + tokens.push({ type: TokenType.AND }); + break; + case 'or': + tokens.push({ type: TokenType.OR }); + break; + case 'xor': + tokens.push({ type: TokenType.XOR }); + break; + case 'not': + tokens.push({ type: TokenType.NOT }); + break; + case '_': + tokens.push({ type: TokenType.WILDCARD }); + break; + default: + tokens.push({ + type: TokenType.IDENTIFIER, + value: value + }); + } continue; } - + + // Parse two-character operators + if (current + 1 < input.length) { + const twoChar = char + input[current + 1]; + switch (twoChar) { + case '->': + tokens.push({ type: TokenType.ARROW }); + current += 2; + continue; + case '==': + tokens.push({ type: TokenType.EQUALS }); + current += 2; + continue; + case '!=': + tokens.push({ type: TokenType.NOT_EQUAL }); + current += 2; + continue; + case '<=': + tokens.push({ type: TokenType.LESS_EQUAL }); + current += 2; + continue; + case '>=': + tokens.push({ type: TokenType.GREATER_EQUAL }); + current += 2; + continue; + case '..': + // Parse IO operations: ..in, ..out, ..assert + if (current + 2 < input.length) { + const ioChar = input[current + 2]; + switch (ioChar) { + case 'i': + if (current + 3 < input.length && input[current + 3] === 'n') { + tokens.push({ type: TokenType.IO_IN }); + current += 4; + continue; + } + break; + case 'o': + if (current + 3 < input.length && input[current + 3] === 'u') { + if (current + 4 < input.length && input[current + 4] === 't') { + tokens.push({ type: TokenType.IO_OUT }); + current += 5; + continue; + } + } + break; + case 'a': + if (current + 3 < input.length && input[current + 3] === 's') { + if (current + 4 < input.length && input[current + 4] === 's') { + if (current + 5 < input.length && input[current + 5] === 'e') { + if (current + 6 < input.length && input[current + 6] === 'r') { + if (current + 7 < input.length && input[current + 7] === 't') { + tokens.push({ type: TokenType.IO_ASSERT }); + current += 8; + continue; + } + } + } + } + } + break; + } + } + // If we get here, it's not a complete IO operation, so skip the '..' + current += 2; + continue; + } + } + + // Parse single character operators + switch (char) { + case '+': + tokens.push({ type: TokenType.PLUS }); + break; + case '-': + tokens.push({ type: TokenType.MINUS }); + break; + case '*': + tokens.push({ type: TokenType.MULTIPLY }); + break; + case '/': + tokens.push({ type: TokenType.DIVIDE }); + break; + case '%': + tokens.push({ type: TokenType.MODULO }); + break; + case '^': + tokens.push({ type: TokenType.POWER }); + break; + case ':': + tokens.push({ type: TokenType.ASSIGNMENT }); + break; + case '(': + tokens.push({ type: TokenType.LEFT_PAREN }); + break; + case ')': + tokens.push({ type: TokenType.RIGHT_PAREN }); + break; + case '{': + tokens.push({ type: TokenType.LEFT_BRACE }); + break; + case '}': + tokens.push({ type: TokenType.RIGHT_BRACE }); + break; + case '[': + tokens.push({ type: TokenType.LEFT_BRACKET }); + break; + case ']': + tokens.push({ type: TokenType.RIGHT_BRACKET }); + break; + case ';': + tokens.push({ type: TokenType.SEMICOLON }); + break; + case ',': + tokens.push({ type: TokenType.COMMA }); + break; + case '.': + tokens.push({ type: TokenType.DOT }); + break; + case '@': + tokens.push({ type: TokenType.FUNCTION_REF }); + break; + case '_': + tokens.push({ type: TokenType.WILDCARD }); + break; + case '=': + tokens.push({ type: TokenType.EQUALS }); + break; + case '<': + tokens.push({ type: TokenType.LESS_THAN }); + break; + case '>': + tokens.push({ type: TokenType.GREATER_THAN }); + break; + default: + throw new Error(`Unexpected character: ${char}`); + } + current++; } + + return tokens; } -// Parser +/** + * Parser: Converts tokens to an Abstract Syntax Tree (AST). + * + * @param {Array.<Object>} tokens - Array of tokens from the lexer + * @returns {Object} Abstract Syntax Tree with program body + * @throws {Error} For parsing errors like unexpected tokens or missing delimiters + * + * @description Implements a recursive descent parser that builds an AST from tokens. + * Handles all language constructs including expressions, statements, function + * definitions, case expressions, table literals, and IO operations. + * + * @how Implements a recursive descent parser, with separate functions for each + * precedence level (expression, term, factor, primary). Handles chained table + * access, function calls, and complex constructs like case expressions and + * function definitions. + * + * @why Recursive descent is chosen for its clarity and flexibility, especially + * for a language with many context-sensitive constructs (e.g., case expressions, + * function definitions, chained access). The parser is structured to minimize + * circular dependencies and infinite recursion, with careful placement of IO + * and case expression parsing. + * + * @note The parser supports multi-parameter case expressions and function + * definitions, using lookahead to distinguish between assignments and function + * declarations. Table literals are parsed with support for both array-like and + * key-value entries, inspired by Lua. + */ function parser(tokens) { - debugLog('Starting parser with tokens', tokens); + let current = 0; + let parsingFunctionArgs = false; // Flag to track when we're parsing function arguments - + // Reset call stack tracker for parser + callStackTracker.reset(); - 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 = []; + // Define all parsing functions outside of walk to avoid circular dependencies + + function parseChainedDotAccess(tableExpr) { + callStackTracker.push('parseChainedDotAccess', ''); + + try { + /** + * Handles chained dot access (e.g., table.key.subkey). + * + * @param {Object} tableExpr - The table expression to chain access from + * @returns {Object} AST node representing the chained access + * @throws {Error} When expected identifier is missing after dot + * + * @description Parses dot notation for table access, building a chain + * of TableAccess nodes for nested property access. + * + * @why Chained access is parsed iteratively rather than recursively to + * avoid deep call stacks and to allow for easy extension (e.g., supporting + * method calls in the future). + */ + let result = tableExpr; - 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)) { - - functionCalls.push(tokens[tempCurrent].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++; - } - } else { - tempCurrent++; + while (current < tokens.length && tokens[current].type === TokenType.DOT) { + current++; // Skip the dot + + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const key = { + type: 'Identifier', + value: tokens[current].value + }; + current++; + + result = { + type: 'TableAccess', + table: result, + key: key + }; + } else { + throw new Error('Expected identifier after dot'); + } + } + + return result; + } finally { + callStackTracker.pop(); + } + } + + function parseChainedTableAccess(tableExpr) { + callStackTracker.push('parseChainedTableAccess', ''); + + try { + /** + * Handles chained bracket and dot access (e.g., table[0].key). + * + * @param {Object} tableExpr - The table expression to chain access from + * @returns {Object} AST node representing the chained access + * @throws {Error} When expected closing bracket is missing + * + * @description Parses both bracket and dot notation for table access, + * supporting mixed access patterns like table[0].key. + * + * @why This function allows for flexible access patterns, supporting both + * array and object semantics. Chaining is handled by checking for further + * access tokens after each access. + */ + if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { + current++; // Skip '[' + const keyExpr = walk(); + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACKET) { + current++; // Skip ']' + + const access = { + type: 'TableAccess', + table: tableExpr, + key: keyExpr + }; + + // Check for chained access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + return parseChainedDotAccess(access); + } + + // Check if this is a function call + if (current < tokens.length && + (tokens[current].type === TokenType.IDENTIFIER || + tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.STRING || + tokens[current].type === TokenType.LEFT_PAREN)) { + return parseFunctionCall(access); } + + return access; } else { - tempCurrent++; + throw new Error('Expected closing bracket'); } } - // 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.`); + // Check for dot access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + const result = parseChainedDotAccess(tableExpr); + + // Check if this is a function call + if (current < tokens.length && + (tokens[current].type === TokenType.IDENTIFIER || + tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.STRING || + tokens[current].type === TokenType.LEFT_PAREN)) { + return parseFunctionCall(result); + } + + return result; } + + return tableExpr; + } finally { + callStackTracker.pop(); } - - // Helper function to parse function calls with proper precedence - function parseFunctionCall() { - if (token.type !== TokenType.IDENTIFIER) { - return null; + } + + function parseArgument() { + callStackTracker.push('parseArgument', ''); + + try { + const token = tokens[current]; + if (!token) { + throw new Error('Unexpected end of input'); } - - // 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.COMPOSE && - tokens[current + 1].type !== TokenType.PIPE && - tokens[current + 1].type !== TokenType.APPLY && - 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 || - tokens[current + 1].type === TokenType.FUNCTION_REF)) { + + // Parse unary operators + if (token.type === TokenType.NOT) { + current++; + const operand = parseArgument(); + return { type: 'NotExpression', operand }; + } + + if (token.type === TokenType.MINUS) { + current++; + const operand = parseArgument(); + return { type: 'UnaryMinusExpression', operand }; + } + + // Parse literals + if (token.type === TokenType.NUMBER) { + current++; + return { type: 'NumberLiteral', value: token.value }; + } else if (token.type === TokenType.STRING) { + current++; + return { type: 'StringLiteral', value: token.value }; + } else if (token.type === TokenType.TRUE) { + current++; + return { type: 'BooleanLiteral', value: true }; + } else if (token.type === TokenType.FALSE) { + current++; + return { type: 'BooleanLiteral', value: false }; + } else if (token.type === TokenType.NULL) { + current++; + return { type: 'NullLiteral' }; + } else if (token.type === TokenType.WILDCARD) { + current++; + return { type: 'WildcardPattern' }; + } else if (token.type === TokenType.FUNCTION_REF) { + current++; + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const functionName = tokens[current].value; + current++; + return { type: 'FunctionReference', name: functionName }; + } else { + throw new Error('Expected function name after @'); + } + } else if (token.type === TokenType.IO_IN) { + current++; + return { type: 'IOInExpression' }; + } else if (token.type === TokenType.IO_OUT) { + current++; + const outputValue = parseLogicalExpression(); + return { type: 'IOOutExpression', value: outputValue }; + } else if (token.type === TokenType.IO_ASSERT) { + current++; + const assertionExpr = parseLogicalExpression(); + return { type: 'IOAssertExpression', value: assertionExpr }; + } + + // Parse identifiers (but NOT as function calls) + if (token.type === TokenType.IDENTIFIER) { + current++; + const identifier = { type: 'Identifier', value: token.value }; - + // Check for table access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + return parseChainedDotAccess(identifier); + } - const funcName = token.value; - current++; // Skip function name - const args = []; + // Check for table access with brackets + if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { + return parseChainedTableAccess(identifier); + } - // 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++; + return identifier; + } + + // Parse parenthesized expressions + if (token.type === TokenType.LEFT_PAREN) { + current++; // Skip '(' + const parenthesizedExpr = parseLogicalExpression(); + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_PAREN) { + current++; // Skip ')' + return parenthesizedExpr; + } else { + throw new Error('Expected closing parenthesis'); + } + } + + // Parse table literals + if (token.type === TokenType.LEFT_BRACE) { + current++; // Skip '{' + const properties = []; + + while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { + if (tokens[current].type === TokenType.IDENTIFIER) { + const key = tokens[current].value; + current++; + + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + const value = parseLogicalExpression(); + properties.push({ key, value }); } else { - throw new Error('Expected function name after @'); + throw new Error('Expected ":" after property name in table literal'); } } 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.COMPOSE && - tokens[tempCurrent + 1].type !== TokenType.PIPE && - tokens[tempCurrent + 1].type !== TokenType.APPLY && - 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); - } - 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); - } - } - - args.push({ - type: 'FunctionCall', - name: nestedFuncName, - args: nestedArgs, - }); - } else { - // Use walk() for other types of arguments - const arg = walk(); - if (arg) { - args.push(arg); - } - } + throw new Error('Expected property name in table literal'); + } + + // Skip comma if present + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { + current++; } } - return { - type: 'FunctionCall', - name: funcName, - args, - }; + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACE) { + current++; // Skip '}' + return { type: 'TableLiteral', properties }; + } else { + throw new Error('Expected closing brace in table literal'); + } } - return null; + // If we get here, we have an unexpected token + throw new Error(`Unexpected token in parseArgument: ${token.type}`); + } finally { + callStackTracker.pop(); } - - // 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', - }; + } + + function parseFunctionCall(functionName) { + callStackTracker.push('parseFunctionCall', ''); + + try { + /** + * Parses function calls with arbitrary argument lists. + * + * @param {Object|string} functionName - Function name or expression to call + * @returns {Object} AST node representing the function call + * + * @description Parses function calls by collecting arguments until a + * clear terminator is found, supporting both curried and regular calls. + * + * @why Arguments are parsed until a clear terminator is found, allowing + * for flexible function call syntax. This approach supports both curried + * and regular function calls, and allows for future extension to variadic functions. + * + * @note Special handling for unary minus arguments to distinguish them + * from binary minus operations. + */ + const args = []; + // Parse arguments until we hit a semicolon or other terminator + while (current < tokens.length && + tokens[current].type !== TokenType.SEMICOLON && + tokens[current].type !== TokenType.RIGHT_PAREN && + tokens[current].type !== TokenType.RIGHT_BRACE && + tokens[current].type !== TokenType.COMMA && + tokens[current].type !== TokenType.AND && + tokens[current].type !== TokenType.OR && + tokens[current].type !== TokenType.XOR) { + + // Special handling for unary minus as argument + if (tokens[current].type === TokenType.MINUS) { + // This is a unary minus, parse it as a new argument + current++; // Skip the minus + if (current < tokens.length && tokens[current].type === TokenType.NUMBER) { + args.push({ + type: 'UnaryMinusExpression', + operand: { + type: 'NumberLiteral', + value: tokens[current].value + } + }); + current++; // Skip the number + } else { + // More complex unary minus expression + args.push({ + type: 'UnaryMinusExpression', + operand: parsePrimary() + }); + } + } else { + // Regular argument parsing - parse as expression but skip function call detection + // Create a temporary parsing context that doesn't trigger function call detection + const savedParsingFunctionArgs = parsingFunctionArgs; + parsingFunctionArgs = true; // Temporarily disable function call detection + const arg = parseExpression(); + parsingFunctionArgs = savedParsingFunctionArgs; // Restore the flag + args.push(arg); + } + } + return { - type: expressionTypes[operator], - left, - right, + type: 'FunctionCall', + name: functionName, + args: args }; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.NUMBER) { - current++; - return { - type: 'NumberLiteral', - value: token.value, - }; + } + + function parseLogicalExpression() { + callStackTracker.push('parseLogicalExpression', ''); + + try { + /** + * Parses logical expressions with lowest precedence. + * + * @returns {Object} AST node representing the logical expression + * + * @description Parses logical operators (and, or, xor) with proper + * precedence handling and left associativity. + * + * @why Logical operators should have lower precedence than arithmetic + * and comparison operators to ensure proper grouping of expressions + * like "isEven 10 and isPositive 5". + */ + let left = parseExpression(); + + while (current < tokens.length && + (tokens[current].type === TokenType.AND || + tokens[current].type === TokenType.OR || + tokens[current].type === TokenType.XOR)) { + + const operator = tokens[current].type; + current++; + const right = parseExpression(); + + switch (operator) { + case TokenType.AND: + left = { type: 'AndExpression', left, right }; + break; + case TokenType.OR: + left = { type: 'OrExpression', left, right }; + break; + case TokenType.XOR: + left = { type: 'XorExpression', left, right }; + break; + } + } + + return left; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.STRING) { - current++; - return { - type: 'StringLiteral', - value: token.value, - }; + } + + function parseExpression() { + callStackTracker.push('parseExpression', ''); + + try { + /** + * Parses expressions with left-associative binary operators. + * + * @returns {Object} AST node representing the expression + * + * @description Parses addition, subtraction, and comparison operators + * with proper precedence and associativity. + * + * @why Operator precedence is handled by splitting parsing into multiple + * functions (expression, term, factor, primary). This structure avoids + * ambiguity and ensures correct grouping of operations. + * + * @note Special case handling for unary minus after function references + * to distinguish from binary minus operations. + */ + let left = parseTerm(); + + while (current < tokens.length && + (tokens[current].type === TokenType.PLUS || + tokens[current].type === TokenType.MINUS || + tokens[current].type === TokenType.EQUALS || + tokens[current].type === TokenType.NOT_EQUAL || + tokens[current].type === TokenType.LESS_THAN || + tokens[current].type === TokenType.GREATER_THAN || + tokens[current].type === TokenType.LESS_EQUAL || + tokens[current].type === TokenType.GREATER_EQUAL)) { + + const operator = tokens[current].type; + + // Special case: Don't treat MINUS as binary operator if left is a FunctionReference + // This handles cases like "filter @isPositive -3" where -3 should be a separate argument + if (operator === TokenType.MINUS && left.type === 'FunctionReference') { + // This is likely a function call with unary minus argument, not a binary operation + // Return the left side and let the caller handle it + return left; + } + + current++; + const right = parseTerm(); + + switch (operator) { + case TokenType.PLUS: + left = { type: 'PlusExpression', left, right }; + break; + case TokenType.MINUS: + left = { type: 'MinusExpression', left, right }; + break; + case TokenType.EQUALS: + left = { type: 'EqualsExpression', left, right }; + break; + case TokenType.NOT_EQUAL: + left = { type: 'NotEqualExpression', left, right }; + break; + case TokenType.LESS_THAN: + left = { type: 'LessThanExpression', left, right }; + break; + case TokenType.GREATER_THAN: + left = { type: 'GreaterThanExpression', left, right }; + break; + case TokenType.LESS_EQUAL: + left = { type: 'LessEqualExpression', left, right }; + break; + case TokenType.GREATER_EQUAL: + left = { type: 'GreaterEqualExpression', left, right }; + break; + } + } + + return left; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.TRUE) { - current++; - return { - type: 'BooleanLiteral', - value: true, - }; + } + + function parseTerm() { + callStackTracker.push('parseTerm', ''); + + try { + /** + * Parses multiplication, division, and modulo operations. + * + * @returns {Object} AST node representing the term + * + * @description Parses multiplicative operators with higher precedence + * than addition/subtraction. + * + * @why By handling these operators at a separate precedence level, the + * parser ensures that multiplication/division bind tighter than + * addition/subtraction, matching standard arithmetic rules. + */ + let left = parseFactor(); + + while (current < tokens.length && + (tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE || + tokens[current].type === TokenType.MODULO)) { + + const operator = tokens[current].type; + current++; + const right = parseFactor(); + + switch (operator) { + case TokenType.MULTIPLY: + left = { type: 'MultiplyExpression', left, right }; + break; + case TokenType.DIVIDE: + left = { type: 'DivideExpression', left, right }; + break; + case TokenType.MODULO: + left = { type: 'ModuloExpression', left, right }; + break; + } + } + + return left; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.FALSE) { - current++; - return { - type: 'BooleanLiteral', - value: false, - }; + } + + function parseFactor() { + callStackTracker.push('parseFactor', ''); + + try { + /** + * Parses exponentiation and primary expressions. + * + * @returns {Object} AST node representing the factor + * + * @description Parses exponentiation with right associativity and + * highest precedence among arithmetic operators. + * + * @why Exponentiation is right-associative and binds tighter than + * multiplication/division, so it is handled at the factor level. This + * also allows for future extension to other high-precedence operators. + */ + let left = parsePrimary(); + + while (current < tokens.length && tokens[current].type === TokenType.POWER) { + current++; + const right = parsePrimary(); + left = { type: 'PowerExpression', left, right }; + } + + return left; + } finally { + callStackTracker.pop(); } - - if (token.type === TokenType.LEFT_BRACE) { - current++; // Skip opening brace - const entries = []; + } + + function parsePrimary() { + callStackTracker.push('parsePrimary', ''); + + try { + /** + * Parses literals, identifiers, function definitions, assignments, + * table literals, and parenthesized expressions. + * + * @returns {Object} AST node representing the primary expression + * @throws {Error} For parsing errors like unexpected tokens + * + * @description The core parsing function that handles all atomic and + * context-sensitive constructs in the language. + * + * @why This function is the core of the recursive descent parser, handling + * all atomic and context-sensitive constructs. Special care is taken to + * avoid circular dependencies by not calling higher-level parsing functions. + */ - // Parse table entries until closing brace - while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { - // Skip leading commas - if (tokens[current].type === TokenType.COMMA) { + const token = tokens[current]; + if (!token) { + throw new Error('Unexpected end of input'); + } + + // Parse unary operators + if (token.type === TokenType.NOT) { + current++; + const operand = parsePrimary(); + return { type: 'NotExpression', operand }; + } + + if (token.type === TokenType.MINUS) { + current++; + const operand = parsePrimary(); + return { type: 'UnaryMinusExpression', operand }; + } + + // Parse literals + if (token.type === TokenType.NUMBER) { + current++; + return { type: 'NumberLiteral', value: token.value }; + } else if (token.type === TokenType.STRING) { + current++; + return { type: 'StringLiteral', value: token.value }; + } else if (token.type === TokenType.TRUE) { + current++; + return { type: 'BooleanLiteral', value: true }; + } else if (token.type === TokenType.FALSE) { + current++; + return { type: 'BooleanLiteral', value: false }; + } else if (token.type === TokenType.NULL) { + current++; + return { type: 'NullLiteral' }; + } else if (token.type === TokenType.WILDCARD) { + current++; + return { type: 'WildcardPattern' }; + } else if (token.type === TokenType.FUNCTION_REF) { + current++; + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const functionName = tokens[current].value; current++; - continue; + return { type: 'FunctionReference', name: functionName }; + } else { + throw new Error('Expected function name after @'); + } + } else if (token.type === TokenType.IO_IN) { + current++; + return { type: 'IOInExpression' }; + } else if (token.type === TokenType.IO_OUT) { + current++; + const outputValue = parseLogicalExpression(); + return { type: 'IOOutExpression', value: outputValue }; + } else if (token.type === TokenType.IO_ASSERT) { + current++; + const assertionExpr = parseLogicalExpression(); + return { type: 'IOAssertExpression', value: assertionExpr }; + } + + // Parse identifiers + if (token.type === TokenType.IDENTIFIER) { + current++; + const identifier = { type: 'Identifier', value: token.value }; + + // Skip function call detection if we're parsing function arguments + if (parsingFunctionArgs) { + return identifier; } - // Parse key (optional) and value - let key = null; - let value; + // Check for function calls + if (current < tokens.length && tokens[current].type === TokenType.LEFT_PAREN) { + return parseFunctionCall(identifier.value); + } - // Check if this is a key-value pair (key: value) or just a value - if (current + 1 < tokens.length && tokens[current + 1].type === TokenType.ASSIGNMENT) { - // This is a key-value pair - // Parse the key (should be an identifier, number, or string) - if (tokens[current].type === TokenType.IDENTIFIER) { - key = { - type: 'Identifier', - value: tokens[current].value, - }; - current++; // Skip the key - } else if (tokens[current].type === TokenType.NUMBER) { - key = { - type: 'NumberLiteral', - value: tokens[current].value, - }; - current++; // Skip the key - } else if (tokens[current].type === TokenType.STRING) { - key = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; // Skip the key - } else { - throw new Error('Invalid key type in table literal'); + // Check if the next token is an operator - if so, don't treat as function call + if (current < tokens.length && + (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)) { + // This is part of a binary expression, don't treat as function call + return identifier; + } + + // Check for function calls without parentheses (e.g., add 3 4) + // Only treat as function call if the next token is a number, string, or left paren + // This prevents treating identifiers as function calls when they're actually arguments + if (current < tokens.length && + (tokens[current].type === TokenType.NUMBER || + tokens[current].type === TokenType.STRING || + tokens[current].type === TokenType.LEFT_PAREN || + tokens[current].type === TokenType.FUNCTION_REF)) { + return parseFunctionCall(identifier.value); + } + + // Special case for unary minus: only treat as function call if it's a unary minus + if (current < tokens.length && tokens[current].type === TokenType.MINUS) { + // Look ahead to see if this is a unary minus (like -5) or binary minus (like n - 1) + const nextToken = current + 1 < tokens.length ? tokens[current + 1] : null; + if (nextToken && nextToken.type === TokenType.NUMBER) { + // This is a unary minus, treat as function call + return parseFunctionCall(identifier.value); } + // This is a binary minus, don't treat as function call + } + + // Special case for function calls with identifier arguments (e.g., add x y) + // Only treat as function call if the next token is an identifier and not followed by an operator + if (!parsingFunctionArgs && current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + // Look ahead to see if the next token is an identifier followed by an operator + const nextToken = current + 1 < tokens.length ? tokens[current + 1] : null; + const nextNextToken = current + 2 < tokens.length ? tokens[current + 2] : null; - current++; // Skip the colon - value = walk(); // Parse the value - } else { - // This is just a value (array-like entry) - value = walk(); + // Only treat as function call if the next token is an identifier and not followed by an operator + if (nextToken && nextToken.type === TokenType.IDENTIFIER && + (!nextNextToken || + (nextNextToken.type !== TokenType.PLUS && + nextNextToken.type !== TokenType.MINUS && + nextNextToken.type !== TokenType.MULTIPLY && + nextNextToken.type !== TokenType.DIVIDE && + nextNextToken.type !== TokenType.MODULO && + nextNextToken.type !== TokenType.POWER && + nextNextToken.type !== TokenType.EQUALS && + nextNextToken.type !== TokenType.NOT_EQUAL && + nextNextToken.type !== TokenType.LESS_THAN && + nextNextToken.type !== TokenType.GREATER_THAN && + nextNextToken.type !== TokenType.LESS_EQUAL && + nextNextToken.type !== TokenType.GREATER_EQUAL))) { + if (process.env.DEBUG) { + console.log(`[DEBUG] Creating function call for ${identifier.value} at position ${current}`); + } + return parseFunctionCall(identifier.value); + } } - entries.push({ key, value }); + - // Skip trailing comma - if (tokens[current] && tokens[current].type === TokenType.COMMA) { - current++; + // Check for table access + if (current < tokens.length && tokens[current].type === TokenType.DOT) { + return parseChainedDotAccess(identifier); } + + // Check for table access with brackets + if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { + return parseChainedTableAccess(identifier); + } + + return identifier; } - // Expect closing brace - if (tokens[current] && tokens[current].type === TokenType.RIGHT_BRACE) { - current++; // Skip closing brace - } else { - throw new Error('Expected closing brace } in table literal'); + // Parse parenthesized expressions + if (token.type === TokenType.LEFT_PAREN) { + current++; // Skip '(' + const parenthesizedExpr = parseLogicalExpression(); + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_PAREN) { + current++; // Skip ')' + return parenthesizedExpr; + } else { + throw new Error('Expected closing parenthesis'); + } } - return { - type: 'TableLiteral', - entries, - }; - } - - - - 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 @'); + // Parse table literals + if (token.type === TokenType.LEFT_BRACE) { + current++; // Skip '{' + const properties = []; + + while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { + if (tokens[current].type === TokenType.IDENTIFIER) { + const key = tokens[current].value; + current++; + + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + const value = parseLogicalExpression(); + properties.push({ key, value }); + } else { + throw new Error('Expected ":" after property name in table literal'); + } + } else { + throw new Error('Expected property name in table literal'); + } + + // Skip comma if present + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { + current++; + } + } + + if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACE) { + current++; // Skip '}' + return { type: 'TableLiteral', properties }; + } else { + throw new Error('Expected closing brace in table literal'); + } } - } - - 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(); + // Parse arrow expressions (function definitions) + if (token.type === TokenType.ARROW) { + current++; // Skip '->' - return { - type: 'IOAssertExpression', - left, - operator, - right, - }; - } else { - throw new Error('Expected comparison operator (=, <, >, <=, >=, !=) in assertion'); + // Parse the function body + const body = parseLogicalExpression(); + + return { type: 'ArrowExpression', body }; } - } - - - - 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 we get here, we have an unexpected token + throw new Error(`Unexpected token in parsePrimary: ${token.type}`); + } finally { + callStackTracker.pop(); } + } + + function walk() { + callStackTracker.push('walk', `position:${current}`); + + try { + - // Functional operators - + - if (token.type === TokenType.LEFT_PAREN) { - 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 + function parseLogicalExpression() { + callStackTracker.push('parseLogicalExpression', ''); - // Check if there's an arithmetic, logical, or functional 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.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(); + try { + /** + * Parses logical expressions with lowest precedence. + * + * @returns {Object} AST node representing the logical expression + * + * @description Parses logical operators (and, or, xor) with proper + * precedence handling and left associativity. + * + * @why Logical operators should have lower precedence than arithmetic + * and comparison operators to ensure proper grouping of expressions + * like "isEven 10 and isPositive 5". + */ + let left = parseExpression(); - const expressionTypes = { - [TokenType.PLUS]: 'PlusExpression', - [TokenType.MINUS]: 'MinusExpression', - [TokenType.MULTIPLY]: 'MultiplyExpression', - [TokenType.DIVIDE]: 'DivideExpression', - [TokenType.MODULO]: 'ModuloExpression', - [TokenType.POWER]: 'PowerExpression', - [TokenType.AND]: 'AndExpression', - [TokenType.OR]: 'OrExpression', - [TokenType.XOR]: 'XorExpression', - [TokenType.NOT]: 'NotExpression', - - }; + while (current < tokens.length && + (tokens[current].type === TokenType.AND || + tokens[current].type === TokenType.OR || + tokens[current].type === TokenType.XOR)) { + + const operator = tokens[current].type; + current++; + const right = parseExpression(); + + switch (operator) { + case TokenType.AND: + left = { type: 'AndExpression', left, right }; + break; + case TokenType.OR: + left = { type: 'OrExpression', left, right }; + break; + case TokenType.XOR: + left = { type: 'XorExpression', left, right }; + break; + } + } - return { - type: expressionTypes[operator], - left: expression, - right, - }; + return left; + } finally { + callStackTracker.pop(); } - - // 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'); - } - } - - 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; } - // Check if this is followed by table access - if (tokens[current + 1] && tokens[current + 1].type === TokenType.LEFT_BRACKET) { - // Table access with bracket notation: identifier[key] - const identifier = { - type: 'Identifier', - value: token.value, - }; - current++; // Skip identifier - current++; // Skip opening bracket - const key = walk(); // Parse the key expression - - // Expect closing bracket - if (tokens[current] && tokens[current].type === TokenType.RIGHT_BRACKET) { - current++; // Skip closing bracket - } else { - throw new Error('Expected closing bracket ] in table access'); - } - - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else if (tokens[current + 1] && tokens[current + 1].type === TokenType.DOT) { - // Table access with dot notation: identifier.key - const identifier = { - type: 'Identifier', - value: token.value, - }; - current++; // Skip identifier - current++; // Skip the dot + function parseExpression() { + callStackTracker.push('parseExpression', ''); - // Expect an identifier after the dot - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - const key = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; // Skip the identifier + try { + /** + * Parses expressions with left-associative binary operators. + * + * @returns {Object} AST node representing the expression + * + * @description Parses addition, subtraction, and comparison operators + * with proper precedence and associativity. + * + * @why Operator precedence is handled by splitting parsing into multiple + * functions (expression, term, factor, primary). This structure avoids + * ambiguity and ensures correct grouping of operations. + * + * @note Special case handling for unary minus after function references + * to distinguish from binary minus operations. + */ + let left = parseTerm(); - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else { - throw new Error('Expected identifier after dot in table access'); + while (current < tokens.length && + (tokens[current].type === TokenType.PLUS || + tokens[current].type === TokenType.MINUS || + tokens[current].type === TokenType.EQUALS || + tokens[current].type === TokenType.NOT_EQUAL || + tokens[current].type === TokenType.LESS_THAN || + tokens[current].type === TokenType.GREATER_THAN || + tokens[current].type === TokenType.LESS_EQUAL || + tokens[current].type === TokenType.GREATER_EQUAL)) { + + const operator = tokens[current].type; + + // Special case: Don't treat MINUS as binary operator if left is a FunctionReference + // This handles cases like "filter @isPositive -3" where -3 should be a separate argument + if (operator === TokenType.MINUS && left.type === 'FunctionReference') { + // This is likely a function call with unary minus argument, not a binary operation + // Return the left side and let the caller handle it + return left; + } + + current++; + const right = parseTerm(); + + switch (operator) { + case TokenType.PLUS: + left = { type: 'PlusExpression', left, right }; + break; + case TokenType.MINUS: + left = { type: 'MinusExpression', left, right }; + break; + case TokenType.EQUALS: + left = { type: 'EqualsExpression', left, right }; + break; + case TokenType.NOT_EQUAL: + left = { type: 'NotEqualExpression', left, right }; + break; + case TokenType.LESS_THAN: + left = { type: 'LessThanExpression', left, right }; + break; + case TokenType.GREATER_THAN: + left = { type: 'GreaterThanExpression', left, right }; + break; + case TokenType.LESS_EQUAL: + left = { type: 'LessEqualExpression', left, right }; + break; + case TokenType.GREATER_EQUAL: + left = { type: 'GreaterEqualExpression', left, right }; + break; + } + } + + return left; + } finally { + callStackTracker.pop(); } } - // 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; + function parseTerm() { + callStackTracker.push('parseTerm', ''); - // 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++; + try { + /** + * Parses multiplication, division, and modulo operations. + * + * @returns {Object} AST node representing the term + * + * @description Parses multiplicative operators with higher precedence + * than addition/subtraction. + * + * @why By handling these operators at a separate precedence level, the + * parser ensures that multiplication/division bind tighter than + * addition/subtraction, matching standard arithmetic rules. + */ + let left = parseFactor(); + + while (current < tokens.length && + (tokens[current].type === TokenType.MULTIPLY || + tokens[current].type === TokenType.DIVIDE || + tokens[current].type === TokenType.MODULO)) { + + const operator = tokens[current].type; + current++; + const right = parseFactor(); + + switch (operator) { + case TokenType.MULTIPLY: + left = { type: 'MultiplyExpression', left, right }; + break; + case TokenType.DIVIDE: + left = { type: 'DivideExpression', left, right }; + break; + case TokenType.MODULO: + left = { type: 'ModuloExpression', left, right }; + break; + } + } + + return left; + } finally { + callStackTracker.pop(); } + } + + function parseFactor() { + callStackTracker.push('parseFactor', ''); - // Check if next token is arrow - if (tempCurrent < tokens.length && tokens[tempCurrent].type === TokenType.ARROW) { - isFunction = true; + try { + /** + * Parses exponentiation and primary expressions. + * + * @returns {Object} AST node representing the factor + * + * @description Parses exponentiation with right associativity and + * highest precedence among arithmetic operators. + * + * @why Exponentiation is right-associative and binds tighter than + * multiplication/division, so it is handled at the factor level. This + * also allows for future extension to other high-precedence operators. + */ + let left = parsePrimary(); + + while (current < tokens.length && tokens[current].type === TokenType.POWER) { + current++; + const right = parsePrimary(); + left = { type: 'PowerExpression', left, right }; + } + + return left; + } finally { + callStackTracker.pop(); } + } + + // Check for IO operations first + if (tokens[current].type === TokenType.IO_IN) { + current++; + return { type: 'IOInExpression' }; + } else if (tokens[current].type === TokenType.IO_OUT) { + current++; + const outputValue = parseLogicalExpression(); + return { type: 'IOOutExpression', value: outputValue }; + } else if (tokens[current].type === TokenType.IO_ASSERT) { + current++; + const assertionExpr = parseLogicalExpression(); + return { type: 'IOAssertExpression', value: assertionExpr }; + } + + + + // Check for assignments (identifier followed by ':') + if (tokens[current].type === TokenType.IDENTIFIER) { + const identifier = tokens[current].value; + current++; - if (isFunction) { - // This is a function declaration - // Advance current to where tempCurrent left off - current = tempCurrent + 1; // Skip the arrow token + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + + // Check if this is a function definition with arrow syntax (x y -> body) + // Look ahead to see if we have parameters followed by -> + const lookAheadTokens = []; + let lookAheadPos = current; - // Check if the body is a case expression - if (tokens[current] && tokens[current].type === TokenType.CASE) { - current++; // Skip 'case' + // Collect tokens until we find -> or hit a terminator + while (lookAheadPos < tokens.length && + tokens[lookAheadPos].type !== TokenType.ARROW && + tokens[lookAheadPos].type !== TokenType.SEMICOLON && + tokens[lookAheadPos].type !== TokenType.ASSIGNMENT) { + lookAheadTokens.push(tokens[lookAheadPos]); + lookAheadPos++; + } + + // If we found ->, this is a function definition with arrow syntax + if (lookAheadPos < tokens.length && tokens[lookAheadPos].type === TokenType.ARROW) { + // Parse parameters (identifiers separated by spaces) + const parameters = []; + let paramIndex = 0; - // 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, - }); + while (paramIndex < lookAheadTokens.length) { + if (lookAheadTokens[paramIndex].type === TokenType.IDENTIFIER) { + parameters.push(lookAheadTokens[paramIndex].value); + paramIndex++; + } else { + // Skip non-identifier tokens (spaces, etc.) + paramIndex++; } - current++; } - // 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'); - } + // Skip the parameters and -> + current = lookAheadPos + 1; // Skip the arrow - const cases = []; - - // 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 - } + // Parse the function body (check if it's a case expression) + let functionBody; + if (current < tokens.length && tokens[current].type === TokenType.CASE) { + // Parse case expression directly + current++; // Skip 'case' - 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, - }); + // Parse the values being matched (can be multiple) + const values = []; + while (current < tokens.length && tokens[current].type !== TokenType.OF) { + if (tokens[current].type === TokenType.IDENTIFIER) { + values.push({ type: 'Identifier', value: tokens[current].value }); + current++; + } else if (tokens[current].type === TokenType.NUMBER) { + values.push({ type: 'NumberLiteral', value: tokens[current].value }); + current++; + } else if (tokens[current].type === TokenType.STRING) { + values.push({ type: 'StringLiteral', value: tokens[current].value }); + current++; + } else { + const value = parsePrimary(); + values.push(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 = []; - - // 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); + // Expect 'of' + if (current >= tokens.length || tokens[current].type !== TokenType.OF) { + throw new Error('Expected "of" after "case"'); } + current++; // Skip 'of' - cases.push({ pattern, result }); + const cases = []; - // 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 + // Parse cases until we hit a semicolon or end + while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { + // If we hit an IO operation, we've reached the end of the case expression + if (current < tokens.length && + (tokens[current].type === TokenType.IO_IN || + tokens[current].type === TokenType.IO_OUT || + tokens[current].type === TokenType.IO_ASSERT)) { + break; + } + const patterns = []; + while (current < tokens.length && + tokens[current].type !== TokenType.ASSIGNMENT && + tokens[current].type !== TokenType.SEMICOLON) { + patterns.push(parsePrimary()); + } + + // Expect ':' after pattern + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + } else { + throw new Error('Expected ":" after pattern in case expression'); + } + + // Temporarily disable function call detection when parsing case expression results + const savedParsingFunctionArgs = parsingFunctionArgs; + parsingFunctionArgs = true; // Disable function call detection + const result = parseLogicalExpression(); + parsingFunctionArgs = savedParsingFunctionArgs; // Restore the flag + cases.push({ + pattern: patterns, + result: [result] + }); + + // Skip semicolon if present (but don't stop parsing cases) + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + // If the next token is an identifier followed by assignment, we've reached the end of the case expression + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const nextPos = current + 1; + if (nextPos < tokens.length && tokens[nextPos].type === TokenType.ASSIGNMENT) { + break; // End of case expression + } + } + } } - } - - const body = { - type: 'CaseExpression', - value, - cases, - }; - - return { - type: 'FunctionDeclaration', - name, - params, - body, - }; - } 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, + functionBody = { + type: 'CaseExpression', + value: values, + cases, }; - } else { - // Fallback to walk() for other cases - debugLog('Using walk() fallback for function body'); - body = walk(); - } + } else { + functionBody = parseLogicalExpression(); + } return { - type: 'FunctionDeclaration', - name, - params, - body, + type: 'Assignment', + identifier, + value: { + type: 'FunctionDefinition', + parameters, + body: functionBody + } }; } - } 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 - }); - - // Parse the value directly without calling walk() - let value; - // 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 = []; + // Check if this is a function definition with 'function' keyword + if (current < tokens.length && tokens[current].type === TokenType.FUNCTION) { + current++; // Skip 'function' - // 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 (current >= tokens.length || tokens[current].type !== TokenType.LEFT_PAREN) { + throw new Error('Expected "(" after "function"'); + } + current++; // Skip '(' + + const parameters = []; + while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_PAREN) { + if (tokens[current].type === TokenType.IDENTIFIER) { + parameters.push(tokens[current].value); + current++; } else { - // Use walk() to parse complex arguments (including arithmetic expressions) - const arg = walk(); - if (arg) { - args.push(arg); - } + throw new Error('Expected parameter name in function definition'); + } + + // Skip comma if present + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { + current++; } } - value = { - type: 'FunctionCall', - name: funcName, - args, - }; + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { + throw new Error('Expected ")" after function parameters'); + } + current++; // Skip ')' - 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 (current >= tokens.length || tokens[current].type !== TokenType.ASSIGNMENT) { + throw new Error('Expected ":" after function parameters'); } - } 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 followed by table access - if (tokens[current + 1] && tokens[current + 1].type === TokenType.LEFT_BRACKET) { - // Table access with bracket notation: identifier[key] - const identifier = { - type: 'Identifier', - value: tokens[current].value, - }; - current++; // Skip identifier - current++; // Skip opening bracket - const key = walk(); // Parse the key expression + current++; // Skip ':' + + // Parse the function body (check if it's a case expression) + let functionBody; + if (current < tokens.length && tokens[current].type === TokenType.CASE) { + // Parse case expression directly + current++; // Skip 'case' - // Expect closing bracket - if (tokens[current] && tokens[current].type === TokenType.RIGHT_BRACKET) { - current++; // Skip closing bracket - } else { - throw new Error('Expected closing bracket ] in table access'); + // Parse the values being matched (can be multiple) + const values = []; + while (current < tokens.length && tokens[current].type !== TokenType.OF) { + const value = parsePrimary(); + values.push(value); } - value = { - type: 'TableAccess', - table: identifier, - key, - }; - } else if (tokens[current + 1] && tokens[current + 1].type === TokenType.DOT) { - // Table access with dot notation: identifier.key - const identifier = { - type: 'Identifier', - value: tokens[current].value, - }; - current++; // Skip identifier - current++; // Skip the dot + // Expect 'of' + if (current >= tokens.length || tokens[current].type !== TokenType.OF) { + throw new Error('Expected "of" after "case"'); + } + current++; // Skip 'of' + + const cases = []; - // Expect an identifier after the dot - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - const key = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; // Skip the identifier + // Parse cases until we hit a semicolon or end + while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { + // If we hit an IO operation, we've reached the end of the case expression + if (current < tokens.length && + (tokens[current].type === TokenType.IO_IN || + tokens[current].type === TokenType.IO_OUT || + tokens[current].type === TokenType.IO_ASSERT)) { + break; + } + const patterns = []; + while (current < tokens.length && + tokens[current].type !== TokenType.ASSIGNMENT && + tokens[current].type !== TokenType.SEMICOLON) { + patterns.push(parsePrimary()); + } - value = { - type: 'TableAccess', - table: identifier, - key, - }; - } else { - throw new Error('Expected identifier after dot in table access'); + // Expect ':' after pattern + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + } else { + throw new Error('Expected ":" after pattern in case expression'); + } + + const result = parseLogicalExpression(); + cases.push({ + pattern: patterns, + result: [result] + }); + + // Skip semicolon if present (but don't stop parsing cases) + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + // If the next token is an identifier followed by assignment, we've reached the end of the case expression + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const nextPos = current + 1; + if (nextPos < tokens.length && tokens[nextPos].type === TokenType.ASSIGNMENT) { + break; // End of case expression + } + } + } } - } else 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, + functionBody = { + type: 'CaseExpression', + value: values, + cases, }; } else { - // Simple identifier value - value = { - type: 'Identifier', - value: tokens[current].value, - }; - current++; + functionBody = parseLogicalExpression(); } - } 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, + return { + type: 'Assignment', + identifier, + value: { + type: 'FunctionDefinition', + parameters, + body: functionBody + } }; } else { - // Fallback to walk() for other cases - debugLog('Using walk() fallback for assignment value', { current: tokens[current] }); - value = walk(); + // Check if this is a case expression + if (current < tokens.length && tokens[current].type === TokenType.CASE) { + // Parse the case expression directly + current++; // Skip 'case' + + // Parse the values being matched (can be multiple) + const values = []; + while (current < tokens.length && tokens[current].type !== TokenType.OF) { + const value = parsePrimary(); + values.push(value); + } + + // Expect 'of' + if (current >= tokens.length || 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) { + // If we hit an IO operation, we've reached the end of the case expression + if (current < tokens.length && + (tokens[current].type === TokenType.IO_IN || + tokens[current].type === TokenType.IO_OUT || + tokens[current].type === TokenType.IO_ASSERT)) { + break; + } + const patterns = []; + while (current < tokens.length && + tokens[current].type !== TokenType.ASSIGNMENT && + tokens[current].type !== TokenType.SEMICOLON) { + patterns.push(parsePrimary()); + } + + // Expect ':' after pattern + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + current++; // Skip ':' + } else { + throw new Error('Expected ":" after pattern in case expression'); + } + + const result = parseLogicalExpression(); + cases.push({ + pattern: patterns, + result: [result] + }); + + // Skip semicolon if present (but don't stop parsing cases) + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + // If the next token is an identifier followed by assignment, we've reached the end of the case expression + if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + const nextPos = current + 1; + if (nextPos < tokens.length && tokens[nextPos].type === TokenType.ASSIGNMENT) { + break; // End of case expression + } + } + } + } + + return { + type: 'Assignment', + identifier, + value: { + type: 'CaseExpression', + value: values, + cases, + } + }; + } else { + // Regular assignment + const value = parseLogicalExpression(); + + return { + type: 'Assignment', + identifier, + value + }; + } } - - 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, - }; - } - - // Check if this is followed by table access - if (tokens[current + 1] && tokens[current + 1].type === TokenType.LEFT_BRACKET) { - // Table access with bracket notation: identifier[key] - const identifier = { - type: 'Identifier', - value: token.value, - }; - current++; // Skip identifier - current++; // Skip opening bracket - const key = walk(); // Parse the key expression - - // Expect closing bracket - if (tokens[current] && tokens[current].type === TokenType.RIGHT_BRACKET) { - current++; // Skip closing bracket - } else { - throw new Error('Expected closing bracket ] in table access'); } - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else if (tokens[current + 1] && tokens[current + 1].type === TokenType.DOT) { - // Table access with dot notation: identifier.key - const identifier = { - type: 'Identifier', - value: token.value, - }; - current++; // Skip identifier - current++; // Skip the dot - - // Expect an identifier after the dot - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - const key = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; // Skip the identifier - - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else { - throw new Error('Expected identifier after dot in table access'); - } + // If it's not an assignment, put the identifier back and continue + current--; } - current++; - return { - type: 'Identifier', - value: token.value, - }; + // For all other token types (identifiers, numbers, operators, etc.), call parsePrimary + // This handles atomic expressions and delegates to the appropriate parsing functions + return parsePrimary(); + } finally { + callStackTracker.pop(); } - - - - // 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()); - } - current++; // Skip right paren - return node; - } - - if (token.type === TokenType.SEMICOLON) { - current++; - return null; - } - - if (token.type === TokenType.IDENTIFIER) { - // Simple identifier - const identifier = { - type: 'Identifier', - value: token.value, - }; - current++; - - // Check if this is followed by table access - if (tokens[current] && tokens[current].type === TokenType.LEFT_BRACKET) { - // Table access with bracket notation: identifier[key] - current++; // Skip opening bracket - const key = walk(); // Parse the key expression - - // Expect closing bracket - if (tokens[current] && tokens[current].type === TokenType.RIGHT_BRACKET) { - current++; // Skip closing bracket - } else { - throw new Error('Expected closing bracket ] in table access'); - } - - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else if (tokens[current] && tokens[current].type === TokenType.DOT) { - // Table access with dot notation: identifier.key - current++; // Skip the dot - - // Expect an identifier after the dot - if (tokens[current] && tokens[current].type === TokenType.IDENTIFIER) { - const key = { - type: 'StringLiteral', - value: tokens[current].value, - }; - current++; // Skip the identifier - - return { - type: 'TableAccess', - table: identifier, - key, - }; - } else { - throw new Error('Expected identifier after dot in table access'); - } - } - - return identifier; - } - - throw new TypeError(token.type); } - - let ast = { + + const ast = { type: 'Program', - body: [], + body: [] }; - + + let lastCurrent = -1; + let loopCount = 0; + const maxLoops = tokens.length * 2; // Safety limit + while (current < tokens.length) { + // Safety check to prevent infinite loops + if (current === lastCurrent) { + loopCount++; + if (loopCount > 10) { // Allow a few iterations at the same position + throw new Error(`Parser stuck at position ${current}, token: ${tokens[current]?.type || 'EOF'}`); + } + } else { + loopCount = 0; + } + + // Safety check for maximum loops + if (loopCount > maxLoops) { + throw new Error(`Parser exceeded maximum loop count. Last position: ${current}`); + } + + lastCurrent = current; + const node = walk(); - if (node !== null && node !== undefined) { + if (node) { ast.body.push(node); } + + // Skip semicolons + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + } } - + return ast; } -// Interpreter +/** + * 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. + * + * @how 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. + * + * @why This approach allows for first-class functions, closures, and lexical + * scoping, while keeping the implementation simple. The interpreter supports + * both synchronous and asynchronous IO operations, returning Promises when necessary. + * + * @note 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. + */ function interpreter(ast) { - debugLog('Starting interpreter with AST', ast); - let globalScope = {}; + const globalScope = {}; initializeStandardLibrary(globalScope); - + + // 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. + */ 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 'BooleanLiteral': - return node.value; - 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; + 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 { - // For other key types (numbers, strings), evaluate normally - key = evalNode(entry.key); + // 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; } - const value = evalNode(entry.value); - table[key] = value; } - } - - return table; - case 'TableAccess': - const tableValue = evalNode(node.table); - const 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': - 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]; + + 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': + 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 'Assignment': + if (globalScope.hasOwnProperty(node.identifier)) { + throw new Error(`Cannot reassign immutable variable: ${node.identifier}`); } - // Create a new evalNode function that uses localScope - const localEvalNode = (node) => { - if (!node) { - return undefined; + 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(); } - 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); + }; + 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; // Renamed from 'func' to avoid redeclaration + if (typeof node.name === 'string') { + // Regular function call with string name + funcToCall = globalScope[node.name]; + } else if (node.name.type === 'Identifier') { + // Function call with identifier + funcToCall = globalScope[node.name.value]; + } else if (node.name.type === 'TableAccess') { + // Function call from table access (e.g., math.add) + funcToCall = evalNode(node.name); + } else { + throw new Error('Invalid function name in function call'); + } - // 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]; + if (funcToCall instanceof Function) { + let args = node.args.map(evalNode); + return funcToCall(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'CaseExpression': + const values = node.value.map(evalNode); + + for (const caseItem of node.cases) { + const pattern = caseItem.pattern.map(evalNode); - // Wildcard always matches - if (patternValue === true) continue; + 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; + } + } - // Otherwise, values must be equal - 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; + 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(); + } + } + + /** + * 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. + */ + const localEvalNodeWithScope = (node, scope) => { + callStackTracker.push('localEvalNodeWithScope', 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 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; - 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]; + 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); + } + const value = localEvalNodeWithScope(entry.value, scope); + table[key] = value; } - // Otherwise join multiple results as string - return results.join(' '); } - } - 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 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': + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + 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]; } - - 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 if (node.name.type === 'TableAccess') { + // Function call from table access (e.g., math.add) + localFunc = localEvalNodeWithScope(node.name, scope); + } else { + throw new Error('Invalid function name in function call'); } - }; - 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]; + 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 'CaseExpression': + const values = node.value.map(val => localEvalNodeWithScope(val, scope)); + + for (const caseItem of node.cases) { + const pattern = caseItem.pattern.map(pat => localEvalNodeWithScope(pat, scope)); - // Wildcard always matches - if (patternValue === true) continue; + 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; + } + } - // Otherwise, values must be equal - if (value !== patternValue) { - matches = false; - break; + 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 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}`); + } + } finally { + callStackTracker.pop(); + } + }; + + /** + * 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. Used to avoid circular dependencies. + */ + 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'); + } + 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; - 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]; + 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; } - // Otherwise join multiple results as string - return results.join(' '); } - } - 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 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': + 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': + // 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]; + } + 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(); + } + }; + 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 if (node.name.type === 'TableAccess') { + // Function call from table access (e.g., math.add) + localFunc = localEvalNode(node.name); + } else { + throw new Error('Invalid function name in function call'); + } + + 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 '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; + case 'IOAssertExpression': + const localAssertionValue = localEvalNode(node.value); + 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 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; @@ -2390,453 +2733,233 @@ 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. + * + * @why 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. + * + * @why 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'); +/** + * 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. + */ +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); + + // Track maximum depth + if (this.stack.length > this.maxDepth) { + this.maxDepth = this.stack.length; + } + + // Count function calls + const key = `${functionName}${context ? `:${context}` : ''}`; + this.callCounts.set(key, (this.callCounts.get(key) || 0) + 1); + + // 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}`); + } + + 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(); + } +}; /** - * Execute a script file + * Reads a file, tokenizes, parses, and interprets it. + * + * @param {string} filePath - Path to the file to execute + * @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. + * + * @why This function is the entry point for file execution, handling all stages + * of the language pipeline. Debug output is provided at each stage for + * transparency and troubleshooting. + * + * @note Supports both synchronous and asynchronous execution, with proper + * error handling and process exit codes. */ function executeFile(filePath) { try { - debugLog(`Executing file: ${filePath}`); + // Validate file extension + if (!filePath.endsWith('.txt')) { + throw new Error('Only .txt files are supported'); + } + + const fs = require('fs'); + const input = fs.readFileSync(filePath, 'utf8'); - const input = fs.readFileSync(filePath, 'utf-8'); - debugLog('Input content', input); + debugLog('Input:', input); const tokens = lexer(input); - debugLog('Tokens generated', tokens); + debugLog('Tokens:', tokens); const ast = parser(tokens); - debugLog('AST generated', ast); + debugLog('AST:', JSON.stringify(ast, null, 2)); const result = interpreter(ast); - debugLog('Interpreter result', result); - // 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); + } + // 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); + } + // 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) { - debugError('Execution failed', 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); } } /** - * Start REPL mode - */ -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 = {}; - initializeStandardLibrary(globalScope); - let currentInput = ''; - - rl.prompt(); - - rl.on('line', (line) => { - const trimmedLine = line.trim(); - - if (trimmedLine === 'exit' || trimmedLine === 'quit') { - rl.close(); - return; - } - - // Add the line to current input - currentInput += line + '\n'; - - // 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); - } - } catch (error) { - console.error(`Error: ${error.message}`); - } - - // Reset current input for next expression - currentInput = ''; - } - - rl.prompt(); - }); - - rl.on('close', () => { - console.log('Goodbye!'); - process.exit(0); - }); -} - -/** - * 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. + * + * @why The language is designed for file execution only (no REPL), so the CLI + * enforces this usage and provides helpful error messages for incorrect invocation. + * + * @note 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 '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); - const 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': - 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; - case 'BooleanLiteral': - return node.value; - 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; - - default: - throw new Error(`Unknown node type: ${node.type}`); - } - } - - 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; - 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; - 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; - }); - } - - return lastResult; -} - -// Check command line arguments const args = process.argv.slice(2); if (args.length === 0) { - // No arguments - start REPL mode - startREPL(); + console.error('Usage: node lang.js <file>'); + console.error(' Provide a file path to execute'); + process.exit(1); } else if (args.length === 1) { - // One argument - execute the file + // 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'); + console.error('Usage: node lang.js <file>'); + console.error(' Provide exactly one file path to execute'); process.exit(1); } \ No newline at end of file |