lang.js

// Baba Yaga
// Cross-platform scripting language implementation
// Supports Node.js, Bun, and browser environments

import { lexer, TokenType } from './lexer.js';
import { parser } from './parser.js';

// Cross-platform environment detection
const isNode = typeof process !== 'undefined' && process.versions && process.versions.node;
const isBun = typeof process !== 'undefined' && process.versions && process.versions.bun;
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';

// Cross-platform debug flag
const DEBUG = (isNode && process.env.DEBUG) || (isBrowser && window.DEBUG) || false;

// Cross-platform IO operations
const createReadline = () => {
    if (isNode || isBun) {
        const readline = require('readline');
        return readline.createInterface({
            input: process.stdin,
            output: process.stdout
        });
    } else if (isBrowser) {
        // Browser fallback - use prompt() for now
        return {
            question: (prompt, callback) => {
                const result = window.prompt(prompt);
                callback(result);
            },
            close: () => {}
        };
    } else {
        // Fallback for other environments
        return {
            question: (prompt, callback) => {
                callback("fallback input");
            },
            close: () => {}
        };
    }
};

const createFileSystem = () => {
    if (isNode || isBun) {
        return require('fs');
    } else if (isBrowser) {
        // Browser fallback - return a mock filesystem
        return {
            readFile: (path, encoding, callback) => {
                callback(new Error('File system not available in browser'));
            },
            writeFile: (path, data, callback) => {
                callback(new Error('File system not available in browser'));
            }
        };
    } else {
        // Fallback for other environments
        return {
            readFile: (path, encoding, callback) => {
                callback(new Error('File system not available in this environment'));
            },
            writeFile: (path, data, callback) => {
                callback(new Error('File system not available in this environment'));
            }
        };
    }
};

// Cross-platform console output
const safeConsoleLog = (message) => {
    if (typeof console !== 'undefined') {
        console.log(message);
    }
};

const safeConsoleError = (message) => {
    if (typeof console !== 'undefined') {
        console.error(message);
    }
};

// Cross-platform process exit
const safeExit = (code) => {
    if (isNode || isBun) {
        process.exit(code);
    } else if (isBrowser) {
        // In browser, we can't exit, but we can throw an error or redirect
        throw new Error(`Process would exit with code ${code}`);
    }
};

/**
 * Environment interface for external system integration
 * 
 * @typedef {Object} Environment
 * @property {Function} getCurrentState - Returns the current state from external system
 * @property {Function} emitValue - Sends a value to the external system
 */

/**
 * Initializes the standard library in the provided scope.
 * 
 * @param {Object} scope - The global scope object to inject functions into
 * @description Injects higher-order functions and combinator functions into the interpreter's global scope.
 * These functions provide functional programming utilities and implement the combinator foundation
 * that reduces parsing ambiguity by translating all operations to function calls.
 * 
 * The standard library includes:
 * - Higher-order functions (map, compose, pipe, apply, filter, reduce, fold, curry)
 * - Arithmetic combinators (add, subtract, multiply, divide, modulo, power, negate)
 * - Comparison combinators (equals, notEquals, lessThan, greaterThan, lessEqual, greaterEqual)
 * - Logical combinators (logicalAnd, logicalOr, logicalXor, logicalNot)
 * - Enhanced combinators (identity, constant, flip, on, both, either)
 * 
 * This approach ensures that user code can access these functions as if they were built-in,
 * without special syntax or reserved keywords. The combinator foundation allows the parser
 * to translate all operators to function calls, eliminating ambiguity while preserving syntax.
 * 
 * Functions are written to check argument types at runtime since the language is dynamically
 * typed and does not enforce arity or types at parse time. The combinator functions are
 * designed to work seamlessly with the parser's operator translation, providing a consistent
 * and extensible foundation for all language operations.
 * 
 * The standard library is the foundation of the combinator-based architecture. Each function
 * is designed to support partial application, enabling currying patterns and function composition.
 * This design choice enables functional programming patterns while maintaining
 * simplicity and consistency across all operations.
 * 
 * Error handling is implemented at the function level, with clear error messages that help
 * users understand what went wrong and how to fix it. This includes type checking for
 * function arguments and validation of input data.
 */
