swahili-lang
Version:
A new programming language with semantics borrowed from the Swahili language to help teach programming concepts to Swahili speaking students.
1,477 lines (1,254 loc) • 40.1 kB
JavaScript
const TT = require('../lexer/tokenTypes');
const LEX = require('../lexer/lexemes');
const ParseResult = require('./parseResult');
const { InvalidSyntaxError } = require('../interpreter/error');
const {
NumberNode,
StringNode,
ObjectNode,
ListNode,
PropAccessNode,
PropAssignNode,
VarAccessNode,
VarAssignNode,
VarDefNode,
BinOpNode,
UnaryOpNode,
IfNode,
ForNode,
ForEachNode,
WhileNode,
FuncDefNode,
CallNode,
TryCatchNode,
ReturnNode,
ContinueNode,
BreakNode,
ThrowNode,
} = require('../interpreter/nodes');
// abstraction to make code shorter
const lc = (str) => str.replace(/\\/g, '').toLowerCase();
class Parser {
/**
* instantiates a parser
* @param {Token[]} tokens list of tokens from the lexer
*/
constructor(tokens) {
this.tokens = tokens;
this.tokIdx = -1;
this.advance();
}
/**
* moves to the next token from the lexer
* @returns {Token}
*/
advance() {
this.tokIdx++;
this.updateCurrentTok();
return this.currentTok;
}
/**
* moves backwards through the tokens for a given number of steps
* @param {Number} amount number of advancements to reverse
* @returns {Token}
*/
reverse(amount = 1) {
this.tokIdx -= amount;
this.updateCurrentTok();
return this.currentTok;
}
/**
* sets the current token to match the parser's token index
* @returns {Token}
*/
updateCurrentTok() {
if (this.tokIdx >= 0 && this.tokIdx < this.tokens.length) {
this.currentTok = this.tokens[this.tokIdx];
}
}
/**
* combines a set of tokens into node(s)
* @returns {ParseResult}
*/
parse() {
let res = this.statements();
if (!res.error && this.currentTok.type !== TT.EOF) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Unexpected ${lc(this.currentTok.type)}`
)
);
}
return res;
}
/** creates nodes based on the statements rule in the grammar document */
statements = () => {
let res = new ParseResult();
let statements = [];
let posStart = this.currentTok.posStart.copy();
while (this.currentTok.type === TT.NEWLINE) {
res.registerAdvancement();
this.advance();
}
let statement = res.register(this.statement());
if (res.error) return res;
statements.push(statement);
let moreStatements = true;
while (true) {
let newLineCount = 0;
while (this.currentTok.type === TT.NEWLINE) {
res.registerAdvancement();
this.advance();
newLineCount += 1;
}
if (newLineCount === 0) moreStatements = false;
if (!moreStatements) break;
// look for any statements
let statement = res.tryRegister(this.statement());
if (!statement) {
this.reverse(res.toReverseCount);
moreStatements = false;
continue;
}
// add it to our list if found
statements.push(statement);
}
return res.success(
new ListNode(statements, posStart, this.currentTok.posEnd.copy())
);
};
/** creates nodes based on the statement rule in the grammar document */
statement = () => {
let res = new ParseResult();
let posStart = this.currentTok.posStart.copy();
if (this.currentTok.matches(TT.KEYWORD, LEX.keywords.return)) {
res.registerAdvancement();
this.advance();
let expr = res.tryRegister(this.expr());
if (!expr) {
this.reverse(res.toReverseCount);
}
return res.success(
new ReturnNode(expr, posStart, this.currentTok.posStart.copy())
);
}
if (this.currentTok.matches(TT.KEYWORD, LEX.keywords.throw)) {
res.registerAdvancement();
this.advance();
let expr = res.register(this.expr());
if (res.error) return res;
return res.success(
new ThrowNode(expr, posStart, this.currentTok.posStart.copy())
);
}
if (this.currentTok.matches(TT.KEYWORD, LEX.keywords.continue)) {
res.registerAdvancement();
this.advance();
return res.success(
new ContinueNode(posStart, this.currentTok.posStart.copy())
);
}
if (this.currentTok.matches(TT.KEYWORD, LEX.keywords.break)) {
res.registerAdvancement();
this.advance();
return res.success(
new BreakNode(posStart, this.currentTok.posStart.copy())
);
}
let advanced = false;
if (this.currentTok.type === TT.IDENTIFIER) {
let varName = this.currentTok;
// skip registering an advancement here just in case we need to back-track
// since this is checking for specific tokens,
// we don't have a method we can run `tryRegister` on
this.advance();
advanced = true;
if (this.currentTok.type === TT.EQ) {
res.registerAdvancement();
this.advance();
let expr = res.register(this.expr());
if (res.error) return res;
res.registerAdvancement();
return res.success(new VarAssignNode(varName, expr));
}
}
if (advanced) this.reverse();
let expr = res.register(this.expr());
if (res.error)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${LEX.keywords.return}', '${LEX.keywords.continue}', '${
LEX.keywords.break
}', '${LEX.keywords.let}', '${LEX.keywords.if}', '${
LEX.keywords.for
}', '${LEX.keywords.while}', '${LEX.keywords.function}', ${lc(
TT.INT
)}, ${lc(TT.FLOAT)}, ${lc(TT.IDENTIFIER)}, '${lc(
LEX.plus.source
)}', '${lc(LEX.hyphen.source)}', '${lc(LEX.leftParen.source)}', '${lc(
LEX.leftSquare.source
)}' or '${lc(LEX.exclamation.source)}'`
)
);
return res.success(expr);
};
/** creates nodes based on the expr rule in the grammar document */
expr = () => {
let res = new ParseResult();
if (this.currentTok.matches(TT.KEYWORD, LEX.keywords.let)) {
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.IDENTIFIER)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected ${lc(TT.IDENTIFIER)}`
)
);
let varName = this.currentTok;
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.EQ)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.equals.source)}'`
)
);
res.registerAdvancement();
this.advance();
let expr = res.register(this.expr());
if (res.error) return res;
return res.success(new VarDefNode(varName, expr));
}
let node = res.register(this.binOp(this.compExpr, [TT.AND, TT.OR]));
if (res.error)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${LEX.keywords.let}', '${LEX.keywords.if}', '${
LEX.keywords.for
}', '${LEX.keywords.while}', '${LEX.keywords.function}', ${lc(
TT.INT
)}, ${lc(TT.FLOAT)}, ${lc(TT.IDENTIFIER)}, '${lc(
LEX.plus.source
)}', '${lc(LEX.hyphen.source)}', '${lc(LEX.leftParen.source)}', '${lc(
LEX.leftSquare.source
)}' or '${lc(LEX.exclamation.source)}'`
)
);
return res.success(node);
};
/** creates nodes based on the comp-expr rule in the grammar document */
compExpr = () => {
let res = new ParseResult();
let node = null;
if (this.currentTok.type === TT.NOT) {
let opToken = this.currentTok;
res.registerAdvancement();
this.advance();
node = res.register(this.compExpr());
if (res.error) return res;
return res.success(new UnaryOpNode(opToken, node));
}
node = res.register(
this.binOp(this.arithExpr, [TT.EE, TT.NE, TT.LT, TT.GT, TT.LTE, TT.GTE])
);
if (res.error)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected ${lc(TT.INT)}, ${lc(TT.FLOAT)}, ${lc(TT.IDENTIFIER)}, '${lc(
LEX.plus.source
)}', '${lc(LEX.hyphen.source)}', '${lc(LEX.leftParen.source)}', '${lc(
LEX.leftSquare.source
)}' or '${lc(LEX.exclamation.source)}'`
)
);
return res.success(node);
};
/** creates nodes based on the arith-expr rule in the grammar document */
arithExpr = () => {
return this.binOp(this.term, [TT.PLUS, TT.MINUS]);
};
/** creates nodes based on the term rule in the grammar document */
term = () => {
return this.binOp(this.factor, [TT.MUL, TT.DIV, TT.MOD]);
};
/** creates nodes based on the power rule in the grammar document */
factor = () => {
let res = new ParseResult();
const tok = this.currentTok;
if ([TT.PLUS, TT.MINUS].includes(tok.type)) {
res.registerAdvancement();
this.advance();
let factor = res.register(this.factor());
if (res.error) return res;
return res.success(new UnaryOpNode(tok, factor));
}
return this.power();
};
/** creates nodes based on the power rule in the grammar document */
power = () => {
return this.binOp(this.call, [TT.POW], this.factor);
};
/** creates a function call node */
call = () => {
let res = new ParseResult();
let atom = res.register(this.atom());
if (res.error) return res;
let argNodes = [];
if (this.currentTok.type === TT.LPAREN) {
res.registerAdvancement();
this.advance();
if (this.currentTok.type === TT.RPAREN) {
res.registerAdvancement();
this.advance();
} else {
argNodes.push(res.register(this.expr()));
if (res.error)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightParen.source)}','${
LEX.keywords.let
}', '${LEX.keywords.if}', '${LEX.keywords.for}', '${
LEX.keywords.while
}', '${LEX.keywords.function}', ${lc(TT.INT)}, ${lc(
TT.FLOAT
)}, ${lc(TT.IDENTIFIER)}, '${lc(LEX.plus.source)}', '${lc(
LEX.hyphen.source
)}', '${lc(LEX.leftParen.source)}', '${lc(
LEX.leftSquare.source
)}' or '${lc(LEX.exclamation.source)}'`
)
);
while (this.currentTok.type === TT.COMMA) {
res.registerAdvancement();
this.advance();
argNodes.push(res.register(this.expr()));
if (res.error) return res;
}
if (this.currentTok.type !== TT.RPAREN) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.comma.source)}' or '${lc(
LEX.rightParen.source
)}'`
)
);
}
res.registerAdvancement();
this.advance();
}
return res.success(new CallNode(atom, argNodes));
}
return res.success(atom);
};
/** creates nodes based on the atom rule in the grammar document */
atom = () => {
let res = new ParseResult();
const tok = this.currentTok;
if ([TT.INT, TT.FLOAT].includes(tok.type)) {
res.registerAdvancement();
this.advance();
return res.success(new NumberNode(tok));
} else if (tok.type === TT.STRING) {
res.registerAdvancement();
this.advance();
return res.success(new StringNode(tok));
} else if (tok.type === TT.LPAREN) {
res.registerAdvancement();
this.advance();
let expr = res.register(this.expr());
if (res.error) return res;
if (this.currentTok.type === TT.RPAREN) {
res.registerAdvancement();
this.advance();
return res.success(expr);
} else {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightParen.source)}'`
)
);
}
} else if (tok.type === TT.IDENTIFIER) {
let access = res.register(this.access());
if (res.error) return res;
return res.success(access);
} else if (tok.type === TT.LCURL) {
let objExpr = res.register(this.objExpr());
if (res.error) return res;
return res.success(objExpr);
} else if (tok.type === TT.LSQUARE) {
let listExpr = res.register(this.listExpr());
if (res.error) return res;
return res.success(listExpr);
} else if (tok.matches(TT.KEYWORD, LEX.keywords.try)) {
let tryExpr = res.register(this.tryExpr());
if (res.error) return res;
return res.success(tryExpr);
} else if (tok.matches(TT.KEYWORD, LEX.keywords.if)) {
let ifExpr = res.register(this.ifExpr());
if (res.error) return res;
return res.success(ifExpr);
} else if (tok.matches(TT.KEYWORD, LEX.keywords.for)) {
let forExpr = res.register(this.forExpr());
if (res.error) return res;
return res.success(forExpr);
} else if (tok.matches(TT.KEYWORD, LEX.keywords.while)) {
let whileExpr = res.register(this.whileExpr());
if (res.error) return res;
return res.success(whileExpr);
} else if (tok.matches(TT.KEYWORD, LEX.keywords.function)) {
let funcDef = res.register(this.funcDef());
if (res.error) return res;
return res.success(funcDef);
}
return res.failure(
new InvalidSyntaxError(
tok.posStart,
tok.posEnd,
`Expected ${lc(TT.INT)}, ${lc(TT.FLOAT)}, ${lc(TT.IDENTIFIER)}, '${lc(
LEX.plus.source
)}', '${lc(LEX.hyphen.source)}', '${lc(LEX.leftParen.source)}', '${lc(
LEX.leftSquare.source
)}', '${LEX.keywords.if}', '${LEX.keywords.for}', '${
LEX.keywords.while
}' or '${LEX.keywords.function}'`
)
);
};
/** parse tokens to make an access token */
access = () => {
let res = new ParseResult();
let nodeChain = [];
if (this.currentTok.type !== TT.IDENTIFIER)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected ${lc(TT.IDENTIFIER)}`
)
);
let varNameTok = this.currentTok;
let currentNode = new PropAccessNode(varNameTok);
nodeChain.push(varNameTok);
res.registerAdvancement();
this.advance();
while (this.currentTok.type === TT.DOT) {
res.registerAdvancement();
this.advance();
if (this.currentTok.type === TT.IDENTIFIER) {
varNameTok = this.currentTok;
currentNode = new PropAccessNode(varNameTok, currentNode);
nodeChain.push(varNameTok);
} else {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected ${lc(TT.IDENTIFIER)}`
)
);
}
res.registerAdvancement();
this.advance();
}
if (!currentNode.parent) {
currentNode = new VarAccessNode(varNameTok);
} else {
if (this.currentTok.type === TT.EQ) {
res.registerAdvancement();
this.advance();
let valueNode = res.register(this.expr());
if (res.error) return res;
currentNode = new PropAssignNode(nodeChain, valueNode);
}
}
return res.success(currentNode);
};
/** parse tokens to make an object node */
objExpr = () => {
let res = new ParseResult();
let propertyNodes = [];
let posStart = this.currentTok.posStart.copy();
if (this.currentTok.type !== TT.LCURL)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(TT.LCURL)}'`
)
);
res.registerAdvancement();
this.advance();
if (this.currentTok.type === TT.RCURL) {
res.registerAdvancement();
this.advance();
} else {
// skip past any new lines after [
while (this.currentTok.type === TT.NEWLINE) {
res.registerAdvancement();
this.advance();
}
if (![TT.IDENTIFIER, TT.STRING].includes(this.currentTok.type))
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected ${lc(TT.IDENTIFIER)} or ${lc(TT.STRING)}`
)
);
let propertyName = this.currentTok;
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.COL)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.col.source)}'`
)
);
res.registerAdvancement();
this.advance();
let valueNode = res.register(this.expr());
if (res.error) return res;
propertyNodes.push(new PropAssignNode([propertyName], valueNode));
while (this.currentTok.type === TT.COMMA) {
res.registerAdvancement();
this.advance();
// skip past any new lines after between properties
while (this.currentTok.type === TT.NEWLINE) {
res.registerAdvancement();
this.advance();
}
if (![TT.IDENTIFIER, TT.STRING].includes(this.currentTok.type))
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected ${lc(TT.IDENTIFIER)} or ${lc(TT.STRING)}`
)
);
propertyName = this.currentTok;
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.COL)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.col.source)}'`
)
);
res.registerAdvancement();
this.advance();
valueNode = res.register(this.expr());
if (res.error) return res;
propertyNodes.push(new PropAssignNode([propertyName], valueNode));
}
// skip past any new lines
while (this.currentTok.type === TT.NEWLINE) {
res.registerAdvancement();
this.advance();
}
if (this.currentTok.type !== TT.RCURL) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.comma.source)}' or '${lc(
LEX.rightCurly.source
)}'`
)
);
}
res.registerAdvancement();
this.advance();
}
return res.success(
new ObjectNode(propertyNodes, posStart, this.currentTok.posEnd.copy())
);
};
/** parse tokens to make a list node */
listExpr = () => {
let res = new ParseResult();
let elementNodes = [];
let posStart = this.currentTok.posStart.copy();
if (this.currentTok.type !== TT.LSQUARE)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(TT.LSQUARE)}'`
)
);
res.registerAdvancement();
this.advance();
if (this.currentTok.type === TT.RSQUARE) {
res.registerAdvancement();
this.advance();
} else {
// skip past any new lines after [
while (this.currentTok.type === TT.NEWLINE) {
res.registerAdvancement();
this.advance();
}
elementNodes.push(res.register(this.expr()));
if (res.error)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightSquare.source)}', '${
LEX.keywords.let
}', '${LEX.keywords.if}', '${LEX.keywords.for}', '${
LEX.keywords.while
}', '${LEX.keywords.function}', ${lc(TT.INT)}, ${lc(
TT.FLOAT
)}, ${lc(TT.IDENTIFIER)}, '${lc(LEX.plus.source)}', '${lc(
LEX.hyphen.source
)}', '${lc(LEX.leftParen.source)}', '${lc(
LEX.leftSquare.source
)}' or '${lc(LEX.exclamation.source)}'`
)
);
while (this.currentTok.type === TT.COMMA) {
res.registerAdvancement();
this.advance();
// skip past any new lines between list items
while (this.currentTok.type === TT.NEWLINE) {
res.registerAdvancement();
this.advance();
}
elementNodes.push(res.register(this.expr()));
if (res.error) return res;
}
// skip past any more new lines
while (this.currentTok.type === TT.NEWLINE) {
res.registerAdvancement();
this.advance();
}
if (this.currentTok.type !== TT.RSQUARE) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.comma.source)}' or '${lc(
LEX.rightSquare.source
)}'`
)
);
}
res.registerAdvancement();
this.advance();
}
return res.success(
new ListNode(elementNodes, posStart, this.currentTok.posEnd.copy())
);
};
/** parse tokens to make a TryCatchNode */
tryExpr = () => {
let res = new ParseResult();
let tryBody = null;
let errVarNameTok = null;
let catchBody = null;
let finallyBody = null;
let posStart = this.currentTok.posStart.copy();
if (!this.currentTok.matches(TT.KEYWORD, LEX.keywords.try))
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${LEX.keywords.try}'`
)
);
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.LCURL)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.leftCurly.source)}'`
)
);
res.registerAdvancement();
this.advance();
tryBody = res.register(this.statements());
if (res.error) return res;
if (this.currentTok.type !== TT.RCURL)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightCurly.source)}'`
)
);
res.registerAdvancement();
this.advance();
if (!this.currentTok.matches(TT.KEYWORD, LEX.keywords.catch))
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${LEX.keywords.catch}'`
)
);
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.LPAREN)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.leftParen.source)}'`
)
);
res.registerAdvancement();
this.advance();
if (!this.currentTok.type === TT.IDENTIFIER)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(TT.IDENTIFIER)}'`
)
);
errVarNameTok = this.currentTok;
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.RPAREN)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightParen.source)}'`
)
);
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.LCURL)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.leftCurly.source)}'`
)
);
res.registerAdvancement();
this.advance();
catchBody = res.register(this.statements());
if (res.error) return res;
if (this.currentTok.type !== TT.RCURL)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightCurly.source)}'`
)
);
res.registerAdvancement();
this.advance();
if (this.currentTok.matches(TT.KEYWORD, LEX.keywords.finally)) {
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.LCURL)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.leftCurly.source)}'`
)
);
res.registerAdvancement();
this.advance();
finallyBody = res.register(this.statements());
if (res.error) return res;
if (this.currentTok.type !== TT.RCURL)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightCurly.source)}'`
)
);
res.registerAdvancement();
this.advance();
}
return res.success(
new TryCatchNode(
tryBody,
errVarNameTok,
catchBody,
finallyBody,
posStart,
this.currentTok.posEnd.copy()
)
);
};
/** parse tokens to make an If Node with cases and an optional else case */
ifExpr = () => {
let res = new ParseResult();
let allCases = res.register(this.ifExprCases(LEX.keywords.if));
if (res.error) return res;
let [cases, elseCase] = allCases;
return res.success(new IfNode(cases, elseCase));
};
/** parse tokens to make an ElseIf portion of an If Node */
ifExprB = () => {
return this.ifExprCases(LEX.keywords.elif);
};
/** parse tokens to make an Else portion of an If Node */
ifExprC = () => {
let res = new ParseResult();
let elseCase = null;
if (this.currentTok.matches(TT.KEYWORD, LEX.keywords.else)) {
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.LCURL) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.leftCurly.source)}'`
)
);
}
res.registerAdvancement();
this.advance();
let statements = res.register(this.statements());
if (res.error) return res;
elseCase = [statements, statements.length > 1];
if (this.currentTok.type === TT.RCURL) {
res.registerAdvancement();
this.advance();
} else {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightCurly.source)}'`
)
);
}
}
return res.success(elseCase);
};
/** creates nodes based on the if-expr-b or if-expr-c rules in the grammar document */
ifExprBOrC = () => {
let res = new ParseResult();
let cases = [];
let elseCase = null;
if (this.currentTok.matches(TT.KEYWORD, LEX.keywords.elif)) {
let allCases = res.register(this.ifExprB());
if (res.error) return res;
[cases, elseCase] = allCases;
} else {
elseCase = res.register(this.ifExprC());
if (res.error) return res;
}
return res.success([cases, elseCase]);
};
/** obtains all the cases and else case for an if node */
ifExprCases = (caseKeyword) => {
let res = new ParseResult();
let cases = [];
let elseCase = null;
let newCases = [];
if (!this.currentTok.matches(TT.KEYWORD, caseKeyword)) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${caseKeyword}'`
)
);
}
res.registerAdvancement();
this.advance();
let condition = res.register(this.expr());
if (res.error) return res;
if (this.currentTok.type !== TT.LCURL) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '{'`
)
);
}
res.registerAdvancement();
this.advance();
let statements = res.register(this.statements());
if (res.error) return res;
cases.push([condition, statements, statements.length > 1]);
if (this.currentTok.type === TT.RCURL) {
res.registerAdvancement();
this.advance();
let allCases = res.register(this.ifExprBOrC());
if (res.error) return res;
[newCases, elseCase] = allCases;
cases = cases.concat(newCases);
} else {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightCurly.source)}'`
)
);
}
return res.success([cases, elseCase]);
};
/** creates a For Node */
forExpr = () => {
let res = new ParseResult();
let body = null;
if (!this.currentTok.matches(TT.KEYWORD, LEX.keywords.for)) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${LEX.keywords.for}'`
)
);
}
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.IDENTIFIER) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected ${lc(TT.IDENTIFIER)}`
)
);
}
let varName = this.currentTok;
res.registerAdvancement();
this.advance();
// check for keyword
if (this.currentTok.matches(TT.KEYWORD, LEX.keywords.in)) {
let forEach = res.register(this.forEachExpr(varName));
if (res.error) return res;
return res.success(forEach);
}
// if not a keyword or an equals, that's a problem
if (this.currentTok.type !== TT.EQ) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.equals.source)}' or '${lc(LEX.keywords.in)}'`
)
);
}
res.registerAdvancement();
this.advance();
let startValue = res.register(this.expr());
if (res.error) return res;
if (!this.currentTok.matches(TT.KEYWORD, LEX.keywords.to)) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${LEX.keywords.to}'`
)
);
}
res.registerAdvancement();
this.advance();
let endValue = res.register(this.expr());
if (res.error) return res;
let stepValue = null;
if (this.currentTok.matches(TT.KEYWORD, LEX.keywords.step)) {
res.registerAdvancement();
this.advance();
stepValue = res.register(this.expr());
if (res.error) return res;
}
if (this.currentTok.type !== TT.LCURL) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.leftCurly.source)}'`
)
);
}
res.registerAdvancement();
this.advance();
body = res.register(this.statements());
if (res.error) return res;
if (this.currentTok.type !== TT.RCURL) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightCurly.source)}'`
)
);
}
res.registerAdvancement();
this.advance();
return res.success(
new ForNode(varName, startValue, endValue, stepValue, body)
);
};
/** generates a for each node */
forEachExpr = (varName) => {
let res = new ParseResult();
// if not a keyword, that's a problem
if (!this.currentTok.matches(TT.KEYWORD, LEX.keywords.in)) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.keywords.in)}'`
)
);
}
res.registerAdvancement();
this.advance();
let iteration = null;
if (this.currentTok.type === TT.LSQUARE) {
iteration = res.register(this.listExpr());
if (res.error) return res;
} else if (this.currentTok.type === TT.STRING) {
iteration = res.register(this.atom());
if (res.error) return res;
} else if (this.currentTok.type === TT.IDENTIFIER) {
iteration = res.register(this.call());
if (res.error) return res;
} else {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.leftSquare.source)}', '${lc(TT.STRING)}' or '${lc(
TT.IDENTIFIER
)}'`
)
);
}
if (this.currentTok.type !== TT.LCURL) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.leftCurly.source)}'`
)
);
}
res.registerAdvancement();
this.advance();
let body = res.register(this.statements());
if (res.error) return res;
if (this.currentTok.type !== TT.RCURL) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightCurly.source)}'`
)
);
}
res.registerAdvancement();
this.advance();
return res.success(new ForEachNode(varName, iteration, body));
};
/** creates a while node */
whileExpr = () => {
let res = new ParseResult();
let body = null;
if (!this.currentTok.matches(TT.KEYWORD, LEX.keywords.while)) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${LEX.keywords.while}'`
)
);
}
res.registerAdvancement();
this.advance();
let condition = res.register(this.expr());
if (res.error) return res;
if (this.currentTok.type !== TT.LCURL) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.leftCurly.source)}'`
)
);
}
res.registerAdvancement();
this.advance();
body = res.register(this.statements());
if (res.error) return res;
if (this.currentTok.type !== TT.RCURL) {
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightCurly.source)}'`
)
);
}
res.registerAdvancement();
this.advance();
return res.success(new WhileNode(condition, body));
};
/** creates a function definition node */
funcDef = () => {
let res = new ParseResult();
let varNameTok = null;
let body = null;
if (!this.currentTok.matches(TT.KEYWORD, LEX.keywords.function))
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${LEX.keywords.function}'`
)
);
res.registerAdvancement();
this.advance();
if (this.currentTok.type === TT.IDENTIFIER) {
varNameTok = this.currentTok;
res.registerAdvancement();
this.advance();
}
if (this.currentTok.type !== TT.LPAREN)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
varNameTok
? `Expected '${lc(LEX.leftParen.source)}'`
: `Expected ${lc(TT.IDENTIFIER)} or '${lc(LEX.leftParen.source)}'`
)
);
res.registerAdvancement();
this.advance();
let argNameToks = [];
// skip past any new lines
while (this.currentTok.type === TT.NEWLINE) {
res.registerAdvancement();
this.advance();
}
if (this.currentTok.type === TT.IDENTIFIER) {
argNameToks.push(this.currentTok);
res.registerAdvancement();
this.advance();
while (this.currentTok.type === TT.COMMA) {
res.registerAdvancement();
this.advance();
// skip past any more new lines
while (this.currentTok.type === TT.NEWLINE) {
res.registerAdvancement();
this.advance();
}
if (this.currentTok.type !== TT.IDENTIFIER)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
varNameTok
? `Expected '${lc(LEX.leftParen.source)}'`
: `Expected ${lc(TT.IDENTIFIER)} or '${lc(
LEX.leftParen.source
)}'`
)
);
argNameToks.push(this.currentTok);
res.registerAdvancement();
this.advance();
}
if (this.currentTok.type !== TT.RPAREN)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.comma.source)}' or '${lc(
LEX.rightParen.source
)}'`
)
);
} else {
if (this.currentTok.type !== TT.RPAREN)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected ${lc(TT.IDENTIFIER)} or '${lc(LEX.rightParen.source)}'`
)
);
}
res.registerAdvancement();
this.advance();
if (this.currentTok.type !== TT.LCURL)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.leftCurly.source)}'`
)
);
res.registerAdvancement();
this.advance();
body = res.register(this.statements());
if (res.error) return res;
if (this.currentTok.type !== TT.RCURL)
return res.failure(
new InvalidSyntaxError(
this.currentTok.posStart,
this.currentTok.posEnd,
`Expected '${lc(LEX.rightCurly.source)}'`
)
);
res.registerAdvancement();
this.advance();
return res.success(new FuncDefNode(varNameTok, argNameToks, body));
};
/**
* creates a binary operation node
* @param {Function} funcA method used to parse the left side of the binary operation node
* @param {String[]} ops list of accepted operators for this operation
* @param {Function} funcB method used to parse the right side of the binary operation node
*/
binOp(funcA, ops, funcB = null) {
if (funcB === null) funcB = funcA;
let res = new ParseResult();
let left = res.register(funcA());
if (res.error) return res;
while (ops.includes(this.currentTok.type)) {
let opTok = this.currentTok;
res.registerAdvancement();
this.advance();
let right = res.register(funcB());
if (res.error) return res;
left = new BinOpNode(left, opTok, right);
}
return res.success(left);
}
}
module.exports = Parser;