// 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 };