function initializeStandardLibrary(scope) {

    /**
     * Map: Apply a function to a value or collection
     * @param {Function} f - Function to apply
     * @param {*} x - Value or collection to apply function to
     * @returns {*} Result of applying f to x
     * @throws {Error} When first argument is not a function
     * @description The map function is a fundamental higher-order function that
     * applies a transformation function to a value or collection. This enables
     * functional programming patterns where data transformations are expressed
     * as function applications rather than imperative operations.
     * 
     * The function implements APL-inspired element-wise operations for tables:
     * when x is a table, map applies the function to each value while preserving
     * the table structure and keys. This reduces the need for explicit loops
     * and enables declarative data transformation patterns.
     * 
     * The function supports partial application: when called with only the function,
     * it returns a new function that waits for the value. This enables currying
     * patterns and function composition chains, which are essential for the
     * combinator-based architecture where all operations are function calls.
     * 
     * This design choice aligns with the language's functional foundation and
     * enables abstractions like `map @double numbers` to transform
     * every element in a collection without explicit iteration.
     * 
     * The function is designed to be polymorphic, working with different data
     * types including scalars, tables, and arrays. This flexibility enables
     * consistent data transformation patterns across different data structures.
     */
    scope.map = function(f, x) {
        if (typeof f !== 'function') {
            throw new Error('map: first argument must be a function');
        }
        
        if (x === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(x) {
                return scope.map(f, x);
            };
        }
        
        // Handle tables (APL-style element-wise operations)
        if (typeof x === 'object' && x !== null && !Array.isArray(x)) {
            const result = {};
            for (const [key, value] of Object.entries(x)) {
                result[key] = f(value);
            }
            return result;
        }
        
        // Handle arrays (future enhancement)
        if (Array.isArray(x)) {
            return x.map(f);
        }
        
        // Default: apply to single value
        return f(x);
    };
    
    /**
     * Compose: Combine two functions into a new function (function composition)
     * @param {Function} f - First function (outer function)
     * @param {Function} [g] - Second function (optional for partial application)
     * @returns {Function} Composed function or partially applied function
     * @throws {Error} When first argument is not a function
     * @description The compose function is a core functional programming primitive
     * that combines two functions into a new function. This is the foundation
     * for the 'via' operator in the language syntax, enabling natural function
     * composition chains like `f via g via h`.
     * 
     * The function implements right-associative composition, meaning that
     * compose(f, compose(g, h)) creates a function that applies h, then g, then f.
     * This matches mathematical function composition notation (f ∘ g ∘ h) and
     * enables natural reading of composition chains from right to left.
     * 
     * The 'via' operator translates to compose calls:
     * - f via g → compose(f, g)
     * - f via g via h → compose(f, compose(g, h))
     * - f via g via h via i → compose(f, compose(g, compose(h, i)))
     * 
     * This right-associative behavior means that composition chains read naturally
     * from right to left, matching mathematical notation where (f ∘ g ∘ h)(x) = f(g(h(x))).
     * 
     * Partial application support enables currying patterns where functions can
     * be built incrementally. This is essential for the combinator-based architecture
     * where operations are built from simple, composable functions.
     * 
     * Examples:
     * - compose(double, increment)(5) → double(increment(5)) → double(6) → 12
     * - compose(increment, double)(5) → increment(double(5)) → increment(10) → 11
     * - double via increment 5 → compose(double, increment)(5) → 12
     * - increment via double via square 3 → compose(increment, compose(double, square))(3) → 19
     */
    scope.compose = function(f, g) {
        if (typeof f !== 'function') {
            throw new Error(`compose: first argument must be a function, got ${typeof f}`);
        }
        
        if (g === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(g) {
                if (typeof g !== 'function') {
                    throw new Error(`compose: second argument must be a function, got ${typeof g}`);
                }
                return function(x) {
                    return f(g(x));
                };
            };
        }
        
        if (typeof g !== 'function') {
            throw new Error(`compose: second argument must be a function, got ${typeof g}`);
        }
        
        return function(x) {
            return f(g(x));
        };
    };
    
    /**
     * 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
     * @description The curry function provides a simplified currying mechanism
     * that allows functions to be applied to arguments incrementally. When called
     * with fewer arguments than the function expects, it returns a new function
     * that waits for the remaining arguments.
     * 
     * This function is designed to work with the parser's one-by-one argument
     * application system, where multi-argument function calls are translated to
     * nested apply calls. The nested partial application checks ensure that
     * functions return partially applied functions until all arguments are received.
     */
    scope.curry = function(f, x, y) { 
        if (typeof f !== 'function') {
            throw new Error('curry: first argument must be a function');
        }
        
        if (x === undefined) {
            // Partial application: return a function that waits for the remaining arguments
            return function(x, y) {
                if (y === undefined) {
                    // Still partial application
                    return function(y) {
                        return f(x, y);
                    };
                }
                return f(x, y);
            };
        }
        
        if (y === undefined) {
            // Partial application: return a function that waits for the last argument
            return function(y) {
                return f(x, y);
            };
        }
        
        // Full application: apply the function to all arguments
        return f(x, y);
    };
    
    /**
     * 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
     * @description The apply function is the fundamental mechanism for function
     * application in the language. It enables the juxtaposition-based function
     * application syntax (f x) by providing an explicit function application
     * primitive. This function is called by the parser whenever function
     * application is detected, ensuring consistent semantics across all
     * function calls.
     * 
     * This function is the core mechanism that enables the parser's juxtaposition
     * detection. When the parser encounters `f x`, it generates `apply(f, x)`,
     * which this function handles. This design reduces the need for special
     * syntax for function calls while maintaining clear precedence rules.
     * 
     * The function supports partial application: when called with only the function,
     * it returns a new function that waits for the argument. This enables the
     * parser to build function application chains incrementally, supporting
     * both immediate evaluation and deferred execution patterns.
     * 
     * This partial application support is essential for the parser's left-associative
     * function application model, where `f g x` becomes `apply(apply(f, g), x)`.
     * The nested partial application ensures that each step returns a function
     * until all arguments are provided.
     */
    scope.apply = function(f, x) { 
        if (typeof f !== 'function') {
            throw new Error('apply: first argument must be a function');
        }
        
        if (x === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(x) {
                return f(x);
            };
        }
        
        // Full application: apply the function to the argument
        return f(x);
    };
    
    /**
     * Pipe: Compose functions in left-to-right order (opposite of compose)
     * @param {Function} f - First function
     * @param {Function} [g] - Second function (optional for partial application)
     * @returns {Function} Function that applies the functions in left-to-right order
     * @throws {Error} When first argument is not a function
     * @description The pipe function provides an alternative to compose that
     * applies functions in left-to-right order, which is often more intuitive
     * for data processing pipelines. This enables functional programming patterns
     * where data flows through a series of transformations in a natural reading order.
     * 
     * The function implements left-associative composition, meaning that
     * pipe(f, pipe(g, h)) creates a function that applies f, then g, then h.
     * This is the opposite of compose and matches the natural reading order
     * for data transformation pipelines, making it intuitive for programmers
     * who think in terms of data flow from left to right.
     * 
     * Like compose, it supports partial application for currying patterns.
      * This enables building transformation pipelines incrementally,
 * which is essential for the combinator-based architecture where
     * operations are built from simple, composable functions.
     * 
     * The left-associative design choice makes pipe ideal for data processing
     * workflows where each step transforms the data and passes it to the next
     * step, creating a natural pipeline that reads like a sequence of operations.
     */
    scope.pipe = function(f, g) {
        if (typeof f !== 'function') {
            throw new Error(`pipe: first argument must be a function, got ${typeof f}`);
        }
        
        if (g === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(g) {
                if (typeof g !== 'function') {
                    throw new Error(`pipe: second argument must be a function, got ${typeof g}`);
                }
                return function(x) {
                    return g(f(x));
                };
            };
        }
        
        if (typeof g !== 'function') {
            throw new Error(`pipe: second argument must be a function, got ${typeof g}`);
        }
        
        return function(x) {
            return g(f(x));
        };
    };
    
    /**
     * Filter: Filter a value or collection based on a predicate
     * @param {Function} p - Predicate function
     * @param {*} x - Value or collection to test
     * @returns {*|0} The value if predicate is true, filtered collection for tables, 0 otherwise
     * @throws {Error} When first argument is not a function
     * @description The filter function applies a predicate to a value or collection,
     * returning the value if the predicate is true, or a filtered collection for tables.
     * This enables functional programming patterns where data selection is expressed
     * as predicate application rather than imperative filtering loops.
     * 
     * The function implements APL-inspired element-wise filtering for tables:
     * when x is a table, filter applies the predicate to each value and returns
     * a new table containing only the key-value pairs where the predicate returns true.
     * This reduces the need for explicit loops and enables declarative data
     * selection patterns.
     * 
     * The function supports partial application: when called with only the predicate,
     * it returns a new function that waits for the value. This enables currying
     * patterns and function composition chains, which are essential for the
     * combinator-based architecture where all operations are function calls.
     * 
     * This design choice aligns with the language's functional foundation and
     * enables abstractions like `filter @isEven numbers` to select
     * elements from a collection without explicit iteration.
     */
    scope.filter = function(p, x) { 
        if (typeof p !== 'function') {
            throw new Error('filter: first argument must be a function');
        }
        
        if (x === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(x) {
                return scope.filter(p, x);
            };
        }
        
        // Handle tables (APL-style element-wise filtering)
        if (typeof x === 'object' && x !== null && !Array.isArray(x)) {
            const result = {};
            for (const [key, value] of Object.entries(x)) {
                if (p(value)) {
                    result[key] = value;
                }
            }
            return result;
        }
        
        // Handle arrays (future enhancement)
        if (Array.isArray(x)) {
            return x.filter(p);
        }
        
        // Default: apply predicate to single value
        return p(x) ? x : 0;
    };
    
    /**
     * 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
     * @description The reduce function applies a binary function to an initial value
     * and a second value, returning the result. This is a simplified version of
     * traditional reduce that works with pairs of values rather than collections.
     * 
     * The function supports partial application with nested checks to handle the
     * parser's one-by-one argument application system. When called with only the
     * function, it returns a function that waits for the initial value. When called
     * with the function and initial value, it returns a function that waits for
     * the second value. This enables currying patterns and incremental function
     * application.
     */
    scope.reduce = function(f, init, x) { 
        if (DEBUG) {
            safeConsoleLog(`[DEBUG] reduce: f =`, typeof f, f);
            safeConsoleLog(`[DEBUG] reduce: init =`, init);
            safeConsoleLog(`[DEBUG] reduce: x =`, x);
        }
        
        if (typeof f !== 'function') {
            throw new Error('reduce: first argument must be a function');
        }
        
        if (init === undefined) {
            // Partial application: return a function that waits for the remaining arguments
            return function(init, x) {
                if (DEBUG) {
                    safeConsoleLog(`[DEBUG] reduce returned function: f =`, typeof f, f);
                    safeConsoleLog(`[DEBUG] reduce returned function: init =`, init);
                    safeConsoleLog(`[DEBUG] reduce returned function: x =`, x);
                }
                if (x === undefined) {
                    // Still partial application
                    return function(x) {
                        return scope.reduce(f, init, x);
                    };
                }
                return scope.reduce(f, init, x);
            };
        }
        
        if (x === undefined) {
            // Partial application: return a function that waits for the last argument
            return function(x) {
                return scope.reduce(f, init, x);
            };
        }
        
        // Handle tables (reduce all values in the table)
        if (typeof x === 'object' && x !== null && !Array.isArray(x)) {
            let result = init;
            for (const [key, value] of Object.entries(x)) {
                result = f(result, value, key);
            }
            return result;
        }
        
        // Handle arrays (future enhancement)
        if (Array.isArray(x)) {
            return x.reduce(f, init);
        }
        
        // Default: apply the function to init and x (original behavior)
        return f(init, x);
    };
    
    /**
     * 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') {
            throw new Error('fold: first argument must be a function');
        }
        
        if (init === undefined) {
            // Partial application: return a function that waits for the remaining arguments
            return function(init, x) {
                if (x === undefined) {
                    // Still partial application
                    return function(x) {
                        return f(init, x);
                    };
                }
                return f(init, x);
            };
        }
        
        if (x === undefined) {
            // Partial application: return a function that waits for the last argument
            return function(x) {
                return f(init, x);
            };
        }
        
        // Full application: apply the function to all arguments
        return f(init, x);
    };
    
    // ===== ARITHMETIC COMBINATORS =====
    
    /**
     * Add: Add two numbers
     * @param {number} x - First number
     * @param {number} y - Second number
     * @returns {number} Sum of x and y
     * @description The add function is a fundamental arithmetic combinator that
     * implements addition. This function is called by the parser when the '+'
     * operator is encountered, translating `x + y` into `add(x, y)`.
     * 
     * As a combinator function, add supports partial application and can be used
     * in function composition chains. This enables patterns like `map @add 10`
     * to add 10 to every element in a collection, or `each @add table1 table2`
     * for element-wise addition of corresponding table elements.
     * 
     * The function is designed to work seamlessly with the parser's operator
     * translation system, providing consistent semantics for all arithmetic
     * operations through the combinator foundation.
     */
    scope.add = function(x, y) {
        if (y === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(y) {
                return 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) {
        if (y === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(y) {
                return 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
     * @description The multiply function is a fundamental arithmetic combinator that
     * implements multiplication. This function is called by the parser when the '*'
     * operator is encountered, translating `x * y` into `multiply(x, y)`.
     * 
     * As a combinator function, multiply supports partial application and can be used
     * in function composition chains. This enables patterns like `map @multiply 2`
     * to double every element in a collection, or `each @multiply table1 table2`
     * for element-wise multiplication of corresponding table elements.
     * 
     * The function is designed to work seamlessly with the parser's operator
     * translation system, providing consistent semantics for all arithmetic
     * operations through the combinator foundation.
     */
    scope.multiply = function(x, y) {
        if (y === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(y) {
                return 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 === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(y) {
                if (y === 0) {
                    throw new Error('Division by zero');
                }
                return x / y;
            };
        }
        if (y === 0) {
            throw new Error('Division by zero');
        }
        return x / y;
    };
    
    /**
     * Modulo: Get remainder of division
     * @param {number} x - First number
     * @param {number} y - Second number
     * @returns {number} Remainder of x divided by y
     */
    scope.modulo = function(x, y) {
        if (y === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(y) {
                return 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) {
        if (y === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(y) {
                return Math.pow(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;
            };
        }
    };
    
    /**
     * 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);
            };
        }
    };
    
    /**
     * On: Apply a function to the results of another function
     * @param {Function} f - Outer function
     * @param {Function} g - Inner function
     * @returns {Function} Function that applies f to the results of g
     */
    scope.on = function(f, g) {
        return function(x, y) {
            return f(g(x), g(y));
        };
    };
    
    /**
     * Both: Check if both predicates are true
     * @param {Function} f - First predicate
     * @param {Function} g - Second predicate
     * @returns {Function} Function that returns true if both predicates are true
     */
    scope.both = function(f, g) {
        return function(x) {
            return f(x) && g(x);
        };
    };
    
    /**
     * Either: Check if either predicate is true
     * @param {Function} f - First predicate
     * @param {Function} g - Second predicate
     * @returns {Function} Function that returns true if either predicate is true
     */
    scope.either = function(f, g) {
        return function(x) {
            return f(x) || g(x);
        };
    };
    
    /**
     * Each: Multi-argument element-wise operations for tables and scalars
     * @param {Function} f - Function to apply element-wise
     * @param {*} x - First argument (table or scalar)
     * @returns {Function|*} Function for partial application or result of element-wise application
     * @throws {Error} When first argument is not a function
     * @description The each combinator provides APL-inspired element-wise operations
     * for multi-argument functions over table structures. This is the primary mechanism
     * for combining multiple tables or tables with scalars in element-wise fashion.
     * 
     * The function is designed for multi-argument operations and aligns with the parser's
     * apply mechanism. When x is a table, each returns a function that waits for the
     * second argument (y), enabling the parser to build `apply(apply(each, f), x)` chains
     * that resolve to element-wise operations when y is provided.
     * 
     * Key behaviors:
     * - Table + Scalar: Applies f to each element of the table with the scalar as second argument
     * - Table + Table: Applies f to corresponding elements from both tables
     * - Scalar + Table: Uses map to apply f with the scalar as first argument to each table element
     * - Scalar + Scalar: Falls back to normal function application for backward compatibility
     * 
     * This design choice enables multi-argument element-wise operations like
     * `each @add table1 table2` for element-wise addition, while maintaining compatibility
     * with the parser's two-argument apply model. The function is specifically designed
     * for multi-argument operations, distinguishing it from map which is for single-table
     * transformations.
     */
    scope.each = function(f, x) {
        if (DEBUG) {
            safeConsoleLog(`[DEBUG] each called with: f=${typeof f}, x=${typeof x}`);
            safeConsoleLog(`[DEBUG] x value:`, x);
        }
        
        if (typeof f !== 'function') {
            throw new Error('each: first argument must be a function, got ' + typeof f);
        }
        
        if (x === undefined) {
            // Partial application: return a function that waits for the second argument
            return function(x) {
                return scope.each(f, x);
            };
        }
        
        // Check if x is a table
        const isXTable = typeof x === 'object' && x !== null && !Array.isArray(x);
        
        if (isXTable) {
            // x is a table - always return a function that can handle the second argument
            return function(y) {
                // Check if y is a table
                const isYTable = typeof y === 'object' && y !== null && !Array.isArray(y);
                
                if (!isYTable) {
                    // x is a table, y is not a table - apply function to each element of x with y as second argument
                    const result = {};
                    for (const [key, value] of Object.entries(x)) {
                        result[key] = f(value, y);
                    }
                    return result;
                }
                
                // Both x and y are tables - they should have the same keys
                const result = {};
                for (const [key, value] of Object.entries(x)) {
                    if (y.hasOwnProperty(key)) {
                        result[key] = f(value, y[key]);
                    }
                }
                return result;
            };
        }
        
        // x is not a table, return a function that waits for the second argument
        return function(y) {
            // Check if y is a table
            const isYTable = typeof y === 'object' && y !== null && !Array.isArray(y);
            
            if (!isYTable) {
                // No tables, apply normally (backward compatibility)
                return f(x, y);
            }
            
            // x is not a table, y is a table - use map
            return scope.map(function(val) { return f(x, val); }, y);
        };
    };
    
    // ===== TABLE OPERATIONS NAMESPACE (t.) =====
    
    /**
     * Table operations namespace (t.)
     * @description Provides immutable table operations that always return new tables,
     * never modifying the original. This namespace implements APL-inspired element-wise
     * operations and functional table manipulation patterns.
     * 
     * All operations in this namespace are designed to work with the language's
     * immutable data philosophy, where data transformations create new structures
     * rather than modifying existing ones. This enables functional programming
     * patterns and reduces side effects from table operations.
     * 
     * The namespace provides both basic table operations (get, set, delete, merge)
     * and higher-order operations (map, filter, reduce) that work element-wise
     * on table values. This design choice enables data transformation
     * patterns while maintaining the functional programming principles of the language.
     * 
     * Key design principles:
     * - Immutability: All operations return new tables, never modify originals
     * - Element-wise operations: Functions operate on table values, not structure
     * - Partial application: All functions support currying patterns
     * - Functional consistency: Operations work with the combinator foundation
     */
    scope.t = {
        /**
         * Map: Apply a function to each value in a table
         * @param {Function} f - Function to apply
         * @param {Object} table - Table to map over
         * @returns {Object} New table with transformed values
         * @throws {Error} When first argument is not a function or second is not a table
         */
        map: function(f, table) {
            if (typeof f !== 'function') {
                throw new Error('t.map: first argument must be a function');
            }
            
            if (table === undefined) {
                // Partial application: return a function that waits for the table
                return function(table) {
                    return scope.t.map(f, table);
                };
            }
            
            if (typeof table !== 'object' || table === null) {
                throw new Error('t.map: second argument must be a table');
            }
            
            const result = {};
            for (const [key, value] of Object.entries(table)) {
                result[key] = f(value);
            }
            return result;
        },
        
        /**
         * Filter: Filter table values based on a predicate
         * @param {Function} p - Predicate function
         * @param {Object} table - Table to filter
         * @returns {Object} New table with only values that pass the predicate
         * @throws {Error} When first argument is not a function or second is not a table
         */
        filter: function(p, table) {
            if (typeof p !== 'function') {
                throw new Error('t.filter: first argument must be a function');
            }
            
            if (table === undefined) {
                // Partial application: return a function that waits for the table
                return function(table) {
                    return scope.t.filter(p, table);
                };
            }
            
            if (typeof table !== 'object' || table === null) {
                throw new Error('t.filter: second argument must be a table');
            }
            
            const result = {};
            for (const [key, value] of Object.entries(table)) {
                if (p(value)) {
                    result[key] = value;
                }
            }
            return result;
        },
        
        /**
         * Reduce: Reduce all values in a table using a binary function
         * @param {Function} f - Binary function
         * @param {*} init - Initial value
         * @param {Object} table - Table to reduce
         * @returns {*} Result of reducing all values
         * @throws {Error} When first argument is not a function or third is not a table
         */
        reduce: function(f, init, table) {
            if (typeof f !== 'function') {
                throw new Error('t.reduce: first argument must be a function');
            }
            
            if (init === undefined) {
                // Partial application: return a function that waits for the remaining arguments
                return function(init, table) {
                    if (table === undefined) {
                        // Still partial application
                        return function(table) {
                            return scope.t.reduce(f, init, table);
                        };
                    }
                    return scope.t.reduce(f, init, table);
                };
            }
            
            if (table === undefined) {
                // Partial application: return a function that waits for the table
                return function(table) {
                    return scope.t.reduce(f, init, table);
                };
            }
            
            if (typeof table !== 'object' || table === null) {
                throw new Error('t.reduce: third argument must be a table');
            }
            
            let result = init;
            for (const [key, value] of Object.entries(table)) {
                result = f(result, value, key);
            }
            return result;
        },
        
        /**
         * Set: Immutably set a key-value pair in a table
         * @param {Object} table - Table to modify
         * @param {*} key - Key to set
         * @param {*} value - Value to set
         * @returns {Object} New table with the key-value pair set
         * @throws {Error} When first argument is not a table
         */
        set: function(table, key, value) {
            if (typeof table !== 'object' || table === null) {
                throw new Error('t.set: first argument must be a table');
            }
            
            if (key === undefined) {
                // Partial application: return a function that waits for the remaining arguments
                return function(key, value) {
                    if (value === undefined) {
                        // Still partial application
                        return function(value) {
                            return scope.t.set(table, key, value);
                        };
                    }
                    return scope.t.set(table, key, value);
                };
            }
            
            if (value === undefined) {
                // Partial application: return a function that waits for the value
                return function(value) {
                    return scope.t.set(table, key, value);
                };
            }
            
            return { ...table, [key]: value };
        },
        
        /**
         * Delete: Immutably delete a key from a table
         * @param {Object} table - Table to modify
         * @param {*} key - Key to delete
         * @returns {Object} New table without the specified key
         * @throws {Error} When first argument is not a table
         */
        delete: function(table, key) {
            if (typeof table !== 'object' || table === null) {
                throw new Error('t.delete: first argument must be a table');
            }
            
            if (key === undefined) {
                // Partial application: return a function that waits for the key
                return function(key) {
                    return scope.t.delete(table, key);
                };
            }
            
            const result = { ...table };
            delete result[key];
            return result;
        },
        
        /**
         * Merge: Immutably merge two tables
         * @param {Object} table1 - First table
         * @param {Object} table2 - Second table (values override table1)
         * @returns {Object} New merged table
         * @throws {Error} When either argument is not a table
         */
        merge: function(table1, table2) {
            if (typeof table1 !== 'object' || table1 === null) {
                throw new Error('t.merge: first argument must be a table');
            }
            
            if (table2 === undefined) {
                // Partial application: return a function that waits for the second table
                return function(table2) {
                    return scope.t.merge(table1, table2);
                };
            }
            
            if (typeof table2 !== 'object' || table2 === null) {
                throw new Error('t.merge: second argument must be a table');
            }
            
            return { ...table1, ...table2 };
        },
        
        /**
         * Pairs: Get all key-value pairs from a table
         * @param {Object} table - Table to get pairs from
         * @returns {Array} Array of [key, value] pairs
         * @throws {Error} When argument is not a table
         */
        pairs: function(table) {
            if (typeof table !== 'object' || table === null) {
                throw new Error('t.pairs: argument must be a table');
            }
            return Object.entries(table);
        },
        
        /**
         * Keys: Get all keys from a table
         * @param {Object} table - Table to get keys from
         * @returns {Array} Array of keys
         * @throws {Error} When argument is not a table
         */
        keys: function(table) {
            if (typeof table !== 'object' || table === null) {
                throw new Error('t.keys: argument must be a table');
            }
            return Object.keys(table);
        },
        
        /**
         * Values: Get all values from a table
         * @param {Object} table - Table to get values from
         * @returns {Array} Array of values
         * @throws {Error} When argument is not a table
         */
        values: function(table) {
            if (typeof table !== 'object' || table === null) {
                throw new Error('t.values: argument must be a table');
            }
            return Object.values(table);
        },
        
        /**
         * Length: Get the number of key-value pairs in a table
         * @param {Object} table - Table to measure
         * @returns {number} Number of key-value pairs
         * @throws {Error} When argument is not a table
         */
        length: function(table) {
            if (typeof table !== 'object' || table === null) {
                throw new Error('t.length: argument must be a table');
            }
            return Object.keys(table).length;
        },
        
        /**
         * Has: Check if a table has a specific key
         * @param {Object} table - Table to check
         * @param {*} key - Key to check for
         * @returns {boolean} True if key exists, false otherwise
         * @throws {Error} When first argument is not a table
         */
        has: function(table, key) {
            if (typeof table !== 'object' || table === null) {
                throw new Error('t.has: first argument must be a table');
            }
            
            if (key === undefined) {
                // Partial application: return a function that waits for the key
                return function(key) {
                    return scope.t.has(table, key);
                };
            }
            
            return table.hasOwnProperty(key);
        },
        
        /**
         * Get: Safely get a value from a table with optional default
         * @param {Object} table - Table to get from
         * @param {*} key - Key to get
         * @param {*} defaultValue - Default value if key doesn't exist
         * @returns {*} Value at key or default value
         * @throws {Error} When first argument is not a table
         */
        get: function(table, key, defaultValue) {
            if (typeof table !== 'object' || table === null) {
                throw new Error('t.get: first argument must be a table');
            }
            
            if (key === undefined) {
                // Partial application: return a function that waits for the remaining arguments
                return function(key, defaultValue) {
                    if (defaultValue === undefined) {
                        // Still partial application
                        return function(defaultValue) {
                            return scope.t.get(table, key, defaultValue);
                        };
                    }
                    return scope.t.get(table, key, defaultValue);
                };
            }
            
            if (defaultValue === undefined) {
                // Partial application: return a function that waits for the default value
                return function(defaultValue) {
                    return scope.t.get(table, key, defaultValue);
                };
            }
            
            return table.hasOwnProperty(key) ? table[key] : defaultValue;
        }
    };
}

/**
 * Interpreter: Walks the AST and evaluates each node using the combinator foundation.
 * 
 * @param {ASTNode} ast - Abstract Syntax Tree to evaluate
 * @param {Environment} [environment=null] - External environment for IO operations
 * @param {Object} [initialState={}] - Initial state for the interpreter
 * @returns {*} The result of evaluating the AST, or a Promise for async operations
 * @throws {Error} For evaluation errors like division by zero, undefined variables, etc.
 * 
 * @description Evaluates an AST by walking through each node and performing the
 * corresponding operations. Manages scope, handles function calls, and supports
 * both synchronous and asynchronous operations.
 * 
 * The interpreter implements a combinator-based architecture where all operations
 * are executed through function calls to standard library combinators. This design
 * reduces parsing ambiguity while preserving intuitive syntax. The parser translates
 * all operators (+, -, *, /, etc.) into FunctionCall nodes that reference combinator
 * functions, ensuring consistent semantics across all operations.
 * 
 * Key architectural features:
 * - Combinator Foundation: All operations are function calls to standard library combinators
 * - Scope Management: Prototypal inheritance for variable lookup and function definitions
 * - Forward Declaration: Recursive functions are supported through placeholder creation
 * - Error Handling: Comprehensive error detection and reporting with call stack tracking
 * - Debug Support: Optional debug mode for development and troubleshooting
 * - IO Operations: Support for input/output operations through environment interface
 * 
 * The interpreter processes legacy operator expressions (PlusExpression, MinusExpression, etc.)
 * for backward compatibility, but the parser now generates FunctionCall nodes for all operators,
 * which are handled by the standard library combinator functions. This ensures that all
 * operations follow the same execution model and can be extended by adding new combinator
 * functions to the standard library.
 * 
 * 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.
 * 
 * The combinator foundation ensures that all operations are executed through
 * function calls, providing a consistent and extensible execution model. This
 * approach enables abstractions and reduces the need for special
 * handling of different operator types in the interpreter.
 * 
 * The interpreter supports both synchronous and asynchronous operations. IO operations
 * like input and output can return Promises, allowing for non-blocking execution
 * when interacting with external systems or user input.
 */
function interpreter(ast, environment = null, initialState = {}) {
    const globalScope = { ...initialState };
    initializeStandardLibrary(globalScope);
    
    // Track whether any IO operations have been performed
    let ioOperationsPerformed = false;
    
    // Debug: Check if combinators are available
    if (DEBUG) {
        safeConsoleLog('[DEBUG] Available functions in global scope:', Object.keys(globalScope));
        safeConsoleLog('[DEBUG] add function exists:', typeof globalScope.add === 'function');
        safeConsoleLog('[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 using the combinator foundation.
     * 
     * @param {ASTNode} 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.
     * This pattern enables natural recursive function definitions without requiring
     * special syntax or pre-declaration.
     * 
     * This function is the primary entry point for AST evaluation and handles
     * all the core language constructs including literals, operators (translated
     * to combinator calls), function definitions, and control structures. It
     * ensures that all operations are executed through the combinator foundation,
     * providing consistent semantics across the language.
     * 
     * The function processes legacy operator expressions (PlusExpression, MinusExpression, etc.)
     * for backward compatibility, but the parser now generates FunctionCall nodes for
     * all operators, which are handled by the standard library combinator functions.
     * This design ensures that all operations follow the same execution model and
     * can be extended by adding new combinator functions to the standard library.
     * 
     * Key evaluation patterns:
     * - Literals: Direct value return
     * - FunctionCall: Delegates to standard library combinator functions
     * - Assignment: Creates variables in global scope with forward declaration support
     * - WhenExpression: Pattern matching with wildcard support
     * - TableLiteral: Creates immutable table structures
     * - TableAccess: Safe property access with error handling
     * - IO Operations: Handles input/output through environment interface
     * 
     * The function maintains call stack tracking for debugging and error reporting.
     * This enables detailed error messages that include the call chain leading to
     * the error, making it easier to debug programs.
     * 
     * Error handling is comprehensive, with specific error messages for common
     * issues like undefined variables, type mismatches, and division by zero.
     * Each error includes context about where the error occurred and what was
     * expected, helping users quickly identify and fix issues.
     */
    function evalNode(node) {
        callStackTracker.push('evalNode', node?.type || 'unknown');
        
        try {
            if (!node) {
                return undefined;
            }
            switch (node.type) {
                case 'NumberLiteral':
                    return parseFloat(node.value);
                case 'StringLiteral':
                    return node.value;
                case 'BooleanLiteral':
                    return node.value;
                case 'PlusExpression':
                    return evalNode(node.left) + evalNode(node.right);
                case 'MinusExpression':
                    return evalNode(node.left) - evalNode(node.right);
                case 'MultiplyExpression':
                    return evalNode(node.left) * evalNode(node.right);
                case 'DivideExpression':
                    const divisor = evalNode(node.right);
                    if (divisor === 0) {
                        throw new Error('Division by zero');
                    }
                    return evalNode(node.left) / evalNode(node.right);
                case 'ModuloExpression':
                    return evalNode(node.left) % evalNode(node.right);
                case 'PowerExpression':
                    return Math.pow(evalNode(node.left), evalNode(node.right));
                case 'EqualsExpression':
                    return evalNode(node.left) === evalNode(node.right);
                case 'LessThanExpression':
                    return evalNode(node.left) < evalNode(node.right);
                case 'GreaterThanExpression':
                    return evalNode(node.left) > evalNode(node.right);
                case 'LessEqualExpression':
                    return evalNode(node.left) <= evalNode(node.right);
                case 'GreaterEqualExpression':
                    return evalNode(node.left) >= evalNode(node.right);
                case 'NotEqualExpression':
                    return evalNode(node.left) !== evalNode(node.right);
                case 'AndExpression':
                    return !!(evalNode(node.left) && evalNode(node.right));
                case 'OrExpression':
                    return !!(evalNode(node.left) || evalNode(node.right));
                case 'XorExpression':
                    const leftVal = evalNode(node.left);
                    const rightVal = evalNode(node.right);
                    return !!((leftVal && !rightVal) || (!leftVal && rightVal));
                case 'NotExpression':
                    return !evalNode(node.operand);
                case 'UnaryMinusExpression':
                    return -evalNode(node.operand);
                case 'TableLiteral':
                    const table = {};
                    let arrayIndex = 1;
                    
                    for (const entry of node.entries) {
                        if (entry.key === null) {
                            // Array-like entry: {1, 2, 3}
                            table[arrayIndex] = evalNode(entry.value);
                            arrayIndex++;
                        } else {
                            // Key-value entry: {name: "Alice", age: 30}
                            let key;
                            if (entry.key.type === 'Identifier') {
                                // Convert identifier keys to strings
                                key = entry.key.value;
                            } else {
                                // For other key types (numbers, strings), evaluate normally
                                key = evalNode(entry.key);
                            }
                            // Special handling for FunctionDeclaration nodes
                            if (DEBUG) {
                                safeConsoleLog(`[DEBUG] TableLiteral: entry.value.type = ${entry.value.type}`);
                            }
                            if (entry.value.type === 'FunctionDeclaration') {
                                // Don't evaluate the function body, just create the function
                                const func = function(...args) {
                                    callStackTracker.push('FunctionCall', entry.value.params.join(','));
                                    try {
                                        // If we have fewer arguments than parameters, return a curried function
                                        if (args.length < entry.value.params.length) {
                                            return function(...moreArgs) {
                                                const allArgs = [...args, ...moreArgs];
                                                if (allArgs.length < entry.value.params.length) {
                                                    // Still not enough arguments, curry again
                                                    return function(...evenMoreArgs) {
                                                        const finalArgs = [...allArgs, ...evenMoreArgs];
                                                        let localScope = Object.create(globalScope);
                                                        for (let i = 0; i < entry.value.params.length; i++) {
                                                            localScope[entry.value.params[i]] = finalArgs[i];
                                                        }
                                                        return localEvalNodeWithScope(entry.value.body, localScope);
                                                    };
                                                } else {
                                                    // We have enough arguments now
                                                    let localScope = Object.create(globalScope);
                                                    for (let i = 0; i < entry.value.params.length; i++) {
                                                        localScope[entry.value.params[i]] = allArgs[i];
                                                    }
                                                    return localEvalNodeWithScope(entry.value.body, localScope);
                                                }
                                            };
                                        } else {
                                            // We have enough arguments, evaluate the function
                                            let localScope = Object.create(globalScope);
                                            for (let i = 0; i < entry.value.params.length; i++) {
                                                localScope[entry.value.params[i]] = args[i];
                                            }
                                            return localEvalNodeWithScope(entry.value.body, localScope);
                                        }
                                    } finally {
                                        callStackTracker.pop();
                                    }
                                };
                                table[key] = func;
                            } else {
                                const value = evalNode(entry.value);
                                table[key] = value;
                            }
                        }
                    }
                    
                    return table;
                case 'TableAccess':
                    const tableValue = evalNode(node.table);
                    let keyValue;
                    
                    // Handle different key types
                    if (node.key.type === 'Identifier') {
                        // For dot notation, use the identifier name as the key
                        keyValue = node.key.value;
                    } else {
                        // For bracket notation, evaluate the key expression
                        keyValue = evalNode(node.key);
                    }
                    
                    if (typeof tableValue !== 'object' || tableValue === null) {
                        throw new Error('Cannot access property of non-table value');
                    }
                    
                    if (tableValue[keyValue] === undefined) {
                        throw new Error(`Key '${keyValue}' not found in table`);
                    }
                    
                    return tableValue[keyValue];
                case 'AssignmentExpression':
                    // Prevent reassignment of standard library functions
                    if (globalScope.hasOwnProperty(node.name)) {
                        throw new Error(`Cannot reassign immutable variable: ${node.name}`);
                    }
                    
                    // Check if this is a function assignment for potential recursion
                    if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') {
                        // Create a placeholder function that will be replaced
                        let placeholder = function(...args) {
                            // This should never be called, but if it is, it means we have a bug
                            throw new Error(`Function ${node.name} is not yet fully defined`);
                        };
                        
                        // Store the placeholder in global scope
                        globalScope[node.name] = placeholder;
                        
                        // Now evaluate the function definition with access to the placeholder
                        const actualFunction = evalNode(node.value);
                        
                        // Replace the placeholder with the actual function
                        globalScope[node.name] = actualFunction;
                        return;
                    }
                    
                    const value = evalNode(node.value);
                    globalScope[node.name] = value;
                    return;
                case 'Assignment':
                    // Prevent reassignment of standard library functions
                    if (globalScope.hasOwnProperty(node.identifier)) {
                        throw new Error(`Cannot reassign immutable variable: ${node.identifier}`);
                    }
                    
                    // Check if this is a function assignment for potential recursion
                    if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') {
                        // Create a placeholder function that will be replaced
                        let placeholder = function(...args) {
                            // This should never be called, but if it is, it means we have a bug
                            throw new Error(`Function ${node.identifier} is not yet fully defined`);
                        };
                        
                        // Store the placeholder in global scope
                        globalScope[node.identifier] = placeholder;
                        
                        // Now evaluate the function definition with access to the placeholder
                        const actualFunction = evalNode(node.value);
                        
                        // Replace the placeholder with the actual function
                        globalScope[node.identifier] = actualFunction;
                        return;
                    }
                    
                    const assignmentValue = evalNode(node.value);
                    globalScope[node.identifier] = assignmentValue;
                    return;
                case 'Identifier':
                    const identifierValue = globalScope[node.value];
                    if (identifierValue === undefined) {
                        throw new Error(`Variable ${node.value} is not defined`);
                    }
                    return identifierValue;
                case 'FunctionDeclaration':
                    // For anonymous functions, the name comes from the assignment
                    // The function itself doesn't have a name, so we just return
                    // The assignment will handle storing it in the global scope
                    return function(...args) {
                        callStackTracker.push('FunctionCall', node.params.join(','));
                        try {
                            // If we have fewer arguments than parameters, return a curried function
                            if (args.length < node.params.length) {
                                return function(...moreArgs) {
                                    const allArgs = [...args, ...moreArgs];
                                    if (allArgs.length < node.params.length) {
                                        // Still not enough arguments, curry again
                                        return function(...evenMoreArgs) {
                                            const finalArgs = [...allArgs, ...evenMoreArgs];
                                            let localScope = Object.create(globalScope);
                                            for (let i = 0; i < node.params.length; i++) {
                                                localScope[node.params[i]] = finalArgs[i];
                                            }
                                            return localEvalNodeWithScope(node.body, localScope);
                                        };
                                    } else {
                                        // We have enough arguments now
                                        let localScope = Object.create(globalScope);
                                        for (let i = 0; i < node.params.length; i++) {
                                            localScope[node.params[i]] = allArgs[i];
                                        }
                                        return localEvalNodeWithScope(node.body, localScope);
                                    }
                                };
                            } else {
                                // We have enough arguments, evaluate the function
                                let localScope = Object.create(globalScope);
                                for (let i = 0; i < node.params.length; i++) {
                                    localScope[node.params[i]] = args[i];
                                }
                                return localEvalNodeWithScope(node.body, localScope);
                            }
                        } finally {
                            callStackTracker.pop();
                        }
                    };
                case 'FunctionDefinition':
                    // Create a function from the function definition
                    return function(...args) {
                        callStackTracker.push('FunctionCall', node.parameters.join(','));
                        try {
                            let localScope = Object.create(globalScope);
                            for (let i = 0; i < node.parameters.length; i++) {
                                localScope[node.parameters[i]] = args[i];
                            }
                            return localEvalNodeWithScope(node.body, localScope);
                        } finally {
                            callStackTracker.pop();
                        }
                    };
                case 'FunctionCall':
                    let funcToCall;
                    if (typeof node.name === 'string') {
                        // Regular function call with string name
                        funcToCall = globalScope[node.name];
                        if (DEBUG) {
                            safeConsoleLog(`[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 (DEBUG) {
                            safeConsoleLog(`[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 (DEBUG) {
                            safeConsoleLog(`[DEBUG] FunctionCall: evaluated function expression, found:`, typeof funcToCall);
                        }
                    }
                    
                    if (typeof funcToCall === 'function') {
                        let args = node.args.map(evalNode);
                        if (DEBUG) {
                            safeConsoleLog(`[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)];
                    
                    if (DEBUG) {
                        safeConsoleLog(`[DEBUG] WhenExpression: whenValues =`, whenValues);
                    }
                    
                    for (const caseItem of node.cases) {
                        // Handle both single patterns and arrays of patterns
                        const patterns = caseItem.pattern.map(evalNode);
                        
                        if (DEBUG) {
                            safeConsoleLog(`[DEBUG] WhenExpression: patterns =`, patterns);
                        }
                        
                        // Check if patterns match the values
                        let matches = true;
                        if (whenValues.length !== patterns.length) {
                            matches = false;
                        } else {
                            for (let i = 0; i < whenValues.length; i++) {
                                const value = whenValues[i];
                                const pattern = patterns[i];
                                
                                if (DEBUG) {
                                    safeConsoleLog(`[DEBUG] WhenExpression: comparing value ${value} with pattern ${pattern}`);
                                }
                                
                                if (pattern === true) { // Wildcard pattern
                                    // Wildcard always matches
                                    if (DEBUG) {
                                        safeConsoleLog(`[DEBUG] WhenExpression: wildcard matches`);
                                    }
                                    continue;
                                } else if (typeof pattern === 'object' && pattern.type === 'FunctionCall') {
                                    // This is a boolean expression pattern (e.g., x < 0)
                                    // We need to substitute the current value for the pattern variable
                                    // For now, let's assume the pattern variable is the first identifier in the function call
                                    let patternToEvaluate = pattern;
                                    if (pattern.args && pattern.args.length > 0 && pattern.args[0].type === 'Identifier') {
                                        // Create a copy of the pattern with the current value substituted
                                        patternToEvaluate = {
                                            ...pattern,
                                            args: [value, ...pattern.args.slice(1)]
                                        };
                                    }
                                    const patternResult = evalNode(patternToEvaluate);
                                    if (DEBUG) {
                                        safeConsoleLog(`[DEBUG] WhenExpression: boolean pattern result = ${patternResult}`);
                                    }
                                    if (!patternResult) {
                                        matches = false;
                                        if (DEBUG) {
                                            safeConsoleLog(`[DEBUG] WhenExpression: boolean pattern does not match`);
                                        }
                                        break;
                                                                            } else {
                                            if (DEBUG) {
                                                safeConsoleLog(`[DEBUG] WhenExpression: boolean pattern matches`);
                                            }
                                        }
                                } else if (typeof pattern === 'object' && pattern !== null && typeof value === 'object' && value !== null) {
                                    // Table pattern matching - check if all pattern properties exist in value
                                    let tableMatches = true;
                                    for (const key in pattern) {
                                        if (pattern.hasOwnProperty(key) && (!value.hasOwnProperty(key) || value[key] !== pattern[key])) {
                                            tableMatches = false;
                                            break;
                                        }
                                    }
                                    if (!tableMatches) {
                                        matches = false;
                                        if (DEBUG) {
                                            safeConsoleLog(`[DEBUG] WhenExpression: table pattern does not match`);
                                        }
                                        break;
                                                                            } else {
                                            if (DEBUG) {
                                                safeConsoleLog(`[DEBUG] WhenExpression: table pattern matches`);
                                            }
                                        }
                                } else if (value !== pattern) {
                                    matches = false;
                                    if (DEBUG) {
                                        safeConsoleLog(`[DEBUG] WhenExpression: pattern does not match`);
                                    }
                                    break;
                                } else {
                                    if (DEBUG) {
                                        safeConsoleLog(`[DEBUG] WhenExpression: pattern matches`);
                                    }
                                }
                            }
                        }
                        
                        if (DEBUG) {
                            safeConsoleLog(`[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 rl = createReadline();
                    
                    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);
                    safeConsoleLog(outputValue);
                    ioOperationsPerformed = true;
                    return outputValue;
                case 'IOAssertExpression':
                    const assertionValue = evalNode(node.value);
                    if (!assertionValue) {
                        throw new Error('Assertion failed');
                    }
                    return assertionValue;
                case 'IOListenExpression':
                    // Return current state from environment if available, otherwise placeholder
                    if (environment && typeof environment.getCurrentState === 'function') {
                        if (DEBUG) {
                            safeConsoleLog('[DEBUG] ..listen called - returning state from environment');
                        }
                        return environment.getCurrentState();
                    } else {
                        if (DEBUG) {
                            safeConsoleLog('[DEBUG] ..listen called - returning placeholder state');
                        }
                        return { status: 'placeholder', message: 'State not available in standalone mode' };
                    }
                case 'IOEmitExpression':
                    const emitValue = evalNode(node.value);
                    // Send value to environment if available, otherwise log to console
                    if (environment && typeof environment.emitValue === 'function') {
                        if (DEBUG) {
                            safeConsoleLog('[DEBUG] ..emit called - sending to environment');
                        }
                        environment.emitValue(emitValue);
                    } else {
                        safeConsoleLog('[EMIT]', emitValue);
                    }
                    ioOperationsPerformed = true;
                    return emitValue;
                case 'FunctionReference':
                    const functionValue = globalScope[node.name];
                    if (DEBUG) {
                        safeConsoleLog(`[DEBUG] FunctionReference: looking up '${node.name}' in globalScope, found:`, typeof functionValue);
                    }
                    if (functionValue === undefined) {
                        throw new Error(`Function ${node.name} is not defined`);
                    }
                    if (typeof functionValue !== 'function') {
                        throw new Error(`${node.name} is not a function`);
                    }
                    return functionValue;
                case 'ArrowExpression':
                    // Arrow expressions are function bodies that should be evaluated
                    return evalNode(node.body);
                default:
                    throw new Error(`Unknown node type: ${node.type}`);
            }
        } finally {
            callStackTracker.pop();
        }
    }

    /**
     * Evaluates AST nodes in a local scope with access to parent scope.
     * 
     * @param {ASTNode} 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.
     * 
     * The function prioritizes local scope lookups over global scope lookups, ensuring
     * that function parameters shadow global variables with the same names. This
     * implements proper lexical scoping semantics.
     * 
     * The function maintains the same call stack tracking as evalNode, enabling
     * consistent debugging and error reporting across both global and local evaluation.
     * This ensures that errors in function bodies can be traced back to their source
     * with the same level of detail as global errors.
     * 
     * Scope management is implemented using JavaScript's prototypal inheritance,
     * where each local scope is created as an object that inherits from the global
     * scope. This approach provides efficient variable lookup while maintaining
     * proper scoping semantics and enabling access to global functions and 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 {
                            // 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;
                        }
                    }
                    
                    return table;
                case 'TableAccess':
                    const tableValue = localEvalNodeWithScope(node.table, scope);
                    let keyValue;
                    
                    // Handle different key types
                    if (node.key.type === 'Identifier') {
                        // For dot notation, use the identifier name as the key
                        keyValue = node.key.value;
                    } else {
                        // For bracket notation, evaluate the key expression
                        keyValue = localEvalNodeWithScope(node.key, scope);
                    }
                    
                    if (typeof tableValue !== 'object' || tableValue === null) {
                        throw new Error('Cannot access property of non-table value');
                    }
                    
                    if (tableValue[keyValue] === undefined) {
                        throw new Error(`Key '${keyValue}' not found in table`);
                    }
                    
                    return tableValue[keyValue];
                case 'AssignmentExpression':
                    // Prevent reassignment of standard library functions
                    if (globalScope.hasOwnProperty(node.name)) {
                        throw new Error(`Cannot reassign immutable variable: ${node.name}`);
                    }
                    
                    // Check if this is a function assignment for potential recursion
                    if (node.value.type === 'FunctionDefinition' || node.value.type === 'FunctionDeclaration') {
                        // Create a placeholder function that will be replaced
                        let placeholder = function(...args) {
                            // This should never be called, but if it is, it means we have a bug
                            throw new Error(`Function ${node.name} is not yet fully defined`);
                        };
                        
                        // Store the placeholder in global scope
                        globalScope[node.name] = placeholder;
                        
                        // 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;
                    }
                    
                    globalScope[node.name] = localEvalNodeWithScope(node.value, scope);
                    return;
                case 'Identifier':
                    // First check local scope, then global scope
                    if (scope && scope.hasOwnProperty(node.value)) {
                        return scope[node.value];
                    }
                    const identifierValue = globalScope[node.value];
                    if (identifierValue === undefined && node.value) {
                        return node.value;
                    }
                    return identifierValue;
                case 'FunctionDeclaration':
                    // For anonymous functions, the name comes from the assignment
                    // The function itself doesn't have a name, so we just return
                    // The assignment will handle storing it in the global scope
                    return function(...args) {
                        callStackTracker.push('FunctionCall', node.params.join(','));
                        try {
                            let nestedScope = Object.create(globalScope);
                            for (let i = 0; i < node.params.length; i++) {
                                nestedScope[node.params[i]] = args[i];
                            }
                            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 = localEvalNodeWithScope(node.name, scope);
                    }
                    
                    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 (DEBUG) {
                        safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: whenValues =`, whenValues);
                    }
                    
                    for (const caseItem of node.cases) {
                        // Handle both single patterns and arrays of patterns
                        const patterns = caseItem.pattern.map(pat => localEvalNodeWithScope(pat, scope));
                        
                        if (DEBUG) {
                            safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: patterns =`, patterns);
                        }
                        
                        // Check if patterns match the values
                        let matches = true;
                        if (whenValues.length !== patterns.length) {
                            matches = false;
                        } else {
                            for (let i = 0; i < whenValues.length; i++) {
                                const value = whenValues[i];
                                const pattern = patterns[i];
                                
                                if (DEBUG) {
                                    safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: comparing value ${value} with pattern ${pattern}`);
                                }
                                
                                if (pattern === true) { // Wildcard pattern
                                    // Wildcard always matches
                                    if (DEBUG) {
                                        safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: wildcard matches`);
                                    }
                                    continue;
                                } else if (typeof pattern === 'object' && pattern !== null && typeof value === 'object' && value !== null) {
                                    // Table pattern matching - check if all pattern properties exist in value
                                    let tableMatches = true;
                                    for (const key in pattern) {
                                        if (pattern.hasOwnProperty(key) && (!value.hasOwnProperty(key) || value[key] !== pattern[key])) {
                                            tableMatches = false;
                                            break;
                                        }
                                    }
                                    if (!tableMatches) {
                                        matches = false;
                                        if (DEBUG) {
                                            safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: table pattern does not match`);
                                        }
                                        break;
                                                                            } else {
                                            if (DEBUG) {
                                                safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: table pattern matches`);
                                            }
                                        }
                                } else if (value !== pattern) {
                                    matches = false;
                                    if (DEBUG) {
                                        safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern does not match`);
                                    }
                                    break;
                                } else {
                                    if (DEBUG) {
                                        safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern matches`);
                                    }
                                }
                            }
                        }
                        
                        if (DEBUG) {
                            safeConsoleLog(`[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(' ');
                        }
                    }
                    throw new Error('No matching pattern found');
                case 'WildcardPattern':
                    return true;
                case 'IOInExpression':
                    const rl2 = createReadline();
                    
                    return new Promise((resolve) => {
                        rl2.question('', (input) => {
                            rl2.close();
                            const num = parseInt(input);
                            resolve(isNaN(num) ? input : num);
                        });
                    });
                case 'IOOutExpression':
                    const localOutputValue = localEvalNodeWithScope(node.value, scope);
                    safeConsoleLog(localOutputValue);
                    ioOperationsPerformed = true;
                    return localOutputValue;
                case 'IOAssertExpression':
                    const localAssertionValue = localEvalNodeWithScope(node.value, scope);
                    if (!localAssertionValue) {
                        throw new Error('Assertion failed');
                    }
                    return localAssertionValue;
                case 'IOListenExpression':
                    // Return current state from environment if available, otherwise placeholder
                    if (environment && typeof environment.getCurrentState === 'function') {
                        if (DEBUG) {
                            safeConsoleLog('[DEBUG] ..listen called - returning state from environment');
                        }
                        return environment.getCurrentState();
                    } else {
                        if (DEBUG) {
                            safeConsoleLog('[DEBUG] ..listen called - returning placeholder state');
                        }
                        return { status: 'placeholder', message: 'State not available in standalone mode' };
                    }
                case 'IOEmitExpression':
                    const localEmitValue = localEvalNodeWithScope(node.value, scope);
                    // Send value to environment if available, otherwise log to console
                    if (environment && typeof environment.emitValue === 'function') {
                        if (DEBUG) {
                            safeConsoleLog('[DEBUG] ..emit called - sending to environment');
                        }
                        environment.emitValue(localEmitValue);
                    } else {
                        safeConsoleLog('[EMIT]', localEmitValue);
                    }
                    ioOperationsPerformed = true;
                    return localEmitValue;
                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.
     * 
     * This function is essential for preventing scope pollution when evaluating
     * nested expressions that should not inherit local scope variables, ensuring
     * that global functions and variables are always accessible regardless of
     * the current evaluation context.
     */
    const localEvalNode = (node) => {
        callStackTracker.push('localEvalNode', node?.type || 'unknown');
        
        try {
            if (!node) {
                return undefined;
            }
            switch (node.type) {
                case 'NumberLiteral':
                    return parseFloat(node.value);
                case 'StringLiteral':
                    return node.value;
                case 'BooleanLiteral':
                    return node.value;
                case 'PlusExpression':
                    return localEvalNode(node.left) + localEvalNode(node.right);
                case 'MinusExpression':
                    return localEvalNode(node.left) - localEvalNode(node.right);
                case 'MultiplyExpression':
                    return localEvalNode(node.left) * localEvalNode(node.right);
                case 'DivideExpression':
                    const divisor = localEvalNode(node.right);
                    if (divisor === 0) {
                        throw new Error('Division by zero');
                    }
                    return localEvalNode(node.left) / localEvalNode(node.right);
                case 'ModuloExpression':
                    return localEvalNode(node.left) % localEvalNode(node.right);
                case 'PowerExpression':
                    return Math.pow(localEvalNode(node.left), localEvalNode(node.right));
                case 'EqualsExpression':
                    return localEvalNode(node.left) === localEvalNode(node.right);
                case 'LessThanExpression':
                    return localEvalNode(node.left) < localEvalNode(node.right);
                case 'GreaterThanExpression':
                    return localEvalNode(node.left) > localEvalNode(node.right);
                case 'LessEqualExpression':
                    return localEvalNode(node.left) <= localEvalNode(node.right);
                case 'GreaterEqualExpression':
                    return localEvalNode(node.left) >= localEvalNode(node.right);
                case 'NotEqualExpression':
                    return localEvalNode(node.left) !== localEvalNode(node.right);
                case 'AndExpression':
                    return !!(localEvalNode(node.left) && localEvalNode(node.right));
                case 'OrExpression':
                    return !!(localEvalNode(node.left) || localEvalNode(node.right));
                case 'XorExpression':
                    const leftVal = localEvalNode(node.left);
                    const rightVal = localEvalNode(node.right);
                    return !!((leftVal && !rightVal) || (!leftVal && rightVal));
                case 'NotExpression':
                    return !localEvalNode(node.operand);
                case 'UnaryMinusExpression':
                    return -localEvalNode(node.operand);
                case 'TableLiteral':
                    const table = {};
                    let arrayIndex = 1;
                    
                    for (const entry of node.entries) {
                        if (entry.key === null) {
                            // Array-like entry: {1, 2, 3}
                            table[arrayIndex] = localEvalNode(entry.value);
                            arrayIndex++;
                        } else {
                            // Key-value entry: {name: "Alice", age: 30}
                            let key;
                            if (entry.key.type === 'Identifier') {
                                // Convert identifier keys to strings
                                key = entry.key.value;
                            } else {
                                // For other key types (numbers, strings), evaluate normally
                                key = localEvalNode(entry.key);
                            }
                            const value = localEvalNode(entry.value);
                            table[key] = value;
                        }
                    }
                    
                    return table;
                case 'TableAccess':
                    const tableValue = localEvalNode(node.table);
                    let keyValue;
                    
                    // Handle different key types
                    if (node.key.type === 'Identifier') {
                        // For dot notation, use the identifier name as the key
                        keyValue = node.key.value;
                    } else {
                        // For bracket notation, evaluate the key expression
                        keyValue = localEvalNode(node.key);
                    }
                    
                    if (typeof tableValue !== 'object' || tableValue === null) {
                        throw new Error('Cannot access property of non-table value');
                    }
                    
                    if (tableValue[keyValue] === undefined) {
                        throw new Error(`Key '${keyValue}' not found in table`);
                    }
                    
                    return tableValue[keyValue];
                case 'AssignmentExpression':
                    // Prevent reassignment of standard library functions
                    if (globalScope.hasOwnProperty(node.name)) {
                        throw new Error(`Cannot reassign immutable variable: ${node.name}`);
                    }
                    
                    // Check if 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 = 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 (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 (typeof pattern === 'object' && pattern !== null && typeof value === 'object' && value !== null) {
                                    // Table pattern matching - check if all pattern properties exist in value
                                    let tableMatches = true;
                                    for (const key in pattern) {
                                        if (pattern.hasOwnProperty(key) && (!value.hasOwnProperty(key) || value[key] !== pattern[key])) {
                                            tableMatches = false;
                                            break;
                                        }
                                    }
                                    if (!tableMatches) {
                                        matches = false;
                                        break;
                                    }
                                } else if (value !== pattern) {
                                    matches = false;
                                    break;
                                }
                            }
                        }
                        
                        if (matches) {
                            const results = caseItem.result.map(localEvalNode);
                            if (results.length === 1) {
                                return results[0];
                            }
                            return results.join(' ');
                        }
                    }
                    throw new Error('No matching pattern found');
                case 'WildcardPattern':
                    return true;
                case 'IOInExpression':
                    const rl3 = createReadline();
                    
                    return new Promise((resolve) => {
                        rl3.question('', (input) => {
                            rl3.close();
                            const num = parseInt(input);
                            resolve(isNaN(num) ? input : num);
                        });
                    });
                case 'IOOutExpression':
                    const localOutputValue = localEvalNode(node.value);
                    safeConsoleLog(localOutputValue);
                    ioOperationsPerformed = true;
                    return localOutputValue;
                case 'IOAssertExpression':
                    const localAssertionValue = localEvalNode(node.value);
                    if (!localAssertionValue) {
                        throw new Error('Assertion failed');
                    }
                    return localAssertionValue;
                case 'IOListenExpression':
                    // Return current state from environment if available, otherwise placeholder
                    if (environment && typeof environment.getCurrentState === 'function') {
                        if (DEBUG) {
                            safeConsoleLog('[DEBUG] ..listen called - returning state from environment');
                        }
                        return environment.getCurrentState();
                    } else {
                        if (DEBUG) {
                            safeConsoleLog('[DEBUG] ..listen called - returning placeholder state');
                        }
                        return { status: 'placeholder', message: 'State not available in standalone mode' };
                    }
                case 'IOEmitExpression':
                    const localEmitValue = localEvalNode(node.value);
                    // Send value to environment if available, otherwise log to console
                    if (environment && typeof environment.emitValue === 'function') {
                        if (DEBUG) {
                            safeConsoleLog('[DEBUG] ..emit called - sending to environment');
                        }
                        environment.emitValue(localEmitValue);
                    } else {
                        safeConsoleLog('[EMIT]', localEmitValue);
                    }
                    ioOperationsPerformed = true;
                    return localEmitValue;
                case 'FunctionReference':
                    const localFunctionValue = globalScope[node.name];
                    if (localFunctionValue === undefined) {
                        throw new Error(`Function ${node.name} is not defined`);
                    }
                    if (typeof localFunctionValue !== 'function') {
                        throw new Error(`${node.name} is not a function`);
                    }
                    return localFunctionValue;
                case 'ArrowExpression':
                    // Arrow expressions are function bodies that should be evaluated
                    return localEvalNode(node.body);
                default:
                    throw new Error(`Unknown node type: ${node.type}`);
            }
        } finally {
            callStackTracker.pop();
        }
    };

    let lastResult;
    for (let node of ast.body) {
        if (node) {
            lastResult = evalNode(node);
        }
    }
    
    if (lastResult instanceof Promise) {
        return lastResult.then(result => {
            return { result: globalScope, ioOperationsPerformed };
        });
    }
    
    return { result: globalScope, ioOperationsPerformed };
}

/**
 * Run script with environment support for harness integration
 * 
 * @param {string} scriptContent - The script content to execute
 * @param {Object} [initialState={}] - Initial state for the interpreter
 * @param {Environment} [environment=null] - Environment for IO operations
 * @returns {*} The result of executing the script
 * @throws {Error} For parsing or evaluation errors
 * 
 * @description Parses and executes a script using the combinator-based language.
 * This function orchestrates the entire execution pipeline from source code
 * to final result.
 * 
 * The function performs the following steps:
 * 1. Tokenize the source code using the lexer
 * 2. Parse the tokens into an AST using the parser
 * 3. Evaluate the AST using the interpreter
 * 4. Return the final result
 * 
 * This is the primary interface for executing scripts in the language.
 * It handles the parsing and evaluation pipeline,
 * providing a simple interface for users to run their code.
 * 
 * The function supports both synchronous and asynchronous execution. When
 * the script contains IO operations that return Promises, the function
 * will return a Promise that resolves to the final result. This enables
 * non-blocking execution for interactive programs.
 * 
 * Error handling is comprehensive, with errors from any stage of the
 * pipeline (lexing, parsing, or evaluation) being caught and re-thrown
 * with appropriate context. This ensures that users get meaningful
 * error messages that help them identify and fix issues in their code.
 * 
 * The function is designed to be stateless, with each call creating
 * a fresh interpreter instance. This ensures that scripts don't interfere
 * with each other and enables safe concurrent execution of multiple scripts.
 */
function run(scriptContent, initialState = {}, environment = null) {
    // Parse the script
    const tokens = lexer(scriptContent);
    const ast = parser(tokens);
    
    // Run the interpreter with environment and initial state
    const result = interpreter(ast, environment, initialState);
    
    // Return the result
    return result.result;
}

/**
 * Debug logging utility function.
 * 
 * @param {string} message - Debug message to log
 * @param {*} [data=null] - Optional data to log with the message
 * 
 * @description Logs debug messages to console when DEBUG environment variable is set.
 * Provides verbose output during development while remaining silent in production.
 * 
 * Debug functions are gated by the DEBUG environment variable, allowing for 
 * verbose output during development and silent operation in production. This 
 * approach makes it easy to trace execution and diagnose issues without 
 * cluttering normal output.
 * 
 * This function is essential for debugging the combinator-based architecture,
 * allowing developers to trace how operators are translated to function calls
 * and how the interpreter executes these calls through the standard library.
 * 
 * The function is designed to be lightweight and safe to call frequently,
 * making it suitable for tracing execution flow through nested
 * expressions and function applications.
 */
function debugLog(message, data = null) {
    if (DEBUG) {
        safeConsoleLog(`[DEBUG] ${message}`);
        if (data) {
            safeConsoleLog(data);
        }
    }
}

/**
 * Debug error logging utility function.
 * 
 * @param {string} message - Debug error message to log
 * @param {Error} [error=null] - Optional error object to log
 * 
 * @description Logs debug error messages to console when DEBUG environment variable is set.
 * Provides verbose error output during development while remaining silent in production.
 * 
 * Debug functions are gated by the DEBUG environment variable, allowing for 
 * verbose output during development and silent operation in production. This 
 * approach makes it easy to trace execution and diagnose issues without 
 * cluttering normal output.
 * 
 * This function is particularly useful for debugging parsing and evaluation errors,
 * providing detailed context about where and why errors occur in the language
 * execution pipeline.
 */
function debugError(message, error = null) {
    if (DEBUG) {
        safeConsoleError(`[DEBUG ERROR] ${message}`);
        if (error) {
            safeConsoleError(error);
        }
    }
}

/**
 * Call stack tracking for debugging recursion issues.
 * 
 * @description Tracks function calls to help identify infinite recursion
 * and deep call stacks that cause stack overflow errors. This is essential
 * for debugging the interpreter's recursive evaluation of AST nodes.
 * 
 * The tracker maintains a stack of function calls with timestamps and context
 * information, counts function calls to identify hot paths, and detects
 * potential infinite recursion by monitoring stack depth.
 * 
 * This tool is particularly important for the combinator-based architecture
 * where function calls are the primary execution mechanism, and
 * nested expressions can lead to deep call stacks. The tracker helps identify
 * when the combinator translation creates unexpectedly deep call chains,
 * enabling optimization of the function composition and application patterns.
 * 
 * The tracker provides detailed statistics about function call patterns,
 * helping developers understand the execution characteristics of their code
 * and identify potential performance bottlenecks in the combinator evaluation.
 */
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 (DEBUG && this.stack.length % 100 === 0) {
            safeConsoleLog(`[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. The file reading capability
 * enables the language to execute scripts from files, supporting the development
 * workflow where tests and examples are stored as .txt files.
 */
async function readFile(filePath) {
    // Use cross-platform filesystem
    const fs = createFileSystem();
    
    return new Promise((resolve, reject) => {
        fs.readFile(filePath, 'utf8', (error, data) => {
            if (error) {
                reject(error);
            } else {
                resolve(data);
            }
        });
    });
}

/**
 * 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. This function demonstrates the
 * complete combinator-based architecture in action, showing how source code
 * is transformed through each stage of the language pipeline.
 * 
 * The function enforces the .txt file extension requirement and provides
 * detailed error reporting with call stack statistics to help developers
 * understand execution behavior and diagnose issues.
 */
async function executeFile(filePath) {
    try {
        // Validate file extension
        if (!filePath.endsWith('.txt') && !filePath.endsWith('.baba')) {
            throw new Error('Only .txt and .baba files are supported');
        }
        
        const input = await readFile(filePath);
        
        debugLog('Input:', input);
        
        const tokens = lexer(input);
        debugLog('Tokens:', tokens);
        
        const ast = parser(tokens);
        debugLog('AST:', JSON.stringify(ast, null, 2));
        
        const result = interpreter(ast);
        
        if (result instanceof Promise) {
            result.then(finalResult => {
                // Only output result if debug mode is enabled (no automatic final result output)
                if (finalResult.result !== undefined && DEBUG) {
                    safeConsoleLog(finalResult.result);
                }
                // Print call stack statistics only in debug mode
                if (DEBUG) {
                    const stats = callStackTracker.getStats();
                    safeConsoleLog('\n=== CALL STACK STATISTICS ===');
                    safeConsoleLog('Maximum call stack depth:', stats.maxDepth);
                    safeConsoleLog('Function call counts:', JSON.stringify(stats.callCounts, null, 2));
                }
            }).catch(error => {
                safeConsoleError(`Error executing file: ${error.message}`);
                // Print call stack statistics on error only in debug mode
                if (DEBUG) {
                    const stats = callStackTracker.getStats();
                    safeConsoleError('\n=== CALL STACK STATISTICS ON ERROR ===');
                    safeConsoleError('Maximum call stack depth:', stats.maxDepth);
                    safeConsoleError('Function call counts:', JSON.stringify(stats.callCounts, null, 2));
                }
                safeExit(1);
            });
        } else {
            // Only output result if debug mode is enabled (no automatic final result output)
            if (result.result !== undefined && DEBUG) {
                safeConsoleLog(result.result);
            }
            // Print call stack statistics only in debug mode
            if (DEBUG) {
                const stats = callStackTracker.getStats();
                safeConsoleLog('\n=== CALL STACK STATISTICS ===');
                safeConsoleLog('Maximum call stack depth:', stats.maxDepth);
                safeConsoleLog('Function call counts:', JSON.stringify(stats.callCounts, null, 2));
            }
        }
    } catch (error) {
        safeConsoleError(`Error executing file: ${error.message}`);
        // Print call stack statistics on error only in debug mode
        if (DEBUG) {
            const stats = callStackTracker.getStats();
            safeConsoleError('\n=== CALL STACK STATISTICS ON ERROR ===');
            safeConsoleError('Maximum call stack depth:', stats.maxDepth);
            safeConsoleError('Function call counts:', JSON.stringify(stats.callCounts, null, 2));
        }
        safeExit(1);
    }
}

/**
 * CLI argument handling and program entry point.
 * 
 * @description Processes command line arguments and executes the specified file.
 * Provides helpful error messages for incorrect usage.
 * 
 * The language is designed for file execution only (no REPL), so the CLI 
 * enforces this usage and provides helpful error messages for incorrect invocation.
 * The function validates that exactly one file path is provided and that the
 * file has the correct .txt extension.
 * 
 * Exits with appropriate error codes for different failure scenarios.
 */
async function main() {
    // Only run main function in Node.js/Bun environments
    if (!isNode && !isBun) {
        return; // Skip in browser environment
    }
    
    const args = process.argv.slice(2);

    if (args.length === 0) {
        safeConsoleError('Usage: node lang.js <file>');
        safeConsoleError('  Provide a file path to execute');
        safeExit(1);
    } else if (args.length === 1) {
        // Execute the file
        const filePath = args[0];
        await executeFile(filePath);
    } else {
        // Too many arguments
        safeConsoleError('Usage: node lang.js <file>');
        safeConsoleError('  Provide exactly one file path to execute');
        safeExit(1);
    }
}

// Start the program only if this file is run directly in Node.js/Bun
if ((isNode || isBun) && process.argv[1] && process.argv[1].endsWith('lang.js')) {
    main().catch(error => {
        safeConsoleError('Fatal error:', error.message);
        safeExit(1);
    });
}

// Export functions for harness integration
export { run, interpreter, lexer, parser };