diff options
Diffstat (limited to 'js/scripting-lang/parser.js')
-rw-r--r-- | js/scripting-lang/parser.js | 1710 |
1 files changed, 1710 insertions, 0 deletions
diff --git a/js/scripting-lang/parser.js b/js/scripting-lang/parser.js new file mode 100644 index 0000000..a5cb45b --- /dev/null +++ b/js/scripting-lang/parser.js @@ -0,0 +1,1710 @@ +// Parser for the scripting language +// Exports: parser(tokens) +// Converts tokens to an Abstract Syntax Tree (AST) + +import { TokenType } from './lexer.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; + +/** + * AST node types for the language + * + * @typedef {Object} ASTNode + * @property {string} type - The node type identifier + * @property {*} [value] - Node value (for literals) + * @property {string} [name] - Identifier name (for identifiers) + * @property {Array.<ASTNode>} [body] - Program or function body + * @property {Array.<ASTNode>} [args] - Function call arguments + * @property {Array.<string>} [params] - Function parameters + * @property {Array.<string>} [parameters] - Function parameters (alternative) + * @property {ASTNode} [left] - Left operand (for binary expressions) + * @property {ASTNode} [right] - Right operand (for binary expressions) + * @property {ASTNode} [operand] - Operand (for unary expressions) + * @property {ASTNode} [table] - Table expression (for table access) + * @property {ASTNode} [key] - Key expression (for table access) + * @property {Array.<Object>} [entries] - Table entries (for table literals) + * @property {Array.<ASTNode>} [cases] - When expression cases + * @property {Array.<ASTNode>} [pattern] - Pattern matching patterns + * @property {Array.<ASTNode>} [result] - Pattern matching results + * @property {ASTNode} [value] - When expression value + */ + +/** + * Parser: Converts tokens to an Abstract Syntax Tree (AST) using combinator-based architecture. + * + * @param {Array.<Token>} tokens - Array of tokens from the lexer + * @returns {ASTNode} Abstract Syntax Tree with program body + * @throws {Error} For parsing errors like unexpected tokens or missing delimiters + * + * @description The parser implements a combinator-based architecture where all + * operators are translated to function calls to standard library combinators. + * This reduces parsing ambiguity while preserving the original syntax. + * + * The parser uses a recursive descent approach with proper operator precedence + * handling. Each operator expression (e.g., x + y) is translated to a FunctionCall + * node (e.g., add(x, y)) that will be executed by the interpreter using the + * corresponding combinator function. + * + * Key architectural decisions: + * - All operators become FunctionCall nodes to eliminate ambiguity + * - Operator precedence is handled through recursive parsing functions + * - Function calls are detected by looking for identifiers followed by expressions + * - When expressions and case patterns are parsed with special handling + * - Table literals and access are parsed as structured data + * - Function composition uses 'via' keyword with right-associative precedence + * - Function application uses juxtaposition with left-associative precedence + * + * The parser maintains a current token index and advances through the token + * stream, building the AST bottom-up from primary expressions to logical + * expressions. This approach ensures that all operations are consistently + * represented as function calls, enabling the interpreter to use the combinator + * foundation for execution. + * + * This design choice reduces the need for special operator handling in the + * interpreter and enables abstractions through the combinator foundation. + * All operations become function calls, providing a consistent and extensible + * execution model that can be enhanced by adding new combinator functions. + * + * The parser implements a top-down recursive descent strategy where each + * parsing function handles a specific precedence level. This approach ensures + * that operator precedence is correctly enforced while maintaining clear + * separation of concerns for different language constructs. + * + * Error handling is designed to provide meaningful feedback by including + * context about what was expected and what was found. This enables users + * to quickly identify and fix parsing errors in their code. + */ +export function parser(tokens) { + let current = 0; + + /** + * Main parsing function that processes the entire token stream + * + * @returns {ASTNode} Complete AST with program body + * @description Iterates through all tokens, parsing each statement or expression + * and building the program body. Handles empty programs gracefully. + * + * This function orchestrates the parsing process by repeatedly calling walk() + * until all tokens are consumed. It ensures that the final AST contains all + * statements and expressions in the correct order, ready for interpretation + * by the combinator-based interpreter. + * + * The function implements the top-level parsing strategy by processing each + * statement or expression in sequence. This approach enables the parser to + * handle programs with multiple statements while maintaining the + * combinator-based architecture where all operations become function calls. + * + * Each call to walk() processes one complete statement or expression, ensuring + * that the parser can handle programs of various sizes while maintaining + * clear separation between different language constructs. + * + * The function returns a Program node that contains all parsed statements + * and expressions in the order they appeared in the source code. This + * structure enables the interpreter to execute statements sequentially + * while maintaining proper scope and state management. + */ + function parse() { + const body = []; + + while (current < tokens.length) { + const node = walk(); + if (node) { + body.push(node); + } + } + + return { type: 'Program', body }; + } + + /** + * Main walk function that dispatches to appropriate parsing functions + * + * @returns {ASTNode|null} Parsed AST node or null for empty statements + * @description Determines the type of construct at the current position + * and delegates to the appropriate parsing function. The order of checks + * determines parsing precedence for top-level constructs. + * + * Parsing order: + * 1. IO operations (highest precedence for top-level constructs) + * 2. Assignments (identifier followed by assignment token) + * 3. When expressions (pattern matching) + * 4. Function definitions (explicit function declarations) + * 5. Logical expressions (default case for all other expressions) + * + * This function implements the top-level parsing strategy by checking for + * specific token patterns that indicate different language constructs. + * The order of checks is crucial for correct parsing precedence and + * ensures that expressions are properly decomposed into their + * constituent parts for combinator translation. + * + * The function uses a pattern-matching approach to identify language constructs + * based on token sequences. This design enables the parser to handle various + * syntax while maintaining clear separation between different constructs. + * Each parsing function is responsible for handling its specific syntax + * and translating it into appropriate AST nodes for the combinator-based + * interpreter. + * + * The function returns null for empty statements or whitespace, allowing + * the parser to gracefully handle programs with empty lines or comments + * without affecting the AST structure. + */ + function walk() { + const token = tokens[current]; + + if (!token) return null; + + // Handle IO operations first + if (token.type === TokenType.IO_IN) { + return parseIOIn(); + } + if (token.type === TokenType.IO_OUT) { + return parseIOOut(); + } + if (token.type === TokenType.IO_ASSERT) { + return parseIOAssert(); + } + if (token.type === TokenType.IO_LISTEN) { + return parseIOListen(); + } + if (token.type === TokenType.IO_EMIT) { + return parseIOEmit(); + } + + // Handle assignments + if (token.type === TokenType.IDENTIFIER && + current + 1 < tokens.length && + tokens[current + 1].type === TokenType.ASSIGNMENT) { + return parseAssignment(); + } + + // Handle when expressions + if (token.type === TokenType.WHEN) { + return parseWhenExpression(); + } + + // Handle function definitions + if (token.type === TokenType.FUNCTION) { + return parseFunctionDefinition(); + } + + + + // For all other expressions, parse as logical expressions + return parseLogicalExpression(); + } + + /** + * Parse assignment statements: identifier : expression; + * + * @returns {ASTNode} Assignment AST node + * @throws {Error} For malformed assignments or missing semicolons + * @description Parses variable assignments and function definitions. + * Supports both simple assignments (x : 42) and arrow function definitions + * (f : x y -> x + y). Also handles when expressions as assignment values. + * + * The function uses lookahead to distinguish between different assignment + * types and parses the value according to the detected type. + * + * Assignment parsing is crucial for the language's variable binding system. + * The function supports multiple assignment patterns to provide flexibility + * while maintaining clear syntax. This includes traditional variable + * assignments, function definitions using arrow syntax, and when expressions + * that can be assigned to variables. + * + * The function implements forward declaration support for recursive functions + * by allowing function definitions to reference themselves during parsing. + * This enables natural recursive function definitions without requiring + * special syntax or pre-declaration. + * + * Error handling includes checks for missing semicolons and malformed + * assignment syntax, providing clear feedback to help users fix syntax errors. + */ + function parseAssignment() { + const identifier = tokens[current].value; + current++; // Skip identifier + current++; // Skip assignment token (:) + + // Check if the value is a when expression + if (tokens[current].type === TokenType.WHEN) { + const value = parseWhenExpression(); + + // Expect semicolon + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + } + + return { + type: 'Assignment', + identifier, + value + }; + } else { + // Check if this is an arrow function: param1 param2 -> body + const params = []; + let isArrowFunction = false; + + // Look ahead to see if this is an arrow function + let lookAhead = current; + while (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.IDENTIFIER) { + lookAhead++; + } + + if (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.ARROW) { + // This is an arrow function + isArrowFunction = true; + + // Parse parameters + while (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + params.push(tokens[current].value); + current++; + } + + if (current >= tokens.length || tokens[current].type !== TokenType.ARROW) { + throw new Error('Expected "->" after parameters in arrow function'); + } + current++; // Skip '->' + + // Check if the body is a when expression + let body; + if (tokens[current].type === TokenType.WHEN) { + body = parseWhenExpression(); + } else { + body = parseLogicalExpression(); + } + + // Expect semicolon + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + } + + return { + type: 'Assignment', + identifier, + value: { + type: 'FunctionDeclaration', + params, + body + } + }; + } else { + // Parse the value as an expression (function calls will be handled by expression parsing) + const value = parseLogicalExpression(); + + // Expect semicolon + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + } + + return { + type: 'Assignment', + identifier, + value + }; + } + } + } + + /** + * Parse when expressions: when value is pattern then result pattern then result; + * + * @returns {ASTNode} WhenExpression AST node + * @throws {Error} For malformed when expressions + * @description Parses pattern matching expressions with support for single + * and multiple values/patterns. The when expression is the primary pattern + * matching construct in the language. + * + * Supports: + * - Single value patterns: when x is 42 then "correct" _ then "wrong" + * - Multiple value patterns: when x y is 0 0 then "both zero" _ _ then "not both" + * - Wildcard patterns: _ (matches any value) + * - Function references: @functionName + * + * The function parses values, patterns, and results, building a structured + * AST that the interpreter can efficiently evaluate. + * + * When expression parsing is essential for pattern matching and conditional + * execution. It allows for flexible conditional logic where + * a single value or multiple values can be matched against a set of patterns, + * and the result of the match determines the next action. + * + * The function implements a recursive descent parser that handles nested + * patterns and results. It correctly identifies the 'when' keyword, + * parses the value(s), and then iterates through cases, parsing patterns + * and results. The 'then' keyword is used to separate patterns from results. + * + * Error handling includes checks for missing 'is' after value, malformed + * patterns, and unexpected tokens during pattern parsing. + */ + function parseWhenExpression() { + if (DEBUG) { + console.log(`[DEBUG] parseWhenExpression: starting, current token = ${tokens[current].type}`); + } + current++; // Skip 'when' + + // Parse the value(s) - can be single value or multiple values + const values = []; + while (current < tokens.length && tokens[current].type !== TokenType.IS) { + // Use parsePrimary to handle all types of expressions including table access and function calls + let value; + if (tokens[current].type === TokenType.IO_LISTEN) { + // Handle IO listen in when expressions + value = parseIOListen(); + } else if (tokens[current].type === TokenType.IO_EMIT) { + // Handle IO emit in when expressions + value = parseIOEmit(); + } else { + // For all other types, use parsePrimary to handle expressions + value = parsePrimary(); + } + values.push(value); + } + + if (current >= tokens.length || tokens[current].type !== TokenType.IS) { + throw new Error('Expected "is" after value in when expression'); + } + current++; // Skip 'is' + + const cases = []; + + while (current < tokens.length) { + if (DEBUG) { + console.log(`[DEBUG] parseWhenExpression: starting new case, current token = ${tokens[current].type}, value = ${tokens[current].value || 'N/A'}`); + } + // Parse pattern(s) - can be single pattern or multiple patterns + const patterns = []; + + // Parse patterns until we hit THEN + while (current < tokens.length && tokens[current].type !== TokenType.THEN) { + let pattern; + if (DEBUG) { + console.log(`[DEBUG] parseWhenExpression: parsing pattern, current token = ${tokens[current].type}, value = ${tokens[current].value || 'N/A'}`); + } + + // Check if this is a comparison expression (starts with identifier followed by comparison operator) + if (tokens[current].type === TokenType.IDENTIFIER && + current + 1 < tokens.length && + (tokens[current + 1].type === TokenType.LESS_THAN || + tokens[current + 1].type === TokenType.GREATER_THAN || + tokens[current + 1].type === TokenType.LESS_EQUAL || + tokens[current + 1].type === TokenType.GREATER_EQUAL || + tokens[current + 1].type === TokenType.EQUALS || + tokens[current + 1].type === TokenType.NOT_EQUAL)) { + // Parse as a comparison expression + pattern = parseExpression(); + } else if (tokens[current].type === TokenType.IDENTIFIER) { + // Check if this is a function call (identifier followed by arguments) + if (current + 1 < tokens.length && isValidArgumentStart(tokens[current + 1])) { + // Parse as a function call, but stop at THEN or semicolon + const functionName = tokens[current].value; + current++; // Skip function name + + // Parse arguments until we hit THEN, semicolon, or end of tokens + const args = []; + while (current < tokens.length && + tokens[current].type !== TokenType.THEN && + tokens[current].type !== TokenType.SEMICOLON) { + const arg = parseLogicalExpression(); + args.push(arg); + } + + pattern = { + type: 'FunctionCall', + name: functionName, + args + }; + } else { + pattern = { type: 'Identifier', value: tokens[current].value }; + current++; + } + } else if (tokens[current].type === TokenType.NUMBER) { + pattern = { type: 'NumberLiteral', value: tokens[current].value }; + current++; + } else if (tokens[current].type === TokenType.STRING) { + pattern = { type: 'StringLiteral', value: tokens[current].value }; + current++; + } else if (tokens[current].type === TokenType.WILDCARD) { + pattern = { type: 'WildcardPattern' }; + current++; + } else if (tokens[current].type === TokenType.FUNCTION_REF) { + pattern = { type: 'FunctionReference', name: tokens[current].name }; + current++; + } else if (tokens[current].type === TokenType.TRUE) { + pattern = { type: 'BooleanLiteral', value: true }; + current++; + } else if (tokens[current].type === TokenType.FALSE) { + pattern = { type: 'BooleanLiteral', value: false }; + current++; + } else if (tokens[current].type === TokenType.MINUS || tokens[current].type === TokenType.UNARY_MINUS) { + // Handle negative numbers in patterns + current++; // Skip minus token + if (current >= tokens.length || tokens[current].type !== TokenType.NUMBER) { + throw new Error('Expected number after minus in pattern'); + } + pattern = { type: 'NumberLiteral', value: -tokens[current].value }; + current++; + } else if (tokens[current].type === TokenType.LEFT_BRACE) { + // Handle table literals in patterns + pattern = parseTableLiteral(); + } else if (tokens[current].type === TokenType.LEFT_PAREN) { + // Handle parenthesized expressions in patterns + current++; // Skip '(' + pattern = parseLogicalExpression(); + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { + throw new Error('Expected ")" after parenthesized expression in pattern'); + } + current++; // Skip ')' + } else { + throw new Error(`Expected pattern (identifier, number, string, wildcard, function reference, boolean, or comparison) in when expression, got ${tokens[current].type}`); + } + patterns.push(pattern); + + // If we have multiple patterns, we need to handle them correctly + // Check if the next token is a valid pattern start (not THEN) + if (current < tokens.length && + tokens[current].type !== TokenType.THEN && + tokens[current].type !== TokenType.SEMICOLON) { + // Continue parsing more patterns + continue; + } + } + + if (current >= tokens.length || tokens[current].type !== TokenType.THEN) { + throw new Error('Expected "then" after pattern in when expression'); + } + current++; // Skip 'then' + + // Parse result - be careful not to parse beyond the result + let result; + + // Check if the next token after THEN is a pattern start + if (current < tokens.length) { + const nextToken = tokens[current]; + if (nextToken.type === TokenType.IDENTIFIER || + nextToken.type === TokenType.NUMBER || + nextToken.type === TokenType.STRING || + nextToken.type === TokenType.WILDCARD || + nextToken.type === TokenType.FUNCTION_REF) { + // Look ahead to see if this is actually a pattern + let lookAhead = current; + while (lookAhead < tokens.length && + tokens[lookAhead].type !== TokenType.THEN && + tokens[lookAhead].type !== TokenType.SEMICOLON) { + lookAhead++; + } + + if (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.THEN) { + // This is a pattern start, so the result is just the current token + if (nextToken.type === TokenType.IDENTIFIER) { + result = { type: 'Identifier', value: nextToken.value }; + } else if (nextToken.type === TokenType.NUMBER) { + result = { type: 'NumberLiteral', value: nextToken.value }; + } else if (nextToken.type === TokenType.STRING) { + result = { type: 'StringLiteral', value: nextToken.value }; + } else if (nextToken.type === TokenType.WILDCARD) { + result = { type: 'WildcardPattern' }; + } else if (nextToken.type === TokenType.FUNCTION_REF) { + result = { type: 'FunctionReference', name: nextToken.name }; + } + current++; // Consume the token + } else { + // This is part of the result, parse normally + result = parseLogicalExpression(); + } + } else if (nextToken.type === TokenType.WHEN) { + // This is a nested when expression, parse it directly + result = parseWhenExpression(); + } else { + // Not a pattern start, parse normally + result = parseLogicalExpression(); + } + } else { + result = parseLogicalExpression(); + } + + cases.push({ + pattern: patterns, + result: [result] + }); + + if (DEBUG) { + console.log(`[DEBUG] parseWhenExpression: finished case, current token = ${tokens[current].type}, value = ${tokens[current].value || 'N/A'}`); + } + + // Enhanced termination logic for when expressions + if (current < tokens.length) { + const nextToken = tokens[current]; + + if (DEBUG) { + console.log(`[DEBUG] parseWhenExpression: checking termination, nextToken = ${nextToken.type}, value = ${nextToken.value || 'N/A'}`); + } + + // Stop on semicolon + if (nextToken.type === TokenType.SEMICOLON) { + if (DEBUG) { + console.log(`[DEBUG] parseWhenExpression: terminating on SEMICOLON`); + } + current++; + break; + } + + // Stop on assignment (for consecutive assignments) + if (nextToken.type === TokenType.ASSIGNMENT) { + if (DEBUG) { + console.log(`[DEBUG] parseWhenExpression: terminating on ASSIGNMENT`); + } + break; + } + + // Stop on identifier that starts a new assignment + if (nextToken.type === TokenType.IDENTIFIER) { + // Look ahead to see if this is the start of a new assignment + let lookAhead = current; + while (lookAhead < tokens.length && + tokens[lookAhead].type !== TokenType.ASSIGNMENT && + tokens[lookAhead].type !== TokenType.SEMICOLON && + tokens[lookAhead].type !== TokenType.THEN) { + lookAhead++; + } + + if (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.ASSIGNMENT) { + // This is the start of a new assignment, terminate the when expression + if (DEBUG) { + console.log(`[DEBUG] parseWhenExpression: terminating on new assignment starting with ${nextToken.value}`); + } + break; + } + } + + // Stop on right brace (for when expressions inside table literals) + if (nextToken.type === TokenType.RIGHT_BRACE) { + if (DEBUG) { + console.log(`[DEBUG] parseWhenExpression: terminating on RIGHT_BRACE`); + } + break; + } + + // Stop on comma (for when expressions inside table literals) + if (nextToken.type === TokenType.COMMA) { + if (DEBUG) { + console.log(`[DEBUG] parseWhenExpression: terminating on COMMA`); + } + break; + } + } + } + + return { + type: 'WhenExpression', + value: values.length === 1 ? values[0] : values, + cases + }; + } + + + + /** + * Parse function definitions: function (params) : body + * + * @returns {ASTNode} FunctionDefinition AST node + * @throws {Error} For malformed function definitions + * @description Parses explicit function declarations with parameter lists + * and function bodies. This is the traditional function definition syntax + * as opposed to arrow functions. + * + * The function expects: + * - function keyword + * - Parenthesized parameter list + * - Assignment token (:) + * - Function body expression + * + * Function definition parsing is fundamental to the language's ability to + * define reusable functions. It supports traditional function declarations + * with explicit parameter lists and function bodies. + * + * The function implements a recursive descent parser that handles the + * 'function' keyword, parameter parsing, and the assignment token. + * It then recursively parses the function body, which can be any valid + * expression. + * + * Error handling includes checks for missing '(' after function keyword, + * missing ')' after function parameters, and missing ':' after parameters. + */ + function parseFunctionDefinition() { + current++; // Skip 'function' + + if (current >= tokens.length || tokens[current].type !== TokenType.LEFT_PAREN) { + throw new Error('Expected "(" after function keyword'); + } + 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++; + + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { + current++; // Skip comma + } + } else { + throw new Error('Expected parameter name in function definition'); + } + } + + 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 ':' + + const body = parseLogicalExpression(); + + return { + type: 'FunctionDefinition', + parameters, + body + }; + } + + /** + * Parse IO input operations: ..in + * + * @returns {ASTNode} IOInExpression AST node + * @description Parses input operations that read from standard input. + * The operation is represented as a simple AST node that the interpreter + * will handle by prompting for user input. + * + * IO input parsing is crucial for interactive programs that require + * user interaction. It allows for simple and direct input operations + * that read values from the standard input stream. + * + * The function implements a recursive descent parser that handles the + * '..in' keyword and expects a semicolon after the operation. + * + * Error handling includes checks for missing semicolon after input operation. + */ + function parseIOIn() { + current++; // Skip IO_IN token + return { type: 'IOInExpression' }; + } + + /** + * Parse IO output operations: ..out expression + * + * @returns {ASTNode} IOOutExpression AST node + * @throws {Error} For malformed output expressions + * @description Parses output operations that write to standard output. + * The expression to output is parsed as a logical expression and will + * be evaluated by the interpreter before being printed. + * + * IO output parsing is essential for programs that need to display + * information to the user. It allows for expressions to be evaluated + * and their results to be printed to the standard output stream. + * + * The function implements a recursive descent parser that handles the + * '..out' keyword and expects a semicolon after the expression. + * + * Error handling includes checks for missing semicolon after output expression. + */ + function parseIOOut() { + current++; // Skip IO_OUT token + const value = parseLogicalExpression(); + + // Expect semicolon + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + } + + return { + type: 'IOOutExpression', + value + }; + } + + /** + * Parse IO assert operations: ..assert expression + * + * @returns {ASTNode} IOAssertExpression AST node + * @throws {Error} For malformed assert expressions + * @description Parses assertion operations that verify conditions. + * The expression is parsed as a logical expression and will be evaluated + * by the interpreter. If the result is falsy, an assertion error is thrown. + * + * IO assert parsing is important for programs that need to perform + * runtime checks or assertions. It allows for expressions to be evaluated + * and their boolean results to be used for conditional execution or + * error reporting. + * + * The function implements a recursive descent parser that handles the + * '..assert' keyword and expects a semicolon after the expression. + * + * Error handling includes checks for missing semicolon after assert expression. + */ + function parseIOAssert() { + current++; // Skip IO_ASSERT token + const value = parseLogicalExpression(); + + // Expect semicolon + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + } + + return { + type: 'IOAssertExpression', + value + }; + } + + /** + * Parse IO listen operations: ..listen + * + * @returns {ASTNode} IOListenExpression AST node + * @description Parses listen operations that retrieve current state. + * Returns the current state from the external system without any parameters. + * + * IO listen parsing is useful for programs that need to query the + * current state of an external system or environment. It allows for + * simple retrieval of state without requiring any input parameters. + * + * The function implements a recursive descent parser that handles the + * '..listen' keyword and expects a semicolon after the operation. + * + * Error handling includes checks for missing semicolon after listen operation. + */ + function parseIOListen() { + current++; // Skip IO_LISTEN token + + // Expect semicolon + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + } + + return { + type: 'IOListenExpression' + }; + } + + /** + * Parse IO emit operations: ..emit expression + * + * @returns {ASTNode} IOEmitExpression AST node + * @throws {Error} For malformed emit expressions + * @description Parses emit operations that send values to external system. + * The expression is parsed as a logical expression and will be evaluated + * by the interpreter before being sent to the external system. + * + * IO emit parsing is essential for programs that need to interact with + * external systems or environments. It allows for expressions to be + * evaluated and their results to be sent to the external system. + * + * The function implements a recursive descent parser that handles the + * '..emit' keyword and expects a semicolon after the expression. + * + * Error handling includes checks for missing semicolon after emit expression. + */ + function parseIOEmit() { + current++; // Skip IO_EMIT token + const value = parseLogicalExpression(); + + // Expect semicolon + if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { + current++; + } + + return { + type: 'IOEmitExpression', + value + }; + } + + /** + * Parse logical expressions with proper precedence + * + * @returns {ASTNode} AST node representing the logical expression + * @description Parses logical expressions (and, or, xor) with the lowest + * precedence. All logical operators are translated to FunctionCall nodes + * using the corresponding combinator functions. + * + * Logical expression parsing is the foundation for conditional logic + * in the language. It handles the lowest precedence operators (and, or, xor) + * and translates them to combinator function calls. + * + * The function implements a recursive descent parser that handles + * operator precedence by repeatedly calling itself with the right operand + * until no more operators of the same precedence are found. + * + * Error handling includes checks for missing operators or operands. + */ + function parseLogicalExpression() { + let left = parseExpression(); + + while (current < tokens.length) { + const token = tokens[current]; + + if (token.type === TokenType.AND || + token.type === TokenType.OR || + token.type === TokenType.XOR) { + current++; + const right = parseExpression(); + left = { + type: 'FunctionCall', + name: token.type === TokenType.AND ? 'logicalAnd' : + token.type === TokenType.OR ? 'logicalOr' : 'logicalXor', + args: [left, right] + }; + } else { + break; + } + } + + return left; + } + + /** + * Parse comparison expressions + * + * @returns {ASTNode} AST node representing the comparison expression + * @description Parses comparison expressions (=, !=, <, >, <=, >=) and + * additive expressions (+, -). All operators are translated to FunctionCall + * nodes using the corresponding combinator functions. + * + * This function implements the core of the combinator-based architecture + * by translating operator expressions to function calls that will be + * executed by the interpreter using standard library combinators. + * + * Comparison expression parsing is crucial for conditional logic + * and arithmetic operations. It handles equality, inequality, + * comparison operators, and additive operators. + * + * The function implements a recursive descent parser that handles + * operator precedence by repeatedly calling itself with the right operand + * until no more operators of the same precedence are found. + * + * Error handling includes checks for missing operators or operands. + */ + function parseExpression() { + if (DEBUG) { + console.log(`[DEBUG] parseExpression: starting, current token = ${tokens[current].type}`); + } + + // Handle IO operations in expressions + if (current < tokens.length) { + const token = tokens[current]; + if (token.type === TokenType.IO_LISTEN) { + return parseIOListen(); + } + if (token.type === TokenType.IO_EMIT) { + return parseIOEmit(); + } + } + + // Handle unary minus at the beginning of expressions + let left; + if (current < tokens.length && (tokens[current].type === TokenType.MINUS || tokens[current].type === TokenType.UNARY_MINUS)) { + if (DEBUG) { + console.log(`[DEBUG] parseExpression: handling unary minus`); + } + current++; + const operand = parseTerm(); + left = { + type: 'FunctionCall', + name: 'negate', + args: [operand] + }; + } else { + left = parseTerm(); + } + + if (DEBUG) { + console.log(`[DEBUG] parseExpression: after parseTerm, current token = ${tokens[current].type}`); + } + + while (current < tokens.length) { + const token = tokens[current]; + + if (DEBUG) { + console.log(`[DEBUG] parseExpression: while loop, current token = ${token.type}, value = ${token.value || 'N/A'}`); + } + + if (token.type === TokenType.PLUS) { + current++; + const right = parseTerm(); + left = { + type: 'FunctionCall', + name: 'add', + args: [left, right] + }; + } else if (token.type === TokenType.MINUS || token.type === TokenType.BINARY_MINUS) { + current++; + const right = parseTerm(); + left = { + type: 'FunctionCall', + name: 'subtract', + args: [left, right] + }; + } else if (token.type === TokenType.EQUALS || + token.type === TokenType.NOT_EQUAL || + token.type === TokenType.LESS_THAN || + token.type === TokenType.GREATER_THAN || + token.type === TokenType.LESS_EQUAL || + token.type === TokenType.GREATER_EQUAL) { + current++; + const right = parseTerm(); + left = { + type: 'FunctionCall', + name: token.type === TokenType.EQUALS ? 'equals' : + token.type === TokenType.NOT_EQUAL ? 'notEquals' : + token.type === TokenType.LESS_THAN ? 'lessThan' : + token.type === TokenType.GREATER_THAN ? 'greaterThan' : + token.type === TokenType.LESS_EQUAL ? 'lessEqual' : 'greaterEqual', + args: [left, right] + }; + } else { + break; + } + } + + return left; + } + + /** + * Parse multiplication and division expressions + * + * @returns {ASTNode} AST node representing the multiplicative expression + * @description Parses multiplicative expressions (*, /, %) with higher + * precedence than additive expressions. All operators are translated to + * FunctionCall nodes using the corresponding combinator functions. + * + * Multiplicative expression parsing is crucial for arithmetic operations + * and mathematical calculations. It handles multiplication, division, + * and modulo operations. + * + * The function implements a recursive descent parser that handles + * operator precedence by repeatedly calling itself with the right operand + * until no more operators of the same precedence are found. + * + * Error handling includes checks for missing operators or operands. + */ + function parseTerm() { + if (DEBUG) { + console.log(`[DEBUG] parseTerm: starting, current token = ${tokens[current].type}`); + } + let left = parseApplication(); + + while (current < tokens.length) { + const token = tokens[current]; + + if (token.type === TokenType.MULTIPLY || + token.type === TokenType.DIVIDE || + token.type === TokenType.MODULO) { + current++; + const right = parseFactor(); + left = { + type: 'FunctionCall', + name: token.type === TokenType.MULTIPLY ? 'multiply' : + token.type === TokenType.DIVIDE ? 'divide' : 'modulo', + args: [left, right] + }; + } else if (token.type === TokenType.MINUS) { + current++; + const right = parseFactor(); + left = { + type: 'FunctionCall', + name: 'subtract', + args: [left, right] + }; + } else { + break; + } + } + + return left; + } + + /** + * Parse power expressions and unary operators + * + * @returns {ASTNode} AST node representing the factor expression + * @description Parses power expressions (^) and unary operators (not, -) + * with the highest precedence among operators. All operators are translated + * to FunctionCall nodes using the corresponding combinator functions. + * + * Factor expression parsing is crucial for exponentiation and unary + * operators. It handles power expressions and unary operators (not, -). + * + * The function implements a recursive descent parser that handles + * operator precedence by repeatedly calling itself with the right operand + * until no more operators of the same precedence are found. + * + * Error handling includes checks for missing operators or operands. + */ + function parseFactor() { + if (DEBUG) { + console.log(`[DEBUG] parseFactor: starting, current token = ${tokens[current].type}`); + } + let left = parsePrimary(); + + // Parse power expressions (existing logic) + while (current < tokens.length) { + const token = tokens[current]; + + if (token.type === TokenType.POWER) { + current++; + const right = parsePrimary(); + left = { + type: 'FunctionCall', + name: 'power', + args: [left, right] + }; + } else { + break; + } + } + + return left; + } + + /** + * Parse function composition expressions using the 'via' keyword + * + * @returns {ASTNode} AST node representing the composition expression + * @throws {Error} For malformed composition expressions + * @description Parses function composition using the 'via' keyword + * with right-associative precedence: f via g via h = compose(f, compose(g, h)) + * + * The 'via' operator provides natural function composition syntax that reads + * from right to left, matching mathematical function composition notation. + * + * Precedence and associativity: + * - 'via' has higher precedence than function application (juxtaposition) + * - 'via' is right-associative: f via g via h = compose(f, compose(g, h)) + * - This means: f via g via h(x) = compose(f, compose(g, h))(x) = f(g(h(x))) + * + * Translation examples: + * - f via g → compose(f, g) + * - f via g via h → compose(f, compose(g, h)) + * - f via g via h via i → compose(f, compose(g, compose(h, i))) + * + * The right-associative design choice enables natural reading of composition + * chains that matches mathematical notation where (f ∘ g ∘ h)(x) = f(g(h(x))). + * + * Function composition is a fundamental feature that allows functions to be + * combined naturally. The right-associative precedence means that composition + * chains are built from right to left, which matches mathematical function + * composition notation. This enables functional programming patterns + * where transformations can be built from simple, composable functions. + * + * Composition parsing is essential for functional programming patterns + * where functions are composed together. It handles the 'via' keyword + * and recursively composes functions from right to left. + * + * The function implements a recursive descent parser that handles the + * 'via' keyword and recursively composes functions. + * + * Error handling includes checks for missing 'via' keyword or malformed + * composition chains. + */ + function parseComposition() { + let left = parseFactor(); + + // Parse right-associative composition: f via g via h = compose(f, compose(g, h)) + while (current < tokens.length && tokens[current].type === TokenType.COMPOSE) { + current++; // Skip 'via' + const right = parseFactor(); + + left = { + type: 'FunctionCall', + name: 'compose', + args: [left, right] + }; + } + + return left; + } + + /** + * Parse function application (juxtaposition) + * + * @returns {ASTNode} AST node representing the function application + * @description Parses function application using juxtaposition (f x) + * with left-associative precedence: f g x = apply(apply(f, g), x) + * + * Function application using juxtaposition is the primary mechanism for + * calling functions in the language. The left-associative precedence means + * that application chains are built from left to right, which is intuitive + * for most programmers. This approach reduces the need for parentheses + * in many cases while maintaining clear precedence rules. + * + * Function application parsing is essential for calling functions in + * the language. It handles juxtaposition of function and argument expressions. + * + * The function implements a recursive descent parser that handles + * left-associative function application. It repeatedly calls itself + * with the right operand until no more function applications are found. + * + * Error handling includes checks for missing function or argument expressions. + */ + function parseApplication() { + let left = parseComposition(); + + // Parse left-associative function application: f g x = apply(apply(f, g), x) + while (current < tokens.length && isValidArgumentStart(tokens[current])) { + const arg = parseComposition(); // Parse the argument as a composition expression + left = { + type: 'FunctionCall', + name: 'apply', + args: [left, arg] + }; + } + + return left; + } + + /** + * Check if a token is a valid start of a function argument + * + * @param {Token} token - Token to check + * @returns {boolean} True if the token can start a function argument + * @description Determines if a token can be the start of a function argument. + * This is used to detect function application (juxtaposition) where function + * application binds tighter than infix operators. + * + * This function is crucial for the juxtaposition-based function application + * system. It determines when the parser should treat an expression as a + * function argument rather than as part of an infix operator expression. + * The tokens that can start arguments are carefully chosen to ensure that + * function application has the correct precedence relative to operators. + */ + function isValidArgumentStart(token) { + return token.type === TokenType.IDENTIFIER || + token.type === TokenType.NUMBER || + token.type === TokenType.STRING || + token.type === TokenType.LEFT_PAREN || + token.type === TokenType.LEFT_BRACE || + token.type === TokenType.TRUE || + token.type === TokenType.FALSE || + token.type === TokenType.FUNCTION_REF || + token.type === TokenType.FUNCTION_ARG || + token.type === TokenType.NOT || + token.type === TokenType.UNARY_MINUS; + } + + /** + * Parse table literals: {key: value, key2: value2} or {value1, value2, value3} + * + * @returns {ASTNode} TableLiteral AST node + * @throws {Error} For malformed table literals + * @description Parses table literals with support for both key-value pairs + * and array-like entries. Tables are the primary data structure in the language. + * + * Supports: + * - Key-value pairs: {name: "Alice", age: 30} + * - Array-like entries: {1, 2, 3} + * - Mixed entries: {1, 2, name: "Alice", 3} + * + * Array-like entries are automatically assigned numeric keys starting from 1. + * + * Table literal parsing is essential for defining and accessing + * key-value or array-like data structures. It handles curly braces, + * keys, and values. + * + * The function implements a recursive descent parser that handles + * nested structures and supports both key-value and array-like entries. + * + * Error handling includes checks for missing braces, malformed keys, + * and unexpected tokens. + */ + function parseTableLiteral() { + current++; // Skip '{' + + const entries = []; + + while (current < tokens.length && tokens[current].type !== TokenType.RIGHT_BRACE) { + // Check if this is a key-value pair or just a value + let key = null; + let value; + + // Parse the first element + if (tokens[current].type === TokenType.IDENTIFIER) { + // Could be a key or a value + const identifier = tokens[current].value; + current++; + + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + // This is a key-value pair: key : value + key = { type: 'Identifier', value: identifier }; + current++; // Skip ':' + + // Check if the value is an arrow function + let isArrowFunction = false; + let lookAhead = current; + + // Look ahead to see if this is an arrow function + while (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.IDENTIFIER) { + lookAhead++; + } + + if (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.ARROW) { + // This is an arrow function + isArrowFunction = true; + + // Parse parameters + const params = []; + while (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + params.push(tokens[current].value); + current++; + } + + if (current >= tokens.length || tokens[current].type !== TokenType.ARROW) { + throw new Error('Expected "->" after parameters in arrow function'); + } + current++; // Skip '->' + + // Check if the body is a when expression + let body; + if (tokens[current].type === TokenType.WHEN) { + body = parseWhenExpression(); + } else { + body = parseLogicalExpression(); + } + + value = { + type: 'FunctionDeclaration', + params, + body + }; + } else { + // This is a regular value + value = parseLogicalExpression(); + } + } else { + // This is just a value (array-like entry) + value = { type: 'Identifier', value: identifier }; + } + } else if (tokens[current].type === TokenType.NUMBER) { + // Could be a numeric key or a value + const number = tokens[current].value; + current++; + + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + // This is a key-value pair: number : value + key = { type: 'NumberLiteral', value: number }; + current++; // Skip ':' + value = parseLogicalExpression(); + } else { + // This is just a value (array-like entry) + value = { type: 'NumberLiteral', value: number }; + } + } else if (tokens[current].type === TokenType.TRUE) { + // Could be a boolean key or a value + current++; + + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + // This is a key-value pair: true : value + key = { type: 'BooleanLiteral', value: true }; + current++; // Skip ':' + value = parseLogicalExpression(); + } else { + // This is just a value (array-like entry) + value = { type: 'BooleanLiteral', value: true }; + } + } else if (tokens[current].type === TokenType.FALSE) { + // Could be a boolean key or a value + current++; + + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + // This is a key-value pair: false : value + key = { type: 'BooleanLiteral', value: false }; + current++; // Skip ':' + value = parseLogicalExpression(); + } else { + // This is just a value (array-like entry) + value = { type: 'BooleanLiteral', value: false }; + } + } else if (tokens[current].type === TokenType.LEFT_PAREN) { + // This could be a computed key or a value + const expression = parseLogicalExpression(); + + if (current < tokens.length && tokens[current].type === TokenType.ASSIGNMENT) { + // This is a key-value pair: (expression) : value + key = expression; + current++; // Skip ':' + value = parseLogicalExpression(); + } else { + // This is just a value (array-like entry) + value = expression; + } + } else { + // Check if this is an arrow function: param1 param2 -> body + let isArrowFunction = false; + let lookAhead = current; + + // Look ahead to see if this is an arrow function + while (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.IDENTIFIER) { + lookAhead++; + } + + if (lookAhead < tokens.length && tokens[lookAhead].type === TokenType.ARROW) { + // This is an arrow function + isArrowFunction = true; + + // Parse parameters + const params = []; + while (current < tokens.length && tokens[current].type === TokenType.IDENTIFIER) { + params.push(tokens[current].value); + current++; + } + + if (current >= tokens.length || tokens[current].type !== TokenType.ARROW) { + throw new Error('Expected "->" after parameters in arrow function'); + } + current++; // Skip '->' + + // Check if the body is a when expression + let body; + if (tokens[current].type === TokenType.WHEN) { + body = parseWhenExpression(); + } else { + body = parseLogicalExpression(); + } + + value = { + type: 'FunctionDeclaration', + params, + body + }; + } else { + // This is a regular value (array-like entry) + value = parseLogicalExpression(); + } + } + + entries.push({ key, value }); + + // Skip comma if present + if (current < tokens.length && tokens[current].type === TokenType.COMMA) { + current++; + } + } + + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_BRACE) { + throw new Error('Expected "}" after table literal'); + } + current++; // Skip '}' + + return { + type: 'TableLiteral', + entries + }; + } + + + + /** + * Parse function calls: functionName arg1 arg2 ... + * + * @returns {ASTNode} FunctionCall AST node + * @description Parses function calls with multiple arguments. This function + * is used by parsePrimary to detect when an identifier is followed by + * expressions that should be treated as function arguments. + * + * Function calls are detected by the presence of an identifier followed + * by expressions that are not operators. The parser uses lookahead to + * determine if an identifier should be treated as a function call. + * + * Function call parsing is essential for calling functions in the language. + * It handles the juxtaposition of function names and their arguments. + * + * The function implements a recursive descent parser that handles + * the function name, followed by a parenthesized list of arguments. + * + * Error handling includes checks for missing function name or arguments. + */ + function parseFunctionCall() { + const functionName = tokens[current].value; + current++; // Skip function name + + // Parse arguments until we hit a semicolon or end of tokens + const args = []; + while (current < tokens.length && tokens[current].type !== TokenType.SEMICOLON) { + const arg = parseLogicalExpression(); + args.push(arg); + } + + return { + type: 'FunctionCall', + name: functionName, + args + }; + } + + /** + * Parse primary expressions (literals, identifiers, parenthesized expressions) + * + * @returns {ASTNode} AST node representing the primary expression + * @throws {Error} For unexpected tokens or malformed expressions + * @description Parses the highest precedence expressions including literals, + * identifiers, function calls, table access, and parenthesized expressions. + * This is the foundation of the expression parsing hierarchy. + * + * The function implements function call detection by looking + * for identifiers followed by expressions that could be arguments. This + * approach allows the language to support both traditional function calls + * and the ML-style function application syntax. + * + * Supports: + * - Literals: numbers, strings, booleans + * - Identifiers: variables and function names + * - Function calls: f(x, y) or f x y + * - Table access: table[key] or table.property + * - Parenthesized expressions: (x + y) + * - Unary operators: not x, -x + * - Function references: @functionName + * + * Primary expression parsing is the foundation of all other expression + * parsing. It handles literals, identifiers, function calls, table access, + * parenthesized expressions, and unary operators. + * + * The function implements a recursive descent parser that handles + * each specific type of primary expression. + * + * Error handling includes checks for missing literals, malformed + * identifiers, and unexpected tokens. + */ + function parsePrimary() { + const token = tokens[current]; + + if (!token) { + throw new Error('Unexpected end of input'); + } + + if (DEBUG) { + console.log(`[DEBUG] parsePrimary: current token = ${token.type}, value = ${token.value || 'N/A'}`); + } + + switch (token.type) { + case TokenType.NUMBER: + current++; + return { type: 'NumberLiteral', value: token.value }; + + case TokenType.STRING: + current++; + return { type: 'StringLiteral', value: token.value }; + + case TokenType.TRUE: + current++; + return { type: 'BooleanLiteral', value: true }; + + case TokenType.FALSE: + current++; + return { type: 'BooleanLiteral', value: false }; + + case TokenType.WHEN: + return parseWhenExpression(); + + + + case TokenType.IDENTIFIER: + const identifierValue = token.value; + current++; + + // Check for table access: identifier[key] or identifier.property + if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { + current++; // Skip '[' + const keyExpression = parseLogicalExpression(); + + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_BRACKET) { + throw new Error('Expected "]" after table key'); + } + current++; // Skip ']' + + let tableNode = { + type: 'TableAccess', + table: { type: 'Identifier', value: identifierValue }, + key: keyExpression + }; + + // Check for chained access: table[key].property or table[key][key2] + while (current < tokens.length && (tokens[current].type === TokenType.DOT || tokens[current].type === TokenType.LEFT_BRACKET)) { + if (tokens[current].type === TokenType.DOT) { + current++; // Skip '.' + + if (current >= tokens.length || tokens[current].type !== TokenType.IDENTIFIER) { + throw new Error('Expected identifier after "." in table access'); + } + + const propertyName = tokens[current].value; + current++; // Skip property name + + tableNode = { + type: 'TableAccess', + table: tableNode, + key: { type: 'Identifier', value: propertyName } + }; + } else if (tokens[current].type === TokenType.LEFT_BRACKET) { + current++; // Skip '[' + const keyExpression2 = parseLogicalExpression(); + + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_BRACKET) { + throw new Error('Expected "]" after table key'); + } + current++; // Skip ']' + + tableNode = { + type: 'TableAccess', + table: tableNode, + key: keyExpression2 + }; + } + } + + return tableNode; + } else if (current < tokens.length && tokens[current].type === TokenType.DOT) { + current++; // Skip '.' + + if (current >= tokens.length || tokens[current].type !== TokenType.IDENTIFIER) { + throw new Error('Expected identifier after "." in table access'); + } + + const propertyName = tokens[current].value; + current++; // Skip property name + + let tableNode = { + type: 'TableAccess', + table: { type: 'Identifier', value: identifierValue }, + key: { type: 'Identifier', value: propertyName } + }; + + // Check for chained access: table.property[key] or table.property.property2 + while (current < tokens.length && (tokens[current].type === TokenType.DOT || tokens[current].type === TokenType.LEFT_BRACKET)) { + if (tokens[current].type === TokenType.DOT) { + current++; // Skip '.' + + if (current >= tokens.length || tokens[current].type !== TokenType.IDENTIFIER) { + throw new Error('Expected identifier after "." in table access'); + } + + const propertyName2 = tokens[current].value; + current++; // Skip property name + + tableNode = { + type: 'TableAccess', + table: tableNode, + key: { type: 'Identifier', value: propertyName2 } + }; + } else if (tokens[current].type === TokenType.LEFT_BRACKET) { + current++; // Skip '[' + const keyExpression = parseLogicalExpression(); + + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_BRACKET) { + throw new Error('Expected "]" after table key'); + } + current++; // Skip ']' + + tableNode = { + type: 'TableAccess', + table: tableNode, + key: keyExpression + }; + } + } + + return tableNode; + } + + // Parenthesized expressions after identifiers are handled by parseApplication + // to support function calls like f(x) + if (current < tokens.length && tokens[current].type === TokenType.LEFT_PAREN) { + // Don't handle this here, let parseApplication handle it + // This ensures that f(x) is parsed as apply(f, x) not just x + } + + // Juxtaposition function calls are now handled in parseFactor() with proper precedence + return { type: 'Identifier', value: identifierValue }; + + case TokenType.LEFT_PAREN: + current++; + if (DEBUG) { + console.log(`[DEBUG] parsePrimary: parsing LEFT_PAREN, current token = ${tokens[current].type}`); + } + const expression = parseLogicalExpression(); + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { + throw new Error('Expected ")" after expression'); + } + current++; + + // Check if this is just a simple identifier in parentheses + if (expression.type === 'Identifier') { + return { + type: 'FunctionCall', + name: 'identity', + args: [expression] + }; + } + + return expression; + + case TokenType.WILDCARD: + current++; + return { type: 'WildcardPattern' }; + + case TokenType.LEFT_BRACE: + return parseTableLiteral(); + + + + case TokenType.NOT: + current++; + const operand = parsePrimary(); + return { + type: 'FunctionCall', + name: 'logicalNot', + args: [operand] + }; + + case TokenType.MINUS: + case TokenType.UNARY_MINUS: + // Delegate unary minus to parseExpression for proper precedence + return parseExpression(); + + case TokenType.ARROW: + current++; + const arrowBody = parseLogicalExpression(); + return { type: 'ArrowExpression', body: arrowBody }; + + case TokenType.FUNCTION_REF: + const functionRef = { type: 'FunctionReference', name: tokens[current].name }; + current++; + return functionRef; + + case TokenType.FUNCTION_ARG: + // @(expression) - parse the parenthesized expression as a function argument + current++; // Skip FUNCTION_ARG token + if (current >= tokens.length || tokens[current].type !== TokenType.LEFT_PAREN) { + throw new Error('Expected "(" after @'); + } + current++; // Skip '(' + const argExpression = parseLogicalExpression(); + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { + throw new Error('Expected ")" after function argument expression'); + } + current++; // Skip ')' + return argExpression; + + default: + throw new Error(`Unexpected token in parsePrimary: ${token.type}`); + } + } + + return parse(); +} \ No newline at end of file |