diff options
Diffstat (limited to 'js/scripting-lang/docs/baba-yaga/0.0.1/lang.js.html')
-rw-r--r-- | js/scripting-lang/docs/baba-yaga/0.0.1/lang.js.html | 3074 |
1 files changed, 3074 insertions, 0 deletions
diff --git a/js/scripting-lang/docs/baba-yaga/0.0.1/lang.js.html b/js/scripting-lang/docs/baba-yaga/0.0.1/lang.js.html new file mode 100644 index 0000000..27fe6d6 --- /dev/null +++ b/js/scripting-lang/docs/baba-yaga/0.0.1/lang.js.html @@ -0,0 +1,3074 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width,initial-scale=1"> + <title>lang.js - Documentation</title> + + <script src="scripts/prettify/prettify.js"></script> + <script src="scripts/prettify/lang-css.js"></script> + <!--[if lt IE 9]> + <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> + <![endif]--> + <link type="text/css" rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"> + <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css"> + <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css"> +</head> +<body> + +<input type="checkbox" id="nav-trigger" class="nav-trigger" /> +<label for="nav-trigger" class="navicon-button x"> + <div class="navicon"></div> +</label> + +<label for="nav-trigger" class="overlay"></label> + +<nav> + <li class="nav-link nav-home-link"><a href="index.html">Home</a></li><li class="nav-heading">Tutorials</li><li class="nav-item"><a href="tutorial-00_Introduction.html">00_Introduction</a></li><li class="nav-item"><a href="tutorial-01_Function_Calls.html">01_Function_Calls</a></li><li class="nav-item"><a href="tutorial-02_Function_Composition.html">02_Function_Composition</a></li><li class="nav-item"><a href="tutorial-03_Table_Operations.html">03_Table_Operations</a></li><li class="nav-item"><a href="tutorial-04_Currying.html">04_Currying</a></li><li class="nav-item"><a href="tutorial-05_Pattern_Matching.html">05_Pattern_Matching</a></li><li class="nav-item"><a href="tutorial-06_Immutable_Tables.html">06_Immutable_Tables</a></li><li class="nav-item"><a href="tutorial-07_Function_References.html">07_Function_References</a></li><li class="nav-item"><a href="tutorial-08_Combinators.html">08_Combinators</a></li><li class="nav-item"><a href="tutorial-09_Expression_Based.html">09_Expression_Based</a></li><li class="nav-item"><a href="tutorial-10_Tables_Deep_Dive.html">10_Tables_Deep_Dive</a></li><li class="nav-item"><a href="tutorial-11_Standard_Library.html">11_Standard_Library</a></li><li class="nav-item"><a href="tutorial-12_IO_Operations.html">12_IO_Operations</a></li><li class="nav-item"><a href="tutorial-13_Error_Handling.html">13_Error_Handling</a></li><li class="nav-item"><a href="tutorial-14_Advanced_Combinators.html">14_Advanced_Combinators</a></li><li class="nav-item"><a href="tutorial-15_Integration_Patterns.html">15_Integration_Patterns</a></li><li class="nav-item"><a href="tutorial-16_Best_Practices.html">16_Best_Practices</a></li><li class="nav-item"><a href="tutorial-README.html">README</a></li><li class="nav-heading"><a href="global.html">Globals</a></li><li class="nav-item"><span class="nav-item-type type-member">M</span><span class="nav-item-name"><a href="global.html#callStackTracker">callStackTracker</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#debugError">debugError</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#debugLog">debugLog</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#executeFile">executeFile</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#initializeStandardLibrary">initializeStandardLibrary</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#interpreter">interpreter</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#lexer">lexer</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#main">main</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#parser">parser</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#readFile">readFile</a></span></li><li class="nav-item"><span class="nav-item-type type-function">F</span><span class="nav-item-name"><a href="global.html#run">run</a></span></li> +</nav> + +<div id="main"> + + <h1 class="page-title">lang.js</h1> + + + + + + + + <section> + <article> + <pre class="prettyprint source linenums"><code>// 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 }; + + +</code></pre> + </article> + </section> + + + + +</div> + +<br class="clear"> + +<footer> + Generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.4</a> on Tue Jul 29 2025 23:15:00 GMT-0400 (Eastern Daylight Time) using the Minami theme. +</footer> + +<script>prettyPrint();</script> +<script src="scripts/linenumber.js"></script> +</body> +</html> |