diff options
Diffstat (limited to 'js/scripting-lang/lang.js')
-rw-r--r-- | js/scripting-lang/lang.js | 600 |
1 files changed, 394 insertions, 206 deletions
diff --git a/js/scripting-lang/lang.js b/js/scripting-lang/lang.js index abe8a7c..070998e 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -1,16 +1,110 @@ +// 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 eliminates parsing ambiguity by translating all operations to function calls. + * 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) @@ -27,8 +121,18 @@ import { parser } from './parser.js'; * 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 @@ -42,7 +146,7 @@ function initializeStandardLibrary(scope) { * * 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 eliminates the need for explicit loops + * 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, @@ -51,10 +155,14 @@ function initializeStandardLibrary(scope) { * combinator-based architecture where all operations are function calls. * * This design choice aligns with the language's functional foundation and - * enables powerful abstractions like `map @double numbers` to transform + * 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) { + scope.map = function(f, x) { if (typeof f !== 'function') { throw new Error('map: first argument must be a function'); } @@ -110,7 +218,7 @@ function initializeStandardLibrary(scope) { * * Partial application support enables currying patterns where functions can * be built incrementally. This is essential for the combinator-based architecture - * where complex operations are built from simple, composable functions. + * where operations are built from simple, composable functions. * * Examples: * - compose(double, increment)(5) → double(increment(5)) → double(6) → 12 @@ -205,7 +313,7 @@ function initializeStandardLibrary(scope) { * * 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 eliminates the need for special + * 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, @@ -252,8 +360,8 @@ function initializeStandardLibrary(scope) { * who think in terms of data flow from left to right. * * Like compose, it supports partial application for currying patterns. - * This enables building complex transformation pipelines incrementally, - * which is essential for the combinator-based architecture where complex + * 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 @@ -300,7 +408,7 @@ function initializeStandardLibrary(scope) { * 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 eliminates the need for explicit loops and enables declarative data + * This reduces the need for explicit loops and enables declarative data * selection patterns. * * The function supports partial application: when called with only the predicate, @@ -309,7 +417,7 @@ function initializeStandardLibrary(scope) { * combinator-based architecture where all operations are function calls. * * This design choice aligns with the language's functional foundation and - * enables powerful abstractions like `filter @isEven numbers` to select + * enables abstractions like `filter @isEven numbers` to select * elements from a collection without explicit iteration. */ scope.filter = function(p, x) { @@ -363,10 +471,10 @@ function initializeStandardLibrary(scope) { * application. */ scope.reduce = function(f, init, x) { - if (process.env.DEBUG) { - console.log(`[DEBUG] reduce: f =`, typeof f, f); - console.log(`[DEBUG] reduce: init =`, init); - console.log(`[DEBUG] reduce: x =`, x); + if (DEBUG) { + safeConsoleLog(`[DEBUG] reduce: f =`, typeof f, f); + safeConsoleLog(`[DEBUG] reduce: init =`, init); + safeConsoleLog(`[DEBUG] reduce: x =`, x); } if (typeof f !== 'function') { @@ -376,10 +484,10 @@ function initializeStandardLibrary(scope) { if (init === undefined) { // Partial application: return a function that waits for the remaining arguments return function(init, x) { - if (process.env.DEBUG) { - console.log(`[DEBUG] reduce returned function: f =`, typeof f, f); - console.log(`[DEBUG] reduce returned function: init =`, init); - console.log(`[DEBUG] reduce returned function: x =`, 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 @@ -474,6 +582,12 @@ function initializeStandardLibrary(scope) { * 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; }; @@ -484,6 +598,12 @@ function initializeStandardLibrary(scope) { * @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; }; @@ -506,6 +626,12 @@ function initializeStandardLibrary(scope) { * 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; }; @@ -517,6 +643,15 @@ function initializeStandardLibrary(scope) { * @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'); } @@ -530,6 +665,12 @@ function initializeStandardLibrary(scope) { * @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; }; @@ -540,6 +681,12 @@ function initializeStandardLibrary(scope) { * @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); }; @@ -769,16 +916,16 @@ function initializeStandardLibrary(scope) { * - 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 powerful multi-argument element-wise operations like + * 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 (process.env.DEBUG) { - console.log(`[DEBUG] each called with: f=${typeof f}, x=${typeof x}`); - console.log(`[DEBUG] x value:`, x); + if (DEBUG) { + safeConsoleLog(`[DEBUG] each called with: f=${typeof f}, x=${typeof x}`); + safeConsoleLog(`[DEBUG] x value:`, x); } if (typeof f !== 'function') { @@ -847,11 +994,11 @@ function initializeStandardLibrary(scope) { * 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 eliminates side effects from table operations. + * 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 powerful data transformation + * on table values. This design choice enables data transformation * patterns while maintaining the functional programming principles of the language. * * Key design principles: @@ -1168,7 +1315,9 @@ function initializeStandardLibrary(scope) { /** * Interpreter: Walks the AST and evaluates each node using the combinator foundation. * - * @param {Object} ast - Abstract Syntax Tree to evaluate + * @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. * @@ -1178,7 +1327,7 @@ function initializeStandardLibrary(scope) { * * The interpreter implements a combinator-based architecture where all operations * are executed through function calls to standard library combinators. This design - * eliminates parsing ambiguity while preserving intuitive syntax. The parser translates + * 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. * @@ -1188,16 +1337,13 @@ function initializeStandardLibrary(scope) { * - 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. - * are translated to function calls to standard library combinators. This eliminates - * parsing ambiguity while preserving the original syntax. The parser generates - * FunctionCall nodes for operators (e.g., x + y becomes add(x, y)), and the - * interpreter executes these calls using the combinator functions in the global scope. * * The interpreter uses a global scope for variable storage and function definitions. * Each function call creates a new scope (using prototypal inheritance) to implement @@ -1214,8 +1360,12 @@ function initializeStandardLibrary(scope) { * * The combinator foundation ensures that all operations are executed through * function calls, providing a consistent and extensible execution model. This - * approach enables powerful abstractions and eliminates the need for special + * 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 }; @@ -1225,10 +1375,10 @@ function interpreter(ast, environment = null, initialState = {}) { let ioOperationsPerformed = false; // Debug: Check if combinators are available - if (process.env.DEBUG) { - console.log('[DEBUG] Available functions in global scope:', Object.keys(globalScope)); - console.log('[DEBUG] add function exists:', typeof globalScope.add === 'function'); - console.log('[DEBUG] subtract function exists:', typeof globalScope.subtract === 'function'); + 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 @@ -1237,7 +1387,7 @@ function interpreter(ast, environment = null, initialState = {}) { /** * Evaluates AST nodes in the global scope using the combinator foundation. * - * @param {Object} node - AST node to evaluate + * @param {ASTNode} node - AST node to evaluate * @returns {*} The result of evaluating the node * @throws {Error} For evaluation errors * @@ -1270,6 +1420,16 @@ function interpreter(ast, environment = null, initialState = {}) { * - 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'); @@ -1345,8 +1505,8 @@ function interpreter(ast, environment = null, initialState = {}) { key = evalNode(entry.key); } // Special handling for FunctionDeclaration nodes - if (process.env.DEBUG) { - console.log(`[DEBUG] TableLiteral: entry.value.type = ${entry.value.type}`); + 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 @@ -1542,27 +1702,27 @@ function interpreter(ast, environment = null, initialState = {}) { if (typeof node.name === 'string') { // Regular function call with string name funcToCall = globalScope[node.name]; - if (process.env.DEBUG) { - console.log(`[DEBUG] FunctionCall: looking up function '${node.name}' in globalScope, found:`, typeof funcToCall); + 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 (process.env.DEBUG) { - console.log(`[DEBUG] FunctionCall: looking up function '${node.name.value}' in globalScope, found:`, typeof funcToCall); + 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 (process.env.DEBUG) { - console.log(`[DEBUG] FunctionCall: evaluated function expression, found:`, typeof funcToCall); + if (DEBUG) { + safeConsoleLog(`[DEBUG] FunctionCall: evaluated function expression, found:`, typeof funcToCall); } } - if (funcToCall instanceof Function) { + if (typeof funcToCall === 'function') { let args = node.args.map(evalNode); - if (process.env.DEBUG) { - console.log(`[DEBUG] FunctionCall: calling function with args:`, args); + if (DEBUG) { + safeConsoleLog(`[DEBUG] FunctionCall: calling function with args:`, args); } return funcToCall(...args); } @@ -1573,16 +1733,16 @@ function interpreter(ast, environment = null, initialState = {}) { ? node.value.map(evalNode) : [evalNode(node.value)]; - if (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: whenValues =`, whenValues); + 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 (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: patterns =`, patterns); + if (DEBUG) { + safeConsoleLog(`[DEBUG] WhenExpression: patterns =`, patterns); } // Check if patterns match the values @@ -1594,14 +1754,14 @@ function interpreter(ast, environment = null, initialState = {}) { const value = whenValues[i]; const pattern = patterns[i]; - if (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: comparing value ${value} with pattern ${pattern}`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] WhenExpression: comparing value ${value} with pattern ${pattern}`); } if (pattern === true) { // Wildcard pattern // Wildcard always matches - if (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: wildcard matches`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] WhenExpression: wildcard matches`); } continue; } else if (typeof pattern === 'object' && pattern.type === 'FunctionCall') { @@ -1617,20 +1777,20 @@ function interpreter(ast, environment = null, initialState = {}) { }; } const patternResult = evalNode(patternToEvaluate); - if (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: boolean pattern result = ${patternResult}`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] WhenExpression: boolean pattern result = ${patternResult}`); } if (!patternResult) { matches = false; - if (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: boolean pattern does not match`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] WhenExpression: boolean pattern does not match`); } break; - } else { - if (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: boolean pattern matches`); + } 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; @@ -1642,31 +1802,31 @@ function interpreter(ast, environment = null, initialState = {}) { } if (!tableMatches) { matches = false; - if (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: table pattern does not match`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] WhenExpression: table pattern does not match`); } break; - } else { - if (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: table pattern matches`); + } else { + if (DEBUG) { + safeConsoleLog(`[DEBUG] WhenExpression: table pattern matches`); + } } - } } else if (value !== pattern) { matches = false; - if (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: pattern does not match`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] WhenExpression: pattern does not match`); } break; } else { - if (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: pattern matches`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] WhenExpression: pattern matches`); } } } } - if (process.env.DEBUG) { - console.log(`[DEBUG] WhenExpression: case matches = ${matches}`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] WhenExpression: case matches = ${matches}`); } if (matches) { @@ -1681,11 +1841,7 @@ function interpreter(ast, environment = null, initialState = {}) { case 'WildcardPattern': return true; case 'IOInExpression': - const readline = require('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); + const rl = createReadline(); return new Promise((resolve) => { rl.question('', (input) => { @@ -1696,7 +1852,7 @@ function interpreter(ast, environment = null, initialState = {}) { }); case 'IOOutExpression': const outputValue = evalNode(node.value); - console.log(outputValue); + safeConsoleLog(outputValue); ioOperationsPerformed = true; return outputValue; case 'IOAssertExpression': @@ -1708,13 +1864,13 @@ function interpreter(ast, environment = null, initialState = {}) { case 'IOListenExpression': // Return current state from environment if available, otherwise placeholder if (environment && typeof environment.getCurrentState === 'function') { - if (process.env.DEBUG) { - console.log('[DEBUG] ..listen called - returning state from environment'); + if (DEBUG) { + safeConsoleLog('[DEBUG] ..listen called - returning state from environment'); } return environment.getCurrentState(); } else { - if (process.env.DEBUG) { - console.log('[DEBUG] ..listen called - returning placeholder state'); + if (DEBUG) { + safeConsoleLog('[DEBUG] ..listen called - returning placeholder state'); } return { status: 'placeholder', message: 'State not available in standalone mode' }; } @@ -1722,19 +1878,19 @@ function interpreter(ast, environment = null, initialState = {}) { const emitValue = evalNode(node.value); // Send value to environment if available, otherwise log to console if (environment && typeof environment.emitValue === 'function') { - if (process.env.DEBUG) { - console.log('[DEBUG] ..emit called - sending to environment'); + if (DEBUG) { + safeConsoleLog('[DEBUG] ..emit called - sending to environment'); } environment.emitValue(emitValue); } else { - console.log('[EMIT]', emitValue); + safeConsoleLog('[EMIT]', emitValue); } ioOperationsPerformed = true; return emitValue; case 'FunctionReference': const functionValue = globalScope[node.name]; - if (process.env.DEBUG) { - console.log(`[DEBUG] FunctionReference: looking up '${node.name}' in globalScope, found:`, typeof functionValue); + 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`); @@ -1757,7 +1913,7 @@ function interpreter(ast, environment = null, initialState = {}) { /** * Evaluates AST nodes in a local scope with access to parent scope. * - * @param {Object} node - AST node to evaluate + * @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 @@ -1778,6 +1934,16 @@ function interpreter(ast, environment = null, initialState = {}) { * 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'); @@ -1971,16 +2137,16 @@ function interpreter(ast, environment = null, initialState = {}) { ? node.value.map(val => localEvalNodeWithScope(val, scope)) : [localEvalNodeWithScope(node.value, scope)]; - if (process.env.DEBUG) { - console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: whenValues =`, whenValues); + 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 (process.env.DEBUG) { - console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: patterns =`, patterns); + if (DEBUG) { + safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: patterns =`, patterns); } // Check if patterns match the values @@ -1992,14 +2158,14 @@ function interpreter(ast, environment = null, initialState = {}) { const value = whenValues[i]; const pattern = patterns[i]; - if (process.env.DEBUG) { - console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: comparing value ${value} with pattern ${pattern}`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: comparing value ${value} with pattern ${pattern}`); } if (pattern === true) { // Wildcard pattern // Wildcard always matches - if (process.env.DEBUG) { - console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: wildcard matches`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: wildcard matches`); } continue; } else if (typeof pattern === 'object' && pattern !== null && typeof value === 'object' && value !== null) { @@ -2013,31 +2179,31 @@ function interpreter(ast, environment = null, initialState = {}) { } if (!tableMatches) { matches = false; - if (process.env.DEBUG) { - console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: table pattern does not match`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: table pattern does not match`); } break; - } else { - if (process.env.DEBUG) { - console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: table pattern matches`); + } else { + if (DEBUG) { + safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: table pattern matches`); + } } - } } else if (value !== pattern) { matches = false; - if (process.env.DEBUG) { - console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern does not match`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern does not match`); } break; } else { - if (process.env.DEBUG) { - console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern matches`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern matches`); } } } } - if (process.env.DEBUG) { - console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: case matches = ${matches}`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] localEvalNodeWithScope WhenExpression: case matches = ${matches}`); } if (matches) { @@ -2052,22 +2218,18 @@ function interpreter(ast, environment = null, initialState = {}) { case 'WildcardPattern': return true; case 'IOInExpression': - const readline = require('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); + const rl2 = createReadline(); return new Promise((resolve) => { - rl.question('', (input) => { - rl.close(); + rl2.question('', (input) => { + rl2.close(); const num = parseInt(input); resolve(isNaN(num) ? input : num); }); }); case 'IOOutExpression': const localOutputValue = localEvalNodeWithScope(node.value, scope); - console.log(localOutputValue); + safeConsoleLog(localOutputValue); ioOperationsPerformed = true; return localOutputValue; case 'IOAssertExpression': @@ -2079,13 +2241,13 @@ function interpreter(ast, environment = null, initialState = {}) { case 'IOListenExpression': // Return current state from environment if available, otherwise placeholder if (environment && typeof environment.getCurrentState === 'function') { - if (process.env.DEBUG) { - console.log('[DEBUG] ..listen called - returning state from environment'); + if (DEBUG) { + safeConsoleLog('[DEBUG] ..listen called - returning state from environment'); } return environment.getCurrentState(); } else { - if (process.env.DEBUG) { - console.log('[DEBUG] ..listen called - returning placeholder state'); + if (DEBUG) { + safeConsoleLog('[DEBUG] ..listen called - returning placeholder state'); } return { status: 'placeholder', message: 'State not available in standalone mode' }; } @@ -2093,12 +2255,12 @@ function interpreter(ast, environment = null, initialState = {}) { const localEmitValue = localEvalNodeWithScope(node.value, scope); // Send value to environment if available, otherwise log to console if (environment && typeof environment.emitValue === 'function') { - if (process.env.DEBUG) { - console.log('[DEBUG] ..emit called - sending to environment'); + if (DEBUG) { + safeConsoleLog('[DEBUG] ..emit called - sending to environment'); } environment.emitValue(localEmitValue); } else { - console.log('[EMIT]', localEmitValue); + safeConsoleLog('[EMIT]', localEmitValue); } ioOperationsPerformed = true; return localEmitValue; @@ -2383,22 +2545,18 @@ function interpreter(ast, environment = null, initialState = {}) { case 'WildcardPattern': return true; case 'IOInExpression': - const readline = require('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); + const rl3 = createReadline(); return new Promise((resolve) => { - rl.question('', (input) => { - rl.close(); + rl3.question('', (input) => { + rl3.close(); const num = parseInt(input); resolve(isNaN(num) ? input : num); }); }); case 'IOOutExpression': const localOutputValue = localEvalNode(node.value); - console.log(localOutputValue); + safeConsoleLog(localOutputValue); ioOperationsPerformed = true; return localOutputValue; case 'IOAssertExpression': @@ -2410,13 +2568,13 @@ function interpreter(ast, environment = null, initialState = {}) { case 'IOListenExpression': // Return current state from environment if available, otherwise placeholder if (environment && typeof environment.getCurrentState === 'function') { - if (process.env.DEBUG) { - console.log('[DEBUG] ..listen called - returning state from environment'); + if (DEBUG) { + safeConsoleLog('[DEBUG] ..listen called - returning state from environment'); } return environment.getCurrentState(); } else { - if (process.env.DEBUG) { - console.log('[DEBUG] ..listen called - returning placeholder state'); + if (DEBUG) { + safeConsoleLog('[DEBUG] ..listen called - returning placeholder state'); } return { status: 'placeholder', message: 'State not available in standalone mode' }; } @@ -2424,12 +2582,12 @@ function interpreter(ast, environment = null, initialState = {}) { const localEmitValue = localEvalNode(node.value); // Send value to environment if available, otherwise log to console if (environment && typeof environment.emitValue === 'function') { - if (process.env.DEBUG) { - console.log('[DEBUG] ..emit called - sending to environment'); + if (DEBUG) { + safeConsoleLog('[DEBUG] ..emit called - sending to environment'); } environment.emitValue(localEmitValue); } else { - console.log('[EMIT]', localEmitValue); + safeConsoleLog('[EMIT]', localEmitValue); } ioOperationsPerformed = true; return localEmitValue; @@ -2472,10 +2630,39 @@ function interpreter(ast, environment = null, initialState = {}) { /** * Run script with environment support for harness integration * - * @param {string} scriptContent - Script content to execute - * @param {Object} initialState - Initial state for the script - * @param {Object} environment - Script environment for IO operations - * @returns {Object} Script execution result + * @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 @@ -2508,14 +2695,14 @@ function run(scriptContent, initialState = {}, environment = null) { * 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 complex nested + * making it suitable for tracing execution flow through nested * expressions and function applications. */ function debugLog(message, data = null) { - if (process.env.DEBUG) { - console.log(`[DEBUG] ${message}`); + if (DEBUG) { + safeConsoleLog(`[DEBUG] ${message}`); if (data) { - console.log(data); + safeConsoleLog(data); } } } @@ -2539,10 +2726,10 @@ function debugLog(message, data = null) { * execution pipeline. */ function debugError(message, error = null) { - if (process.env.DEBUG) { - console.error(`[DEBUG ERROR] ${message}`); + if (DEBUG) { + safeConsoleError(`[DEBUG ERROR] ${message}`); if (error) { - console.error(error); + safeConsoleError(error); } } } @@ -2559,7 +2746,7 @@ function debugError(message, error = null) { * potential infinite recursion by monitoring stack depth. * * This tool is particularly important for the combinator-based architecture - * where function calls are the primary execution mechanism, and complex + * 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. @@ -2603,8 +2790,8 @@ const callStackTracker = { throw new Error(`Potential infinite recursion detected. Call stack depth: ${this.stack.length}`); } - if (process.env.DEBUG && this.stack.length % 100 === 0) { - console.log(`[DEBUG] Call stack depth: ${this.stack.length}, Max: ${this.maxDepth}`); + if (DEBUG && this.stack.length % 100 === 0) { + safeConsoleLog(`[DEBUG] Call stack depth: ${this.stack.length}, Max: ${this.maxDepth}`); } }, @@ -2664,22 +2851,18 @@ const callStackTracker = { * workflow where tests and examples are stored as .txt files. */ async function readFile(filePath) { - // Check if we're in a browser environment - if (typeof window !== 'undefined') { - // Browser environment - would need to implement file input or fetch - throw new Error('File I/O not supported in browser environment'); - } + // Use cross-platform filesystem + const fs = createFileSystem(); - // Node.js or Bun environment - try { - // Try dynamic import for ES modules compatibility - const fs = await import('fs'); - return fs.readFileSync(filePath, 'utf8'); - } catch (error) { - // Fallback to require for older Node.js versions - const fs = require('fs'); - return fs.readFileSync(filePath, 'utf8'); - } + return new Promise((resolve, reject) => { + fs.readFile(filePath, 'utf8', (error, data) => { + if (error) { + reject(error); + } else { + resolve(data); + } + }); + }); } /** @@ -2714,8 +2897,8 @@ async function readFile(filePath) { async function executeFile(filePath) { try { // Validate file extension - if (!filePath.endsWith('.txt')) { - throw new Error('Only .txt files are supported'); + if (!filePath.endsWith('.txt') && !filePath.endsWith('.baba')) { + throw new Error('Only .txt and .baba files are supported'); } const input = await readFile(filePath); @@ -2733,50 +2916,50 @@ async function executeFile(filePath) { if (result instanceof Promise) { result.then(finalResult => { // Only output result if debug mode is enabled (no automatic final result output) - if (finalResult.result !== undefined && process.env.DEBUG) { - console.log(finalResult.result); + if (finalResult.result !== undefined && DEBUG) { + safeConsoleLog(finalResult.result); } // Print call stack statistics only in debug mode - if (process.env.DEBUG) { + if (DEBUG) { const stats = callStackTracker.getStats(); - console.log('\n=== CALL STACK STATISTICS ==='); - console.log('Maximum call stack depth:', stats.maxDepth); - console.log('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); + safeConsoleLog('\n=== CALL STACK STATISTICS ==='); + safeConsoleLog('Maximum call stack depth:', stats.maxDepth); + safeConsoleLog('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); } }).catch(error => { - console.error(`Error executing file: ${error.message}`); + safeConsoleError(`Error executing file: ${error.message}`); // Print call stack statistics on error only in debug mode - if (process.env.DEBUG) { + if (DEBUG) { const stats = callStackTracker.getStats(); - console.error('\n=== CALL STACK STATISTICS ON ERROR ==='); - console.error('Maximum call stack depth:', stats.maxDepth); - console.error('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); + safeConsoleError('\n=== CALL STACK STATISTICS ON ERROR ==='); + safeConsoleError('Maximum call stack depth:', stats.maxDepth); + safeConsoleError('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); } - process.exit(1); + safeExit(1); }); } else { // Only output result if debug mode is enabled (no automatic final result output) - if (result.result !== undefined && process.env.DEBUG) { - console.log(result.result); + if (result.result !== undefined && DEBUG) { + safeConsoleLog(result.result); } // Print call stack statistics only in debug mode - if (process.env.DEBUG) { + if (DEBUG) { const stats = callStackTracker.getStats(); - console.log('\n=== CALL STACK STATISTICS ==='); - console.log('Maximum call stack depth:', stats.maxDepth); - console.log('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); + safeConsoleLog('\n=== CALL STACK STATISTICS ==='); + safeConsoleLog('Maximum call stack depth:', stats.maxDepth); + safeConsoleLog('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); } } } catch (error) { - console.error(`Error executing file: ${error.message}`); + safeConsoleError(`Error executing file: ${error.message}`); // Print call stack statistics on error only in debug mode - if (process.env.DEBUG) { + if (DEBUG) { const stats = callStackTracker.getStats(); - console.error('\n=== CALL STACK STATISTICS ON ERROR ==='); - console.error('Maximum call stack depth:', stats.maxDepth); - console.error('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); + safeConsoleError('\n=== CALL STACK STATISTICS ON ERROR ==='); + safeConsoleError('Maximum call stack depth:', stats.maxDepth); + safeConsoleError('Function call counts:', JSON.stringify(stats.callCounts, null, 2)); } - process.exit(1); + safeExit(1); } } @@ -2794,29 +2977,34 @@ async function executeFile(filePath) { * 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) { - console.error('Usage: node lang.js <file>'); - console.error(' Provide a file path to execute'); - process.exit(1); + 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 - console.error('Usage: node lang.js <file>'); - console.error(' Provide exactly one file path to execute'); - process.exit(1); + 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 -if (process.argv[1] && process.argv[1].endsWith('lang.js')) { +// 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 => { - console.error('Fatal error:', error.message); - process.exit(1); + safeConsoleError('Fatal error:', error.message); + safeExit(1); }); } |