// Parser for the scripting language // Exports: parser(tokens) // Converts tokens to an Abstract Syntax Tree (AST) import { TokenType } from './lexer.js'; /** * Parser: Converts tokens to an Abstract Syntax Tree (AST). * * @param {Array.} 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 The parser implements a combinator-based architecture where all * operators are translated to function calls to standard library combinators. * This eliminates 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 complex * logical expressions. This approach ensures that all operations are consistently * represented as function calls, enabling the interpreter to use the combinator * foundation for execution. */ export function parser(tokens) { let current = 0; /** * Main parsing function that processes the entire token stream * * @returns {Object} 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. */ 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 {Object|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 complex expressions are properly decomposed into their * constituent parts for combinator translation. */ 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(); } // 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 {Object} 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. */ 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 {Object} 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. */ function parseWhenExpression() { 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) { // For when expressions, we want to parse simple identifiers and expressions // but not treat them as function calls let value; if (tokens[current].type === TokenType.IDENTIFIER) { // Single identifier value value = { type: 'Identifier', value: tokens[current].value }; current++; } else { // For other types, use normal expression parsing value = parseLogicalExpression(); } 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) { // 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 (process.env.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) { 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 { 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 const result = parseLogicalExpression(); cases.push({ pattern: patterns, result: [result] }); // Stop parsing cases when we hit a semicolon if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { current++; break; } else { // No semicolon, but check if next token is a valid pattern if ( current >= tokens.length || (tokens[current].type !== TokenType.IDENTIFIER && tokens[current].type !== TokenType.NUMBER && tokens[current].type !== TokenType.STRING && tokens[current].type !== TokenType.WILDCARD && tokens[current].type !== TokenType.FUNCTION_REF) ) { break; } } } return { type: 'WhenExpression', value: values.length === 1 ? values[0] : values, cases }; } /** * Parse function definitions: function (params) : body * * @returns {Object} 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 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 {Object} 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. */ function parseIOIn() { current++; // Skip IO_IN token return { type: 'IOInExpression' }; } /** * Parse IO output operations: ..out expression * * @returns {Object} 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. */ 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 {Object} 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. */ 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 logical expressions with proper precedence * * @returns {Object} 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. * * Operator precedence (lowest to highest): * 1. Logical operators (and, or, xor) * 2. Comparison operators (=, !=, <, >, <=, >=) * 3. Additive operators (+, -) * 4. Multiplicative operators (*, /, %) * 5. Power operator (^) * 6. Unary operators (not, -) * 7. Primary expressions (literals, identifiers, function calls, parentheses) */ 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 {Object} 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. */ function parseExpression() { // Handle unary minus at the beginning of expressions if (current < tokens.length && tokens[current].type === TokenType.MINUS) { current++; const operand = parseTerm(); return { type: 'FunctionCall', name: 'negate', args: [operand] }; } let left = parseTerm(); while (current < tokens.length) { const token = tokens[current]; if (process.env.DEBUG) { console.log(`[DEBUG] parseExpression: 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) { 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 {Object} 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. */ function parseTerm() { 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 { break; } } return left; } /** * Parse power expressions and unary operators * * @returns {Object} 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. */ function parseFactor() { 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 * * @returns {Object} AST node representing the composition expression * @description Parses function composition using the 'via' keyword * with right-associative precedence: f via g via h = compose(f, compose(g, h)) * * 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 powerful functional programming patterns * where complex transformations can be built from simple, composable functions. */ 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 {Object} 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 eliminates the need for parentheses * in many cases while maintaining clear precedence rules. */ 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 {Object} 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 || // Removed: token.type === TokenType.MINUS || token.type === TokenType.NOT; } /** * Parse table literals: {key: value, key2: value2} or {value1, value2, value3} * * @returns {Object} 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. */ 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 ':' value = parseLogicalExpression(); } else { // This is just a value (array-like entry) value = { type: 'Identifier', value: identifier }; } } else { // This is a 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 {Object} 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 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 {Object} 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 sophisticated 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 */ function parsePrimary() { const token = tokens[current]; if (!token) { throw new Error('Unexpected end of input'); } if (process.env.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 ']' return { type: 'TableAccess', table: { type: 'Identifier', value: identifierValue }, key: keyExpression }; } 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 return { type: 'TableAccess', table: { type: 'Identifier', value: identifierValue }, key: { type: 'Identifier', value: propertyName } }; } // Parenthesized expressions are handled as simple grouping, not function calls // This maintains consistency with the juxtaposition pattern if (current < tokens.length && tokens[current].type === TokenType.LEFT_PAREN) { current++; // consume '(' // Parse the expression inside parentheses const expression = parseLogicalExpression(); if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { throw new Error('Expected ")" after expression'); } current++; // consume ')' return expression; } // Juxtaposition function calls are now handled in parseFactor() with proper precedence return { type: 'Identifier', value: identifierValue }; case TokenType.LEFT_PAREN: current++; if (process.env.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: // 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(); }