diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Error.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Error.java index bdd2c69..5c7b611 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Error.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Error.java @@ -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(); } } diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Lexer.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Lexer.java index 41ce5ed..dedea5f 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Lexer.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Lexer.java @@ -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 == '$'; } } diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Parser.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Parser.java index fa04bd2..4aa2439 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Parser.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/compiler/Parser.java @@ -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 diff --git a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/ErrorTest.java b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/ErrorTest.java new file mode 100644 index 0000000..8bdb04b --- /dev/null +++ b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/ErrorTest.java @@ -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()); + } +} diff --git a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/StringTest.java b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/StringTest.java index 6269278..8ddb2f4 100644 --- a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/StringTest.java +++ b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/StringTest.java @@ -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'")); - } }