diff options
Diffstat (limited to 'js/scripting-lang/lang.js')
-rw-r--r-- | js/scripting-lang/lang.js | 2378 |
1 files changed, 804 insertions, 1574 deletions
diff --git a/js/scripting-lang/lang.js b/js/scripting-lang/lang.js index 0f827bf..9fa048f 100644 --- a/js/scripting-lang/lang.js +++ b/js/scripting-lang/lang.js @@ -1,19 +1,32 @@ +// Cross-platform scripting language implementation +// Supports Node.js, Bun, and browser environments + +import { lexer, TokenType } from './lexer.js'; +import { parser } from './parser.js'; + /** * 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. * - * @description Injects higher-order functions into the interpreter's global scope. - * These functions provide functional programming utilities like map, compose, pipe, etc. + * 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) * - * @why Injecting the standard library directly into the scope ensures that user code - * can access these functions as if they were built-in, without special syntax or - * reserved keywords. This approach also allows for easy extension and testing, as - * the library is just a set of regular functions in the scope chain. + * 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. * - * @how Each function is added as a property of the scope object. 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. + * 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. */ function initializeStandardLibrary(scope) { /** @@ -22,6 +35,10 @@ function initializeStandardLibrary(scope) { * @param {*} x - Value 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. This enables functional + * programming patterns where data transformations are expressed as function + * applications rather than imperative operations. */ scope.map = function(f, x) { if (typeof f === 'function') { @@ -32,25 +49,41 @@ function initializeStandardLibrary(scope) { }; /** - * Compose: Compose two functions (f ∘ g)(x) = f(g(x)) - * @param {Function} f - Outer function - * @param {Function} g - Inner function - * @param {*} [x] - Optional argument to apply composed function to - * @returns {Function|*} Either a composed function or the result of applying it - * @throws {Error} When first two arguments are not functions + * Compose: Compose functions (f ∘ g)(x) = f(g(x)) + * @param {Function} f - First 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. When used with partial + * application, it enables currying patterns where functions can be built + * incrementally. This supports the 'via' operator in the language syntax + * and enables powerful function composition chains. */ - scope.compose = function(f, g, x) { - if (typeof f === 'function' && typeof g === 'function') { - if (arguments.length === 3) { - return f(g(x)); - } else { + 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)); }; - } - } else { - throw new Error('compose: first two arguments must be functions'); + }; + } + + if (typeof g !== 'function') { + throw new Error(`compose: second argument must be a function, got ${typeof g}`); } + + return function(x) { + return f(g(x)); + }; }; /** @@ -75,6 +108,12 @@ function initializeStandardLibrary(scope) { * @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. */ scope.apply = function(f, x) { if (typeof f === 'function') { @@ -87,23 +126,39 @@ function initializeStandardLibrary(scope) { /** * Pipe: Compose functions in left-to-right order (opposite of compose) * @param {Function} f - First function - * @param {Function} g - Second function - * @param {*} [x] - Optional argument to apply piped function to - * @returns {Function|*} Either a piped function or the result of applying it - * @throws {Error} When first two arguments are not functions + * @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. Like compose, it supports partial application + * for currying patterns. This enables functional programming patterns where + * data flows through a series of transformations in a natural reading order. */ - scope.pipe = function(f, g, x) { - if (typeof f === 'function' && typeof g === 'function') { - if (arguments.length === 3) { - return g(f(x)); - } else { + 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)); }; - } - } else { - throw new Error('pipe: first two arguments must be functions'); + }; + } + + if (typeof g !== 'function') { + throw new Error(`pipe: second argument must be a function, got ${typeof g}`); } + + return function(x) { + return g(f(x)); + }; }; /** @@ -152,1478 +207,277 @@ function initializeStandardLibrary(scope) { throw new Error('fold: first argument must be a function'); } }; -} - -/** - * TokenType enumeration for all supported token types. - * - * @type {Object.<string, string>} - * - * @description A flat object mapping token names to their string representations. - * This approach allows for fast string comparisons and easy extensibility. - * - * @why Using a flat object avoids the need for import/export or enum boilerplate, - * and makes it easy to add new token types as the language evolves. - */ -const TokenType = { - NUMBER: 'NUMBER', - PLUS: 'PLUS', - MINUS: 'MINUS', - MULTIPLY: 'MULTIPLY', - DIVIDE: 'DIVIDE', - IDENTIFIER: 'IDENTIFIER', - ASSIGNMENT: 'ASSIGNMENT', - ARROW: 'ARROW', - CASE: 'CASE', - OF: 'OF', - WILDCARD: 'WILDCARD', - FUNCTION: 'FUNCTION', - LEFT_PAREN: 'LEFT_PAREN', - RIGHT_PAREN: 'RIGHT_PAREN', - LEFT_BRACE: 'LEFT_BRACE', - RIGHT_BRACE: 'RIGHT_BRACE', - LEFT_BRACKET: 'LEFT_BRACKET', - RIGHT_BRACKET: 'RIGHT_BRACKET', - SEMICOLON: 'SEMICOLON', - COMMA: 'COMMA', - DOT: 'DOT', - STRING: 'STRING', - TRUE: 'TRUE', - FALSE: 'FALSE', - AND: 'AND', - OR: 'OR', - XOR: 'XOR', - NOT: 'NOT', - EQUALS: 'EQUALS', - LESS_THAN: 'LESS_THAN', - GREATER_THAN: 'GREATER_THAN', - LESS_EQUAL: 'LESS_EQUAL', - GREATER_EQUAL: 'GREATER_EQUAL', - NOT_EQUAL: 'NOT_EQUAL', - MODULO: 'MODULO', - POWER: 'POWER', - IO_IN: 'IO_IN', - IO_OUT: 'IO_OUT', - IO_ASSERT: 'IO_ASSERT', - FUNCTION_REF: 'FUNCTION_REF' -}; - -/** - * Lexer: Converts source code to tokens. - * - * @param {string} input - Source code to tokenize - * @returns {Array.<Object>} Array of token objects with type and value properties - * @throws {Error} For unterminated strings or unexpected characters - * - * @description Performs lexical analysis by converting source code into a stream of tokens. - * Handles whitespace, nested comments, numbers (integers and decimals), strings, - * identifiers/keywords, and both single- and multi-character operators. - * - * @how Uses a single pass with a while loop and manual character inspection. - * Each character is examined to determine the appropriate token type, with - * special handling for multi-character tokens and nested constructs. - * - * @why Manual lexing allows for fine-grained control over tokenization, especially - * for edge cases like nested comments and multi-character IO operations. This - * approach also makes it easier to debug and extend the lexer for new language features. - * - * @note IO operations (..in, ..out, ..assert) are recognized as multi-character - * tokens to avoid ambiguity with the dot operator. Decimal numbers are parsed - * as a single token to support floating point arithmetic. - */ -function lexer(input) { - let current = 0; - const tokens = []; - while (current < input.length) { - let char = input[current]; - - // Skip whitespace - if (/\s/.test(char)) { - current++; - continue; - } - - // Handle nested comments: /* ... */ with support for /* /* ... */ */ - if (char === '/' && input[current + 1] === '*') { - let commentDepth = 1; - current += 2; // Skip /* - - while (current < input.length && commentDepth > 0) { - if (input[current] === '/' && input[current + 1] === '*') { - commentDepth++; - current += 2; - } else if (input[current] === '*' && input[current + 1] === '/') { - commentDepth--; - current += 2; - } else { - current++; - } - } - continue; - } - - // Parse numbers (integers and decimals) - if (/[0-9]/.test(char)) { - let value = ''; - while (current < input.length && /[0-9]/.test(input[current])) { - value += input[current]; - current++; - } - - // Check for decimal point - if (current < input.length && input[current] === '.') { - value += input[current]; - current++; - - // Parse decimal part - while (current < input.length && /[0-9]/.test(input[current])) { - value += input[current]; - current++; - } - - tokens.push({ - type: TokenType.NUMBER, - value: parseFloat(value) - }); - } else { - tokens.push({ - type: TokenType.NUMBER, - value: parseInt(value) - }); - } - continue; - } - - // Parse string literals - if (char === '"') { - let value = ''; - current++; // Skip opening quote - - while (current < input.length && input[current] !== '"') { - value += input[current]; - current++; - } - - if (current < input.length) { - current++; // Skip closing quote - tokens.push({ - type: TokenType.STRING, - value: value - }); - } else { - throw new Error('Unterminated string'); - } - continue; - } - - // Parse identifiers and keywords - if (/[a-zA-Z_]/.test(char)) { - let value = ''; - while (current < input.length && /[a-zA-Z0-9_]/.test(input[current])) { - value += input[current]; - current++; - } - - // Check for keywords - switch (value) { - case 'case': - tokens.push({ type: TokenType.CASE }); - break; - case 'of': - tokens.push({ type: TokenType.OF }); - break; - case 'function': - tokens.push({ type: TokenType.FUNCTION }); - break; - case 'true': - tokens.push({ type: TokenType.TRUE }); - break; - case 'false': - tokens.push({ type: TokenType.FALSE }); - break; - case 'and': - tokens.push({ type: TokenType.AND }); - break; - case 'or': - tokens.push({ type: TokenType.OR }); - break; - case 'xor': - tokens.push({ type: TokenType.XOR }); - break; - case 'not': - tokens.push({ type: TokenType.NOT }); - break; - case '_': - tokens.push({ type: TokenType.WILDCARD }); - break; - default: - tokens.push({ - type: TokenType.IDENTIFIER, - value: value - }); - } - continue; - } - - // Parse two-character operators - if (current + 1 < input.length) { - const twoChar = char + input[current + 1]; - switch (twoChar) { - case '->': - tokens.push({ type: TokenType.ARROW }); - current += 2; - continue; - case '==': - tokens.push({ type: TokenType.EQUALS }); - current += 2; - continue; - case '!=': - tokens.push({ type: TokenType.NOT_EQUAL }); - current += 2; - continue; - case '<=': - tokens.push({ type: TokenType.LESS_EQUAL }); - current += 2; - continue; - case '>=': - tokens.push({ type: TokenType.GREATER_EQUAL }); - current += 2; - continue; - case '..': - // Parse IO operations: ..in, ..out, ..assert - if (current + 2 < input.length) { - const ioChar = input[current + 2]; - switch (ioChar) { - case 'i': - if (current + 3 < input.length && input[current + 3] === 'n') { - tokens.push({ type: TokenType.IO_IN }); - current += 4; - continue; - } - break; - case 'o': - if (current + 3 < input.length && input[current + 3] === 'u') { - if (current + 4 < input.length && input[current + 4] === 't') { - tokens.push({ type: TokenType.IO_OUT }); - current += 5; - continue; - } - } - break; - case 'a': - if (current + 3 < input.length && input[current + 3] === 's') { - if (current + 4 < input.length && input[current + 4] === 's') { - if (current + 5 < input.length && input[current + 5] === 'e') { - if (current + 6 < input.length && input[current + 6] === 'r') { - if (current + 7 < input.length && input[current + 7] === 't') { - tokens.push({ type: TokenType.IO_ASSERT }); - current += 8; - continue; - } - } - } - } - } - break; - } - } - // If we get here, it's not a complete IO operation, so skip the '..' - current += 2; - continue; - } - } - - // Parse single character operators - switch (char) { - case '+': - tokens.push({ type: TokenType.PLUS }); - break; - case '-': - tokens.push({ type: TokenType.MINUS }); - break; - case '*': - tokens.push({ type: TokenType.MULTIPLY }); - break; - case '/': - tokens.push({ type: TokenType.DIVIDE }); - break; - case '%': - tokens.push({ type: TokenType.MODULO }); - break; - case '^': - tokens.push({ type: TokenType.POWER }); - break; - case ':': - tokens.push({ type: TokenType.ASSIGNMENT }); - break; - case '(': - tokens.push({ type: TokenType.LEFT_PAREN }); - break; - case ')': - tokens.push({ type: TokenType.RIGHT_PAREN }); - break; - case '{': - tokens.push({ type: TokenType.LEFT_BRACE }); - break; - case '}': - tokens.push({ type: TokenType.RIGHT_BRACE }); - break; - case '[': - tokens.push({ type: TokenType.LEFT_BRACKET }); - break; - case ']': - tokens.push({ type: TokenType.RIGHT_BRACKET }); - break; - case ';': - tokens.push({ type: TokenType.SEMICOLON }); - break; - case ',': - tokens.push({ type: TokenType.COMMA }); - break; - case '.': - tokens.push({ type: TokenType.DOT }); - break; - case '@': - tokens.push({ type: TokenType.FUNCTION_REF }); - break; - case '_': - tokens.push({ type: TokenType.WILDCARD }); - break; - case '=': - tokens.push({ type: TokenType.EQUALS }); - break; - case '<': - tokens.push({ type: TokenType.LESS_THAN }); - break; - case '>': - tokens.push({ type: TokenType.GREATER_THAN }); - break; - default: - throw new Error(`Unexpected character: ${char}`); - } - - current++; - } + // ===== ARITHMETIC COMBINATORS ===== - return tokens; -} - -/** - * Parser: Converts tokens to an Abstract Syntax Tree (AST). - * - * @param {Array.<Object>} tokens - Array of tokens from the lexer - * @returns {Object} Abstract Syntax Tree with program body - * @throws {Error} For parsing errors like unexpected tokens or missing delimiters - * - * @description Implements a recursive descent parser that builds an AST from tokens. - * Handles all language constructs including expressions, statements, function - * definitions, case expressions, table literals, and IO operations. - * - * @how Implements a recursive descent parser, with separate functions for each - * precedence level (expression, term, factor, primary). Handles chained table - * access, function calls, and complex constructs like case expressions and - * function definitions. - * - * @why Recursive descent is chosen for its clarity and flexibility, especially - * for a language with many context-sensitive constructs (e.g., case expressions, - * function definitions, chained access). The parser is structured to minimize - * circular dependencies and infinite recursion, with careful placement of IO - * and case expression parsing. - * - * @note The parser supports multi-parameter case expressions and function - * definitions, using lookahead to distinguish between assignments and function - * declarations. Table literals are parsed with support for both array-like and - * key-value entries, inspired by Lua. - */ -function parser(tokens) { - let current = 0; + /** + * Add: Add two numbers + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Sum of x and y + */ + scope.add = function(x, y) { + return x + y; + }; - // Reset call stack tracker for parser - callStackTracker.reset(); + /** + * 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) { + return x - y; + }; - // Define all parsing functions outside of walk to avoid circular dependencies + /** + * Multiply: Multiply two numbers + * @param {number} x - First number + * @param {number} y - Second number + * @returns {number} Product of x and y + */ + scope.multiply = function(x, y) { + return x * y; + }; - function parseLogicalExpression() { - callStackTracker.push('parseLogicalExpression', ''); - - try { - /** - * Parses logical expressions with lowest precedence. - * - * @returns {Object} AST node representing the logical expression - * - * @description Parses logical operators (and, or, xor) with proper - * precedence handling and left associativity. - * - * @why Logical operators should have lower precedence than arithmetic - * and comparison operators to ensure proper grouping of expressions - * like "isEven 10 and isPositive 5". - */ - let left = parseExpression(); - - while (current < tokens.length && - (tokens[current].type === TokenType.AND || - tokens[current].type === TokenType.OR || - tokens[current].type === TokenType.XOR)) { - - const operator = tokens[current].type; - current++; - const right = parseExpression(); - - switch (operator) { - case TokenType.AND: - left = { type: 'AndExpression', left, right }; - break; - case TokenType.OR: - left = { type: 'OrExpression', left, right }; - break; - case TokenType.XOR: - left = { type: 'XorExpression', left, right }; - break; - } - } - - return left; - } finally { - callStackTracker.pop(); + /** + * 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 === 0) { + throw new Error('Division by zero'); } - } + return x / y; + }; - function parseExpression() { - callStackTracker.push('parseExpression', ''); - - try { - /** - * Parses expressions with left-associative binary operators. - * - * @returns {Object} AST node representing the expression - * - * @description Parses addition, subtraction, and comparison operators - * with proper precedence and associativity. - * - * @why Operator precedence is handled by splitting parsing into multiple - * functions (expression, term, factor, primary). This structure avoids - * ambiguity and ensures correct grouping of operations. - * - * @note Special case handling for unary minus after function references - * to distinguish from binary minus operations. - */ - let left = parseTerm(); - - while (current < tokens.length && - (tokens[current].type === TokenType.PLUS || - tokens[current].type === TokenType.MINUS || - tokens[current].type === TokenType.EQUALS || - tokens[current].type === TokenType.NOT_EQUAL || - tokens[current].type === TokenType.LESS_THAN || - tokens[current].type === TokenType.GREATER_THAN || - tokens[current].type === TokenType.LESS_EQUAL || - tokens[current].type === TokenType.GREATER_EQUAL)) { - - const operator = tokens[current].type; - - // Special case: Don't treat MINUS as binary operator if left is a FunctionReference - // This handles cases like "filter @isPositive -3" where -3 should be a separate argument - if (operator === TokenType.MINUS && left.type === 'FunctionReference') { - // This is likely a function call with unary minus argument, not a binary operation - // Return the left side and let the caller handle it - return left; - } - - current++; - const right = parseTerm(); - - switch (operator) { - case TokenType.PLUS: - left = { type: 'PlusExpression', left, right }; - break; - case TokenType.MINUS: - left = { type: 'MinusExpression', left, right }; - break; - case TokenType.EQUALS: - left = { type: 'EqualsExpression', left, right }; - break; - case TokenType.NOT_EQUAL: - left = { type: 'NotEqualExpression', left, right }; - break; - case TokenType.LESS_THAN: - left = { type: 'LessThanExpression', left, right }; - break; - case TokenType.GREATER_THAN: - left = { type: 'GreaterThanExpression', left, right }; - break; - case TokenType.LESS_EQUAL: - left = { type: 'LessEqualExpression', left, right }; - break; - case TokenType.GREATER_EQUAL: - left = { type: 'GreaterEqualExpression', left, right }; - break; - } - } - - return left; - } finally { - callStackTracker.pop(); - } - } + /** + * 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) { + return x % y; + }; - function parseTerm() { - callStackTracker.push('parseTerm', ''); - - try { - /** - * Parses multiplication, division, and modulo operations. - * - * @returns {Object} AST node representing the term - * - * @description Parses multiplicative operators with higher precedence - * than addition/subtraction. - * - * @why By handling these operators at a separate precedence level, the - * parser ensures that multiplication/division bind tighter than - * addition/subtraction, matching standard arithmetic rules. - */ - let left = parseFactor(); - - while (current < tokens.length && - (tokens[current].type === TokenType.MULTIPLY || - tokens[current].type === TokenType.DIVIDE || - tokens[current].type === TokenType.MODULO)) { - - const operator = tokens[current].type; - current++; - const right = parseFactor(); - - switch (operator) { - case TokenType.MULTIPLY: - left = { type: 'MultiplyExpression', left, right }; - break; - case TokenType.DIVIDE: - left = { type: 'DivideExpression', left, right }; - break; - case TokenType.MODULO: - left = { type: 'ModuloExpression', left, right }; - break; - } - } - - return left; - } finally { - callStackTracker.pop(); - } - } + /** + * 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) { + return Math.pow(x, y); + }; - function parseFactor() { - callStackTracker.push('parseFactor', ''); - - try { - /** - * Parses exponentiation and primary expressions. - * - * @returns {Object} AST node representing the factor - * - * @description Parses exponentiation with right associativity and - * highest precedence among arithmetic operators. - * - * @why Exponentiation is right-associative and binds tighter than - * multiplication/division, so it is handled at the factor level. This - * also allows for future extension to other high-precedence operators. - */ - let left = parsePrimary(); - - while (current < tokens.length && tokens[current].type === TokenType.POWER) { - current++; - const right = parsePrimary(); - left = { type: 'PowerExpression', left, right }; - } - - return left; - } finally { - callStackTracker.pop(); - } - } + /** + * Negate: Negate a number + * @param {number} x - Number to negate + * @returns {number} Negated value of x + */ + scope.negate = function(x) { + return -x; + }; - function parsePrimary() { - callStackTracker.push('parsePrimary', ''); - - try { - /** - * Parses literals, identifiers, function definitions, assignments, - * table literals, and parenthesized expressions. - * - * @returns {Object} AST node representing the primary expression - * @throws {Error} For parsing errors like unexpected tokens - * - * @description The core parsing function that handles all atomic and - * context-sensitive constructs in the language. - * - * @why This function is the core of the recursive descent parser, handling - * all atomic and context-sensitive constructs. Special care is taken to - * avoid circular dependencies by not calling higher-level parsing functions. - */ - - const token = tokens[current]; - if (!token) { - throw new Error('Unexpected end of input'); - } - - // Parse unary operators - if (token.type === TokenType.NOT) { - current++; - const operand = parsePrimary(); - return { type: 'NotExpression', operand }; - } - - if (token.type === TokenType.MINUS) { - current++; - const operand = parsePrimary(); - return { type: 'UnaryMinusExpression', operand }; - } - - // Parse literals - if (token.type === TokenType.NUMBER) { - current++; - return { type: 'NumberLiteral', value: token.value }; - } else if (token.type === TokenType.STRING) { - current++; - return { type: 'StringLiteral', value: token.value }; - } else if (token.type === TokenType.TRUE) { - current++; - return { type: 'BooleanLiteral', value: true }; - } else if (token.type === TokenType.FALSE) { - current++; - return { type: 'BooleanLiteral', value: false }; - } else if (token.type === TokenType.NULL) { - current++; - return { type: 'NullLiteral' }; - } - - // Parse identifiers - if (token.type === TokenType.IDENTIFIER) { - current++; - const identifier = { type: 'Identifier', value: token.value }; - - // Check for function calls - if (current < tokens.length && tokens[current].type === TokenType.LEFT_PAREN) { - return parseFunctionCall(identifier.value); - } - - // Check for table access - if (current < tokens.length && tokens[current].type === TokenType.DOT) { - return parseChainedDotAccess(identifier); - } - - // Check for table access with brackets - if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { - return parseChainedTableAccess(identifier); - } - - return identifier; - } - - // Parse parenthesized expressions - if (token.type === TokenType.LEFT_PAREN) { - current++; // Skip '(' - const parenthesizedExpr = parseLogicalExpression(); - - if (current < tokens.length && tokens[current].type === TokenType.RIGHT_PAREN) { - current++; // Skip ')' - return parenthesizedExpr; - } else { - throw new Error('Expected closing parenthesis'); - } - } - - // Parse table literals - if (token.type === TokenType.LEFT_BRACE) { - current++; // Skip '{' - const properties = []; - - while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { - if (tokens[current].type === TokenType.IDENTIFIER) { - const key = tokens[current].value; - current++; - - if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { - current++; // Skip ':' - const value = parseLogicalExpression(); - properties.push({ key, value }); - } else { - throw new Error('Expected ":" after property name in table literal'); - } - } else { - throw new Error('Expected property name in table literal'); - } - - // Skip comma if present - if (current < tokens.length && tokens[current].type === TokenType.COMMA) { - current++; - } - } - - if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACE) { - current++; // Skip '}' - return { type: 'TableLiteral', properties }; - } else { - throw new Error('Expected closing brace in table literal'); - } - } - - // Parse function definitions - if (token.type === TokenType.FUNCTION) { - current++; // Skip 'function' - - if (current >= tokens.length || tokens[current].type !== TokenType.LEFT_PAREN) { - throw new Error('Expected "(" after "function"'); - } - current++; // Skip '(' - - const parameters = []; - while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_PAREN) { - if (tokens[current].type === TokenType.IDENTIFIER) { - parameters.push(tokens[current].value); - current++; - } else { - throw new Error('Expected parameter name in function definition'); - } - - // Skip comma if present - if (current < tokens.length && tokens[current].type === TokenType.COMMA) { - current++; - } - } - - if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { - throw new Error('Expected ")" after function parameters'); - } - current++; // Skip ')' - - if (current >= tokens.length || tokens[current].type !== TokenType.ASSIGNMENT) { - throw new Error('Expected ":" after function parameters'); - } - current++; // Skip ':' - - // Parse the function body (which could be a case expression or other expression) - const functionBody = parseLogicalExpression(); - - return { - type: 'FunctionDefinition', - parameters, - body: functionBody - }; - } - - // Parse assignments - if (token.type === TokenType.IDENTIFIER) { - const identifier = token.value; - current++; - - if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { - current++; // Skip ':' - - // Check if this is a function definition - if (current < tokens.length && tokens[current].type === TokenType.FUNCTION) { - current++; // Skip 'function' - - if (current >= tokens.length || tokens[current].type !== TokenType.LEFT_PAREN) { - throw new Error('Expected "(" after "function"'); - } - current++; // Skip '(' - - const parameters = []; - while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_PAREN) { - if (tokens[current].type === TokenType.IDENTIFIER) { - parameters.push(tokens[current].value); - current++; - } else { - throw new Error('Expected parameter name in function definition'); - } - - // Skip comma if present - if (current < tokens.length && tokens[current].type === TokenType.COMMA) { - current++; - } - } - - if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { - throw new Error('Expected ")" after function parameters'); - } - current++; // Skip ')' - - if (current >= tokens.length || tokens[current].type !== TokenType.ASSIGNMENT) { - throw new Error('Expected ":" after function parameters'); - } - current++; // Skip ':' - - // Parse the function body (which could be a case expression or other expression) - const functionBody = parseLogicalExpression(); - - return { - type: 'Assignment', - identifier, - value: { - type: 'FunctionDefinition', - parameters, - body: functionBody - } - }; - } else { - // Regular assignment - const value = parseLogicalExpression(); - - return { - type: 'Assignment', - identifier, - value - }; - } - } - } - - // If we get here, we have an unexpected token - throw new Error(`Unexpected token in parsePrimary: ${token.type}`); - } finally { - callStackTracker.pop(); - } - } + // ===== COMPARISON COMBINATORS ===== - function walk() { - callStackTracker.push('walk', `position:${current}`); - - try { - function parseChainedDotAccess(tableExpr) { - callStackTracker.push('parseChainedDotAccess', ''); - - try { - /** - * Handles chained dot access (e.g., table.key.subkey). - * - * @param {Object} tableExpr - The table expression to chain access from - * @returns {Object} AST node representing the chained access - * @throws {Error} When expected identifier is missing after dot - * - * @description Parses dot notation for table access, building a chain - * of TableAccess nodes for nested property access. - * - * @why Chained access is parsed iteratively rather than recursively to - * avoid deep call stacks and to allow for easy extension (e.g., supporting - * method calls in the future). - */ - let result = tableExpr; - - while (current < tokens.length && tokens[current].type === TokenType.DOT) { - current++; // Skip the dot - - if (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { - const key = { - type: 'Identifier', - value: tokens[current].value - }; - current++; - - result = { - type: 'TableAccess', - table: result, - key: key - }; - } else { - throw new Error('Expected identifier after dot'); - } - } - - return result; - } finally { - callStackTracker.pop(); - } - } - - function parseChainedTableAccess(tableExpr) { - callStackTracker.push('parseChainedTableAccess', ''); - - try { - /** - * Handles chained bracket and dot access (e.g., table[0].key). - * - * @param {Object} tableExpr - The table expression to chain access from - * @returns {Object} AST node representing the chained access - * @throws {Error} When expected closing bracket is missing - * - * @description Parses both bracket and dot notation for table access, - * supporting mixed access patterns like table[0].key. - * - * @why This function allows for flexible access patterns, supporting both - * array and object semantics. Chaining is handled by checking for further - * access tokens after each access. - */ - if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { - current++; // Skip '[' - const keyExpr = walk(); - - if (current < tokens.length && tokens[current].type === TokenType.RIGHT_BRACKET) { - current++; // Skip ']' - - const access = { - type: 'TableAccess', - table: tableExpr, - key: keyExpr - }; - - // Check for chained access - if (current < tokens.length && tokens[current].type === TokenType.DOT) { - return parseChainedDotAccess(access); - } - - // Check if this is a function call - if (current < tokens.length && - (tokens[current].type === TokenType.IDENTIFIER || - tokens[current].type === TokenType.NUMBER || - tokens[current].type === TokenType.STRING || - tokens[current].type === TokenType.LEFT_PAREN)) { - return parseFunctionCall(access); - } - - return access; - } else { - throw new Error('Expected closing bracket'); - } - } - - // Check for dot access - if (current < tokens.length && tokens[current].type === TokenType.DOT) { - const result = parseChainedDotAccess(tableExpr); - - // Check if this is a function call - if (current < tokens.length && - (tokens[current].type === TokenType.IDENTIFIER || - tokens[current].type === TokenType.NUMBER || - tokens[current].type === TokenType.STRING || - tokens[current].type === TokenType.LEFT_PAREN)) { - return parseFunctionCall(result); - } - - return result; - } - - return tableExpr; - } finally { - callStackTracker.pop(); - } - } - - function parseFunctionCall(functionName) { - callStackTracker.push('parseFunctionCall', ''); - - try { - /** - * Parses function calls with arbitrary argument lists. - * - * @param {Object|string} functionName - Function name or expression to call - * @returns {Object} AST node representing the function call - * - * @description Parses function calls by collecting arguments until a - * clear terminator is found, supporting both curried and regular calls. - * - * @why Arguments are parsed until a clear terminator is found, allowing - * for flexible function call syntax. This approach supports both curried - * and regular function calls, and allows for future extension to variadic functions. - * - * @note Special handling for unary minus arguments to distinguish them - * from binary minus operations. - */ - const args = []; - - // Parse arguments until we hit a semicolon or other terminator - while (current < tokens.length && - tokens[current].type !== TokenType.SEMICOLON && - tokens[current].type !== TokenType.RIGHT_PAREN && - tokens[current].type !== TokenType.RIGHT_BRACE && - tokens[current].type !== TokenType.COMMA && - tokens[current].type !== TokenType.AND && - tokens[current].type !== TokenType.OR && - tokens[current].type !== TokenType.XOR) { - - // Special handling for unary minus as argument - if (tokens[current].type === TokenType.MINUS) { - // This is a unary minus, parse it as a new argument - current++; // Skip the minus - if (current < tokens.length && tokens[current].type === TokenType.NUMBER) { - args.push({ - type: 'UnaryMinusExpression', - operand: { - type: 'NumberLiteral', - value: tokens[current].value - } - }); - current++; // Skip the number - } else { - // More complex unary minus expression - args.push({ - type: 'UnaryMinusExpression', - operand: parsePrimary() - }); - } - } else { - // Regular argument parsing - use parseExpression to avoid circular dependency - args.push(parseExpression()); - } - } - - return { - type: 'FunctionCall', - name: functionName, - args: args - }; - } finally { - callStackTracker.pop(); - } - } - - function parseLogicalExpression() { - callStackTracker.push('parseLogicalExpression', ''); - - try { - /** - * Parses logical expressions with lowest precedence. - * - * @returns {Object} AST node representing the logical expression - * - * @description Parses logical operators (and, or, xor) with proper - * precedence handling and left associativity. - * - * @why Logical operators should have lower precedence than arithmetic - * and comparison operators to ensure proper grouping of expressions - * like "isEven 10 and isPositive 5". - */ - let left = parseExpression(); - - while (current < tokens.length && - (tokens[current].type === TokenType.AND || - tokens[current].type === TokenType.OR || - tokens[current].type === TokenType.XOR)) { - - const operator = tokens[current].type; - current++; - const right = parseExpression(); - - switch (operator) { - case TokenType.AND: - left = { type: 'AndExpression', left, right }; - break; - case TokenType.OR: - left = { type: 'OrExpression', left, right }; - break; - case TokenType.XOR: - left = { type: 'XorExpression', left, right }; - break; - } - } - - return left; - } finally { - callStackTracker.pop(); - } - } - - function parseExpression() { - callStackTracker.push('parseExpression', ''); - - try { - /** - * Parses expressions with left-associative binary operators. - * - * @returns {Object} AST node representing the expression - * - * @description Parses addition, subtraction, and comparison operators - * with proper precedence and associativity. - * - * @why Operator precedence is handled by splitting parsing into multiple - * functions (expression, term, factor, primary). This structure avoids - * ambiguity and ensures correct grouping of operations. - * - * @note Special case handling for unary minus after function references - * to distinguish from binary minus operations. - */ - let left = parseTerm(); - - while (current < tokens.length && - (tokens[current].type === TokenType.PLUS || - tokens[current].type === TokenType.MINUS || - tokens[current].type === TokenType.EQUALS || - tokens[current].type === TokenType.NOT_EQUAL || - tokens[current].type === TokenType.LESS_THAN || - tokens[current].type === TokenType.GREATER_THAN || - tokens[current].type === TokenType.LESS_EQUAL || - tokens[current].type === TokenType.GREATER_EQUAL)) { - - const operator = tokens[current].type; - - // Special case: Don't treat MINUS as binary operator if left is a FunctionReference - // This handles cases like "filter @isPositive -3" where -3 should be a separate argument - if (operator === TokenType.MINUS && left.type === 'FunctionReference') { - // This is likely a function call with unary minus argument, not a binary operation - // Return the left side and let the caller handle it - return left; - } - - current++; - const right = parseTerm(); - - switch (operator) { - case TokenType.PLUS: - left = { type: 'PlusExpression', left, right }; - break; - case TokenType.MINUS: - left = { type: 'MinusExpression', left, right }; - break; - case TokenType.EQUALS: - left = { type: 'EqualsExpression', left, right }; - break; - case TokenType.NOT_EQUAL: - left = { type: 'NotEqualExpression', left, right }; - break; - case TokenType.LESS_THAN: - left = { type: 'LessThanExpression', left, right }; - break; - case TokenType.GREATER_THAN: - left = { type: 'GreaterThanExpression', left, right }; - break; - case TokenType.LESS_EQUAL: - left = { type: 'LessEqualExpression', left, right }; - break; - case TokenType.GREATER_EQUAL: - left = { type: 'GreaterEqualExpression', left, right }; - break; - } - } - - return left; - } finally { - callStackTracker.pop(); - } - } - - function parseTerm() { - callStackTracker.push('parseTerm', ''); - - try { - /** - * Parses multiplication, division, and modulo operations. - * - * @returns {Object} AST node representing the term - * - * @description Parses multiplicative operators with higher precedence - * than addition/subtraction. - * - * @why By handling these operators at a separate precedence level, the - * parser ensures that multiplication/division bind tighter than - * addition/subtraction, matching standard arithmetic rules. - */ - let left = parseFactor(); - - while (current < tokens.length && - (tokens[current].type === TokenType.MULTIPLY || - tokens[current].type === TokenType.DIVIDE || - tokens[current].type === TokenType.MODULO)) { - - const operator = tokens[current].type; - current++; - const right = parseFactor(); - - switch (operator) { - case TokenType.MULTIPLY: - left = { type: 'MultiplyExpression', left, right }; - break; - case TokenType.DIVIDE: - left = { type: 'DivideExpression', left, right }; - break; - case TokenType.MODULO: - left = { type: 'ModuloExpression', left, right }; - break; - } - } - - return left; - } finally { - callStackTracker.pop(); - } - } - - function parseFactor() { - callStackTracker.push('parseFactor', ''); - - try { - /** - * Parses exponentiation and primary expressions. - * - * @returns {Object} AST node representing the factor - * - * @description Parses exponentiation with right associativity and - * highest precedence among arithmetic operators. - * - * @why Exponentiation is right-associative and binds tighter than - * multiplication/division, so it is handled at the factor level. This - * also allows for future extension to other high-precedence operators. - */ - let left = parsePrimary(); - - while (current < tokens.length && tokens[current].type === TokenType.POWER) { - current++; - const right = parsePrimary(); - left = { type: 'PowerExpression', left, right }; - } - - return left; - } finally { - callStackTracker.pop(); - } - } - - - - // Check for IO operations before calling parsePrimary - if (tokens[current].type === TokenType.IO_IN) { - current++; - return { type: 'IOInExpression' }; - } else if (tokens[current].type === TokenType.IO_OUT) { - current++; - const outputValue = parseLogicalExpression(); - return { type: 'IOOutExpression', value: outputValue }; - } else if (tokens[current].type === TokenType.IO_ASSERT) { - current++; - const assertionExpr = parseLogicalExpression(); - return { type: 'IOAssertExpression', value: assertionExpr }; - } - - // Check for case expressions at top level - if (tokens[current].type === TokenType.CASE) { - current++; // Skip 'case' - - // Parse the values being matched (can be multiple) - const values = []; - while (current < tokens.length && tokens[current].type !== TokenType.OF) { - const value = parsePrimary(); - values.push(value); - } - - // Expect 'of' - if (current >= tokens.length || tokens[current].type !== TokenType.OF) { - throw new Error('Expected "of" after "case"'); - } - current++; // Skip 'of' - - const cases = []; - - // Parse cases until we hit a semicolon or end - while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { - const patterns = []; - while (current < tokens.length && - tokens[current].type !== TokenType.ASSIGNMENT && - tokens[current].type !== TokenType.SEMICOLON) { - patterns.push(parsePrimary()); - } - - // Expect ':' after pattern - if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { - current++; // Skip ':' - } else { - throw new Error('Expected ":" after pattern in case expression'); - } - - const result = parseLogicalExpression(); - cases.push({ - pattern: patterns, - result: [result] - }); - } - - return { - type: 'CaseExpression', - value: values, - cases, - }; - } - - // Check for IO operations before calling parsePrimary - if (tokens[current].type === TokenType.IO_IN) { - current++; - return { type: 'IOInExpression' }; - } else if (tokens[current].type === TokenType.IO_OUT) { - current++; - const outputValue = parseLogicalExpression(); - return { type: 'IOOutExpression', value: outputValue }; - } else if (tokens[current].type === TokenType.IO_ASSERT) { - current++; - const assertionExpr = parseLogicalExpression(); - return { type: 'IOAssertExpression', value: assertionExpr }; - } - - // Check for case expressions at top level - if (tokens[current].type === TokenType.CASE) { - current++; // Skip 'case' - - // Parse the values being matched (can be multiple) - const values = []; - while (current < tokens.length && tokens[current].type !== TokenType.OF) { - const value = parsePrimary(); - values.push(value); - } - - // Expect 'of' - if (current >= tokens.length || tokens[current].type !== TokenType.OF) { - throw new Error('Expected "of" after "case"'); - } - current++; // Skip 'of' - - const cases = []; - - // Parse cases until we hit a semicolon or end - while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { - const patterns = []; - while (current < tokens.length && - tokens[current].type !== TokenType.ASSIGNMENT && - tokens[current].type !== TokenType.SEMICOLON) { - patterns.push(parseLogicalExpression()); - } - - // Expect ':' after pattern - if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { - current++; // Skip ':' - } else { - throw new Error('Expected ":" after pattern in case expression'); - } - - const result = parseLogicalExpression(); - cases.push({ - pattern: patterns, - result: [result] - }); - } - - return { - type: 'CaseExpression', - value: values, - cases, - }; - } - - // Check for assignments (identifier followed by ':') - if (tokens[current].type === TokenType.IDENTIFIER) { - const identifier = tokens[current].value; - current++; - - if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { - current++; // Skip ':' - - // Check if this is a function definition - if (current < tokens.length && tokens[current].type === TokenType.FUNCTION) { - current++; // Skip 'function' - - if (current >= tokens.length || tokens[current].type !== TokenType.LEFT_PAREN) { - throw new Error('Expected "(" after "function"'); - } - current++; // Skip '(' - - const parameters = []; - while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_PAREN) { - if (tokens[current].type === TokenType.IDENTIFIER) { - parameters.push(tokens[current].value); - current++; - } else { - throw new Error('Expected parameter name in function definition'); - } - - // Skip comma if present - if (current < tokens.length && tokens[current].type === TokenType.COMMA) { - current++; - } - } - - if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { - throw new Error('Expected ")" after function parameters'); - } - current++; // Skip ')' - - if (current >= tokens.length || tokens[current].type !== TokenType.ASSIGNMENT) { - throw new Error('Expected ":" after function parameters'); - } - current++; // Skip ':' - - // Parse the function body (which could be a case expression or other expression) - const functionBody = parseLogicalExpression(); - - return { - type: 'Assignment', - identifier, - value: { - type: 'FunctionDefinition', - parameters, - body: functionBody - } - }; - } else { - // Regular assignment - const value = parseLogicalExpression(); - - return { - type: 'Assignment', - identifier, - value - }; - } - } - - // If it's not an assignment, put the identifier back and continue - current--; - } - - // For all other token types (identifiers, numbers, operators, etc.), call parsePrimary - // This handles atomic expressions and delegates to the appropriate parsing functions - return parsePrimary(); - } finally { - callStackTracker.pop(); - } - } + /** + * 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; + }; - const ast = { - type: 'Program', - body: [] + /** + * 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; }; - let lastCurrent = -1; - let loopCount = 0; - const maxLoops = tokens.length * 2; // Safety limit + /** + * 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; + }; - while (current < tokens.length) { - // Safety check to prevent infinite loops - if (current === lastCurrent) { - loopCount++; - if (loopCount > 10) { // Allow a few iterations at the same position - throw new Error(`Parser stuck at position ${current}, token: ${tokens[current]?.type || 'EOF'}`); - } + /** + * 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 { - loopCount = 0; - } - - // Safety check for maximum loops - if (loopCount > maxLoops) { - throw new Error(`Parser exceeded maximum loop count. Last position: ${current}`); + return function(y) { + return x; + }; } - - lastCurrent = current; - - const node = walk(); - if (node) { - ast.body.push(node); - } - - // Skip semicolons - if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { - current++; + }; + + /** + * 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); + }; } - } + }; - return ast; + /** + * 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); + }; + }; } /** @@ -1637,23 +491,41 @@ function parser(tokens) { * corresponding operations. Manages scope, handles function calls, and supports * both synchronous and asynchronous operations. * - * @how Uses a global scope for variable storage and function definitions. Each - * function call creates a new scope (using prototypal inheritance) to implement + * The interpreter implements a combinator-based architecture where all operations + * 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 * lexical scoping. Immutability is enforced by preventing reassignment in the * global scope. * - * @why This approach allows for first-class functions, closures, and lexical - * scoping, while keeping the implementation simple. The interpreter supports - * both synchronous and asynchronous IO operations, returning Promises when necessary. - * - * @note The interpreter is split into three functions: evalNode (global), + * 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 powerful abstractions and eliminates the need for special + * handling of different operator types in the interpreter. */ function interpreter(ast) { const globalScope = {}; initializeStandardLibrary(globalScope); + // 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'); + } + // Reset call stack tracker at the start of interpretation callStackTracker.reset(); @@ -1665,7 +537,18 @@ function interpreter(ast) { * @throws {Error} For evaluation errors * * @description Main evaluation function that handles all node types in the - * global scope context. + * 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 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. */ function evalNode(node) { callStackTracker.push('evalNode', node?.type || 'unknown'); @@ -1769,16 +652,58 @@ function interpreter(ast) { 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; @@ -1804,45 +729,132 @@ function interpreter(ast) { 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; // Renamed from 'func' to avoid redeclaration + let funcToCall; 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); + } } else if (node.name.type === 'Identifier') { // Function call with identifier funcToCall = globalScope[node.name.value]; - } else if (node.name.type === 'TableAccess') { - // Function call from table access (e.g., math.add) - funcToCall = evalNode(node.name); + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: looking up function '${node.name.value}' in globalScope, found:`, typeof funcToCall); + } } else { - throw new Error('Invalid function name in function call'); + // 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 (funcToCall instanceof Function) { let args = node.args.map(evalNode); + if (process.env.DEBUG) { + console.log(`[DEBUG] FunctionCall: calling function with args:`, args); + } return funcToCall(...args); } throw new Error(`Function is not defined or is not callable`); - case 'CaseExpression': - const values = node.value.map(evalNode); + case 'WhenExpression': + // Handle both single values and arrays of values + const whenValues = Array.isArray(node.value) + ? node.value.map(evalNode) + : [evalNode(node.value)]; + + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: whenValues =`, whenValues); + } for (const caseItem of node.cases) { - const pattern = caseItem.pattern.map(evalNode); + // Handle both single patterns and arrays of patterns + const patterns = caseItem.pattern.map(evalNode); + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: patterns =`, patterns); + } + + // Check if patterns match the values let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; - - if (patternValue === true) continue; - - if (value !== patternValue) { - matches = false; - break; + 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 (process.env.DEBUG) { + console.log(`[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`); + } + 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 (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: boolean pattern result = ${patternResult}`); + } + if (!patternResult) { + matches = false; + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: boolean pattern does not match`); + } + break; + } else { + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: boolean pattern matches`); + } + } + } else if (value !== pattern) { + matches = false; + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: pattern does not match`); + } + break; + } else { + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: pattern matches`); + } + } } } + if (process.env.DEBUG) { + console.log(`[DEBUG] WhenExpression: case matches = ${matches}`); + } + if (matches) { const results = caseItem.result.map(evalNode); if (results.length === 1) { @@ -1887,6 +899,9 @@ function interpreter(ast) { 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}`); } @@ -1904,7 +919,17 @@ function interpreter(ast) { * @throws {Error} For evaluation errors * * @description Used for evaluating function bodies and other expressions - * that need access to both local and global variables. + * 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. */ const localEvalNodeWithScope = (node, scope) => { callStackTracker.push('localEvalNodeWithScope', node?.type || 'unknown'); @@ -2008,9 +1033,30 @@ function interpreter(ast) { 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': @@ -2039,6 +1085,20 @@ function interpreter(ast) { 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') { @@ -2047,11 +1107,9 @@ function interpreter(ast) { } else if (node.name.type === 'Identifier') { // Function call with identifier localFunc = globalScope[node.name.value]; - } else if (node.name.type === 'TableAccess') { - // Function call from table access (e.g., math.add) - localFunc = localEvalNodeWithScope(node.name, scope); } else { - throw new Error('Invalid function name in function call'); + // Function call from expression (e.g., parenthesized function, higher-order) + localFunc = localEvalNodeWithScope(node.name, scope); } if (localFunc instanceof Function) { @@ -2059,25 +1117,61 @@ function interpreter(ast) { return localFunc(...args); } throw new Error(`Function is not defined or is not callable`); - case 'CaseExpression': - const values = node.value.map(val => localEvalNodeWithScope(val, scope)); + 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 (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: whenValues =`, whenValues); + } for (const caseItem of node.cases) { - const pattern = caseItem.pattern.map(pat => localEvalNodeWithScope(pat, scope)); + // 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); + } + + // Check if patterns match the values let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; - - if (patternValue === true) continue; - - if (value !== patternValue) { - matches = false; - break; + 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 (process.env.DEBUG) { + console.log(`[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`); + } + continue; + } else if (value !== pattern) { + matches = false; + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern does not match`); + } + break; + } else { + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: pattern matches`); + } + } } } + if (process.env.DEBUG) { + console.log(`[DEBUG] localEvalNodeWithScope WhenExpression: case matches = ${matches}`); + } + if (matches) { const results = caseItem.result.map(res => localEvalNodeWithScope(res, scope)); if (results.length === 1) { @@ -2122,6 +1216,9 @@ function interpreter(ast) { 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}`); } @@ -2138,7 +1235,17 @@ function interpreter(ast) { * @throws {Error} For evaluation errors * * @description Internal helper function for recursive evaluation that - * always uses the global scope. Used to avoid circular dependencies. + * 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. */ const localEvalNode = (node) => { callStackTracker.push('localEvalNode', node?.type || 'unknown'); @@ -2242,9 +1349,30 @@ function interpreter(ast) { 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': @@ -2269,6 +1397,20 @@ function interpreter(ast) { 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') { @@ -2277,11 +1419,9 @@ function interpreter(ast) { } else if (node.name.type === 'Identifier') { // Function call with identifier localFunc = globalScope[node.name.value]; - } else if (node.name.type === 'TableAccess') { - // Function call from table access (e.g., math.add) - localFunc = localEvalNode(node.name); } else { - throw new Error('Invalid function name in function call'); + // Function call from expression (e.g., parenthesized function, higher-order) + localFunc = localEvalNode(node.name); } if (localFunc instanceof Function) { @@ -2289,22 +1429,32 @@ function interpreter(ast) { return localFunc(...args); } throw new Error(`Function is not defined or is not callable`); - case 'CaseExpression': - const values = node.value.map(localEvalNode); + 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) { - const pattern = caseItem.pattern.map(localEvalNode); + // Handle both single patterns and arrays of patterns + const patterns = caseItem.pattern.map(localEvalNode); + // Check if patterns match the values let matches = true; - for (let i = 0; i < Math.max(values.length, pattern.length); i++) { - const value = values[i]; - const patternValue = pattern[i]; - - if (patternValue === true) continue; - - if (value !== patternValue) { - matches = false; - break; + 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 (value !== pattern) { + matches = false; + break; + } } } @@ -2352,6 +1502,9 @@ function interpreter(ast) { 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}`); } @@ -2385,10 +1538,14 @@ function interpreter(ast) { * @description Logs debug messages to console when DEBUG environment variable is set. * Provides verbose output during development while remaining silent in production. * - * @why Debug functions are gated by the DEBUG environment variable, allowing for + * 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. */ function debugLog(message, data = null) { if (process.env.DEBUG) { @@ -2408,7 +1565,7 @@ function debugLog(message, data = null) { * @description Logs debug error messages to console when DEBUG environment variable is set. * Provides verbose error output during development while remaining silent in production. * - * @why Debug functions are gated by the DEBUG environment variable, allowing for + * 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. @@ -2426,7 +1583,18 @@ function debugError(message, error = null) { * 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. + * 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 complex + * 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. */ const callStackTracker = { stack: [], @@ -2504,25 +1672,77 @@ const callStackTracker = { }; /** + * 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) { + // 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'); + } + + // 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'); + } +} + +/** * 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. * - * @why This function is the entry point for file execution, handling all stages - * of the language pipeline. Debug output is provided at each stage for - * transparency and troubleshooting. + * 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 * - * @note Supports both synchronous and asynchronous execution, with proper - * error handling and process exit codes. + * 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. */ -function executeFile(filePath) { +async function executeFile(filePath) { try { - const fs = require('fs'); - const input = fs.readFileSync(filePath, 'utf8'); + // Validate file extension + if (!filePath.endsWith('.txt')) { + throw new Error('Only .txt files are supported'); + } + + const input = await readFile(filePath); debugLog('Input:', input); @@ -2580,24 +1800,34 @@ function executeFile(filePath) { * @description Processes command line arguments and executes the specified file. * Provides helpful error messages for incorrect usage. * - * @why The language is designed for file execution only (no REPL), so the CLI + * 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. * - * @note Exits with appropriate error codes for different failure scenarios. + * Exits with appropriate error codes for different failure scenarios. */ -const args = process.argv.slice(2); +async function main() { + 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); -} else if (args.length === 1) { - // Execute the file - const filePath = args[0]; - executeFile(filePath); -} else { - // Too many arguments - console.error('Usage: node lang.js <file>'); - console.error(' Provide exactly one file path to execute'); + if (args.length === 0) { + console.error('Usage: node lang.js <file>'); + console.error(' Provide a file path to execute'); + process.exit(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); + } +} + +// Start the program +main().catch(error => { + console.error('Fatal error:', error.message); process.exit(1); -} \ No newline at end of file +}); \ No newline at end of file |