diff options
Diffstat (limited to 'js/scripting-lang/parser.js')
-rw-r--r-- | js/scripting-lang/parser.js | 653 |
1 files changed, 489 insertions, 164 deletions
diff --git a/js/scripting-lang/parser.js b/js/scripting-lang/parser.js index 055b3c0..01f3648 100644 --- a/js/scripting-lang/parser.js +++ b/js/scripting-lang/parser.js @@ -6,15 +6,49 @@ import { TokenType } from './lexer.js'; /** * Parser: Converts tokens to an Abstract Syntax Tree (AST). + * * @param {Array.<Object>} tokens - Array of tokens from the lexer * @returns {Object} Abstract Syntax Tree with program body * @throws {Error} For parsing errors like unexpected tokens or missing delimiters + * + * @description 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 = []; @@ -31,6 +65,24 @@ export function parser(tokens) { /** * 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]; @@ -65,12 +117,23 @@ export function parser(tokens) { 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; @@ -140,103 +203,40 @@ export function parser(tokens) { } }; } else { - // Check if this is a function call: functionName arg1 arg2 ... - if (tokens[current].type === TokenType.IDENTIFIER) { - const functionName = tokens[current].value; - current++; // Skip function name - - // Look ahead to see if this is a function call - // A function call should have arguments that are not operators - let lookAhead = current; - let hasArguments = false; - let isFunctionCall = false; - - while (lookAhead < tokens.length && - lookAhead < current + 5 && // Limit lookahead to avoid infinite loops - tokens[lookAhead].type !== TokenType.SEMICOLON) { - if (tokens[lookAhead].type === TokenType.IDENTIFIER || - tokens[lookAhead].type === TokenType.NUMBER || - tokens[lookAhead].type === TokenType.STRING || - tokens[lookAhead].type === TokenType.TRUE || - tokens[lookAhead].type === TokenType.FALSE || - tokens[lookAhead].type === TokenType.LEFT_PAREN) { - hasArguments = true; - isFunctionCall = true; - break; - } else if (tokens[lookAhead].type === TokenType.PLUS || - tokens[lookAhead].type === TokenType.MINUS || - tokens[lookAhead].type === TokenType.MULTIPLY || - tokens[lookAhead].type === TokenType.DIVIDE || - tokens[lookAhead].type === TokenType.MODULO || - tokens[lookAhead].type === TokenType.POWER || - tokens[lookAhead].type === TokenType.EQUALS || - tokens[lookAhead].type === TokenType.NOT_EQUAL || - tokens[lookAhead].type === TokenType.LESS_THAN || - tokens[lookAhead].type === TokenType.GREATER_THAN || - tokens[lookAhead].type === TokenType.LESS_EQUAL || - tokens[lookAhead].type === TokenType.GREATER_EQUAL || - tokens[lookAhead].type === TokenType.AND || - tokens[lookAhead].type === TokenType.OR || - tokens[lookAhead].type === TokenType.XOR || - tokens[lookAhead].type === TokenType.ARROW) { - // This is an expression, not a function call - isFunctionCall = false; - break; - } - lookAhead++; - } - - if (isFunctionCall && hasArguments) { - const value = parseFunctionCall(functionName); - - // Expect semicolon - if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { - current++; - } - - return { - type: 'Assignment', - identifier, - value - }; - } else { - // Not a function call, parse as expression - // Reset current position and parse the entire value as expression - current = current - 1; // Go back to the identifier - const value = parseLogicalExpression(); - - // Expect semicolon - if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { - current++; - } - - return { - type: 'Assignment', - identifier, - value - }; - } - } else { - // Regular expression - const value = parseLogicalExpression(); - - // Expect semicolon - if (current < tokens.length && tokens[current].type === TokenType.SEMICOLON) { - current++; - } - - return { - type: 'Assignment', - identifier, - value - }; + // 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' @@ -244,7 +244,17 @@ export function parser(tokens) { // Parse the value(s) - can be single value or multiple values const values = []; while (current < tokens.length && tokens[current].type !== TokenType.IS) { - const value = parseLogicalExpression(); + // 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); } @@ -262,19 +272,55 @@ export function parser(tokens) { // Parse patterns until we hit THEN while (current < tokens.length && tokens[current].type !== TokenType.THEN) { let pattern; - if (tokens[current].type === TokenType.IDENTIFIER) { + 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, or wildcard) in when expression'); + 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,7 +346,9 @@ export function parser(tokens) { current >= tokens.length || (tokens[current].type !== TokenType.IDENTIFIER && tokens[current].type !== TokenType.NUMBER && - tokens[current].type !== TokenType.WILDCARD) + tokens[current].type !== TokenType.STRING && + tokens[current].type !== TokenType.WILDCARD && + tokens[current].type !== TokenType.FUNCTION_REF) ) { break; } @@ -316,6 +364,18 @@ export function parser(tokens) { /** * 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' @@ -360,6 +420,11 @@ export function parser(tokens) { /** * 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 @@ -368,6 +433,12 @@ export function parser(tokens) { /** * 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 @@ -386,6 +457,12 @@ export function parser(tokens) { /** * 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 @@ -404,6 +481,20 @@ export function parser(tokens) { /** * 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(); @@ -417,10 +508,10 @@ export function parser(tokens) { current++; const right = parseExpression(); left = { - type: token.type === TokenType.AND ? 'AndExpression' : - token.type === TokenType.OR ? 'OrExpression' : 'XorExpression', - left, - right + type: 'FunctionCall', + name: token.type === TokenType.AND ? 'logicalAnd' : + token.type === TokenType.OR ? 'logicalOr' : 'logicalXor', + args: [left, right] }; } else { break; @@ -432,20 +523,52 @@ export function parser(tokens) { /** * 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 (token.type === TokenType.PLUS || token.type === TokenType.MINUS) { + 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: token.type === TokenType.PLUS ? 'PlusExpression' : 'MinusExpression', - left, - right + type: 'FunctionCall', + name: 'subtract', + args: [left, right] }; } else if (token.type === TokenType.EQUALS || token.type === TokenType.NOT_EQUAL || @@ -456,13 +579,13 @@ export function parser(tokens) { current++; const right = parseTerm(); left = { - type: token.type === TokenType.EQUALS ? 'EqualsExpression' : - token.type === TokenType.NOT_EQUAL ? 'NotEqualExpression' : - token.type === TokenType.LESS_THAN ? 'LessThanExpression' : - token.type === TokenType.GREATER_THAN ? 'GreaterThanExpression' : - token.type === TokenType.LESS_EQUAL ? 'LessEqualExpression' : 'GreaterEqualExpression', - left, - right + 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; @@ -474,9 +597,14 @@ export function parser(tokens) { /** * 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 = parseFactor(); + let left = parseApplication(); while (current < tokens.length) { const token = tokens[current]; @@ -487,10 +615,10 @@ export function parser(tokens) { current++; const right = parseFactor(); left = { - type: token.type === TokenType.MULTIPLY ? 'MultiplyExpression' : - token.type === TokenType.DIVIDE ? 'DivideExpression' : 'ModuloExpression', - left, - right + type: 'FunctionCall', + name: token.type === TokenType.MULTIPLY ? 'multiply' : + token.type === TokenType.DIVIDE ? 'divide' : 'modulo', + args: [left, right] }; } else { break; @@ -502,10 +630,16 @@ export function parser(tokens) { /** * 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]; @@ -513,9 +647,9 @@ export function parser(tokens) { current++; const right = parsePrimary(); left = { - type: 'PowerExpression', - left, - right + type: 'FunctionCall', + name: 'power', + args: [left, right] }; } else { break; @@ -526,7 +660,108 @@ 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 || + // 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 '{' @@ -577,17 +812,27 @@ export function parser(tokens) { }; } + + /** * 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(functionName, insideParentheses = false) { - const args = []; + function parseFunctionCall() { + const functionName = tokens[current].value; + current++; // Skip function name - // Parse arguments until we hit a semicolon or right parenthesis (if inside parentheses) - while (current < tokens.length && - tokens[current].type !== TokenType.SEMICOLON && - (!insideParentheses || tokens[current].type !== TokenType.RIGHT_PAREN)) { - // Parse the argument + // 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); } @@ -601,6 +846,26 @@ export function parser(tokens) { /** * 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]; @@ -609,6 +874,10 @@ export function parser(tokens) { 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++; @@ -626,67 +895,109 @@ export function parser(tokens) { current++; return { type: 'BooleanLiteral', value: false }; - case TokenType.IDENTIFIER: - current++; - return { type: 'Identifier', value: token.value }; + case TokenType.WHEN: + return parseWhenExpression(); - case TokenType.WILDCARD: - current++; - return { type: 'WildcardPattern' }; + - case TokenType.LEFT_PAREN: + case TokenType.IDENTIFIER: + const identifierValue = token.value; current++; - // Check if this is a function call inside parentheses - if (tokens[current].type === TokenType.IDENTIFIER) { - const functionName = tokens[current].value; - current++; // Skip function name + // Check for table access: identifier[key] or identifier.property + if (current < tokens.length && tokens[current].type === TokenType.LEFT_BRACKET) { + current++; // Skip '[' + const keyExpression = parseLogicalExpression(); - // Check if there are arguments - if (current < tokens.length && tokens[current].type !== TokenType.RIGHT_PAREN) { - const functionCall = parseFunctionCall(functionName, true); - - if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { - throw new Error('Expected ")" after function call'); - } - current++; - - return functionCall; - } else { - // Just a function name in parentheses - const identifier = { type: 'Identifier', value: functionName }; - - if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_PAREN) { - throw new Error('Expected ")" after identifier'); - } - current++; - - return identifier; + if (current >= tokens.length || tokens[current].type !== TokenType.RIGHT_BRACKET) { + throw new Error('Expected "]" after table key'); } - } else { - // Regular parenthesized expression + 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++; + 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: + + + case TokenType.NOT: current++; const operand = parsePrimary(); - return { type: 'NotExpression', operand }; + return { + type: 'FunctionCall', + name: 'logicalNot', + args: [operand] + }; case TokenType.MINUS: - current++; - const unaryOperand = parsePrimary(); - return { type: 'UnaryMinusExpression', operand: unaryOperand }; + // Delegate unary minus to parseExpression for proper precedence + return parseExpression(); case TokenType.ARROW: current++; @@ -694,10 +1005,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}`); } |