petcarescript
Version:
PetCareScript - A modern, expressive programming language designed for humans
1,399 lines (1,162 loc) • 49.8 kB
JavaScript
/**
* PetCareScript Parser
* Converts tokens into an Abstract Syntax Tree (AST)
*/
const { TokenType } = require('../lexer/tokens');
const {
ProgramNode,
VariableDeclarationNode,
FunctionDeclarationNode,
BlueprintDeclarationNode,
BlockStatementNode,
ExpressionStatementNode,
IfStatementNode,
UnlessStatementNode,
WhileStatementNode,
ForStatementNode,
ForEachStatementNode,
SwitchStatementNode,
CaseClauseNode,
ReturnStatementNode,
PrintStatementNode,
BreakStatementNode,
ContinueStatementNode,
TryStatementNode,
ThrowStatementNode,
BinaryExpressionNode,
UnaryExpressionNode,
CallExpressionNode,
MemberExpressionNode,
AssignmentExpressionNode,
ConditionalExpressionNode,
AwaitExpressionNode,
LiteralNode,
IdentifierNode,
ArrayExpressionNode,
ObjectExpressionNode,
TemplateStringNode,
InterfaceDeclarationNode,
EnumDeclarationNode,
ImportDeclarationNode,
ExportDeclarationNode,
ModuleDeclarationNode,
ServerCallNode,
ServerMethodNode,
ServerListenNode,
ArrowFunctionNode,
SpreadElementNode,
TestSuiteNode,
TestCaseNode,
AssertionNode
} = require('./ast');
class ParseError extends Error {
constructor(message, token) {
super(message);
this.token = token;
}
}
class Parser {
constructor(tokens) {
this.tokens = tokens;
this.current = 0;
this.errors = [];
}
parse() {
const statements = [];
while (!this.isAtEnd()) {
try {
const stmt = this.declaration();
if (stmt) statements.push(stmt);
} catch (error) {
this.errors.push(error);
this.synchronize();
if (this.errors.length > 10) {
throw new Error(`Too many parse errors: ${this.errors.map(e => e.message).join(', ')}`);
}
}
}
if (this.errors.length > 0) {
throw new Error(`Parse errors: ${this.errors.map(e => e.message).join(', ')}`);
}
return new ProgramNode(statements);
}
// Helper method to consume identifier or keyword as identifier
consumeIdentifier(message) {
const validIdentifierTypes = [
TokenType.IDENTIFIER,
TokenType.SERVER,
TokenType.GET,
TokenType.POST,
TokenType.PUT,
TokenType.DELETE,
TokenType.USE,
TokenType.LISTEN,
TokenType.PROMISE,
TokenType.ASYNC,
TokenType.JSON,
TokenType.CORS,
TokenType.LOGGER,
TokenType.MIDDLEWARE,
TokenType.ROUTE,
TokenType.CREATE_SERVER,
// Add testing keywords that can be used as identifiers
TokenType.DESCRIBE,
TokenType.IT,
TokenType.EXPECT,
TokenType.ASSERT,
TokenType.MOCK,
TokenType.SPY,
TokenType.BEFORE,
TokenType.AFTER,
TokenType.BEFORE_EACH,
TokenType.AFTER_EACH,
// Add keywords that can be used as functions/variables
TokenType.CONTAINS,
TokenType.IN,
TokenType.LIKE,
TokenType.BETWEEN,
// Add logical operators as valid identifiers
TokenType.AND,
TokenType.OR,
TokenType.NOT,
TokenType.ALSO,
TokenType.EITHER,
// Add Promise-related keywords
TokenType.RESOLVE,
TokenType.REJECT,
TokenType.THEN,
TokenType.ALL,
TokenType.RACE,
TokenType.PARALLEL,
// Add decorator keywords that can be used as functions
TokenType.TIMEOUT,
TokenType.RETRY,
TokenType.MEMOIZE
];
for (const type of validIdentifierTypes) {
if (this.check(type)) {
return this.advance();
}
}
throw new ParseError(`${message} Got: ${this.peek().lexeme} at line ${this.peek().line}`, this.peek());
}
declaration() {
try {
// Testing declarations
if (this.match(TokenType.DESCRIBE)) return this.testSuiteDeclaration();
if (this.match(TokenType.IT)) return this.testCaseDeclaration();
// Async functions
if (this.match(TokenType.ASYNC)) {
if (this.check(TokenType.BUILD) || this.check(TokenType.FUNCTION)) {
return this.asyncFunctionDeclaration();
}
// If async is not followed by function, treat as identifier
this.current--;
return this.statement();
}
// Regular declarations
if (this.match(TokenType.BLUEPRINT, TokenType.CLASS)) return this.blueprintDeclaration();
if (this.match(TokenType.BUILD, TokenType.FUNCTION)) return this.functionDeclaration();
if (this.match(TokenType.STORE, TokenType.LET, TokenType.VAR, TokenType.CONST)) return this.variableDeclaration();
if (this.match(TokenType.INTERFACE)) return this.interfaceDeclaration();
if (this.match(TokenType.ENUM)) return this.enumDeclaration();
if (this.match(TokenType.MODULE)) return this.moduleDeclaration();
if (this.match(TokenType.IMPORT)) return this.importDeclaration();
if (this.match(TokenType.EXPORT)) return this.exportDeclaration();
return this.statement();
} catch (error) {
this.synchronize();
throw error;
}
}
// Testing-related parsing methods
testSuiteDeclaration() {
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'describe'.");
const name = this.expression();
this.consume(TokenType.COMMA, "Expected ',' after test suite name.");
const body = this.expression(); // This should be a function
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after test suite body.");
this.consume(TokenType.SEMICOLON, "Expected ';' after test suite declaration.");
return new TestSuiteNode(name, body);
}
testCaseDeclaration() {
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'it'.");
const name = this.expression();
this.consume(TokenType.COMMA, "Expected ',' after test case name.");
const body = this.expression(); // This should be a function
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after test case body.");
this.consume(TokenType.SEMICOLON, "Expected ';' after test case declaration.");
return new TestCaseNode(name, body);
}
asyncFunctionDeclaration() {
this.consumeOneOf([TokenType.BUILD, TokenType.FUNCTION], "Expected 'build' or 'function' after 'async'.");
const name = this.consumeIdentifier("Expected function name.");
this.consume(TokenType.LEFT_PAREN, "Expected '(' after function name.");
const parameters = [];
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
if (parameters.length >= 255) {
throw new ParseError("Cannot have more than 255 parameters.", this.peek());
}
parameters.push(this.consumeIdentifier("Expected parameter name."));
} while (this.match(TokenType.COMMA));
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters.");
this.consume(TokenType.LEFT_BRACE, "Expected '{' before function body.");
const body = this.block();
return new FunctionDeclarationNode(name, parameters, body, true);
}
interfaceDeclaration() {
const name = this.consumeIdentifier("Expected interface name.");
let extends_ = null;
if (this.match(TokenType.EXTENDS)) {
extends_ = this.consumeIdentifier("Expected parent interface name.");
}
this.consume(TokenType.LEFT_BRACE, "Expected '{' before interface body.");
const properties = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
const propName = this.consumeIdentifier("Expected property name.");
if (this.match(TokenType.COLON)) {
const propType = this.typeAnnotation();
properties.push({ name: propName, type: propType });
} else if (this.match(TokenType.LEFT_PAREN)) {
const params = [];
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
params.push(this.consumeIdentifier("Expected parameter name."));
} while (this.match(TokenType.COMMA));
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters.");
this.consume(TokenType.COLON, "Expected ':' after method signature.");
const returnType = this.typeAnnotation();
properties.push({
name: propName,
type: 'function',
params: params,
returnType: returnType
});
} else {
throw new ParseError("Expected ':' or '(' after property name.", this.peek());
}
this.match(TokenType.SEMICOLON);
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after interface body.");
return new InterfaceDeclarationNode(name, properties, extends_);
}
enumDeclaration() {
const name = this.consumeIdentifier("Expected enum name.");
this.consume(TokenType.LEFT_BRACE, "Expected '{' before enum body.");
const members = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
const memberName = this.consumeIdentifier("Expected enum member name.");
let value = null;
if (this.match(TokenType.EQUAL)) {
value = this.expression();
}
members.push({ name: memberName, value: value });
if (!this.check(TokenType.RIGHT_BRACE)) {
this.consume(TokenType.COMMA, "Expected ',' between enum members.");
}
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after enum body.");
return new EnumDeclarationNode(name, members);
}
moduleDeclaration() {
const name = this.consumeIdentifier("Expected module name.");
this.consume(TokenType.LEFT_BRACE, "Expected '{' before module body.");
const exports = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
exports.push(this.declaration());
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after module body.");
return new ModuleDeclarationNode(name, exports);
}
importDeclaration() {
const specifiers = [];
let isDefault = false;
if (this.checkIdentifier()) {
specifiers.push(this.advance());
isDefault = true;
} else if (this.match(TokenType.LEFT_BRACE)) {
do {
const name = this.consumeIdentifier("Expected import name.");
let alias = name;
if (this.match(TokenType.AS)) {
alias = this.consumeIdentifier("Expected alias name.");
}
specifiers.push({ name, alias });
} while (this.match(TokenType.COMMA));
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after import specifiers.");
}
this.consume(TokenType.FROM, "Expected 'from' after import specifiers.");
const source = this.consume(TokenType.STRING, "Expected module path.");
this.consume(TokenType.SEMICOLON, "Expected ';' after import statement.");
return new ImportDeclarationNode(specifiers, source, isDefault);
}
exportDeclaration() {
if (this.match(TokenType.DEFAULT)) {
const declaration = this.declaration();
return new ExportDeclarationNode(declaration, null, null, true);
} else if (this.match(TokenType.LEFT_BRACE)) {
const specifiers = [];
do {
const name = this.consumeIdentifier("Expected export name.");
let alias = name;
if (this.match(TokenType.AS)) {
alias = this.consumeIdentifier("Expected alias name.");
}
specifiers.push({ name, alias });
} while (this.match(TokenType.COMMA));
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after export specifiers.");
let source = null;
if (this.match(TokenType.FROM)) {
source = this.consume(TokenType.STRING, "Expected module path.");
}
this.consume(TokenType.SEMICOLON, "Expected ';' after export statement.");
return new ExportDeclarationNode(null, specifiers, source);
} else {
const declaration = this.declaration();
return new ExportDeclarationNode(declaration);
}
}
typeAnnotation() {
if (this.check(TokenType.LEFT_PAREN)) {
this.advance();
const params = [];
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
params.push(this.consumeIdentifier("Expected parameter type."));
} while (this.match(TokenType.COMMA));
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameter types.");
this.consume(TokenType.ARROW, "Expected '=>' in function type.");
const returnType = this.consumeOneOf([TokenType.IDENTIFIER, TokenType.VOID], "Expected return type.");
return { type: 'function', params, returnType };
}
return this.consumeOneOf([TokenType.IDENTIFIER, TokenType.VOID], "Expected type name.");
}
blueprintDeclaration() {
const name = this.consumeIdentifier("Expected blueprint name.");
let parentBlueprint = null;
if (this.match(TokenType.EXTENDS)) {
parentBlueprint = new IdentifierNode(this.consumeIdentifier("Expected parent blueprint name."));
}
this.consume(TokenType.LEFT_BRACE, "Expected '{' before blueprint body.");
const methods = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
let isAsync = false;
if (this.match(TokenType.ASYNC)) {
isAsync = true;
}
if (this.match(TokenType.BUILD, TokenType.FUNCTION)) {
// explicit function keyword consumed
}
const methodName = this.consumeIdentifier("Expected method name.");
this.consume(TokenType.LEFT_PAREN, "Expected '(' after method name.");
const parameters = [];
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
if (parameters.length >= 255) {
throw new ParseError("Cannot have more than 255 parameters.", this.peek());
}
parameters.push(this.consumeIdentifier("Expected parameter name."));
} while (this.match(TokenType.COMMA));
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters.");
this.consume(TokenType.LEFT_BRACE, "Expected '{' before method body.");
const body = this.block();
const method = new FunctionDeclarationNode(methodName, parameters, body, isAsync);
methods.push(method);
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after blueprint body.");
return new BlueprintDeclarationNode(name, parentBlueprint, methods);
}
functionDeclaration() {
const name = this.consumeIdentifier("Expected function name.");
this.consume(TokenType.LEFT_PAREN, "Expected '(' after function name.");
const parameters = [];
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
if (parameters.length >= 255) {
throw new ParseError("Cannot have more than 255 parameters.", this.peek());
}
parameters.push(this.consumeIdentifier("Expected parameter name."));
} while (this.match(TokenType.COMMA));
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters.");
this.consume(TokenType.LEFT_BRACE, "Expected '{' before function body.");
const body = this.block();
return new FunctionDeclarationNode(name, parameters, body);
}
variableDeclaration() {
const name = this.consumeIdentifier("Expected variable name.");
let initializer = null;
if (this.match(TokenType.EQUAL)) {
initializer = this.expression();
}
this.consume(TokenType.SEMICOLON, "Expected ';' after variable declaration.");
return new VariableDeclarationNode(name, initializer);
}
statement() {
if (this.match(TokenType.WHEN, TokenType.IF)) return this.ifStatement();
if (this.match(TokenType.UNLESS)) return this.unlessStatement();
if (this.match(TokenType.SHOW)) return this.printStatement();
if (this.match(TokenType.CONSOLE)) return this.consoleStatement();
if (this.match(TokenType.GIVE, TokenType.RETURN)) return this.returnStatement();
if (this.match(TokenType.REPEAT, TokenType.AGAIN, TokenType.WHILE)) return this.whileStatement();
if (this.match(TokenType.UNTIL)) return this.untilStatement();
if (this.match(TokenType.LOOP, TokenType.FOR)) return this.forStatement();
if (this.match(TokenType.FOREACH)) return this.forEachStatement();
if (this.match(TokenType.SWITCH)) return this.switchStatement();
if (this.match(TokenType.BREAK)) return this.breakStatement();
if (this.match(TokenType.CONTINUE)) return this.continueStatement();
if (this.match(TokenType.ATTEMPT, TokenType.TRY)) return this.tryStatement();
if (this.match(TokenType.THROW)) return this.throwStatement();
if (this.match(TokenType.LEFT_BRACE)) return new BlockStatementNode(this.block());
return this.expressionStatement();
}
consoleStatement() {
this.consume(TokenType.DOT, "Expected '.' after 'console'.");
this.consume(TokenType.LOG, "Expected 'log' after 'console.'.");
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'console.log'.");
const value = this.expression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after console.log argument.");
this.consume(TokenType.SEMICOLON, "Expected ';' after console.log.");
return new PrintStatementNode(value);
}
unlessStatement() {
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'unless'.");
const condition = this.expression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after condition.");
const thenBranch = this.statement();
let elseBranch = null;
if (this.match(TokenType.OTHERWISE, TokenType.ELSE)) {
elseBranch = this.statement();
}
return new UnlessStatementNode(condition, thenBranch, elseBranch);
}
forEachStatement() {
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'foreach'.");
const variable = this.consumeIdentifier("Expected variable name.");
this.consume(TokenType.IN, "Expected 'in' after variable.");
const iterable = this.expression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after foreach clauses.");
const body = this.statement();
return new ForEachStatementNode(variable, iterable, body);
}
switchStatement() {
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'switch'.");
const discriminant = this.expression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after switch discriminant.");
this.consume(TokenType.LEFT_BRACE, "Expected '{' before switch body.");
const cases = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
if (this.match(TokenType.CASE)) {
const test = this.expression();
this.consume(TokenType.COLON, "Expected ':' after case value.");
const consequent = [];
while (!this.check(TokenType.CASE) && !this.check(TokenType.DEFAULT) && !this.check(TokenType.RIGHT_BRACE)) {
consequent.push(this.statement());
}
cases.push(new CaseClauseNode(test, consequent));
} else if (this.match(TokenType.DEFAULT)) {
this.consume(TokenType.COLON, "Expected ':' after 'default'.");
const consequent = [];
while (!this.check(TokenType.CASE) && !this.check(TokenType.RIGHT_BRACE)) {
consequent.push(this.statement());
}
cases.push(new CaseClauseNode(null, consequent, true));
} else {
throw new ParseError("Expected 'case' or 'default' in switch statement.", this.peek());
}
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after switch body.");
return new SwitchStatementNode(discriminant, cases);
}
untilStatement() {
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'until'.");
const condition = this.expression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after condition.");
const body = this.statement();
return new WhileStatementNode(condition, body, true);
}
ifStatement() {
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'when' or 'if'.");
const condition = this.expression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after condition.");
const thenBranch = this.statement();
let elseBranch = null;
if (this.match(TokenType.OTHERWISE, TokenType.ELSE)) {
elseBranch = this.statement();
}
return new IfStatementNode(condition, thenBranch, elseBranch);
}
printStatement() {
const value = this.expression();
this.consume(TokenType.SEMICOLON, "Expected ';' after value.");
return new PrintStatementNode(value);
}
returnStatement() {
const keyword = this.previous();
let value = null;
if (!this.check(TokenType.SEMICOLON)) {
value = this.expression();
}
this.consume(TokenType.SEMICOLON, "Expected ';' after return value.");
return new ReturnStatementNode(keyword, value);
}
whileStatement() {
this.consume(TokenType.LEFT_PAREN, "Expected '(' after while keyword.");
const condition = this.expression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after condition.");
const body = this.statement();
return new WhileStatementNode(condition, body);
}
forStatement() {
this.consume(TokenType.LEFT_PAREN, "Expected '(' after 'loop' or 'for'.");
let initializer;
if (this.match(TokenType.SEMICOLON)) {
initializer = null;
} else if (this.match(TokenType.STORE, TokenType.LET, TokenType.VAR, TokenType.CONST)) {
const varName = this.consumeIdentifier("Expected variable name.");
let varInitializer = null;
if (this.match(TokenType.EQUAL)) {
varInitializer = this.expression();
}
initializer = new VariableDeclarationNode(varName, varInitializer);
} else {
const expr = this.expression();
initializer = new ExpressionStatementNode(expr);
}
this.consume(TokenType.SEMICOLON, "Expected ';' after loop initializer.");
let condition = null;
if (!this.check(TokenType.SEMICOLON)) {
condition = this.expression();
}
this.consume(TokenType.SEMICOLON, "Expected ';' after loop condition.");
let increment = null;
if (!this.check(TokenType.RIGHT_PAREN)) {
increment = this.expression();
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after loop clauses.");
const body = this.statement();
return new ForStatementNode(initializer, condition, increment, body);
}
breakStatement() {
this.consume(TokenType.SEMICOLON, "Expected ';' after 'break'.");
return new BreakStatementNode();
}
continueStatement() {
this.consume(TokenType.SEMICOLON, "Expected ';' after 'continue'.");
return new ContinueStatementNode();
}
throwStatement() {
const expression = this.expression();
this.consume(TokenType.SEMICOLON, "Expected ';' after throw expression.");
return new ThrowStatementNode(expression);
}
tryStatement() {
const tryBlock = this.statement();
let catchBlock = null;
let catchVariable = null;
if (this.match(TokenType.CATCH)) {
if (this.match(TokenType.LEFT_PAREN)) {
catchVariable = this.consumeIdentifier("Expected variable name in catch.");
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after catch variable.");
}
catchBlock = this.statement();
}
let finallyBlock = null;
if (this.match(TokenType.FINALLY)) {
finallyBlock = this.statement();
}
if (catchBlock === null && finallyBlock === null) {
throw new ParseError("Expected 'catch' or 'finally' after 'attempt' or 'try'.", this.peek());
}
return new TryStatementNode(tryBlock, catchBlock, catchVariable, finallyBlock);
}
block() {
const statements = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
const stmt = this.declaration();
if (stmt) statements.push(stmt);
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after block.");
return statements;
}
expression() {
return this.ternary();
}
ternary() {
let expr = this.assignment();
if (this.match(TokenType.QUESTION)) {
const thenBranch = this.expression();
this.consume(TokenType.COLON, "Expected ':' after '?' in ternary expression.");
const elseBranch = this.ternary();
return new ConditionalExpressionNode(expr, thenBranch, elseBranch);
}
return expr;
}
assignment() {
const expr = this.or();
if (this.match(TokenType.EQUAL)) {
const value = this.assignment();
if (expr instanceof IdentifierNode) {
return new AssignmentExpressionNode(expr.name, value);
} else if (expr instanceof MemberExpressionNode) {
return new AssignmentExpressionNode(expr, value);
}
throw new ParseError("Invalid assignment target.", this.previous());
}
return expr;
}
or() {
let expr = this.and();
while (this.match(TokenType.EITHER, TokenType.OR, TokenType.OR_OR)) {
const operator = this.previous();
const right = this.and();
expr = new BinaryExpressionNode(expr, operator, right);
}
return expr;
}
and() {
let expr = this.equality();
while (this.match(TokenType.ALSO, TokenType.AND, TokenType.AND_AND)) {
const operator = this.previous();
const right = this.equality();
expr = new BinaryExpressionNode(expr, operator, right);
}
return expr;
}
equality() {
let expr = this.comparison();
while (this.match(TokenType.BANG_EQUAL, TokenType.EQUAL_EQUAL, TokenType.IS, TokenType.ISNT)) {
const operator = this.previous();
const right = this.comparison();
expr = new BinaryExpressionNode(expr, operator, right);
}
return expr;
}
comparison() {
let expr = this.term();
while (this.match(
TokenType.GREATER,
TokenType.GREATER_EQUAL,
TokenType.LESS,
TokenType.LESS_EQUAL,
TokenType.BETWEEN,
TokenType.CONTAINS,
TokenType.IN,
TokenType.LIKE
)) {
const operator = this.previous();
// Handle special three-operand 'between' operator
if (operator.type === TokenType.BETWEEN) {
const middle = this.term();
this.consume(TokenType.AND, "Expected 'and' after first operand in 'between' expression.");
const right = this.term();
// Create a compound expression: expr >= middle && expr <= right
const leftComparison = new BinaryExpressionNode(
expr,
{ type: TokenType.GREATER_EQUAL, lexeme: '>=' },
middle
);
const rightComparison = new BinaryExpressionNode(
expr,
{ type: TokenType.LESS_EQUAL, lexeme: '<=' },
right
);
expr = new BinaryExpressionNode(
leftComparison,
{ type: TokenType.AND_AND, lexeme: '&&' },
rightComparison
);
} else {
const right = this.term();
expr = new BinaryExpressionNode(expr, operator, right);
}
}
return expr;
}
term() {
let expr = this.factor();
while (this.match(TokenType.MINUS, TokenType.PLUS)) {
const operator = this.previous();
const right = this.factor();
expr = new BinaryExpressionNode(expr, operator, right);
}
return expr;
}
factor() {
let expr = this.unary();
while (this.match(TokenType.SLASH, TokenType.STAR, TokenType.PERCENT)) {
const operator = this.previous();
const right = this.unary();
expr = new BinaryExpressionNode(expr, operator, right);
}
return expr;
}
unary() {
if (this.match(TokenType.BANG, TokenType.MINUS, TokenType.NOT)) {
const operator = this.previous();
const right = this.unary();
return new UnaryExpressionNode(operator, right);
}
if (this.match(TokenType.AWAIT)) {
const expression = this.unary();
return new AwaitExpressionNode(expression);
}
return this.call();
}
call() {
let expr = this.primary();
while (true) {
if (this.match(TokenType.LEFT_PAREN)) {
expr = this.finishCall(expr);
} else if (this.match(TokenType.DOT)) {
const name = this.advance();
expr = new MemberExpressionNode(expr, name, false);
} else if (this.match(TokenType.LEFT_BRACKET)) {
const index = this.expression();
this.consume(TokenType.RIGHT_BRACKET, "Expected ']' after index.");
expr = new MemberExpressionNode(expr, index, true);
} else {
break;
}
}
return expr;
}
finishCall(callee) {
const args = [];
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
if (args.length >= 255) {
throw new ParseError("Cannot have more than 255 arguments.", this.peek());
}
// Parse argument - improved to handle all function types
args.push(this.parseArgument());
} while (this.match(TokenType.COMMA));
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after arguments.");
return new CallExpressionNode(callee, args);
}
parseArgument() {
// Check for async function expressions in arguments
if (this.check(TokenType.ASYNC)) {
const asyncPos = this.current;
this.advance(); // consume 'async'
if (this.check(TokenType.BUILD) || this.check(TokenType.FUNCTION)) {
// This is an async function expression
this.current = asyncPos; // go back
return this.parseAsyncFunction();
} else {
// Not an async function, go back and parse as normal expression
this.current = asyncPos;
return this.expression();
}
} else if (this.check(TokenType.BUILD) || this.check(TokenType.FUNCTION)) {
// Regular function expression - check if it has parentheses after
const functionPos = this.current;
this.advance(); // consume 'build' or 'function'
if (this.check(TokenType.LEFT_PAREN)) {
// This is a function with parameters
this.current = functionPos; // go back
return this.parseAnonymousFunction(false);
} else {
// This might be just an identifier
this.current = functionPos; // go back
return this.expression();
}
} else {
// Regular expression
return this.expression();
}
}
parseAsyncFunction() {
this.consume(TokenType.ASYNC, "Expected 'async'.");
this.consumeOneOf([TokenType.BUILD, TokenType.FUNCTION], "Expected 'build' or 'function' after 'async'.");
this.consume(TokenType.LEFT_PAREN, "Expected '(' after function keyword.");
const parameters = [];
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
if (parameters.length >= 255) {
throw new ParseError("Cannot have more than 255 parameters.", this.peek());
}
parameters.push(this.consumeIdentifier("Expected parameter name."));
} while (this.match(TokenType.COMMA));
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters.");
this.consume(TokenType.LEFT_BRACE, "Expected '{' before function body.");
const body = this.block();
// Create an anonymous function node
const anonName = { lexeme: '<anonymous>', type: TokenType.IDENTIFIER };
return new FunctionDeclarationNode(anonName, parameters, body, true);
}
// Helper method to check if current token can be used as identifier
checkIdentifier() {
const validIdentifierTypes = [
TokenType.IDENTIFIER,
TokenType.SERVER,
TokenType.GET,
TokenType.POST,
TokenType.PUT,
TokenType.DELETE,
TokenType.USE,
TokenType.LISTEN,
TokenType.PROMISE,
TokenType.ASYNC,
TokenType.JSON,
TokenType.CORS,
TokenType.LOGGER,
TokenType.MIDDLEWARE,
TokenType.ROUTE,
TokenType.CREATE_SERVER,
// Testing keywords
TokenType.DESCRIBE,
TokenType.IT,
TokenType.EXPECT,
TokenType.ASSERT,
TokenType.MOCK,
TokenType.SPY,
TokenType.BEFORE,
TokenType.AFTER,
TokenType.BEFORE_EACH,
TokenType.AFTER_EACH,
// Add keywords that can be used as functions
TokenType.CONTAINS,
TokenType.IN,
TokenType.LIKE,
TokenType.BETWEEN,
// Add logical operators as valid identifiers
TokenType.AND,
TokenType.OR,
TokenType.NOT,
TokenType.ALSO,
TokenType.EITHER,
// Add Promise-related keywords
TokenType.RESOLVE,
TokenType.REJECT,
TokenType.THEN,
TokenType.ALL,
TokenType.RACE,
TokenType.PARALLEL,
// Add decorator keywords that can be used as functions
TokenType.TIMEOUT,
TokenType.RETRY,
TokenType.MEMOIZE
];
return validIdentifierTypes.includes(this.peek().type);
}
primary() {
if (this.match(TokenType.YES, TokenType.TRUE)) return new LiteralNode(true);
if (this.match(TokenType.NO, TokenType.FALSE)) return new LiteralNode(false);
if (this.match(TokenType.EMPTY, TokenType.NULL, TokenType.UNDEFINED)) return new LiteralNode(null);
if (this.match(TokenType.NUMBER, TokenType.STRING)) {
return new LiteralNode(this.previous().literal);
}
if (this.match(TokenType.TEMPLATE_STRING)) {
return this.templateString();
}
if (this.match(TokenType.SELF, TokenType.THIS)) return new IdentifierNode(this.previous());
if (this.match(TokenType.PARENT)) return new IdentifierNode(this.previous());
// Handle async anonymous functions - improved handling
if (this.check(TokenType.ASYNC)) {
const asyncPos = this.current;
this.advance(); // consume 'async'
if (this.check(TokenType.BUILD) || this.check(TokenType.FUNCTION)) {
this.current = asyncPos; // go back
return this.parseAsyncFunction();
} else {
// It's just an identifier named 'async'
this.current = asyncPos;
return new IdentifierNode(this.advance());
}
}
// Handle anonymous functions (build/function without name) - IMPROVED
if (this.check(TokenType.BUILD) || this.check(TokenType.FUNCTION)) {
const functionPos = this.current;
this.advance(); // consume 'build' or 'function'
if (this.check(TokenType.LEFT_PAREN)) {
// This is definitely a function declaration
this.current = functionPos; // go back
return this.parseAnonymousFunction(false);
} else {
// This is just an identifier
this.current = functionPos; // go back
return new IdentifierNode(this.advance());
}
}
// Handle identifiers (including keywords that can be identifiers)
if (this.checkIdentifier()) {
const identifier = this.advance();
return new IdentifierNode(identifier);
}
if (this.match(TokenType.LEFT_PAREN)) {
// Check if this might be an arrow function
const parenPos = this.current - 1; // position of LEFT_PAREN
// Try to parse as arrow function first
if (this.isArrowFunction()) {
this.current = parenPos; // go back to LEFT_PAREN
return this.parseArrowFunction();
}
// Parse as regular grouped expression
const expr = this.expression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after expression.");
return expr;
}
if (this.match(TokenType.LEFT_BRACKET)) {
const elements = [];
if (!this.check(TokenType.RIGHT_BRACKET)) {
do {
elements.push(this.expression());
} while (this.match(TokenType.COMMA));
}
this.consume(TokenType.RIGHT_BRACKET, "Expected ']' after array elements.");
return new ArrayExpressionNode(elements);
}
if (this.match(TokenType.LEFT_BRACE)) {
return this.parseObject();
}
throw new ParseError(`Unexpected token: ${this.peek().lexeme}`, this.peek());
}
// Check if current position is an arrow function
isArrowFunction() {
const currentPos = this.current;
let hasParams = false;
try {
// Skip parameter list
if (this.check(TokenType.RIGHT_PAREN)) {
// Empty parameter list ()
this.advance();
} else {
// Parse parameters
do {
if (this.checkIdentifier()) {
this.advance();
hasParams = true;
} else {
// Not valid parameter, not an arrow function
this.current = currentPos;
return false;
}
} while (this.match(TokenType.COMMA));
if (!this.match(TokenType.RIGHT_PAREN)) {
// No closing paren, not an arrow function
this.current = currentPos;
return false;
}
}
// Check for arrow
const isArrow = this.match(TokenType.ARROW);
this.current = currentPos;
return isArrow;
} catch (error) {
this.current = currentPos;
return false;
}
}
// Parse arrow function
parseArrowFunction() {
this.consume(TokenType.LEFT_PAREN, "Expected '(' for arrow function parameters.");
const parameters = [];
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
if (parameters.length >= 255) {
throw new ParseError("Cannot have more than 255 parameters.", this.peek());
}
parameters.push(this.consumeIdentifier("Expected parameter name."));
} while (this.match(TokenType.COMMA));
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after arrow function parameters.");
this.consume(TokenType.ARROW, "Expected '=>' in arrow function.");
// Parse body - can be expression or block
let body;
if (this.check(TokenType.LEFT_BRACE)) {
// Block body
this.advance(); // consume '{'
body = this.block();
} else {
// Expression body - wrap in return statement
const expr = this.expression();
body = [new ReturnStatementNode(null, expr)];
}
return new ArrowFunctionNode(parameters, body);
}
parseObject() {
const properties = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
// Handle spread syntax in objects
if (this.match(TokenType.DOT_DOT_DOT)) {
const spreadExpr = this.expression();
properties.push({ spread: true, value: spreadExpr });
} else {
// Parse property key
let key;
if (this.checkIdentifier()) {
key = this.advance();
} else if (this.check(TokenType.STRING)) {
key = this.advance();
} else {
throw new ParseError("Expected property name.", this.peek());
}
this.consume(TokenType.COLON, "Expected ':' after property name.");
// CORREÇÃO: Better property value parsing
let value;
try {
value = this.parsePropertyValue();
} catch (error) {
// Fallback to expression parsing if property value parsing fails
value = this.expression();
}
properties.push({ key, value, spread: false });
}
// CORREÇÃO: Better comma handling
if (this.check(TokenType.RIGHT_BRACE)) {
break; // End of object
}
if (!this.match(TokenType.COMMA)) {
// If no comma and not end of object, it's an error
if (!this.check(TokenType.RIGHT_BRACE)) {
throw new ParseError("Expected ',' or '}' after object property.", this.peek());
}
}
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after object properties.");
return new ObjectExpressionNode(properties);
}
parsePropertyValue() {
// Check if it's a function property (async build/build/function)
if (this.check(TokenType.ASYNC)) {
const asyncPos = this.current;
this.advance(); // consume 'async'
if (this.check(TokenType.BUILD) || this.check(TokenType.FUNCTION)) {
this.current = asyncPos; // go back
return this.parseAsyncFunction();
} else {
// Not an async function, go back and parse as normal expression
this.current = asyncPos;
return this.expression();
}
} else if (this.check(TokenType.BUILD) || this.check(TokenType.FUNCTION)) {
// CORREÇÃO: Parse function property directly
return this.parseAnonymousFunction(false);
} else {
return this.expression();
}
}
// Parse anonymous functions (functions without names)
parseAnonymousFunction(isAsync) {
// Consume function keyword
this.consumeOneOf([TokenType.BUILD, TokenType.FUNCTION], "Expected 'build' or 'function'.");
this.consume(TokenType.LEFT_PAREN, "Expected '(' after function keyword.");
const parameters = [];
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
if (parameters.length >= 255) {
throw new ParseError("Cannot have more than 255 parameters.", this.peek());
}
parameters.push(this.consumeIdentifier("Expected parameter name."));
} while (this.match(TokenType.COMMA));
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters.");
this.consume(TokenType.LEFT_BRACE, "Expected '{' before function body.");
const body = this.block();
// CORREÇÃO: Create proper anonymous function node
const anonName = { lexeme: '<anonymous>', type: 'IDENTIFIER' };
return new FunctionDeclarationNode(anonName, parameters, body, isAsync);
}
templateString() {
const quasis = [];
const expressions = [];
while (this.check(TokenType.TEMPLATE_STRING) || this.check(TokenType.TEMPLATE_EXPRESSION)) {
if (this.match(TokenType.TEMPLATE_STRING)) {
quasis.push(this.previous().literal);
}
if (this.match(TokenType.TEMPLATE_EXPRESSION)) {
const exprTokens = this.previous().literal;
const exprParser = new Parser(exprTokens);
const expr = exprParser.expression();
expressions.push(expr);
}
}
this.consume(TokenType.TEMPLATE_END, "Expected end of template string.");
return new TemplateStringNode(quasis, expressions);
}
expressionStatement() {
const expr = this.expression();
this.consume(TokenType.SEMICOLON, "Expected ';' after expression.");
return new ExpressionStatementNode(expr);
}
match(...types) {
for (const type of types) {
if (this.check(type)) {
this.advance();
return true;
}
}
return false;
}
check(type) {
if (this.isAtEnd()) return false;
return this.peek().type === type;
}
advance() {
if (!this.isAtEnd()) this.current++;
return this.previous();
}
isAtEnd() {
return this.peek().type === TokenType.EOF;
}
peek() {
return this.tokens[this.current];
}
peekNext() {
if (this.current + 1 >= this.tokens.length) return null;
return this.tokens[this.current + 1];
}
previous() {
return this.tokens[this.current - 1];
}
consume(type, message) {
if (this.check(type)) return this.advance();
throw new ParseError(`${message} Got: ${this.peek().lexeme} at line ${this.peek().line}`, this.peek());
}
consumeOneOf(types, message) {
for (const type of types) {
if (this.check(type)) return this.advance();
}
throw new ParseError(`${message} Got: ${this.peek().lexeme} at line ${this.peek().line}`, this.peek());
}
synchronize() {
this.advance();
while (!this.isAtEnd()) {
if (this.previous().type === TokenType.SEMICOLON) return;
switch (this.peek().type) {
case TokenType.BLUEPRINT:
case TokenType.CLASS:
case TokenType.BUILD:
case TokenType.FUNCTION:
case TokenType.STORE:
case TokenType.LET:
case TokenType.VAR:
case TokenType.CONST:
case TokenType.LOOP:
case TokenType.FOR:
case TokenType.WHEN:
case TokenType.IF:
case TokenType.REPEAT:
case TokenType.WHILE:
case TokenType.AGAIN:
case TokenType.SHOW:
case TokenType.GIVE:
case TokenType.RETURN:
case TokenType.THROW:
case TokenType.DESCRIBE:
case TokenType.IT:
return;
}
this.advance();
}
}
}
module.exports = Parser;