diff options
Diffstat (limited to 'js/scripting-lang/lang.js')
-rw-r--r-- | js/scripting-lang/lang.js | 3215 |
1 files changed, 1489 insertions, 1726 deletions
diff --git a/js/scripting-lang/lang.js b/js/scripting-lang/lang.js index b66271c..d3bc0b5 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -1,14 +1,40 @@ +// Cross-platform scripting language implementation +// Supports Node.js, Bun, and browser environments + +import { lexer, TokenType } from './lexer.js'; +import { parser } from './parser.js'; + /** * Initializes the standard library in the provided scope. * - * 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. + * @param {Object} scope - The global scope object to inject functions into + * @description Injects higher-order functions and combinator functions into the interpreter's global scope. + * These functions provide functional programming utilities and implement the combinator foundation + * that eliminates parsing ambiguity by translating all operations to function calls. + * + * The standard library includes: + * - Higher-order functions (map, compose, pipe, apply, filter, reduce, fold, curry) + * - Arithmetic combinators (add, subtract, multiply, divide, modulo, power, negate) + * - Comparison combinators (equals, notEquals, lessThan, greaterThan, lessEqual, greaterEqual) + * - Logical combinators (logicalAnd, logicalOr, logicalXor, logicalNot) + * - Enhanced combinators (identity, constant, flip, on, both, either) * - * 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. + * This approach ensures that user code can access these functions as if they were built-in, + * without special syntax or reserved keywords. The combinator foundation allows the parser + * to translate all operators to function calls, eliminating ambiguity while preserving syntax. + * + * Functions are written to check argument types at runtime since the language is dynamically + * typed and does not enforce arity or types at parse time. */ function initializeStandardLibrary(scope) { - // Map: Apply a function to 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 { @@ -16,14 +42,19 @@ 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') { if (arguments.length === 3) { - // compose f g x = f(g(x)) return f(g(x)); } else { - // compose f g = function that takes x and returns f(g(x)) return function(x) { return f(g(x)); }; @@ -33,8 +64,14 @@ function initializeStandardLibrary(scope) { } }; - // 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); @@ -43,7 +80,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); @@ -52,15 +95,19 @@ 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') { if (arguments.length === 3) { - // pipe f g x = g(f(x)) return g(f(x)); } else { - // pipe f g = function that takes x and returns g(f(x)) return function(x) { return g(f(x)); }; @@ -70,8 +117,13 @@ function initializeStandardLibrary(scope) { } }; - // 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; @@ -80,8 +132,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); @@ -90,7 +148,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); @@ -98,1776 +163,1271 @@ function initializeStandardLibrary(scope) { throw new Error('fold: first argument must be a function'); } }; -} - -/** - * TokenType is a flat object, not an enum, to allow 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', - MINUS: 'MINUS', - MULTIPLY: 'MULTIPLY', - DIVIDE: 'DIVIDE', - IDENTIFIER: 'IDENTIFIER', - ASSIGNMENT: 'ASSIGNMENT', - ARROW: 'ARROW', - CASE: 'CASE', - OF: 'OF', - WILDCARD: 'WILDCARD', - FUNCTION: 'FUNCTION', - LEFT_PAREN: 'LEFT_PAREN', - RIGHT_PAREN: 'RIGHT_PAREN', - LEFT_BRACE: 'LEFT_BRACE', - RIGHT_BRACE: 'RIGHT_BRACE', - LEFT_BRACKET: 'LEFT_BRACKET', - RIGHT_BRACKET: 'RIGHT_BRACKET', - SEMICOLON: 'SEMICOLON', - COMMA: 'COMMA', - DOT: 'DOT', - STRING: 'STRING', - 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', - MODULO: 'MODULO', - POWER: 'POWER', - IO_IN: 'IO_IN', - IO_OUT: 'IO_OUT', - IO_ASSERT: 'IO_ASSERT', - FUNCTION_REF: 'FUNCTION_REF' -}; - -/** - * Lexer: Converts source code to tokens. - * - * How: Uses a single pass with a while loop and manual character inspection. Handles whitespace, comments (with nesting), numbers (including decimals), strings, identifiers/keywords, and both single- and multi-character operators. - * - * 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. - * - * Notably, 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) { - let current = 0; - const tokens = []; - while (current < input.length) { - let char = input[current]; - - // Skip whitespace - if (/\s/.test(char)) { - current++; - continue; - } - - // Skip comments - if (char === '/' && input[current + 1] === '*') { - let commentDepth = 1; - current += 2; // Skip /* - - while (current < input.length && commentDepth > 0) { - if (input[current] === '/' && input[current + 1] === '*') { - commentDepth++; - current += 2; - } else if (input[current] === '*' && input[current + 1] === '/') { - commentDepth--; - current += 2; - } else { - current++; - } - } - continue; - } - - // Numbers - if (/[0-9]/.test(char)) { - let value = ''; - while (current < input.length && /[0-9]/.test(input[current])) { - value += input[current]; - current++; - } - - // 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.NUMBER, - value: parseFloat(value) - }); - } else { - tokens.push({ - type: TokenType.NUMBER, - value: parseInt(value) - }); - } - continue; - } - - // Strings - if (char === '"') { - let value = ''; - current++; // Skip opening quote - - while (current < input.length && input[current] !== '"') { - value += input[current]; - current++; - } - - if (current < input.length) { - current++; // Skip closing quote - tokens.push({ - type: TokenType.STRING, - value: value - }); - } else { - throw new Error('Unterminated string'); - } - continue; - } - - // 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; + // ===== ARITHMETIC COMBINATORS ===== + + /** + * Add: Add two numbers + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Sum of x and y + */ + scope.add = function(x, y) { + return x + y; + }; + + /** + * Subtract: Subtract second number from first + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Difference of x and y + */ + scope.subtract = function(x, y) { + return x - y; + }; + + /** + * Multiply: Multiply two numbers + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Product of x and y + */ + scope.multiply = function(x, y) { + return x * y; + }; + + /** + * Divide: Divide first number by second + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Quotient of x and y + * @throws {Error} When second argument is zero + */ + scope.divide = function(x, y) { + if (y === 0) { + throw new Error('Division by zero'); } - - // 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 '..': - // Check for IO operations - 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; - } + return x / y; + }; + + /** + * Modulo: Get remainder of division + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Remainder of x divided by y + */ + scope.modulo = function(x, y) { + return x % y; + }; + + /** + * Power: Raise first number to power of second + * @param {number} x - Base number + * @param {number} y - Exponent + * @returns {number} x raised to the power of y + */ + scope.power = function(x, y) { + return Math.pow(x, y); + }; + + /** + * Negate: Negate a number + * @param {number} x - Number to negate + * @returns {number} Negated value of x + */ + scope.negate = function(x) { + return -x; + }; + + // ===== COMPARISON COMBINATORS ===== + + /** + * Equals: Check if two values are equal + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x equals y + */ + scope.equals = function(x, y) { + return x === y; + }; + + /** + * NotEquals: Check if two values are not equal + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x does not equal y + */ + scope.notEquals = function(x, y) { + return x !== y; + }; + + /** + * LessThan: Check if first value is less than second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x < y + */ + scope.lessThan = function(x, y) { + return x < y; + }; + + /** + * GreaterThan: Check if first value is greater than second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x > y + */ + scope.greaterThan = function(x, y) { + return x > y; + }; + + /** + * LessEqual: Check if first value is less than or equal to second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x <= y + */ + scope.lessEqual = function(x, y) { + return x <= y; + }; + + /** + * GreaterEqual: Check if first value is greater than or equal to second + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if x >= y + */ + scope.greaterEqual = function(x, y) { + return x >= y; + }; + + // ===== LOGICAL COMBINATORS ===== + + /** + * LogicalAnd: Logical AND of two values + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if both x and y are truthy + */ + scope.logicalAnd = function(x, y) { + return !!(x && y); + }; + + /** + * LogicalOr: Logical OR of two values + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if either x or y is truthy + */ + scope.logicalOr = function(x, y) { + return !!(x || y); + }; + + /** + * LogicalXor: Logical XOR of two values + * @param {*} x - First value + * @param {*} y - Second value + * @returns {boolean} True if exactly one of x or y is truthy + */ + scope.logicalXor = function(x, y) { + return !!((x && !y) || (!x && y)); + }; + + /** + * LogicalNot: Logical NOT of a value + * @param {*} x - Value to negate + * @returns {boolean} True if x is falsy, false if x is truthy + */ + scope.logicalNot = function(x) { + return !x; + }; + + // ===== ASSIGNMENT COMBINATOR ===== + + /** + * Assign: Assign a value to a variable name + * @param {string} name - Variable name + * @param {*} value - Value to assign + * @returns {*} The assigned value + * @throws {Error} When trying to reassign an immutable variable + * @note This function needs access to the global scope, so it will be + * set up during interpreter initialization + */ + // Note: assign will be set up in the interpreter with access to globalScope + + // ===== ENHANCED HIGHER-ORDER COMBINATORS ===== + + /** + * Identity: Return the input unchanged + * @param {*} x - Any value + * @returns {*} The same value + */ + scope.identity = function(x) { + return x; + }; + + /** + * Constant: Create a function that always returns the same value + * @param {*} x - Value to return + * @param {*} [y] - Optional second argument (ignored) + * @returns {*} The value x, or a function if only one argument provided + */ + scope.constant = function(x, y) { + if (arguments.length === 2) { + return x; + } else { + return function(y) { + return x; + }; } - - // 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}`); + }; + + /** + * Flip: Flip the order of arguments for a binary function + * @param {Function} f - Binary function + * @param {*} [x] - Optional first argument + * @param {*} [y] - Optional second argument + * @returns {Function|*} Function with flipped argument order, or result if arguments provided + */ + scope.flip = function(f, x, y) { + if (arguments.length === 3) { + return f(y, x); + } else { + return function(x, y) { + return f(y, x); + }; } - - current++; - } + }; + + /** + * On: Apply a function to the results of another function + * @param {Function} f - Outer function + * @param {Function} g - Inner function + * @returns {Function} Function that applies f to the results of g + */ + scope.on = function(f, g) { + return function(x, y) { + return f(g(x), g(y)); + }; + }; + + /** + * Both: Check if both predicates are true + * @param {Function} f - First predicate + * @param {Function} g - Second predicate + * @returns {Function} Function that returns true if both predicates are true + */ + scope.both = function(f, g) { + return function(x) { + return f(x) && g(x); + }; + }; - return tokens; + /** + * Either: Check if either predicate is true + * @param {Function} f - First predicate + * @param {Function} g - Second predicate + * @returns {Function} Function that returns true if either predicate is true + */ + scope.either = function(f, g) { + return function(x) { + return f(x) || g(x); + }; + }; } /** - * Parser: Converts tokens to an Abstract Syntax Tree (AST). + * 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. * - * 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. + * @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. * - * 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. + * The interpreter implements a combinator-based architecture where all operations + * are translated to function calls to standard library combinators. This eliminates + * parsing ambiguity while preserving the original syntax. The parser generates + * FunctionCall nodes for operators (e.g., x + y becomes add(x, y)), and the + * interpreter executes these calls using the combinator functions in the global scope. * - * The parser also 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. + * The interpreter uses a global scope for variable storage and function definitions. + * Each function call creates a new scope (using prototypal inheritance) to implement + * lexical scoping. Immutability is enforced by preventing reassignment in the + * global scope. + * + * The interpreter is split into three functions: evalNode (global), + * localEvalNodeWithScope (for function bodies), and localEvalNode (for internal + * recursion). This separation allows for correct scope handling and easier debugging. + * + * Recursive function support is implemented using a forward declaration pattern: + * a placeholder function is created in the global scope before evaluation, allowing + * the function body to reference itself during evaluation. */ -function parser(tokens) { - let current = 0; +function interpreter(ast) { + const globalScope = {}; + initializeStandardLibrary(globalScope); - function walk() { - function parseChainedDotAccess(tableExpr) { - /** - * Handles chained dot access (e.g., table.key.subkey). - * - * 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 (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; - } + // Debug: Check if combinators are available + if (process.env.DEBUG) { + console.log('[DEBUG] Available functions in global scope:', Object.keys(globalScope)); + console.log('[DEBUG] add function exists:', typeof globalScope.add === 'function'); + console.log('[DEBUG] subtract function exists:', typeof globalScope.subtract === 'function'); + } + + // Reset call stack tracker at the start of interpretation + callStackTracker.reset(); + + /** + * Evaluates AST nodes in the global scope. + * + * @param {Object} node - AST node to evaluate + * @returns {*} The result of evaluating the node + * @throws {Error} For evaluation errors + * + * @description Main evaluation function that handles all node types in the + * global scope context. This function processes the core language constructs + * and delegates to combinator functions for all operations. + * + * The function implements the forward declaration pattern for recursive functions: + * when a function assignment is detected, a placeholder is created in the global + * scope before evaluation, allowing the function body to reference itself. + */ + function evalNode(node) { + callStackTracker.push('evalNode', node?.type || 'unknown'); - function parseChainedTableAccess(tableExpr) { - /** - * Handles chained bracket and dot access (e.g., 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); + 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; - return access; - } else { - throw new Error('Expected closing bracket'); - } - } - - // 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; - } - - function detectAmbiguousFunctionCalls() { - // This is a placeholder for future ambiguous function call detection - // For now, we'll assume the parser handles function calls correctly - } - - function parseFunctionCall(functionName) { - /** - * Parses function calls with arbitrary argument lists. - * - * 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. - */ - 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 + 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); } - }); - current++; // Skip the number - } else { - // More complex unary minus expression - args.push({ - type: 'UnaryMinusExpression', - operand: parsePrimary() - }); + const value = evalNode(entry.value); + table[key] = value; + } } - } else { - // Regular argument parsing - use parseExpression to avoid circular dependency - args.push(parseExpression()); - } - } - - return { - type: 'FunctionCall', - name: functionName, - args: args - }; - } - - function parseLogicalExpression() { - /** - * Parses logical expressions with lowest precedence. - * - * 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; - } - - function parseExpression() { - /** - * Parses expressions with left-associative binary operators. - * - * 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. - */ - 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; - } - - function parseTerm() { - /** - * Parses multiplication, division, and modulo operations. - * - * 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; - } - - function parseFactor() { - /** - * Parses exponentiation and primary expressions. - * - * 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; - } - - function parsePrimary() { - /** - * Parses literals, identifiers, function definitions, assignments, table literals, case expressions, IO operations, and parenthesized expressions. - * - * Why: This function is the core of the recursive descent parser, handling all atomic and context-sensitive constructs. Special care is taken to distinguish between assignments and function definitions using lookahead, and to parse multi-parameter case expressions and function calls. - * - * The parser avoids circular recursion by handling IO and case expressions at the top level, and by using explicit checks for each construct. - */ - const token = tokens[current]; - - if (token.type === TokenType.CASE) { - current++; // Skip 'case' - - // Parse the values being matched (can be multiple) - const values = []; - while (current < tokens.length && tokens[current].type !== TokenType.OF) { - // Parse simple expressions (identifiers, numbers, etc.) - 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++; + + 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 more complex expressions, fall back to parseLogicalExpression - const value = parseLogicalExpression(); - 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) { - const patterns = []; - while (current < tokens.length && - tokens[current].type !== TokenType.ASSIGNMENT && - tokens[current].type !== TokenType.SEMICOLON) { - patterns.push(parseLogicalExpression()); + // For bracket notation, evaluate the key expression + keyValue = evalNode(node.key); } - // Expect ':' after pattern - if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { - current++; // Skip ':' - } else { - throw new Error('Expected ":" after pattern in case expression'); + if (typeof tableValue !== 'object' || tableValue === null) { + throw new Error('Cannot access property of non-table value'); } - const result = parseLogicalExpression(); - cases.push({ - pattern: patterns, - result: [result] - }); - } - - return { - type: 'CaseExpression', - value: values, - cases, - }; - } - - 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 }; - } - - if (token.type === TokenType.NUMBER) { - current++; - return { - type: 'NumberLiteral', - value: token.value - }; - } - - if (token.type === TokenType.STRING) { - current++; - return { - type: 'StringLiteral', - value: token.value - }; - } - - if (token.type === TokenType.TRUE) { - current++; - return { - type: 'BooleanLiteral', - value: true - }; - } - - if (token.type === TokenType.FALSE) { - current++; - return { - type: 'BooleanLiteral', - value: false - }; - } - - 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'); - } - } - - if (token.type === TokenType.IDENTIFIER) { - const identifier = { - type: 'Identifier', - value: token.value - }; - current++; - - // Check if this is an assignment - if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { - current++; // Skip ':' + if (tableValue[keyValue] === undefined) { + throw new Error(`Key '${keyValue}' not found in table`); + } - // Check if this is a function definition - let isFunction = false; - let params = []; + return tableValue[keyValue]; + case 'AssignmentExpression': + // Prevent reassignment of standard library functions + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } - // Look ahead to see if this is a function definition - let lookAhead = current; - while (lookAhead < tokens.length && - tokens[lookAhead].type !== TokenType.ARROW && - tokens[lookAhead].type !== TokenType.SEMICOLON) { - if (tokens[lookAhead].type === TokenType.IDENTIFIER) { - params.push(tokens[lookAhead].value); - } - lookAhead++; + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.name} is not yet fully defined`); + }; + + // Store the placeholder in global scope + globalScope[node.name] = placeholder; + + // Now evaluate the function definition with access to the placeholder + const actualFunction = evalNode(node.value); + + // Replace the placeholder with the actual function + globalScope[node.name] = actualFunction; + return; } - if (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.ARROW) { - isFunction = true; + const value = evalNode(node.value); + globalScope[node.name] = value; + return; + case 'Assignment': + // Prevent reassignment of standard library functions + if (globalScope.hasOwnProperty(node.identifier)) { + throw new Error(`Cannot reassign immutable variable: ${node.identifier}`); } - if (isFunction) { - // Clear params array and parse function parameters - params = []; - while (current < tokens.length && tokens[current].type !== TokenType.ARROW) { - if (tokens[current].type === TokenType.IDENTIFIER) { - params.push(tokens[current].value); - } - current++; - } + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.identifier} is not yet fully defined`); + }; - current++; // Skip '->' + // Store the placeholder in global scope + globalScope[node.identifier] = placeholder; - // Parse the function body (which could be a case expression or other expression) - const functionBody = parseLogicalExpression(); + // Now evaluate the function definition with access to the placeholder + const actualFunction = evalNode(node.value); - return { - type: 'AssignmentExpression', - name: identifier.value, - value: { - type: 'FunctionDeclaration', - name: null, // Anonymous function - params, - body: functionBody, - } - }; - } else { - // Regular assignment - const value = parseLogicalExpression(); - return { - type: 'AssignmentExpression', - name: identifier.value, - value: value - }; + // Replace the placeholder with the actual function + globalScope[node.identifier] = actualFunction; + return; } - } - - // Check if this is table access - if (current < tokens.length && - (tokens[current].type === TokenType.LEFT_BRACKET || - tokens[current].type === TokenType.DOT)) { - return parseChainedTableAccess(identifier); - } - - // 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 || - tokens[current].type === TokenType.FUNCTION_REF)) { - return parseFunctionCall(identifier); - } - - // Special case: Check for function call with unary minus argument - // This handles cases like "isPositive -3" where -3 should be a separate argument - // Only trigger if the identifier looks like a function name (contains letters) - if (current < tokens.length && tokens[current].type === TokenType.MINUS) { - // Look ahead to see if we have MINUS NUMBER - if (current + 1 < tokens.length && tokens[current + 1].type === TokenType.NUMBER) { - // Check if the identifier looks like a function name (not a simple variable like 'n') - if (identifier.value.length > 1 && /[a-zA-Z]/.test(identifier.value)) { - return parseFunctionCall(identifier); - } + + 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 identifier; - } - - if (token.type === TokenType.FUNCTION_REF) { - current++; // Skip '@' - if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { - const funcName = tokens[current].value; - current++; - return { - type: 'FunctionReference', - name: funcName + 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(); + } }; - } else { - throw new Error('Expected function name after @'); - } - } - - if (token.type === TokenType.WILDCARD) { - current++; // Skip '_' - return { type: 'WildcardPattern' }; - } - - - - - - // If we get here, it's an operator token that should be handled by parseExpression - // But we need to handle it here to avoid circular dependency - if (token.type === TokenType.LEFT_BRACE) { - - current++; // Skip '{' - const entries = []; - let arrayIndex = 1; - - while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { - // Skip leading commas - if (tokens[current].type === TokenType.COMMA) { - current++; - continue; + case 'FunctionDefinition': + // Create a function from the function definition + return function(...args) { + callStackTracker.push('FunctionCall', node.parameters.join(',')); + try { + let localScope = Object.create(globalScope); + for (let i = 0; i < node.parameters.length; i++) { + localScope[node.parameters[i]] = args[i]; + } + return localEvalNodeWithScope(node.body, localScope); + } finally { + callStackTracker.pop(); + } + }; + case 'FunctionCall': + let funcToCall; + if (typeof node.name === 'string') { + // Regular function call with string name + funcToCall = globalScope[node.name]; + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: looking up function '${node.name}' in globalScope, found:`, typeof funcToCall); + } + } else if (node.name.type === 'Identifier') { + // Function call with identifier + funcToCall = globalScope[node.name.value]; + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: looking up function '${node.name.value}' in globalScope, found:`, typeof funcToCall); + } + } else { + // Function call from expression (e.g., parenthesized function, higher-order) + funcToCall = evalNode(node.name); + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: evaluated function expression, found:`, typeof funcToCall); + } } - let key = null; - let value; + if (funcToCall instanceof Function) { + let args = node.args.map(evalNode); + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: calling function with args:`, args); + } + return funcToCall(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'WhenExpression': + // Handle both single values and arrays of values + const whenValues = Array.isArray(node.value) + ? node.value.map(evalNode) + : [evalNode(node.value)]; - // Check if this is a key-value pair or just a value + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: whenValues =`, whenValues); + } - // Check if this is a key-value pair or just a value - if (current + 1 < tokens.length && tokens[current + 1].type === TokenType.ASSIGNMENT) { - // This is a key-value pair: key: value - 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 if (tokens[current].type === TokenType.TRUE) { - key = { - type: 'BooleanLiteral', - value: true, - }; - current++; // Skip the key - } else if (tokens[current].type === TokenType.FALSE) { - key = { - type: 'BooleanLiteral', - value: false, - }; - current++; // Skip the key - + for (const caseItem of node.cases) { + // Handle both single patterns and arrays of patterns + const patterns = caseItem.pattern.map(evalNode); + + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: patterns =`, patterns); + } + + // Check if patterns match the values + let matches = true; + if (whenValues.length !== patterns.length) { + matches = false; } else { - throw new Error('Invalid key type in table literal'); + for (let i = 0; i < whenValues.length; i++) { + const value = whenValues[i]; + const pattern = patterns[i]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: comparing value ${value} with pattern ${pattern}`); + } + + if (pattern === true) { // Wildcard pattern + // Wildcard always matches + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: wildcard matches`); + } + continue; + } else if (value !== pattern) { + matches = false; + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: pattern does not match`); + } + break; + } else { + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: pattern matches`); + } + } + } } - current++; // Skip ':' - value = parseExpression(); - } else { - // This is just a value (array-like entry) - value = parseExpression(); + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: case matches = ${matches}`); + } + + if (matches) { + const results = caseItem.result.map(evalNode); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); + } } + 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 + }); - entries.push({ key, value }); - - - // Skip trailing commas - if (current < tokens.length && tokens[current].type === TokenType.COMMA) { - current++; + 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'); } - } - - if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACE) { - current++; // Skip '}' - return { - type: 'TableLiteral', - entries: entries - }; - } else { - throw new Error('Expected closing brace'); - } - } - - // If we get here, it's an operator token that should be handled by parseExpression - // But we need to handle it here to avoid circular dependency - if (token.type === TokenType.PLUS || - token.type === TokenType.MINUS || - token.type === TokenType.MULTIPLY || - token.type === TokenType.DIVIDE || - token.type === TokenType.MODULO || - token.type === TokenType.POWER || - token.type === TokenType.EQUALS || - token.type === TokenType.NOT_EQUAL || - token.type === TokenType.LESS_THAN || - token.type === TokenType.GREATER_THAN || - token.type === TokenType.LESS_EQUAL || - token.type === TokenType.GREATER_EQUAL || - token.type === TokenType.AND || - token.type === TokenType.OR || - token.type === TokenType.XOR) { - // Reset current to parse the expression properly - return parseExpression(); - } - - // If we get here, we have an unexpected token - - throw new Error(`Unexpected token in parsePrimary: ${token.type}`); - } - - // Check for IO operations before calling parsePrimary - 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 case expressions at top level - if (tokens[current].type === TokenType.CASE) { - current++; // Skip 'case' - - // Parse the values being matched (can be multiple) - const values = []; - while (current < tokens.length && tokens[current].type !== TokenType.OF) { - const value = parseLogicalExpression(); - 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) { - const patterns = []; - while (current < tokens.length && - tokens[current].type !== TokenType.ASSIGNMENT && - tokens[current].type !== TokenType.SEMICOLON) { - patterns.push(parseLogicalExpression()); - } - - // 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] - }); + 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}`); } - - return { - type: 'CaseExpression', - value: values, - cases, - }; + } finally { + callStackTracker.pop(); } - - // Simple wrapper that calls parseLogicalExpression for all token types - return parseLogicalExpression(); } - - const ast = { - type: 'Program', - 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) { - ast.body.push(node); - } - - // Skip semicolons - if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { - current++; - } - } - - return ast; -} -/** - * Interpreter: Walks the AST and evaluates each node. - * - * 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. - * - * 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) { - const globalScope = {}; - initializeStandardLibrary(globalScope); - - function evalNode(node) { - if (!node) { - return undefined; - } - switch (node.type) { - case 'NumberLiteral': - return parseFloat(node.value); - case 'StringLiteral': - return node.value; - case 'BooleanLiteral': - return node.value; - case 'PlusExpression': - return evalNode(node.left) + evalNode(node.right); - case 'MinusExpression': - return evalNode(node.left) - evalNode(node.right); - case 'MultiplyExpression': - return evalNode(node.left) * evalNode(node.right); - case 'DivideExpression': - const divisor = evalNode(node.right); - if (divisor === 0) { - throw new Error('Division by zero'); - } - return evalNode(node.left) / evalNode(node.right); - case 'ModuloExpression': - return evalNode(node.left) % evalNode(node.right); - case 'PowerExpression': - return Math.pow(evalNode(node.left), evalNode(node.right)); - case 'EqualsExpression': - return evalNode(node.left) === evalNode(node.right); - case 'LessThanExpression': - return evalNode(node.left) < evalNode(node.right); - case 'GreaterThanExpression': - return evalNode(node.left) > evalNode(node.right); - case 'LessEqualExpression': - return evalNode(node.left) <= evalNode(node.right); - case 'GreaterEqualExpression': - return evalNode(node.left) >= evalNode(node.right); - case 'NotEqualExpression': - return evalNode(node.left) !== evalNode(node.right); - case 'AndExpression': - return !!(evalNode(node.left) && evalNode(node.right)); - case 'OrExpression': - return !!(evalNode(node.left) || evalNode(node.right)); - case 'XorExpression': - const leftVal = evalNode(node.left); - const rightVal = evalNode(node.right); - return !!((leftVal && !rightVal) || (!leftVal && rightVal)); - case 'NotExpression': - return !evalNode(node.operand); - case 'UnaryMinusExpression': - return -evalNode(node.operand); - case 'TableLiteral': - const table = {}; - let arrayIndex = 1; - - for (const entry of node.entries) { - if (entry.key === null) { - // Array-like entry: {1, 2, 3} - table[arrayIndex] = evalNode(entry.value); - arrayIndex++; - } else { - // Key-value entry: {name: "Alice", age: 30} - let key; - if (entry.key.type === 'Identifier') { - // Convert identifier keys to strings - key = entry.key.value; + /** + * Evaluates AST nodes in a local scope with access to parent scope. + * + * @param {Object} node - AST node to evaluate + * @param {Object} scope - Local scope object (prototypally inherits from global) + * @returns {*} The result of evaluating the node + * @throws {Error} For evaluation errors + * + * @description Used for evaluating function bodies and other expressions + * that need access to both local and global variables. This function implements + * lexical scoping by creating a local scope that prototypally inherits from + * the global scope, allowing access to both local parameters and global functions. + * + * The function handles the same node types as evalNode but uses the local scope + * for variable lookups. It also implements the forward declaration pattern for + * recursive functions, ensuring that function definitions can reference themselves + * during evaluation. + * + * This separation of global and local evaluation allows for proper scope management + * and prevents variable name conflicts between function parameters and global variables. + */ + const localEvalNodeWithScope = (node, scope) => { + callStackTracker.push('localEvalNodeWithScope', node?.type || 'unknown'); + + try { + if (!node) { + return undefined; + } + 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; + + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = localEvalNodeWithScope(entry.value, scope); + 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 = localEvalNodeWithScope(entry.key, scope); + } + const value = localEvalNodeWithScope(entry.value, scope); + table[key] = value; } - const value = evalNode(entry.value); - table[key] = value; } - } - - return table; - case 'TableAccess': - const tableValue = evalNode(node.table); - let keyValue; - - // Handle different key types - if (node.key.type === 'Identifier') { - // For dot notation, use the identifier name as the key - keyValue = node.key.value; - } else { - // For bracket notation, evaluate the key expression - keyValue = evalNode(node.key); - } - - if (typeof tableValue !== 'object' || tableValue === null) { - throw new Error('Cannot access property of non-table value'); - } - - if (tableValue[keyValue] === undefined) { - throw new Error(`Key '${keyValue}' not found in table`); - } - - return tableValue[keyValue]; - case 'AssignmentExpression': - 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': - // 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) { - 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 = 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 localEvalNodeWithScope(node.body, localScope); - }; - 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'); - } - - 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); - let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; + return tableValue[keyValue]; + case 'AssignmentExpression': + // Prevent reassignment of standard library functions + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); + } + + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.name} is not yet fully defined`); + }; - if (patternValue === true) continue; + // Store the placeholder in global scope + globalScope[node.name] = placeholder; - if (value !== patternValue) { - matches = false; - break; - } + // Now evaluate the function definition with access to the placeholder + const actualFunction = localEvalNodeWithScope(node.value, scope); + + // Replace the placeholder with the actual function + globalScope[node.name] = actualFunction; + return; } - if (matches) { - const results = caseItem.result.map(evalNode); - if (results.length === 1) { - return results[0]; - } - return results.join(' '); + 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]; } - } - 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; - default: - throw new Error(`Unknown node type: ${node.type}`); - } - } - - const localEvalNodeWithScope = (node, scope) => { - 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; - - 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 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(); } - const value = localEvalNodeWithScope(entry.value, scope); - table[key] = value; + }; + 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 { + // Function call from expression (e.g., parenthesized function, higher-order) + localFunc = localEvalNodeWithScope(node.name, scope); } - } - - 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) { - let nestedScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - nestedScope[node.params[i]] = args[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 'WhenExpression': + // Handle both single values and arrays of values + const whenValues = Array.isArray(node.value) + ? node.value.map(val => localEvalNodeWithScope(val, scope)) + : [localEvalNodeWithScope(node.value, scope)]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: whenValues =`, whenValues); } - return localEvalNodeWithScope(node.body, nestedScope); - }; - 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'); - } - - 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)); - let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; + for (const caseItem of node.cases) { + // Handle both single patterns and arrays of patterns + const patterns = caseItem.pattern.map(pat => localEvalNodeWithScope(pat, scope)); - if (patternValue === true) continue; + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: patterns =`, patterns); + } - if (value !== patternValue) { + // Check if patterns match the values + let matches = true; + if (whenValues.length !== patterns.length) { matches = false; - break; + } else { + for (let i = 0; i < whenValues.length; i++) { + const value = whenValues[i]; + const pattern = patterns[i]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: comparing value ${value} with pattern ${pattern}`); + } + + if (pattern === true) { // Wildcard pattern + // Wildcard always matches + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: wildcard matches`); + } + continue; + } else if (value !== pattern) { + matches = false; + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern does not match`); + } + break; + } else { + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern matches`); + } + } + } } - } - - if (matches) { - const results = caseItem.result.map(res => localEvalNodeWithScope(res, scope)); - if (results.length === 1) { - return results[0]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: case matches = ${matches}`); + } + + if (matches) { + const results = caseItem.result.map(res => localEvalNodeWithScope(res, scope)); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); } - 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); + 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 }); - }); - 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; - default: - throw new Error(`Unknown node type: ${node.type}`); + + 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. This function is used to avoid circular + * dependencies and infinite recursion when evaluating nested expressions + * that need access to the global scope. + * + * This function duplicates the logic of evalNode but without the scope + * parameter, ensuring that all variable lookups go through the global scope. + * It's primarily used for evaluating function bodies and other expressions + * that need to be isolated from local scope contexts. + * + * The function also implements the forward declaration pattern for recursive + * functions, maintaining consistency with the other evaluation functions. + */ const localEvalNode = (node) => { - 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; - - 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; + 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; + + for (const entry of node.entries) { + if (entry.key === null) { + // Array-like entry: {1, 2, 3} + table[arrayIndex] = localEvalNode(entry.value); + arrayIndex++; } else { - // For other key types (numbers, strings), evaluate normally - key = localEvalNode(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 = localEvalNode(entry.key); + } + const value = localEvalNode(entry.value); + table[key] = value; } - const value = localEvalNode(entry.value); - table[key] = value; } - } - - 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) { - let nestedScope = Object.create(globalScope); - for (let i = 0; i < node.params.length; i++) { - nestedScope[node.params[i]] = args[i]; + + return table; + case 'TableAccess': + const tableValue = localEvalNode(node.table); + let keyValue; + + // Handle different key types + if (node.key.type === 'Identifier') { + // For dot notation, use the identifier name as the key + keyValue = node.key.value; + } else { + // For bracket notation, evaluate the key expression + keyValue = localEvalNode(node.key); + } + + if (typeof tableValue !== 'object' || tableValue === null) { + throw new Error('Cannot access property of non-table value'); + } + + if (tableValue[keyValue] === undefined) { + throw new Error(`Key '${keyValue}' not found in table`); + } + + return tableValue[keyValue]; + case 'AssignmentExpression': + // Prevent reassignment of standard library functions + if (globalScope.hasOwnProperty(node.name)) { + throw new Error(`Cannot reassign immutable variable: ${node.name}`); } - return localEvalNodeWithScope(node.body, nestedScope); - }; - 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]; + // Check if this is a function assignment for potential recursion + if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') { + // Create a placeholder function that will be replaced + let placeholder = function(...args) { + // This should never be called, but if it is, it means we have a bug + throw new Error(`Function ${node.name} is not yet fully defined`); + }; - if (patternValue === true) continue; + // Store the placeholder in global scope + globalScope[node.name] = placeholder; - if (value !== patternValue) { - matches = false; - break; + // Now evaluate the function definition with access to the placeholder + const actualFunction = localEvalNode(node.value); + + // Replace the placeholder with the actual function + globalScope[node.name] = actualFunction; + return; + } + + 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 { + // Function call from expression (e.g., parenthesized function, higher-order) + localFunc = localEvalNode(node.name); } - if (matches) { - const results = caseItem.result.map(localEvalNode); - if (results.length === 1) { - return results[0]; + if (localFunc instanceof Function) { + let args = node.args.map(localEvalNode); + return localFunc(...args); + } + throw new Error(`Function is not defined or is not callable`); + case 'WhenExpression': + // Handle both single values and arrays of values + const whenValues = Array.isArray(node.value) + ? node.value.map(localEvalNode) + : [localEvalNode(node.value)]; + + for (const caseItem of node.cases) { + // Handle both single patterns and arrays of patterns + const patterns = caseItem.pattern.map(localEvalNode); + + // Check if patterns match the values + let matches = true; + if (whenValues.length !== patterns.length) { + matches = false; + } else { + for (let i = 0; i < whenValues.length; i++) { + const value = whenValues[i]; + const pattern = patterns[i]; + + if (pattern === true) { // Wildcard pattern + // Wildcard always matches + continue; + } else if (value !== pattern) { + matches = false; + break; + } + } + } + + if (matches) { + const results = caseItem.result.map(localEvalNode); + if (results.length === 1) { + return results[0]; + } + return results.join(' '); } - 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); + 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 }); - }); - 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; - default: - throw new Error(`Unknown node type: ${node.type}`); + + 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(); } }; @@ -1888,9 +1448,18 @@ function interpreter(ast) { } /** - * Debug logging and error reporting. + * 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. + * 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 (process.env.DEBUG) { @@ -1902,9 +1471,18 @@ function debugLog(message, data = null) { } /** - * Debug logging and error reporting. + * Debug error logging utility function. * - * 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. + * @param {string} message - Debug error message to log + * @param {Error} [error=null] - Optional error object to log + * + * @description Logs debug error messages to console when DEBUG environment variable is set. + * Provides verbose error output during development while remaining silent in production. + * + * Debug functions are gated by the DEBUG environment variable, allowing for + * verbose output during development and silent operation in production. This + * approach makes it easy to trace execution and diagnose issues without + * cluttering normal output. */ function debugError(message, error = null) { if (process.env.DEBUG) { @@ -1916,14 +1494,163 @@ function debugError(message, error = null) { } /** - * Reads a file, tokenizes, parses, and interprets it. + * Call stack tracking for debugging recursion issues. + * + * @description Tracks function calls to help identify infinite recursion + * and deep call stacks that cause stack overflow errors. This is essential + * for debugging the interpreter's recursive evaluation of AST nodes. * - * 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. + * The tracker maintains a stack of function calls with timestamps and context + * information, counts function calls to identify hot paths, and detects + * potential infinite recursion by monitoring stack depth. + * + * This tool is particularly important for the combinator-based architecture + * where function calls are the primary execution mechanism, and complex + * nested expressions can lead to deep call stacks. */ -function executeFile(filePath) { +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(); + } +}; + +/** + * Cross-platform file I/O utility + * + * @param {string} filePath - Path to the file to read + * @returns {Promise<string>} File contents as a string + * @throws {Error} For file reading errors + * + * @description Handles file reading across different platforms (Node.js, Bun, browser) + * with appropriate fallbacks for each environment. This function is essential for + * the language's file execution model where scripts are loaded from .txt files. + * + * The function prioritizes ES modules compatibility by using dynamic import, + * but falls back to require for older Node.js versions. Browser environments + * are not supported for file I/O operations. + * + * This cross-platform approach ensures the language can run in various JavaScript + * environments while maintaining consistent behavior. + */ +async function readFile(filePath) { + // Check if we're in a browser environment + if (typeof window !== 'undefined') { + // Browser environment - would need to implement file input or fetch + throw new Error('File I/O not supported in browser environment'); + } + + // Node.js or Bun environment try { + // Try dynamic import for ES modules compatibility + const fs = await import('fs'); + return fs.readFileSync(filePath, 'utf8'); + } catch (error) { + // Fallback to require for older Node.js versions const fs = require('fs'); - const input = fs.readFileSync(filePath, 'utf8'); + return fs.readFileSync(filePath, 'utf8'); + } +} + +/** + * Reads a file, tokenizes, parses, and interprets it. + * + * @param {string} filePath - Path to the file to execute + * @returns {Promise<*>} The result of executing the file + * @throws {Error} For file reading, parsing, or execution errors + * + * @description Main entry point for file execution. Handles the complete language + * pipeline: file reading, lexical analysis, parsing, and interpretation. + * + * This function orchestrates the entire language execution process: + * 1. Reads the source file using cross-platform I/O utilities + * 2. Tokenizes the source code using the lexer + * 3. Parses tokens into an AST using the combinator-based parser + * 4. Interprets the AST using the combinator-based interpreter + * + * The function provides comprehensive error handling and debug output at each + * stage for transparency and troubleshooting. It also manages the call stack + * tracker to provide execution statistics and detect potential issues. + * + * Supports both synchronous and asynchronous execution, with proper + * error handling and process exit codes. + */ +async function executeFile(filePath) { + try { + // Validate file extension + if (!filePath.endsWith('.txt')) { + throw new Error('Only .txt files are supported'); + } + + const input = await readFile(filePath); debugLog('Input:', input); @@ -1940,39 +1667,75 @@ function executeFile(filePath) { 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) { 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); } } /** - * CLI argument handling. + * CLI argument handling and program entry point. * - * 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. + * @description Processes command line arguments and executes the specified file. + * Provides helpful error messages for incorrect usage. + * + * The language is designed for file execution only (no REPL), so the CLI + * enforces this usage and provides helpful error messages for incorrect invocation. + * The function validates that exactly one file path is provided and that the + * file has the correct .txt extension. + * + * Exits with appropriate error codes for different failure scenarios. */ -const args = process.argv.slice(2); +async function main() { + const args = process.argv.slice(2); -if (args.length === 0) { - console.error('Usage: node lang.js <file>'); - console.error(' Provide a file path to execute'); - process.exit(1); -} else if (args.length === 1) { - // Execute the file - const filePath = args[0]; - executeFile(filePath); -} else { - // Too many arguments - console.error('Usage: node lang.js <file>'); - console.error(' Provide exactly one file path to execute'); + if (args.length === 0) { + console.error('Usage: node lang.js <file>'); + console.error(' Provide a file path to execute'); + process.exit(1); + } else if (args.length === 1) { + // Execute the file + const filePath = args[0]; + await executeFile(filePath); + } else { + // Too many arguments + console.error('Usage: node lang.js <file>'); + console.error(' Provide exactly one file path to execute'); + process.exit(1); + } +} + +// Start the program +main().catch(error => { + console.error('Fatal error:', error.message); process.exit(1); -} \ No newline at end of file +}); \ No newline at end of file |