[muscript] Slightly better debugging experience

This commit is contained in:
Johannes Frohnmeyer 2022-06-13 10:44:59 +02:00
parent 626580af63
commit ed680f57a9
Signed by: Johannes
GPG Key ID: E76429612C2929F4
5 changed files with 64 additions and 32 deletions

View File

@ -3,9 +3,29 @@ package io.gitlab.jfronny.muscript.compiler;
/**
* Class for storing errors produced while parsing.
*/
public record Error(int line, int character, char ch, String message) {
public record Error(String source, int character, String message) {
@Override
public String toString() {
return String.format("[line %d, character %d] at '%s': %s", line, character, ch, message);
StringBuilder builder = new StringBuilder();
if (character >= source.length()) {
builder.append("Error at character ").append(character);
}
else {
builder.append("Error at '").append(source.charAt(character)).append("' (character ").append(character).append(")");
}
builder.append(": ").append(message);
int lineStart = source.lastIndexOf('\n', character);
int lineEnd = source.indexOf('\n', character);
if (lineEnd == -1) lineEnd = source.length();
String line = source.substring(lineStart + 1, lineEnd);
int lineNumber = lineStart > 0 ? (int) source.substring(0, lineStart).chars().filter(c -> c == '\n').count() : 1;
String linePrefix = String.format("%1$6d", lineNumber) + " | ";
builder.append('\n').append(linePrefix).append(line);
builder.append('\n').append(" ".repeat(linePrefix.length() + (character - lineStart - 1))).append("^-- Here");
return builder.toString();
}
}

View File

@ -7,11 +7,10 @@ public class Lexer {
/** The string representation of the token */
public String lexeme;
public int line = 1, character = -1;
public char ch;
private final String source;
private int start, current;
public final String source;
public int start, current;
public Lexer(String source) {
this.source = source;
@ -35,8 +34,8 @@ public class Lexer {
char c = advance();
if (isDigit(c) || (c == '-' && isDigit(peek()))) number();
else if (isAlpha(c)) identifier();
if (isDigit(c)) number();
else if (isIdentifier(c)) identifier();
else {
switch (c) {
case '\'', '"' -> string(c);
@ -72,7 +71,6 @@ public class Lexer {
private void string(char stringChar) {
while (!isAtEnd() && peek() != stringChar) {
if (peek() == '\n') line++;
advance();
}
@ -100,7 +98,7 @@ public class Lexer {
private void identifier() {
while (!isAtEnd()) {
char c = peek();
if (isAlpha(c) || isDigit(c)) {
if (isIdentifier(c) || isDigit(c)) {
advance();
} else if (c == ':' && peekNext() == ':') {
advance();
@ -123,11 +121,7 @@ public class Lexer {
char c = peek();
switch (c) {
case ' ', '\r', '\t' -> advance();
case '\n' -> {
line++;
advance();
}
case ' ', '\r', '\t', '\n' -> advance();
default -> {
start = current;
return;
@ -139,7 +133,7 @@ public class Lexer {
// Helpers
private void unexpected() {
createToken(Token.Error, "Unexpected character.");
createToken(Token.Error, "Unexpected character");
}
private void createToken(Token token, String lexeme) {
@ -160,7 +154,6 @@ public class Lexer {
}
private char advance() {
character++;
return ch = source.charAt(current++);
}
@ -182,7 +175,7 @@ public class Lexer {
return c >= '0' && c <= '9';
}
private boolean isAlpha(char c) {
private boolean isIdentifier(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' || c == '$';
}
}

View File

@ -230,7 +230,7 @@ public class Parser {
// Helpers
private ParseException error(String message) {
return new ParseException(new Error(current.line, current.character, current.ch, message));
return new ParseException(new Error(lexer.source, current.current - 1, message));
}
private TokenData consume(Token token, String message) {
@ -258,7 +258,7 @@ public class Parser {
previous.set(current);
lexer.next();
current.set(lexer.token, lexer.lexeme, lexer.line, lexer.character, lexer.ch);
current.set(lexer.token, lexer.lexeme, lexer.start, lexer.current, lexer.ch);
if (current.token == Token.Error) {
throw error(current.lexeme);
@ -275,19 +275,19 @@ public class Parser {
private static class TokenData {
public Token token;
public String lexeme;
public int line, character;
public int start, current;
public char ch;
public void set(Token token, String lexeme, int line, int character, char ch) {
public void set(Token token, String lexeme, int start, int current, char ch) {
this.token = token;
this.lexeme = lexeme;
this.line = line;
this.character = character;
this.start = start;
this.current = current;
this.ch = ch;
}
public void set(TokenData data) {
set(data.token, data.lexeme, data.line, data.character, data.ch);
set(data.token, data.lexeme, data.start, data.current, data.ch);
}
@Override

View File

@ -0,0 +1,27 @@
package io.gitlab.jfronny.muscript.test;
import io.gitlab.jfronny.muscript.compiler.*;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
public class ErrorTest {
@Test
void invalidCode() {
assertEquals("""
Error at 'e' (character 8): Expected number but is Boolean
1 | 15 + true
^-- Here""",
assertThrows(Parser.ParseException.class, () -> Parser.parse("15 + true")).error.toString());
assertEquals("""
Error at ''' (character 9): Expected number but is String
1 | 15 + 'yes'
^-- Here""",
assertThrows(Parser.ParseException.class, () -> Parser.parse("15 + 'yes'")).error.toString());
assertEquals("""
Error at '=' (character 7): Unexpected character
1 | string = 'Value'
^-- Here""",
assertThrows(Parser.ParseException.class, () -> Parser.parse("string = 'Value'")).error.toString());
}
}

View File

@ -1,6 +1,5 @@
package io.gitlab.jfronny.muscript.test;
import io.gitlab.jfronny.muscript.compiler.*;
import org.junit.jupiter.api.*;
import static io.gitlab.jfronny.muscript.test.MuTestUtil.*;
@ -15,11 +14,4 @@ public class StringTest {
assertTrue(bool("string == 'Value'"));
assertFalse(bool("string == 'Something else'"));
}
@Test
void invalidCode() {
assertThrows(Parser.ParseException.class, () -> Parser.parse("15 + true"));
assertThrows(Parser.ParseException.class, () -> Parser.parse("15 + true"));
assertThrows(Parser.ParseException.class, () -> Parser.parse("string = 'Value'"));
}
}