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, 0 insertions, 3074 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 deleted file mode 100644 index 27fe6d6..0000000 --- a/js/scripting-lang/docs/baba-yaga/0.0.1/lang.js.html +++ /dev/null @@ -1,3074 +0,0 @@ -<!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> |