// 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();
}