Rename folders
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
import {Binary, ContextAccess, Expr, FunctionCall, Grouping, IndexAccess, Literal, Logical, Star, Unary} from "./ast";
|
||||
import * as data from "./data";
|
||||
import {ErrorType, ExpressionError, MAX_PARSER_DEPTH} from "./errors";
|
||||
import {ParseContext, validateFunction} from "./funcs";
|
||||
import {FunctionInfo} from "./funcs/info";
|
||||
import {Token, TokenType} from "./lexer";
|
||||
|
||||
export class Parser {
|
||||
private extContexts: Map<string, boolean>;
|
||||
private extFuncs: Map<string, FunctionInfo>;
|
||||
|
||||
private offset: number = 0;
|
||||
private depth: number = 0;
|
||||
|
||||
private context: ParseContext;
|
||||
|
||||
/**
|
||||
* Constructs a new parser for the given tokens
|
||||
*
|
||||
* @param tokens Tokens to build a parse tree from
|
||||
* @param extensionContexts Available context names
|
||||
* @param extensionFunctions Available functions (beyond the built-in ones)
|
||||
*/
|
||||
constructor(private tokens: Token[], extensionContexts: string[], extensionFunctions: FunctionInfo[]) {
|
||||
this.extContexts = new Map<string, boolean>();
|
||||
this.extFuncs = new Map();
|
||||
|
||||
for (const contextName of extensionContexts) {
|
||||
this.extContexts.set(contextName.toLowerCase(), true);
|
||||
}
|
||||
|
||||
for (const {name, func} of extensionFunctions.map(x => ({
|
||||
name: x.name,
|
||||
func: x
|
||||
}))) {
|
||||
this.extFuncs.set(name.toLowerCase(), func);
|
||||
}
|
||||
|
||||
this.context = {
|
||||
allowUnknownKeywords: false,
|
||||
extensionContexts: this.extContexts,
|
||||
extensionFunctions: this.extFuncs
|
||||
};
|
||||
}
|
||||
|
||||
public parse(): Expr {
|
||||
let result!: Expr;
|
||||
|
||||
// No tokens
|
||||
if (this.atEnd()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = this.expression();
|
||||
|
||||
if (!this.atEnd()) {
|
||||
throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private expression(): Expr {
|
||||
this.incrDepth();
|
||||
|
||||
try {
|
||||
return this.logicalOr();
|
||||
} finally {
|
||||
this.decrDepth();
|
||||
}
|
||||
}
|
||||
|
||||
private logicalOr(): Expr {
|
||||
// && is higher precedence than ||
|
||||
let expr = this.logicalAnd();
|
||||
|
||||
if (this.check(TokenType.OR)) {
|
||||
// Track depth
|
||||
this.incrDepth();
|
||||
|
||||
try {
|
||||
const logical = new Logical(this.peek(), [expr]);
|
||||
expr = logical;
|
||||
|
||||
while (this.match(TokenType.OR)) {
|
||||
const right = this.logicalAnd();
|
||||
logical.args.push(right);
|
||||
}
|
||||
} finally {
|
||||
this.decrDepth();
|
||||
}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private logicalAnd(): Expr {
|
||||
// == and != are higher precedence than &&
|
||||
let expr = this.equality();
|
||||
|
||||
if (this.check(TokenType.AND)) {
|
||||
// Track depth
|
||||
this.incrDepth();
|
||||
|
||||
try {
|
||||
const logical = new Logical(this.peek(), [expr]);
|
||||
expr = logical;
|
||||
|
||||
while (this.match(TokenType.AND)) {
|
||||
const right = this.equality();
|
||||
logical.args.push(right);
|
||||
}
|
||||
} finally {
|
||||
this.decrDepth();
|
||||
}
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private equality(): Expr {
|
||||
// >, >=, <, <= are higher precedence than == and !=
|
||||
let expr = this.comparison();
|
||||
|
||||
while (this.match(TokenType.BANG_EQUAL, TokenType.EQUAL_EQUAL)) {
|
||||
const operator = this.previous();
|
||||
const right = this.comparison();
|
||||
|
||||
expr = new Binary(expr, operator, right);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private comparison(): Expr {
|
||||
// ! is higher precedence than >, >=, <, <=
|
||||
let expr = this.unary();
|
||||
|
||||
while (this.match(TokenType.GREATER, TokenType.GREATER_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL)) {
|
||||
const operator = this.previous();
|
||||
const right = this.unary();
|
||||
|
||||
expr = new Binary(expr, operator, right);
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private unary(): Expr {
|
||||
if (this.match(TokenType.BANG)) {
|
||||
// Track depth
|
||||
this.incrDepth();
|
||||
|
||||
const operator = this.previous();
|
||||
const unary = this.unary();
|
||||
|
||||
try {
|
||||
return new Unary(operator, unary);
|
||||
} finally {
|
||||
this.decrDepth();
|
||||
}
|
||||
}
|
||||
|
||||
return this.index();
|
||||
}
|
||||
|
||||
private index(): Expr {
|
||||
let expr = this.call();
|
||||
|
||||
let depthIncreased = 0;
|
||||
|
||||
if (expr instanceof Grouping || expr instanceof FunctionCall || expr instanceof ContextAccess) {
|
||||
let cont = true;
|
||||
while (cont) {
|
||||
switch (true) {
|
||||
case this.match(TokenType.LEFT_BRACKET):
|
||||
let indexExpr: Expr;
|
||||
if (this.match(TokenType.STAR)) {
|
||||
indexExpr = new Star();
|
||||
} else {
|
||||
indexExpr = this.expression();
|
||||
}
|
||||
|
||||
this.consume(TokenType.RIGHT_BRACKET, ErrorType.ErrorUnexpectedSymbol);
|
||||
|
||||
// Track depth
|
||||
this.incrDepth();
|
||||
depthIncreased++;
|
||||
expr = new IndexAccess(expr, indexExpr);
|
||||
break;
|
||||
|
||||
case this.match(TokenType.DOT):
|
||||
// Track depth
|
||||
this.incrDepth();
|
||||
depthIncreased++;
|
||||
|
||||
if (this.match(TokenType.IDENTIFIER)) {
|
||||
let property = this.previous();
|
||||
expr = new IndexAccess(expr, new Literal(new data.StringData(property.lexeme), property));
|
||||
} else if (this.match(TokenType.STAR)) {
|
||||
expr = new IndexAccess(expr, new Star());
|
||||
} else {
|
||||
throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek());
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
cont = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < depthIncreased; i++) {
|
||||
this.decrDepth();
|
||||
}
|
||||
|
||||
return expr;
|
||||
}
|
||||
|
||||
private call(): Expr {
|
||||
if (!this.check(TokenType.IDENTIFIER)) {
|
||||
return this.primary();
|
||||
}
|
||||
|
||||
const identifier = this.next();
|
||||
|
||||
if (!this.match(TokenType.LEFT_PAREN)) {
|
||||
if (!this.extContexts.has(identifier.lexeme.toLowerCase())) {
|
||||
throw this.buildError(ErrorType.ErrorUnrecognizedContext, identifier);
|
||||
}
|
||||
return new ContextAccess(identifier);
|
||||
}
|
||||
|
||||
// Function call
|
||||
const args: Expr[] = [];
|
||||
|
||||
// Arguments
|
||||
while (!this.match(TokenType.RIGHT_PAREN)) {
|
||||
const aexp = this.expression();
|
||||
|
||||
args.push(aexp);
|
||||
|
||||
if (!this.check(TokenType.RIGHT_PAREN)) {
|
||||
this.consume(TokenType.COMMA, ErrorType.ErrorUnexpectedSymbol);
|
||||
}
|
||||
}
|
||||
|
||||
validateFunction(this.context, identifier, args.length);
|
||||
|
||||
return new FunctionCall(identifier, args);
|
||||
}
|
||||
|
||||
private primary(): Expr {
|
||||
switch (true) {
|
||||
case this.match(TokenType.FALSE):
|
||||
return new Literal(new data.BooleanData(false), this.previous());
|
||||
|
||||
case this.match(TokenType.TRUE):
|
||||
return new Literal(new data.BooleanData(true), this.previous());
|
||||
|
||||
case this.match(TokenType.NULL):
|
||||
return new Literal(new data.Null(), this.previous());
|
||||
|
||||
case this.match(TokenType.NUMBER):
|
||||
return new Literal(new data.NumberData(this.previous().value as number), this.previous());
|
||||
|
||||
case this.match(TokenType.STRING):
|
||||
return new Literal(new data.StringData(this.previous().value as string), this.previous());
|
||||
|
||||
case this.match(TokenType.LEFT_PAREN):
|
||||
const expr = this.expression();
|
||||
|
||||
if (this.atEnd()) {
|
||||
throw this.buildError(ErrorType.ErrorUnexpectedEndOfExpression, this.previous()); // Back up to get the last token before the EOF
|
||||
}
|
||||
|
||||
this.consume(TokenType.RIGHT_PAREN, ErrorType.ErrorUnexpectedSymbol);
|
||||
return new Grouping(expr);
|
||||
|
||||
case this.atEnd():
|
||||
throw this.buildError(ErrorType.ErrorUnexpectedEndOfExpression, this.previous()); // Back up to get the last token before the EOF
|
||||
}
|
||||
|
||||
throw this.buildError(ErrorType.ErrorUnexpectedSymbol, this.peek());
|
||||
}
|
||||
|
||||
// match consumes the next token if it matches any of the given types
|
||||
private match(...tokenTypes: TokenType[]): boolean {
|
||||
for (const tokenType of tokenTypes) {
|
||||
if (this.check(tokenType)) {
|
||||
this.next();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// check peeks whether the next token is of the given type
|
||||
private check(tokenType: TokenType): boolean {
|
||||
if (this.atEnd()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.peek().type == tokenType;
|
||||
}
|
||||
|
||||
// atEnd peeks whether the next token is EOF
|
||||
private atEnd(): boolean {
|
||||
return this.peek().type == TokenType.EOF;
|
||||
}
|
||||
|
||||
private next(): Token {
|
||||
if (!this.atEnd()) {
|
||||
this.offset++;
|
||||
}
|
||||
|
||||
return this.previous();
|
||||
}
|
||||
|
||||
private peek(): Token {
|
||||
return this.tokens[this.offset];
|
||||
}
|
||||
|
||||
// previous returns the previous token
|
||||
private previous(): Token {
|
||||
return this.tokens[this.offset - 1];
|
||||
}
|
||||
|
||||
// consume attempts to consume the next token if it matches the given type. It returns an error of
|
||||
// the given ParseErrorKind otherwise.
|
||||
private consume(tokenType: TokenType, errorType: ErrorType) {
|
||||
if (this.check(tokenType)) {
|
||||
this.next();
|
||||
return;
|
||||
}
|
||||
|
||||
throw this.buildError(errorType, this.peek());
|
||||
}
|
||||
|
||||
private incrDepth() {
|
||||
this.depth++;
|
||||
if (this.depth > MAX_PARSER_DEPTH) {
|
||||
throw this.buildError(ErrorType.ErrorExceededMaxDepth, this.peek());
|
||||
}
|
||||
}
|
||||
|
||||
private decrDepth() {
|
||||
this.depth--;
|
||||
}
|
||||
|
||||
private buildError(errType: ErrorType, token: Token): Error {
|
||||
return new ExpressionError(errType, token);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user