@ordojs/core
Version:
Core compiler and runtime for OrdoJS framework
745 lines (744 loc) • 26.7 kB
JavaScript
/**
* @fileoverview OrdoJS Parser - Full implementation for syntax analysis
*/
import { ExpressionType, LifecycleType, StatementType, SyntaxError, TokenType } from '../types/index.js';
export class OrdoJSParser {
constructor(tokenStream, options = {}, filename = 'unknown') {
this.current = 0;
this.errors = [];
this.tokenStream = tokenStream;
this.options = {
allowRecovery: false,
maxErrors: 1,
...options
};
this.filename = filename;
}
parse() {
try {
const component = this.parseComponent();
if (this.errors.length > 0) {
throw this.errors[0];
}
return {
component,
dependencies: [],
exports: [],
sourceMap: {
version: 3,
sources: [this.filename],
names: [],
mappings: '',
sourcesContent: []
}
};
}
catch (error) {
if (error instanceof SyntaxError) {
throw error;
}
throw new SyntaxError(`Parser error: ${error instanceof Error ? error.message : 'Unknown error'}`, this.getCurrentPosition(), ['valid component syntax'], this.peek().value, this.filename);
}
}
getErrors() {
return this.errors;
}
parseComponent() {
const start = this.getCurrentPosition();
// Expect 'component' keyword
if (!this.match(TokenType.COMPONENT)) {
this.throwError("Expected 'component' keyword", ['component']);
}
// Parse component name
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected component name');
const name = nameToken.value;
// Validate component name (must start with uppercase)
if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
this.throwError('Component name must start with uppercase letter and contain only alphanumeric characters', []);
}
// Parse optional props
const props = this.parseProps();
// Expect component body
this.consume(TokenType.LEFT_BRACE, "Expected '{' after component declaration");
// Parse component blocks
let clientBlock;
let serverBlock;
let markupBlock;
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
if (this.match(TokenType.CLIENT)) {
if (clientBlock) {
this.throwError('Duplicate client block', []);
}
clientBlock = this.parseClientBlock();
}
else if (this.match(TokenType.SERVER)) {
if (serverBlock) {
this.throwError('Duplicate server block', []);
}
serverBlock = this.parseServerBlock();
}
else if (this.match(TokenType.MARKUP)) {
if (markupBlock) {
this.throwError('Duplicate markup block', []);
}
markupBlock = this.parseMarkupBlock();
}
else {
this.throwError('Expected client, server, or markup block', ['client', 'server', 'markup']);
}
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after component body");
// Markup block is required
if (!markupBlock) {
this.throwError('Component must have a markup block', ['markup']);
}
const end = this.getCurrentPosition();
return {
type: 'Component',
name,
props,
clientBlock,
serverBlock,
markupBlock: markupBlock,
range: { start, end }
};
}
parseProps() {
const props = [];
if (!this.check(TokenType.LEFT_PAREN)) {
return props;
}
this.advance(); // consume '('
while (!this.check(TokenType.RIGHT_PAREN) && !this.isAtEnd()) {
props.push(this.parseProp());
if (!this.match(TokenType.COMMA)) {
break;
}
}
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after props");
return props;
}
parseProp() {
const start = this.getCurrentPosition();
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected prop name');
this.consume(TokenType.COLON, "Expected ':' after prop name");
const dataType = this.parseTypeAnnotation();
let defaultValue;
let isRequired = true;
if (this.match(TokenType.ASSIGN)) {
isRequired = false;
defaultValue = this.parseExpression();
}
const end = this.getCurrentPosition();
return {
type: 'PropDefinition',
name: nameToken.value,
dataType,
defaultValue,
isRequired,
range: { start, end }
};
}
parseTypeAnnotation() {
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected type name');
let isArray = false;
if (this.match(TokenType.LEFT_BRACKET)) {
this.consume(TokenType.RIGHT_BRACKET, "Expected ']' after '['");
isArray = true;
}
return {
name: nameToken.value,
isArray,
isOptional: false,
genericTypes: []
};
}
parseClientBlock() {
const start = this.getCurrentPosition();
this.consume(TokenType.LEFT_BRACE, "Expected '{' after 'client'");
const reactiveVariables = [];
const computedValues = [];
const eventHandlers = [];
const functions = [];
const lifecycle = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
if (this.check(TokenType.LET) || this.check(TokenType.CONST)) {
reactiveVariables.push(this.parseReactiveVariable());
}
else if (this.checkLifecycleHook()) {
lifecycle.push(this.parseLifecycleHook());
}
else if (this.check(TokenType.IDENTIFIER)) {
functions.push(this.parseFunction());
}
else {
this.throwError('Expected variable declaration, function, or lifecycle hook', ['let', 'const', 'function']);
}
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after client block");
const end = this.getCurrentPosition();
return {
type: 'ClientBlock',
reactiveVariables,
computedValues,
eventHandlers,
functions,
lifecycle,
range: { start, end }
};
}
parseServerBlock() {
const start = this.getCurrentPosition();
this.consume(TokenType.LEFT_BRACE, "Expected '{' after 'server'");
const functions = [];
const middleware = [];
const dataFetchers = [];
const imports = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
if (this.check(TokenType.PUBLIC) || this.check(TokenType.IDENTIFIER)) {
functions.push(this.parseServerFunction());
}
else {
this.throwError('Expected function declaration', ['public', 'function']);
}
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after server block");
const end = this.getCurrentPosition();
return {
type: 'ServerBlock',
functions,
middleware,
dataFetchers,
imports,
range: { start, end }
};
}
parseMarkupBlock() {
const start = this.getCurrentPosition();
this.consume(TokenType.LEFT_BRACE, "Expected '{' after 'markup'");
const elements = [];
const textNodes = [];
const interpolations = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
if (this.check(TokenType.HTML_TAG_OPEN)) {
elements.push(this.parseHTMLElement());
}
else if (this.check(TokenType.INTERPOLATION_START)) {
interpolations.push(this.parseInterpolation());
}
else if (this.check(TokenType.HTML_TEXT)) {
textNodes.push(this.parseTextNode());
}
else {
// Skip whitespace and try to parse HTML
if (this.peek().value.trim()) {
this.advance();
}
else {
this.advance();
}
}
}
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after markup block");
const end = this.getCurrentPosition();
return {
type: 'MarkupBlock',
elements,
textNodes,
interpolations,
range: { start, end }
};
}
parseReactiveVariable() {
const start = this.getCurrentPosition();
const isConst = this.match(TokenType.CONST);
if (!isConst) {
this.consume(TokenType.LET, "Expected 'let' or 'const'");
}
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected variable name');
let dataType = { name: 'any', isArray: false, isOptional: false, genericTypes: [] };
if (this.match(TokenType.COLON)) {
dataType = this.parseTypeAnnotation();
}
this.consume(TokenType.ASSIGN, "Expected '=' after variable declaration");
const initialValue = this.parseExpression();
this.match(TokenType.SEMICOLON); // Optional semicolon
const end = this.getCurrentPosition();
return {
type: 'ReactiveVariable',
name: nameToken.value,
initialValue,
dataType,
isConst,
range: { start, end }
};
}
parseFunction() {
const start = this.getCurrentPosition();
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected function name');
this.consume(TokenType.LEFT_PAREN, "Expected '(' after function name");
const parameters = this.parseParameters();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters");
this.consume(TokenType.COLON, "Expected ':' after parameters");
const returnType = this.parseTypeAnnotation();
this.consume(TokenType.LEFT_BRACE, "Expected '{' after function signature");
const body = this.parseStatements();
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after function body");
const end = this.getCurrentPosition();
return {
type: 'Function',
name: nameToken.value,
parameters,
body,
returnType,
isAsync: false,
range: { start, end }
};
}
parseServerFunction() {
const start = this.getCurrentPosition();
const isPublic = this.match(TokenType.PUBLIC);
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected function name');
this.consume(TokenType.LEFT_PAREN, "Expected '(' after function name");
const parameters = this.parseParameters();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after parameters");
this.consume(TokenType.COLON, "Expected ':' after parameters");
const returnType = this.parseTypeAnnotation();
this.consume(TokenType.LEFT_BRACE, "Expected '{' after function signature");
const body = this.parseStatements();
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after function body");
const end = this.getCurrentPosition();
return {
type: 'ServerFunction',
name: nameToken.value,
parameters,
body,
returnType,
isAsync: false,
isPublic,
middleware: [],
permissions: [],
range: { start, end }
};
}
parseLifecycleHook() {
const start = this.getCurrentPosition();
const hookName = this.advance().value;
let hookType;
switch (hookName) {
case 'onMount':
hookType = LifecycleType.ON_MOUNT;
break;
case 'onUnmount':
hookType = LifecycleType.ON_UNMOUNT;
break;
case 'onUpdate':
hookType = LifecycleType.ON_UPDATE;
break;
default:
this.throwError(`Unknown lifecycle hook: ${hookName}`, ['onMount', 'onUnmount', 'onUpdate']);
hookType = LifecycleType.ON_MOUNT;
}
this.consume(TokenType.LEFT_PAREN, "Expected '(' after lifecycle hook");
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after lifecycle hook");
this.consume(TokenType.LEFT_BRACE, "Expected '{' after lifecycle hook");
const handler = this.parseStatements();
this.consume(TokenType.RIGHT_BRACE, "Expected '}' after lifecycle hook body");
const end = this.getCurrentPosition();
return {
type: 'LifecycleHook',
hookType,
handler,
range: { start, end }
};
}
parseParameters() {
const parameters = [];
while (!this.check(TokenType.RIGHT_PAREN) && !this.isAtEnd()) {
parameters.push(this.parseParameter());
if (!this.match(TokenType.COMMA)) {
break;
}
}
return parameters;
}
parseParameter() {
const start = this.getCurrentPosition();
const nameToken = this.consume(TokenType.IDENTIFIER, 'Expected parameter name');
this.consume(TokenType.COLON, "Expected ':' after parameter name");
const dataType = this.parseTypeAnnotation();
let defaultValue;
let isOptional = false;
if (this.match(TokenType.ASSIGN)) {
isOptional = true;
defaultValue = this.parseExpression();
}
const end = this.getCurrentPosition();
return {
type: 'Parameter',
name: nameToken.value,
dataType,
defaultValue,
isOptional,
range: { start, end }
};
}
parseStatements() {
const statements = [];
while (!this.check(TokenType.RIGHT_BRACE) && !this.isAtEnd()) {
statements.push(this.parseStatement());
}
return statements;
}
parseStatement() {
const start = this.getCurrentPosition();
// For now, treat everything as expression statements
const expression = this.parseExpression();
this.match(TokenType.SEMICOLON); // Optional semicolon
const end = this.getCurrentPosition();
return {
type: 'Statement',
statementType: StatementType.EXPRESSION,
expression,
range: { start, end }
};
}
parseExpression() {
return this.parseAssignment();
}
parseAssignment() {
const expr = this.parseLogicalOr();
if (this.match(TokenType.ASSIGN)) {
const operator = this.previous().value;
const right = this.parseAssignment();
return {
type: 'Expression',
expressionType: ExpressionType.ASSIGNMENT,
operator,
left: expr,
right,
range: { start: expr.range.start, end: right.range.end }
};
}
return expr;
}
parseLogicalOr() {
let expr = this.parseLogicalAnd();
while (this.match(TokenType.LOGICAL_OR)) {
const operator = this.previous().value;
const right = this.parseLogicalAnd();
expr = {
type: 'Expression',
expressionType: ExpressionType.BINARY,
operator,
left: expr,
right,
range: { start: expr.range.start, end: right.range.end }
};
}
return expr;
}
parseLogicalAnd() {
let expr = this.parseEquality();
while (this.match(TokenType.LOGICAL_AND)) {
const operator = this.previous().value;
const right = this.parseEquality();
expr = {
type: 'Expression',
expressionType: ExpressionType.BINARY,
operator,
left: expr,
right,
range: { start: expr.range.start, end: right.range.end }
};
}
return expr;
}
parseEquality() {
let expr = this.parseComparison();
while (this.match(TokenType.EQUALS, TokenType.NOT_EQUALS)) {
const operator = this.previous().value;
const right = this.parseComparison();
expr = {
type: 'Expression',
expressionType: ExpressionType.BINARY,
operator,
left: expr,
right,
range: { start: expr.range.start, end: right.range.end }
};
}
return expr;
}
parseComparison() {
let expr = this.parseTerm();
while (this.match(TokenType.GREATER_THAN, TokenType.GREATER_EQUAL, TokenType.LESS_THAN, TokenType.LESS_EQUAL)) {
const operator = this.previous().value;
const right = this.parseTerm();
expr = {
type: 'Expression',
expressionType: ExpressionType.BINARY,
operator,
left: expr,
right,
range: { start: expr.range.start, end: right.range.end }
};
}
return expr;
}
parseTerm() {
let expr = this.parseFactor();
while (this.match(TokenType.MINUS, TokenType.PLUS)) {
const operator = this.previous().value;
const right = this.parseFactor();
expr = {
type: 'Expression',
expressionType: ExpressionType.BINARY,
operator,
left: expr,
right,
range: { start: expr.range.start, end: right.range.end }
};
}
return expr;
}
parseFactor() {
let expr = this.parseUnary();
while (this.match(TokenType.DIVIDE, TokenType.MULTIPLY, TokenType.MODULO)) {
const operator = this.previous().value;
const right = this.parseUnary();
expr = {
type: 'Expression',
expressionType: ExpressionType.BINARY,
operator,
left: expr,
right,
range: { start: expr.range.start, end: right.range.end }
};
}
return expr;
}
parseUnary() {
if (this.match(TokenType.LOGICAL_NOT, TokenType.MINUS)) {
const operator = this.previous().value;
const right = this.parseUnary();
return {
type: 'Expression',
expressionType: ExpressionType.UNARY,
operator,
right,
range: { start: this.previous().range.start, end: right.range.end }
};
}
return this.parseCall();
}
parseCall() {
let expr = this.parsePrimary();
while (true) {
if (this.match(TokenType.LEFT_PAREN)) {
expr = this.finishCall(expr);
}
else if (this.match(TokenType.DOT)) {
const name = this.consume(TokenType.IDENTIFIER, "Expected property name after '.'");
expr = {
type: 'Expression',
expressionType: ExpressionType.MEMBER,
object: expr,
property: {
type: 'Expression',
expressionType: ExpressionType.IDENTIFIER,
identifier: name.value,
range: name.range
},
range: { start: expr.range.start, end: name.range.end }
};
}
else {
break;
}
}
return expr;
}
finishCall(callee) {
const args = [];
if (!this.check(TokenType.RIGHT_PAREN)) {
do {
args.push(this.parseExpression());
} while (this.match(TokenType.COMMA));
}
const paren = this.consume(TokenType.RIGHT_PAREN, "Expected ')' after arguments");
return {
type: 'Expression',
expressionType: ExpressionType.CALL,
callee,
arguments: args,
range: { start: callee.range.start, end: paren.range.end }
};
}
parsePrimary() {
const start = this.getCurrentPosition();
if (this.match(TokenType.BOOLEAN)) {
const value = this.previous().value === 'true';
return {
type: 'Expression',
expressionType: ExpressionType.LITERAL,
value,
range: { start, end: this.getCurrentPosition() }
};
}
if (this.match(TokenType.NUMBER)) {
const value = parseFloat(this.previous().value);
return {
type: 'Expression',
expressionType: ExpressionType.LITERAL,
value,
range: { start, end: this.getCurrentPosition() }
};
}
if (this.match(TokenType.STRING)) {
const value = this.previous().value;
return {
type: 'Expression',
expressionType: ExpressionType.LITERAL,
value,
range: { start, end: this.getCurrentPosition() }
};
}
if (this.match(TokenType.IDENTIFIER)) {
const identifier = this.previous().value;
return {
type: 'Expression',
expressionType: ExpressionType.IDENTIFIER,
identifier,
range: { start, end: this.getCurrentPosition() }
};
}
if (this.match(TokenType.LEFT_PAREN)) {
const expr = this.parseExpression();
this.consume(TokenType.RIGHT_PAREN, "Expected ')' after expression");
return expr;
}
this.throwError('Expected expression', ['identifier', 'number', 'string', 'boolean', '(']);
// Return dummy expression for error recovery
return {
type: 'Expression',
expressionType: ExpressionType.LITERAL,
value: null,
range: { start, end: this.getCurrentPosition() }
};
}
parseHTMLElement() {
const start = this.getCurrentPosition();
// This is a simplified HTML parser - in a real implementation,
// you'd need much more sophisticated HTML parsing
const tagName = 'div'; // Placeholder
const attributes = [];
const children = [];
const end = this.getCurrentPosition();
return {
type: 'HTMLElement',
tagName,
attributes,
children,
isSelfClosing: false,
isVoidElement: false,
range: { start, end }
};
}
parseInterpolation() {
const start = this.getCurrentPosition();
this.consume(TokenType.INTERPOLATION_START, "Expected '{' for interpolation");
const expression = this.parseExpression();
this.consume(TokenType.INTERPOLATION_END, "Expected '}' after interpolation");
const end = this.getCurrentPosition();
return {
type: 'Interpolation',
expression,
range: { start, end }
};
}
parseTextNode() {
const start = this.getCurrentPosition();
const content = this.consume(TokenType.HTML_TEXT, 'Expected text content').value;
const end = this.getCurrentPosition();
return {
type: 'Text',
content,
range: { start, end }
};
}
checkLifecycleHook() {
const value = this.peek().value;
return value === 'onMount' || value === 'onUnmount' || value === 'onUpdate';
}
// Utility methods
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.tokenStream.tokens[this.current];
}
previous() {
return this.tokenStream.tokens[this.current - 1];
}
consume(type, message) {
if (this.check(type))
return this.advance();
this.throwError(message, [type.toString()]);
// Return dummy token for error recovery
return {
type: TokenType.EOF,
value: '',
position: this.getCurrentPosition(),
range: { start: this.getCurrentPosition(), end: this.getCurrentPosition() }
};
}
throwError(message, expected) {
const error = new SyntaxError(message, this.getCurrentPosition(), expected, this.peek().value, this.filename);
if (this.options.allowRecovery && this.errors.length < (this.options.maxErrors || 1)) {
this.errors.push(error);
this.synchronize();
throw error; // Still throw after synchronize for proper error handling
}
throw error;
}
synchronize() {
this.advance();
while (!this.isAtEnd()) {
if (this.previous().type === TokenType.SEMICOLON)
return;
switch (this.peek().type) {
case TokenType.COMPONENT:
case TokenType.CLIENT:
case TokenType.SERVER:
case TokenType.MARKUP:
case TokenType.LET:
case TokenType.CONST:
return;
}
this.advance();
}
}
getCurrentPosition() {
return this.peek().position;
}
}