diff options
Diffstat (limited to 'js/scripting-lang/parser.js')
-rw-r--r-- | js/scripting-lang/parser.js | 661 |
1 files changed, 568 insertions, 93 deletions
diff --git a/js/scripting-lang/parser.js b/js/scripting-lang/parser.js index b1aa77f..32837f7 100644 --- a/js/scripting-lang/parser.js +++ b/js/scripting-lang/parser.js @@ -5,7 +5,7 @@ import { TokenType } from './lexer.js'; /** - * Parser: Converts tokens to an Abstract Syntax Tree (AST). + * Parser: Converts tokens to an Abstract Syntax Tree (AST) using combinator-based architecture. * * @param {Array.<Object>} tokens - Array of tokens from the lexer * @returns {Object} Abstract Syntax Tree with program body @@ -26,10 +26,19 @@ import { TokenType } from './lexer.js'; * - 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. + * 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 eliminates the need for special operator handling in the + * interpreter and enables powerful 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. */ export function parser(tokens) { let current = 0; @@ -40,6 +49,20 @@ export function parser(tokens) { * @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. + * + * The function implements the top-level parsing strategy by processing each + * statement or expression in sequence. This approach enables the parser to + * handle complex 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 any complexity while maintaining + * clear separation between different language constructs. */ function parse() { const body = []; @@ -68,6 +91,19 @@ export function parser(tokens) { * 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. + * + * The function uses a pattern-matching approach to identify language constructs + * based on token sequences. This design enables the parser to handle complex + * 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. */ function walk() { const token = tokens[current]; @@ -224,6 +260,9 @@ export function parser(tokens) { * AST that the interpreter can efficiently evaluate. */ function parseWhenExpression() { + if (process.env.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 @@ -233,23 +272,9 @@ export function parser(tokens) { // but not treat them as function calls let value; if (tokens[current].type === TokenType.IDENTIFIER) { - // Check if this is followed by another identifier (multi-value case) - if (current + 1 < tokens.length && - tokens[current + 1].type === TokenType.IDENTIFIER && - tokens[current + 2].type === TokenType.IS) { - // This is a multi-value case like "when x y is" - value = { type: 'Identifier', value: tokens[current].value }; - current++; - values.push(value); - value = { type: 'Identifier', value: tokens[current].value }; - current++; - values.push(value); - break; // We've consumed both values and will hit IS next - } else { - // Single identifier value - value = { type: 'Identifier', value: tokens[current].value }; - current++; - } + // Single identifier value + value = { type: 'Identifier', value: tokens[current].value }; + current++; } else { // For other types, use normal expression parsing value = parseLogicalExpression(); @@ -265,6 +290,9 @@ export function parser(tokens) { const cases = []; while (current < tokens.length) { + if (process.env.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 = []; @@ -274,7 +302,19 @@ export function parser(tokens) { if (process.env.DEBUG) { console.log(`[DEBUG] parseWhenExpression: parsing pattern, current token = ${tokens[current].type}, value = ${tokens[current].value || 'N/A'}`); } - if (tokens[current].type === TokenType.IDENTIFIER) { + + // 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) { @@ -289,10 +329,25 @@ export function parser(tokens) { } 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, or function reference) in when expression, got ${tokens[current].type}`); + 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) { @@ -300,28 +355,121 @@ export function parser(tokens) { } current++; // Skip 'then' - // Parse result - const result = parseLogicalExpression(); + // 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] }); - // 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) - ) { + if (process.env.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 (process.env.DEBUG) { + console.log(`[DEBUG] parseWhenExpression: checking termination, nextToken = ${nextToken.type}, value = ${nextToken.value || 'N/A'}`); + } + + // Stop on semicolon + if (nextToken.type === TokenType.SEMICOLON) { + if (process.env.DEBUG) { + console.log(`[DEBUG] parseWhenExpression: terminating on SEMICOLON`); + } + current++; + break; + } + + // Stop on assignment (for consecutive assignments) + if (nextToken.type === TokenType.ASSIGNMENT) { + if (process.env.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 (process.env.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 (process.env.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 (process.env.DEBUG) { + console.log(`[DEBUG] parseWhenExpression: terminating on COMMA`); + } break; } } @@ -334,6 +482,8 @@ export function parser(tokens) { }; } + + /** * Parse function definitions: function (params) : body * @@ -506,21 +656,51 @@ export function parser(tokens) { * executed by the interpreter using standard library combinators. */ function parseExpression() { + if (process.env.DEBUG) { + console.log(`[DEBUG] parseExpression: starting, current token = ${tokens[current].type}`); + } + + // Handle unary minus at the beginning of expressions + if (current < tokens.length && tokens[current].type === TokenType.MINUS) { + if (process.env.DEBUG) { + console.log(`[DEBUG] parseExpression: handling unary minus`); + } + current++; + const operand = parseTerm(); + return { + type: 'FunctionCall', + name: 'negate', + args: [operand] + }; + } + let left = parseTerm(); + if (process.env.DEBUG) { + console.log(`[DEBUG] parseExpression: after parseTerm, current token = ${tokens[current].type}`); + } + 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'}`); + console.log(`[DEBUG] parseExpression: while loop, current token = ${token.type}, value = ${token.value || 'N/A'}`); } - if (token.type === TokenType.PLUS || token.type === TokenType.MINUS) { + if (token.type === TokenType.PLUS) { current++; const right = parseTerm(); left = { type: 'FunctionCall', - name: token.type === TokenType.PLUS ? 'add' : 'subtract', + 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 || @@ -557,7 +737,10 @@ export function parser(tokens) { * FunctionCall nodes using the corresponding combinator functions. */ function parseTerm() { - let left = parseFactor(); + if (process.env.DEBUG) { + console.log(`[DEBUG] parseTerm: starting, current token = ${tokens[current].type}`); + } + let left = parseApplication(); while (current < tokens.length) { const token = tokens[current]; @@ -590,8 +773,12 @@ export function parser(tokens) { * to FunctionCall nodes using the corresponding combinator functions. */ function parseFactor() { + if (process.env.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]; @@ -612,6 +799,94 @@ export function parser(tokens) { } /** + * 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 || + token.type === TokenType.NOT; + } + + /** * Parse table literals: {key: value, key2: value2} or {value1, value2, value3} * * @returns {Object} TableLiteral AST node @@ -646,14 +921,149 @@ export function parser(tokens) { // This is a key-value pair: key : value key = { type: 'Identifier', value: identifier }; current++; // Skip ':' - value = parseLogicalExpression(); + + // 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 { - // This is a value (array-like entry) - value = parseLogicalExpression(); + // 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 }); @@ -758,6 +1168,11 @@ export function parser(tokens) { current++; return { type: 'BooleanLiteral', value: false }; + case TokenType.WHEN: + return parseWhenExpression(); + + + case TokenType.IDENTIFIER: const identifierValue = token.value; current++; @@ -772,11 +1187,47 @@ export function parser(tokens) { } current++; // Skip ']' - return { + 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 '.' @@ -787,52 +1238,57 @@ export function parser(tokens) { const propertyName = tokens[current].value; current++; // Skip property name - return { + let tableNode = { type: 'TableAccess', table: { type: 'Identifier', value: identifierValue }, key: { type: 'Identifier', value: propertyName } }; - } - - // Parse function call arguments (including parenthesized expressions) - const args = []; - while ( - current < tokens.length && - ( - tokens[current].type === TokenType.IDENTIFIER || - tokens[current].type === TokenType.NUMBER || - tokens[current].type === TokenType.STRING || - tokens[current].type === TokenType.LEFT_PAREN || - tokens[current].type === TokenType.LEFT_BRACE || - tokens[current].type === TokenType.TRUE || - tokens[current].type === TokenType.FALSE || - tokens[current].type === TokenType.FUNCTION_REF || - (tokens[current].type === TokenType.MINUS && - current + 1 < tokens.length && - tokens[current + 1].type === TokenType.NUMBER) - ) - ) { - // Special case: if we see FUNCTION_REF followed by MINUS followed by NUMBER, - // parse them as separate arguments - if (tokens[current].type === TokenType.FUNCTION_REF && - current + 1 < tokens.length && tokens[current + 1].type === TokenType.MINUS && - current + 2 < tokens.length && tokens[current + 2].type === TokenType.NUMBER) { - // Parse the function reference - args.push(parsePrimary()); - // Parse the unary minus as a separate argument - args.push(parsePrimary()); - } else { - // Parse each argument as a complete expression - args.push(parseExpression()); + + // 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; } - if (args.length > 0) { - return { - type: 'FunctionCall', - name: identifierValue, - args - }; + + // 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: @@ -845,6 +1301,16 @@ export function parser(tokens) { 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: @@ -856,7 +1322,7 @@ export function parser(tokens) { - case TokenType.NOT: + case TokenType.NOT: current++; const operand = parsePrimary(); return { @@ -866,13 +1332,8 @@ export function parser(tokens) { }; case TokenType.MINUS: - current++; - const unaryOperand = parsePrimary(); - return { - type: 'FunctionCall', - name: 'negate', - args: [unaryOperand] - }; + // Delegate unary minus to parseExpression for proper precedence + return parseExpression(); case TokenType.ARROW: current++; @@ -880,10 +1341,24 @@ export function parser(tokens) { return { type: 'ArrowExpression', body: arrowBody }; case TokenType.FUNCTION_REF: - const functionRef = { type: 'FunctionReference', name: token.name }; + 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}`); } |