diff --git a/lex.js b/lex.js index 7f2215a..d1d18c8 100644 --- a/lex.js +++ b/lex.js @@ -144,7 +144,7 @@ } previousLinesLength = this.newlines[i] + 1; } - return {line:line, pos:pos - previousLinesLength + 1}; + return {inputPos:pos, line:line, pos:pos - previousLinesLength + 1}; } tokenIndexToLinePos(index) diff --git a/lex.js b/lex.js index 7f2215a..d1d18c8 100644 --- a/lex.js +++ b/lex.js @@ -144,7 +144,7 @@ } previousLinesLength = this.newlines[i] + 1; } - return {line:line, pos:pos - previousLinesLength + 1}; + return {inputPos:pos, line:line, pos:pos - previousLinesLength + 1}; } tokenIndexToLinePos(index) diff --git a/parse.js b/parse.js index 7b132a3..9746b7c 100644 --- a/parse.js +++ b/parse.js @@ -82,6 +82,8 @@ Statements:9, // a continuous text block of statements Block:10, // a set of statements in a { } block If:11, // if statement with condition, trueCase, and optional falseCase + For:12, // a for statement with loop variable, range, and loop body + Map:13, // like a for statement, but is actually an expression where you can 'return' in the body and generate an array }); const StatementsWithSemicolon = Object.freeze([ @@ -124,7 +126,7 @@ let error, mostErrorMatched = 0; while (!error) { - if (!this.tokenStream.peek(lex.TokenType.Op, "}").error) + if (!this.tokenStream.peek(lex.TokenType.Op, "}").error || this.tokenStream.eof()) { break; } @@ -160,13 +162,98 @@ { if (error = this.tokenStream.consume(lex.TokenType.Op, ';').error) { - return this.getError(error); + return this.getError(error, true); } } return { ast:stmt, error: null }; } + exprInBrackets(expectingMatch = true) + { + let error; + + error = this.tokenStream.consume(lex.TokenType.Op, "(").error; + if (error) return this.getError(error, expectingMatch); + + let expr; + ({ ast:expr, error } = this.attemptSubtree(this.expression)); + if (error) return this.getError(error, expectingMatch); + + error = this.tokenStream.consume(lex.TokenType.Op, ")").error; + if (error) return this.getError(error, expectingMatch); + + return { ast:expr, error: null }; + } + + onStmt() + { + let error; + + error = this.tokenStream.consume(lex.TokenType.Word, "on").error; + if (error) return this.getError(error); + + let condition; + ({ ast:condition, error } = this.exprInBrackets()); + if (error) return this.getError(error, true); + + let trueCase; + ({ ast:trueCase, error } = this.attemptSubtree(this.block)); + if (error) return this.getError(error, true); + + let ret = { ast: { node: ASTNode.If, condition: condition, trueCase: trueCase }, error: null }; + + if (!this.tokenStream.consume(lex.TokenType.Word, "else").error) + { + let falseCase; + ({ ast:falseCase, error } = this.attemptSubtree(this.block)); + if (error) return this.getError(error, true); + + ret.ast.falseCase = falseCase; + } + + return ret; + } + + forStmt() + { + let error = this.tokenStream.consume(lex.TokenType.Word, "for").error; + if (error) return this.getError(error); + + let { ast:forBody, error:bodyError } = this.forBody(); + if (bodyError) return this.getError(bodyError, true); + + return { ast: { node: ASTNode.For, variable: forBody.variable, range: forBody.range, loop: forBody.loop }, error: null }; + } + + forBody() + { + let error; + + error = this.tokenStream.consume(lex.TokenType.Op, "(").error; + if (error) return this.getError(error, true); + + let variable; + ({ token:variable, error } = this.tokenStream.consume(lex.TokenType.Word)); + if (error) return this.getError(error, true); + + error = this.tokenStream.consume(lex.TokenType.Word, "in").error; + if (error) return this.getError(error, true); + + let range; + ({ ast:range, error } = this.attemptSubtree(this.expression)); + if (error) return this.getError(error, true); + + error = this.tokenStream.consume(lex.TokenType.Op, ")").error; + if (error) return this.getError(error, true); + + let loop; + ({ ast:loop, error } = this.attemptSubtree(this.block)); + if (error) return this.getError(error, true); + + return { ast: { variable: variable, range: range, loop: loop }, error: null }; + } + ifStmt() { let error; @@ -174,14 +261,8 @@ error = this.tokenStream.consume(lex.TokenType.Word, "if").error; if (error) return this.getError(error); - error = this.tokenStream.consume(lex.TokenType.Op, "(").error; - if (error) return this.getError(error, true); - let condition; - ({ ast:condition, error } = this.attemptSubtree(this.expression)); - if (error) return this.getError(error, true); - - error = this.tokenStream.consume(lex.TokenType.Op, ")").error; + ({ ast:condition, error } = this.exprInBrackets()); if (error) return this.getError(error, true); let trueCase; @@ -208,6 +289,7 @@ [ { parse: this.expression, handle: ast => ({ node: ASTNode.ExprStatement, expr: ast }) }, { parse: this.ifStmt, handle: ast => ast }, + { parse: this.forStmt, handle: ast => ast }, ], "Expecting statement"); let error, bestError, numMatched, bestNumMatched = 0; @@ -216,7 +298,7 @@ expression() { let result = this.attemptSubtree(this.expressionInternal, 0); - if (result.error) return this.getError(result.error, result.numMatched > this.mustMatchBeforeConsiderExpression); + if (result.error) return this.getError(result.error, result.error.expectingMatch || result.numMatched > this.mustMatchBeforeConsiderExpression); return result; } @@ -291,7 +373,14 @@ } else if (type == lex.TokenType.Word) { - if (Keywords.includes(token)) + if (token == "map") + { + let { ast:mapBody, error } = this.forBody(); + if (error) return this.getError(error, true); + + lhs = { node:ASTNode.Map, variable: mapBody.variable, range: mapBody.range, loop: mapBody.loop }; + } + else if (Keywords.includes(token)) { // TODO: handle keywords that can go in expressions return this.getError({ msg: "Keyword '" + token + "' not expected", ...this.tokenStream.stringPosToLinePos(pos) }); @@ -346,7 +435,7 @@ } //... or our current lhs, in which case we get to consume the token - this.tokenStream.consumeCount(token.length); + this.tokenStream.consumeCount(opToken.length); if (opToken == "[") { diff --git a/lex.js b/lex.js index 7f2215a..d1d18c8 100644 --- a/lex.js +++ b/lex.js @@ -144,7 +144,7 @@ } previousLinesLength = this.newlines[i] + 1; } - return {line:line, pos:pos - previousLinesLength + 1}; + return {inputPos:pos, line:line, pos:pos - previousLinesLength + 1}; } tokenIndexToLinePos(index) diff --git a/parse.js b/parse.js index 7b132a3..9746b7c 100644 --- a/parse.js +++ b/parse.js @@ -82,6 +82,8 @@ Statements:9, // a continuous text block of statements Block:10, // a set of statements in a { } block If:11, // if statement with condition, trueCase, and optional falseCase + For:12, // a for statement with loop variable, range, and loop body + Map:13, // like a for statement, but is actually an expression where you can 'return' in the body and generate an array }); const StatementsWithSemicolon = Object.freeze([ @@ -124,7 +126,7 @@ let error, mostErrorMatched = 0; while (!error) { - if (!this.tokenStream.peek(lex.TokenType.Op, "}").error) + if (!this.tokenStream.peek(lex.TokenType.Op, "}").error || this.tokenStream.eof()) { break; } @@ -160,13 +162,98 @@ { if (error = this.tokenStream.consume(lex.TokenType.Op, ';').error) { - return this.getError(error); + return this.getError(error, true); } } return { ast:stmt, error: null }; } + exprInBrackets(expectingMatch = true) + { + let error; + + error = this.tokenStream.consume(lex.TokenType.Op, "(").error; + if (error) return this.getError(error, expectingMatch); + + let expr; + ({ ast:expr, error } = this.attemptSubtree(this.expression)); + if (error) return this.getError(error, expectingMatch); + + error = this.tokenStream.consume(lex.TokenType.Op, ")").error; + if (error) return this.getError(error, expectingMatch); + + return { ast:expr, error: null }; + } + + onStmt() + { + let error; + + error = this.tokenStream.consume(lex.TokenType.Word, "on").error; + if (error) return this.getError(error); + + let condition; + ({ ast:condition, error } = this.exprInBrackets()); + if (error) return this.getError(error, true); + + let trueCase; + ({ ast:trueCase, error } = this.attemptSubtree(this.block)); + if (error) return this.getError(error, true); + + let ret = { ast: { node: ASTNode.If, condition: condition, trueCase: trueCase }, error: null }; + + if (!this.tokenStream.consume(lex.TokenType.Word, "else").error) + { + let falseCase; + ({ ast:falseCase, error } = this.attemptSubtree(this.block)); + if (error) return this.getError(error, true); + + ret.ast.falseCase = falseCase; + } + + return ret; + } + + forStmt() + { + let error = this.tokenStream.consume(lex.TokenType.Word, "for").error; + if (error) return this.getError(error); + + let { ast:forBody, error:bodyError } = this.forBody(); + if (bodyError) return this.getError(bodyError, true); + + return { ast: { node: ASTNode.For, variable: forBody.variable, range: forBody.range, loop: forBody.loop }, error: null }; + } + + forBody() + { + let error; + + error = this.tokenStream.consume(lex.TokenType.Op, "(").error; + if (error) return this.getError(error, true); + + let variable; + ({ token:variable, error } = this.tokenStream.consume(lex.TokenType.Word)); + if (error) return this.getError(error, true); + + error = this.tokenStream.consume(lex.TokenType.Word, "in").error; + if (error) return this.getError(error, true); + + let range; + ({ ast:range, error } = this.attemptSubtree(this.expression)); + if (error) return this.getError(error, true); + + error = this.tokenStream.consume(lex.TokenType.Op, ")").error; + if (error) return this.getError(error, true); + + let loop; + ({ ast:loop, error } = this.attemptSubtree(this.block)); + if (error) return this.getError(error, true); + + return { ast: { variable: variable, range: range, loop: loop }, error: null }; + } + ifStmt() { let error; @@ -174,14 +261,8 @@ error = this.tokenStream.consume(lex.TokenType.Word, "if").error; if (error) return this.getError(error); - error = this.tokenStream.consume(lex.TokenType.Op, "(").error; - if (error) return this.getError(error, true); - let condition; - ({ ast:condition, error } = this.attemptSubtree(this.expression)); - if (error) return this.getError(error, true); - - error = this.tokenStream.consume(lex.TokenType.Op, ")").error; + ({ ast:condition, error } = this.exprInBrackets()); if (error) return this.getError(error, true); let trueCase; @@ -208,6 +289,7 @@ [ { parse: this.expression, handle: ast => ({ node: ASTNode.ExprStatement, expr: ast }) }, { parse: this.ifStmt, handle: ast => ast }, + { parse: this.forStmt, handle: ast => ast }, ], "Expecting statement"); let error, bestError, numMatched, bestNumMatched = 0; @@ -216,7 +298,7 @@ expression() { let result = this.attemptSubtree(this.expressionInternal, 0); - if (result.error) return this.getError(result.error, result.numMatched > this.mustMatchBeforeConsiderExpression); + if (result.error) return this.getError(result.error, result.error.expectingMatch || result.numMatched > this.mustMatchBeforeConsiderExpression); return result; } @@ -291,7 +373,14 @@ } else if (type == lex.TokenType.Word) { - if (Keywords.includes(token)) + if (token == "map") + { + let { ast:mapBody, error } = this.forBody(); + if (error) return this.getError(error, true); + + lhs = { node:ASTNode.Map, variable: mapBody.variable, range: mapBody.range, loop: mapBody.loop }; + } + else if (Keywords.includes(token)) { // TODO: handle keywords that can go in expressions return this.getError({ msg: "Keyword '" + token + "' not expected", ...this.tokenStream.stringPosToLinePos(pos) }); @@ -346,7 +435,7 @@ } //... or our current lhs, in which case we get to consume the token - this.tokenStream.consumeCount(token.length); + this.tokenStream.consumeCount(opToken.length); if (opToken == "[") { diff --git a/test.js b/test.js index 8ba2fe7..0b0949a 100644 --- a/test.js +++ b/test.js @@ -9,9 +9,22 @@ const parser = new parse.Parser(tokens); - const result = parser.block(); + const result = parser.statements(); - console.format(parse.getPrettyAST(result)); + if (result.error) + { + console.log("Error: " + result.error.msg + " on line " + result.error.line + " char " + result.error.pos); + + let relevent = expr.substring(Math.max(0, result.error.inputPos - 50), result.error.inputPos + 1).trim(); + const parts = relevent.split("\n"); + relevent = parts[parts.length - 1]; + console.log(relevent); + console.log(" ".repeat(Math.max(0, relevent.length - 1)) + "^"); + } + else + { + console.format(parse.getPrettyAST(result)); + } } debugExpr(fs.readFileSync(process.argv[2], "utf8"));