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; package com.annimon.ownlang.exceptions;
import com.annimon.ownlang.parser.Pos;
/** /**
* *
* @author aNNiMON * @author aNNiMON
*/ */
public final class ParseException extends RuntimeException { public final class ParseException extends RuntimeException {
public ParseException() { private final Pos start;
super(); private final Pos end;
public ParseException(String message) {
this(message, Pos.ZERO, Pos.ZERO);
} }
public ParseException(String string) { public ParseException(String message, Pos pos) {
super(string); 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 @Override
public String toString() { 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.lib.UserDefinedFunction;
import com.annimon.ownlang.parser.ast.*; import com.annimon.ownlang.parser.ast.*;
import java.util.*; 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)) { while (!match(TokenType.EOF)) {
try { try {
result.add(statement()); result.add(statement());
} catch (ParseException parseException) {
parseErrors.add(parseException, parseException.getStart());
recover();
} catch (Exception ex) { } catch (Exception ex) {
parseErrors.add(ex, getErrorPos()); parseErrors.add(ex, getPos());
recover(); recover();
} }
} }
@ -79,7 +84,7 @@ public final class Parser {
return result; return result;
} }
private Pos getErrorPos() { private Pos getPos() {
if (size == 0) return new Pos(0, 0); if (size == 0) return new Pos(0, 0);
if (pos >= size) return tokens.get(size - 1).pos(); if (pos >= size) return tokens.get(size - 1).pos();
return tokens.get(pos).pos(); return tokens.get(pos).pos();
@ -166,7 +171,8 @@ public final class Parser {
private UseStatement useStatement() { private UseStatement useStatement() {
final var modules = new HashSet<String>(); final var modules = new HashSet<String>();
do { do {
modules.add(consume(TokenType.WORD).text()); modules.add(consumeOrExplainError(TokenType.WORD,
Parser::explainUseStatementError).text());
} while (match(TokenType.COMMA)); } while (match(TokenType.COMMA));
return new UseStatement(modules); return new UseStatement(modules);
} }
@ -179,21 +185,26 @@ public final class Parser {
if (expression instanceof Statement statement) { if (expression instanceof Statement statement) {
return statement; return statement;
} }
throw new ParseException("Unknown statement: " + get(0)); throw error("Unknown statement: " + get(0));
} }
private DestructuringAssignmentStatement destructuringAssignment() { private DestructuringAssignmentStatement destructuringAssignment() {
// extract(var1, var2, ...) = ... // extract(var1, var2, ...) = ...
final var startPos = getPos();
consume(TokenType.LPAREN); consume(TokenType.LPAREN);
final List<String> variables = new ArrayList<>(); final List<String> variables = new ArrayList<>();
while (!match(TokenType.RPAREN)) { while (!match(TokenType.RPAREN)) {
if (lookMatch(0, TokenType.WORD)) { final Token current = get(0);
variables.add(consume(TokenType.WORD).text()); variables.add(switch (current.type()) {
} else { case WORD -> consume(TokenType.WORD).text();
variables.add(null); case COMMA -> null;
} default -> throw error(errorUnexpectedTokens(current, TokenType.WORD, TokenType.COMMA));
});
match(TokenType.COMMA); match(TokenType.COMMA);
} }
if (variables.isEmpty() || variables.stream().allMatch(Objects::isNull)) {
throw error(errorDestructuringAssignmentEmpty(), startPos, getPos());
}
consume(TokenType.EQ); consume(TokenType.EQ);
return new DestructuringAssignmentStatement(variables, expression()); return new DestructuringAssignmentStatement(variables, expression());
} }
@ -299,7 +310,7 @@ public final class Parser {
} else if (!startsOptionalArgs) { } else if (!startsOptionalArgs) {
arguments.addRequired(name); arguments.addRequired(name);
} else { } else {
throw new ParseException("Required argument cannot be after optional"); throw error(errorRequiredArgumentAfterOptional());
} }
match(TokenType.COMMA); match(TokenType.COMMA);
} }
@ -374,7 +385,7 @@ public final class Parser {
private MatchExpression match() { private MatchExpression match() {
// match expression { // match expression {
// case pattern1: result1 // case pattern1: result1
// case pattern2 if extr: result2 // case pattern2 if expr: result2
// } // }
final Expression expression = expression(); final Expression expression = expression();
consume(TokenType.LBRACE); consume(TokenType.LBRACE);
@ -425,7 +436,7 @@ public final class Parser {
} }
if (pattern == null) { if (pattern == null) {
throw new ParseException("Wrong pattern in match expression: " + current); throw error("Wrong pattern in match expression: " + current);
} }
if (match(TokenType.IF)) { if (match(TokenType.IF)) {
// case e if e > 0: // case e if e > 0:
@ -461,7 +472,7 @@ public final class Parser {
if (fieldDeclaration != null) { if (fieldDeclaration != null) {
classDeclaration.addField(fieldDeclaration); classDeclaration.addField(fieldDeclaration);
} else { } 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)); } while (!match(TokenType.RBRACE));
@ -873,12 +884,12 @@ public final class Parser {
} }
return strExpr; return strExpr;
} }
throw new ParseException("Unknown expression: " + current); throw error("Unknown expression: " + current);
} }
private Number createNumber(String text, int radix) { private Number createNumber(String text, int radix) {
// Double // Double
if (text.contains(".")) { if (text.contains(".") || text.contains("e") || text.contains("E")) {
return Double.parseDouble(text); return Double.parseDouble(text);
} }
// Integer // Integer
@ -889,13 +900,23 @@ public final class Parser {
} }
} }
private Token consume(TokenType type) { private Token consume(TokenType expectedType) {
final Token current = get(0); final Token actual = get(0);
if (type != current.type()) { if (expectedType != actual.type()) {
throw new ParseException("Token " + current + " doesn't match " + type); throw error(errorUnexpectedToken(actual, expectedType));
} }
pos++; 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) { private boolean match(TokenType type) {
@ -916,4 +937,38 @@ public final class Parser {
if (position >= size) return EOF; if (position >= size) return EOF;
return tokens.get(position); 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; package com.annimon.ownlang.parser;
public record Pos(int row, int col) { 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() { public Pos normalize() {
return new Pos(Math.max(0, row - 1), Math.max(0, col - 1)); 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 record Token(TokenType type, String text, Pos pos) {
public String shortDescription() {
return type().name() + " " + text;
}
@Override @Override
public String toString() { public String toString() {
return type.name() + " " + pos().format() + " " + text; return type.name() + " " + pos().format() + " " + text;