Better explaining parse errors

This commit is contained in:
aNNiMON 2023-09-16 22:22:29 +03:00 committed by Victor Melnik
parent 95d32a6c91
commit da950f96c2
5 changed files with 107 additions and 27 deletions

View File

@ -1,16 +1,35 @@
package com.annimon.ownlang.exceptions;
import com.annimon.ownlang.parser.Pos;
/**
*
* @author aNNiMON
*/
public final class ParseException extends RuntimeException {
public ParseException() {
super();
private final Pos start;
private final Pos end;
public ParseException(String message) {
this(message, Pos.ZERO, Pos.ZERO);
}
public ParseException(String string) {
super(string);
public ParseException(String message, Pos pos) {
this(message, pos, pos);
}
public ParseException(String message, Pos start, Pos end) {
super(message);
this.start = start;
this.end = end;
}
public Pos getStart() {
return start;
}
public Pos getEnd() {
return end;
}
}

View File

@ -4,6 +4,6 @@ public record ParseError(Exception exception, Pos pos) {
@Override
public String toString() {
return "ParseError on line " + pos.row() + ": " + exception;
return "Error on line " + pos.row() + ": " + exception;
}
}

View File

@ -6,6 +6,8 @@ import com.annimon.ownlang.lib.StringValue;
import com.annimon.ownlang.lib.UserDefinedFunction;
import com.annimon.ownlang.parser.ast.*;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
*
@ -70,8 +72,11 @@ public final class Parser {
while (!match(TokenType.EOF)) {
try {
result.add(statement());
} catch (ParseException parseException) {
parseErrors.add(parseException, parseException.getStart());
recover();
} catch (Exception ex) {
parseErrors.add(ex, getErrorPos());
parseErrors.add(ex, getPos());
recover();
}
}
@ -79,7 +84,7 @@ public final class Parser {
return result;
}
private Pos getErrorPos() {
private Pos getPos() {
if (size == 0) return new Pos(0, 0);
if (pos >= size) return tokens.get(size - 1).pos();
return tokens.get(pos).pos();
@ -166,7 +171,8 @@ public final class Parser {
private UseStatement useStatement() {
final var modules = new HashSet<String>();
do {
modules.add(consume(TokenType.WORD).text());
modules.add(consumeOrExplainError(TokenType.WORD,
Parser::explainUseStatementError).text());
} while (match(TokenType.COMMA));
return new UseStatement(modules);
}
@ -179,21 +185,26 @@ public final class Parser {
if (expression instanceof Statement statement) {
return statement;
}
throw new ParseException("Unknown statement: " + get(0));
throw error("Unknown statement: " + get(0));
}
private DestructuringAssignmentStatement destructuringAssignment() {
// extract(var1, var2, ...) = ...
final var startPos = getPos();
consume(TokenType.LPAREN);
final List<String> variables = new ArrayList<>();
while (!match(TokenType.RPAREN)) {
if (lookMatch(0, TokenType.WORD)) {
variables.add(consume(TokenType.WORD).text());
} else {
variables.add(null);
}
final Token current = get(0);
variables.add(switch (current.type()) {
case WORD -> consume(TokenType.WORD).text();
case COMMA -> null;
default -> throw error(errorUnexpectedTokens(current, TokenType.WORD, TokenType.COMMA));
});
match(TokenType.COMMA);
}
if (variables.isEmpty() || variables.stream().allMatch(Objects::isNull)) {
throw error(errorDestructuringAssignmentEmpty(), startPos, getPos());
}
consume(TokenType.EQ);
return new DestructuringAssignmentStatement(variables, expression());
}
@ -299,7 +310,7 @@ public final class Parser {
} else if (!startsOptionalArgs) {
arguments.addRequired(name);
} else {
throw new ParseException("Required argument cannot be after optional");
throw error(errorRequiredArgumentAfterOptional());
}
match(TokenType.COMMA);
}
@ -374,7 +385,7 @@ public final class Parser {
private MatchExpression match() {
// match expression {
// case pattern1: result1
// case pattern2 if extr: result2
// case pattern2 if expr: result2
// }
final Expression expression = expression();
consume(TokenType.LBRACE);
@ -425,7 +436,7 @@ public final class Parser {
}
if (pattern == null) {
throw new ParseException("Wrong pattern in match expression: " + current);
throw error("Wrong pattern in match expression: " + current);
}
if (match(TokenType.IF)) {
// case e if e > 0:
@ -461,7 +472,7 @@ public final class Parser {
if (fieldDeclaration != null) {
classDeclaration.addField(fieldDeclaration);
} else {
throw new ParseException("Class can contain only assignments and function declarations");
throw error("Class can contain only assignments and function declarations");
}
}
} while (!match(TokenType.RBRACE));
@ -873,12 +884,12 @@ public final class Parser {
}
return strExpr;
}
throw new ParseException("Unknown expression: " + current);
throw error("Unknown expression: " + current);
}
private Number createNumber(String text, int radix) {
// Double
if (text.contains(".")) {
if (text.contains(".") || text.contains("e") || text.contains("E")) {
return Double.parseDouble(text);
}
// Integer
@ -889,13 +900,23 @@ public final class Parser {
}
}
private Token consume(TokenType type) {
final Token current = get(0);
if (type != current.type()) {
throw new ParseException("Token " + current + " doesn't match " + type);
private Token consume(TokenType expectedType) {
final Token actual = get(0);
if (expectedType != actual.type()) {
throw error(errorUnexpectedToken(actual, expectedType));
}
pos++;
return current;
return actual;
}
private Token consumeOrExplainError(TokenType expectedType, Function<Token, String> errorMessageFunction) {
final Token actual = get(0);
if (expectedType != actual.type()) {
throw error(errorUnexpectedToken(actual, expectedType)
+ errorMessageFunction.apply(actual));
}
pos++;
return actual;
}
private boolean match(TokenType type) {
@ -916,4 +937,38 @@ public final class Parser {
if (position >= size) return EOF;
return tokens.get(position);
}
private ParseException error(String message) {
return new ParseException(message, getPos());
}
private static ParseException error(String message, Pos start, Pos end) {
return new ParseException(message, start, end);
}
private static String errorUnexpectedToken(Token actual, TokenType expectedType) {
return "Expected token with type " + expectedType + ", but found " + actual.shortDescription();
}
private static String errorUnexpectedTokens(Token actual, TokenType... expectedTypes) {
String tokenTypes = Arrays.stream(expectedTypes).map(Enum::toString).collect(Collectors.joining(", "));
return "Expected tokens with types one of " + tokenTypes + ", but found " + actual.shortDescription();
}
private static String errorDestructuringAssignmentEmpty() {
return "Destructuring assignment should contain at least one variable name to assign." +
"\nCorrect syntax: extract(v1, , , v4) = ";
}
private static String errorRequiredArgumentAfterOptional() {
return "Required argument cannot be placed after optional.";
}
private static String explainUseStatementError(Token current) {
String example = current.type().equals(TokenType.TEXT)
? "use " + current.text()
: "use std, math";
return "\nNote: as of OwnLang 2.0.0 use statement simplifies modules list syntax. " +
"Correct syntax: " + example;
}
}

View File

@ -1,6 +1,8 @@
package com.annimon.ownlang.parser;
public record Pos(int row, int col) {
public static final Pos UNKNOWN = new Pos(-1, -1);
public static final Pos ZERO = new Pos(0, 0);
public Pos normalize() {
return new Pos(Math.max(0, row - 1), Math.max(0, col - 1));

View File

@ -5,6 +5,10 @@ package com.annimon.ownlang.parser;
*/
public record Token(TokenType type, String text, Pos pos) {
public String shortDescription() {
return type().name() + " " + text;
}
@Override
public String toString() {
return type.name() + " " + pos().format() + " " + text;