diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/Console.java b/ownlang-core/src/main/java/com/annimon/ownlang/Console.java index 888d9f7..76cafa9 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/Console.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/Console.java @@ -3,10 +3,15 @@ package com.annimon.ownlang; import com.annimon.ownlang.lib.CallStack; import com.annimon.ownlang.outputsettings.ConsoleOutputSettings; import com.annimon.ownlang.outputsettings.OutputSettings; +import com.annimon.ownlang.stages.StagesData; +import com.annimon.ownlang.util.ErrorsLocationFormatterStage; +import com.annimon.ownlang.util.ExceptionConverterStage; +import com.annimon.ownlang.util.ExceptionStackTraceToStringStage; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.PrintStream; import java.nio.charset.StandardCharsets; +import java.util.List; public class Console { @@ -58,6 +63,17 @@ public class Console { outputSettings.error(value); } + public static void handleException(StagesData stagesData, Thread thread, Exception exception) { + String mainError = new ExceptionConverterStage() + .then((data, error) -> List.of(error)) + .then(new ErrorsLocationFormatterStage()) + .perform(stagesData, exception); + String callStack = CallStack.getFormattedCalls(); + String stackTrace = new ExceptionStackTraceToStringStage() + .perform(stagesData, exception); + error(String.join("\n", mainError, "Thread: " + thread.getName(), callStack, stackTrace)); + } + public static void handleException(Thread thread, Throwable throwable) { final ByteArrayOutputStream baos = new ByteArrayOutputStream(); try(final PrintStream ps = new PrintStream(baos)) { diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/exceptions/OwnLangRuntimeException.java b/ownlang-core/src/main/java/com/annimon/ownlang/exceptions/OwnLangRuntimeException.java index b85c0df..b353fc0 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/exceptions/OwnLangRuntimeException.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/exceptions/OwnLangRuntimeException.java @@ -1,19 +1,36 @@ package com.annimon.ownlang.exceptions; +import com.annimon.ownlang.util.Range; +import com.annimon.ownlang.util.SourceLocatedError; + /** * Base type for all runtime exceptions */ -public class OwnLangRuntimeException extends RuntimeException { +public class OwnLangRuntimeException extends RuntimeException implements SourceLocatedError { + + private final Range range; public OwnLangRuntimeException() { super(); + this.range = null; } public OwnLangRuntimeException(String message) { + this(message, (Range) null); + } + + public OwnLangRuntimeException(String message, Range range) { super(message); + this.range = range; } public OwnLangRuntimeException(String message, Throwable ex) { super(message, ex); + this.range = null; + } + + @Override + public Range getRange() { + return range; } } \ No newline at end of file diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/lib/CallStack.java b/ownlang-core/src/main/java/com/annimon/ownlang/lib/CallStack.java index e0a8cfe..0b8fffb 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/lib/CallStack.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/lib/CallStack.java @@ -1,7 +1,9 @@ package com.annimon.ownlang.lib; +import com.annimon.ownlang.util.Range; import java.util.Deque; import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.stream.Collectors; public final class CallStack { @@ -13,7 +15,7 @@ public final class CallStack { calls.clear(); } - public static synchronized void enter(String name, Function function, String position) { + public static synchronized void enter(String name, Function function, Range range) { String func = function.toString(); if (func.contains("com.annimon.ownlang.modules")) { func = func.replaceAll( @@ -22,7 +24,7 @@ public final class CallStack { if (func.contains("\n")) { func = func.substring(0, func.indexOf("\n")).trim(); } - calls.push(new CallInfo(name, func, position)); + calls.push(new CallInfo(name, func, range)); } public static synchronized void exit() { @@ -32,14 +34,24 @@ public final class CallStack { public static synchronized Deque getCalls() { return calls; } + + public static String getFormattedCalls() { + return calls.stream() + .map(CallInfo::format) + .collect(Collectors.joining("\n")); + } - public record CallInfo(String name, String function, String position) { + public record CallInfo(String name, String function, Range range) { + String format() { + return "\tat " + this; + } + @Override public String toString() { - if (position == null) { + if (range == null) { return String.format("%s: %s", name, function); } else { - return String.format("%s: %s %s", name, function, position); + return String.format("%s: %s %s", name, function, range.format()); } } } diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsLocationFormatterStage.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsLocationFormatterStage.java new file mode 100644 index 0000000..8712cbf --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsLocationFormatterStage.java @@ -0,0 +1,55 @@ +package com.annimon.ownlang.util; + +import com.annimon.ownlang.Console; +import com.annimon.ownlang.stages.Stage; +import com.annimon.ownlang.stages.StagesData; + +public class ErrorsLocationFormatterStage implements Stage, String> { + + @Override + public String perform(StagesData stagesData, Iterable input) { + final var sb = new StringBuilder(); + final String source = stagesData.get(SourceLoaderStage.TAG_SOURCE); + final var lines = source.split("\r?\n"); + for (SourceLocatedError error : input) { + sb.append(Console.newline()); + sb.append(error); + sb.append(Console.newline()); + final Range range = error.getRange(); + if (range != null) { + printPosition(sb, range.normalize(), lines); + } + } + return sb.toString(); + } + + private static void printPosition(StringBuilder sb, Range range, String[] lines) { + final Pos start = range.start(); + final int linesCount = lines.length;; + if (range.isOnSameLine()) { + if (start.row() < linesCount) { + sb.append(lines[start.row()]); + sb.append(Console.newline()); + sb.append(" ".repeat(start.col())); + sb.append("^".repeat(range.end().col() - start.col() + 1)); + sb.append(Console.newline()); + } + } else { + if (start.row() < linesCount) { + String line = lines[start.row()]; + sb.append(line); + sb.append(Console.newline()); + sb.append(" ".repeat(start.col())); + sb.append("^".repeat(Math.max(1, line.length() - start.col()))); + sb.append(Console.newline()); + } + final Pos end = range.end(); + if (end.row() < linesCount) { + sb.append(lines[end.row()]); + sb.append(Console.newline()); + sb.append("^".repeat(end.col())); + sb.append(Console.newline()); + } + } + } +} \ No newline at end of file diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsStackTraceFormatterStage.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsStackTraceFormatterStage.java new file mode 100644 index 0000000..1f0356e --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsStackTraceFormatterStage.java @@ -0,0 +1,20 @@ +package com.annimon.ownlang.util; + +import com.annimon.ownlang.Console; +import com.annimon.ownlang.stages.Stage; +import com.annimon.ownlang.stages.StagesData; + +public class ErrorsStackTraceFormatterStage implements Stage, String> { + + @Override + public String perform(StagesData stagesData, Iterable input) { + final var sb = new StringBuilder(); + for (SourceLocatedError error : input) { + if (!error.hasStackTrace()) continue; + for (StackTraceElement el : error.getStackTrace()) { + sb.append("\t").append(el).append(Console.newline()); + } + } + return sb.toString(); + } +} \ No newline at end of file diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/ExceptionConverterStage.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/ExceptionConverterStage.java new file mode 100644 index 0000000..1b912d1 --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/ExceptionConverterStage.java @@ -0,0 +1,12 @@ +package com.annimon.ownlang.util; + +import com.annimon.ownlang.stages.Stage; +import com.annimon.ownlang.stages.StagesData; + +public class ExceptionConverterStage implements Stage { + @Override + public SourceLocatedError perform(StagesData stagesData, Exception ex) { + if (ex instanceof SourceLocatedError sle) return sle; + return new SimpleError(ex.getMessage()); + } +} diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/ExceptionStackTraceToStringStage.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/ExceptionStackTraceToStringStage.java new file mode 100644 index 0000000..3b5150f --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/ExceptionStackTraceToStringStage.java @@ -0,0 +1,20 @@ +package com.annimon.ownlang.util; + +import com.annimon.ownlang.stages.Stage; +import com.annimon.ownlang.stages.StagesData; +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + +public class ExceptionStackTraceToStringStage implements Stage { + @Override + public String perform(StagesData stagesData, Exception ex) { + final var baos = new ByteArrayOutputStream(); + try (final PrintStream ps = new PrintStream(baos)) { + for (StackTraceElement traceElement : ex.getStackTrace()) { + ps.println("\tat " + traceElement); + } + } + return baos.toString(StandardCharsets.UTF_8); + } +} diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/SimpleError.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/SimpleError.java new file mode 100644 index 0000000..3b17494 --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/SimpleError.java @@ -0,0 +1,13 @@ +package com.annimon.ownlang.util; + +public record SimpleError(String message) implements SourceLocatedError { + @Override + public String getMessage() { + return message; + } + + @Override + public String toString() { + return message; + } +} diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLoaderStage.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLoaderStage.java new file mode 100644 index 0000000..f117c2e --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLoaderStage.java @@ -0,0 +1,48 @@ +package com.annimon.ownlang.util; + +import com.annimon.ownlang.exceptions.OwnLangRuntimeException; +import com.annimon.ownlang.stages.Stage; +import com.annimon.ownlang.stages.StagesData; +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class SourceLoaderStage implements Stage { + + public static final String TAG_SOURCE = "source"; + + @Override + public String perform(StagesData stagesData, String name) { + try { + String result = readSource(name); + stagesData.put(TAG_SOURCE, result); + return result; + } catch (IOException e) { + throw new OwnLangRuntimeException("Unable to read input " + name, e); + } + } + + private String readSource(String name) throws IOException { + try (InputStream is = getClass().getResourceAsStream("/" + name)) { + if (is != null) { + return readStream(is); + } + } + try (InputStream is = new FileInputStream(name)) { + return readStream(is); + } + } + + public static String readStream(InputStream is) throws IOException { + final ByteArrayOutputStream result = new ByteArrayOutputStream(); + final int bufferSize = 1024; + final byte[] buffer = new byte[bufferSize]; + int read; + while ((read = is.read(buffer)) != -1) { + result.write(buffer, 0, read); + } + return result.toString(StandardCharsets.UTF_8); + } +} \ No newline at end of file diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLocatedError.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLocatedError.java new file mode 100644 index 0000000..9487aaf --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLocatedError.java @@ -0,0 +1,19 @@ +package com.annimon.ownlang.util; + +public interface SourceLocatedError extends SourceLocation { + + String getMessage(); + + default StackTraceElement[] getStackTrace() { + return new StackTraceElement[0]; + } + + default boolean hasStackTrace() { + return !stackTraceIsEmpty(); + } + + private boolean stackTraceIsEmpty() { + final var st = getStackTrace(); + return st == null || st.length == 0; + } +} diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLocation.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLocation.java index f380314..af7aa00 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLocation.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLocation.java @@ -5,9 +5,4 @@ public interface SourceLocation { default Range getRange() { return null; } - - default String formatRange() { - final var range = getRange(); - return range == null ? "" : range.format(); - } } diff --git a/ownlang-desktop/src/main/java/com/annimon/ownlang/Main.java b/ownlang-desktop/src/main/java/com/annimon/ownlang/Main.java index dd02879..4ba9469 100644 --- a/ownlang-desktop/src/main/java/com/annimon/ownlang/Main.java +++ b/ownlang-desktop/src/main/java/com/annimon/ownlang/Main.java @@ -2,7 +2,9 @@ package com.annimon.ownlang; import com.annimon.ownlang.exceptions.OwnLangParserException; import com.annimon.ownlang.exceptions.StoppedException; -import com.annimon.ownlang.parser.*; +import com.annimon.ownlang.parser.Beautifier; +import com.annimon.ownlang.parser.SourceLoader; +import com.annimon.ownlang.parser.Token; import com.annimon.ownlang.parser.ast.Statement; import com.annimon.ownlang.parser.error.ParseErrorsFormatterStage; import com.annimon.ownlang.parser.linters.LinterStage; @@ -163,7 +165,7 @@ public final class Main { } catch (StoppedException ex) { // skip } catch (Exception ex) { - Console.handleException(Thread.currentThread(), ex); + Console.handleException(stagesData, Thread.currentThread(), ex); } finally { if (options.showTokens) { final List tokens = stagesData.get(LexerStage.TAG_TOKENS); diff --git a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/Parser.java b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/Parser.java index 149c3b7..a9dbd62 100644 --- a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/Parser.java +++ b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/Parser.java @@ -81,7 +81,7 @@ public final class Parser { parseErrors.add(new ParseError(ex.getMessage(), ex.getRange())); recover(); } catch (Exception ex) { - parseErrors.add(new ParseError(ex.getMessage(), getRange(), List.of(ex.getStackTrace()))); + parseErrors.add(new ParseError(ex.getMessage(), getRange(), ex.getStackTrace())); recover(); } } diff --git a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/FunctionalExpression.java b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/FunctionalExpression.java index 59d538e..f64734f 100644 --- a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/FunctionalExpression.java +++ b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/FunctionalExpression.java @@ -51,7 +51,7 @@ public final class FunctionalExpression extends InterruptableNode values[i] = arguments.get(i).eval(); } final Function f = consumeFunction(functionExpr); - CallStack.enter(functionExpr.toString(), f, formatRange()); + CallStack.enter(functionExpr.toString(), f, range); final Value result = f.execute(values); CallStack.exit(); return result; diff --git a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/error/ParseError.java b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/error/ParseError.java index d4dd19d..d67b108 100644 --- a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/error/ParseError.java +++ b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/error/ParseError.java @@ -1,16 +1,31 @@ package com.annimon.ownlang.parser.error; import com.annimon.ownlang.util.Range; -import java.util.Collections; -import java.util.List; +import com.annimon.ownlang.util.SourceLocatedError; + +public record ParseError( + String message, + Range range, + StackTraceElement[] stackTraceElements +) implements SourceLocatedError { -public record ParseError(String message, Range range, List stackTraceElements) { public ParseError(String message, Range range) { - this(message, range, Collections.emptyList()); + this(message, range, new StackTraceElement[0]); } - public boolean hasStackTrace() { - return !stackTraceElements.isEmpty(); + @Override + public String getMessage() { + return message; + } + + @Override + public Range getRange() { + return range; + } + + @Override + public StackTraceElement[] getStackTrace() { + return stackTraceElements; } @Override diff --git a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/error/ParseErrorsFormatterStage.java b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/error/ParseErrorsFormatterStage.java index cafc52f..9e9611f 100644 --- a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/error/ParseErrorsFormatterStage.java +++ b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/error/ParseErrorsFormatterStage.java @@ -1,63 +1,18 @@ package com.annimon.ownlang.parser.error; -import com.annimon.ownlang.Console; -import com.annimon.ownlang.util.Pos; -import com.annimon.ownlang.util.Range; -import com.annimon.ownlang.stages.SourceLoaderStage; import com.annimon.ownlang.stages.Stage; import com.annimon.ownlang.stages.StagesData; +import com.annimon.ownlang.util.ErrorsLocationFormatterStage; +import com.annimon.ownlang.util.ErrorsStackTraceFormatterStage; public class ParseErrorsFormatterStage implements Stage { @Override public String perform(StagesData stagesData, ParseErrors input) { - final var sb = new StringBuilder(); - final String source = stagesData.get(SourceLoaderStage.TAG_SOURCE); - final var lines = source.split("\r?\n"); - for (ParseError parseError : input) { - sb.append(Console.newline()); - sb.append(parseError); - sb.append(Console.newline()); - final Range range = parseError.range().normalize(); - printPosition(sb, range, lines); - if (parseError.hasStackTrace()) { - sb.append("Stack trace:"); - sb.append(Console.newline()); - for (StackTraceElement el : parseError.stackTraceElements()) { - sb.append(" ").append(el).append(Console.newline()); - } - } - } - return sb.toString(); - } - - private static void printPosition(StringBuilder sb, Range range, String[] lines) { - final Pos start = range.start(); - final int linesCount = lines.length;; - if (range.isOnSameLine()) { - if (start.row() < linesCount) { - sb.append(lines[start.row()]); - sb.append(Console.newline()); - sb.append(" ".repeat(start.col())); - sb.append("^".repeat(range.end().col() - start.col() + 1)); - sb.append(Console.newline()); - } - } else { - if (start.row() < linesCount) { - String line = lines[start.row()]; - sb.append(line); - sb.append(Console.newline()); - sb.append(" ".repeat(start.col())); - sb.append("^".repeat(Math.max(1, line.length() - start.col()))); - sb.append(Console.newline()); - } - final Pos end = range.end(); - if (end.row() < linesCount) { - sb.append(lines[end.row()]); - sb.append(Console.newline()); - sb.append("^".repeat(end.col())); - sb.append(Console.newline()); - } - } + String error = new ErrorsLocationFormatterStage() + .perform(stagesData, input); + String stackTrace = new ErrorsStackTraceFormatterStage() + .perform(stagesData, input); + return error + "\n" + stackTrace; } } diff --git a/ownlang-parser/src/main/java/com/annimon/ownlang/stages/SourceLoaderStage.java b/ownlang-parser/src/main/java/com/annimon/ownlang/stages/SourceLoaderStage.java deleted file mode 100644 index a9d70fc..0000000 --- a/ownlang-parser/src/main/java/com/annimon/ownlang/stages/SourceLoaderStage.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.annimon.ownlang.stages; - -import com.annimon.ownlang.exceptions.OwnLangRuntimeException; -import com.annimon.ownlang.parser.SourceLoader; -import java.io.IOException; - -public class SourceLoaderStage implements Stage { - - public static final String TAG_SOURCE = "source"; - - @Override - public String perform(StagesData stagesData, String input) { - try { - String result = SourceLoader.readSource(input); - stagesData.put(TAG_SOURCE, result); - return result; - } catch (IOException e) { - throw new OwnLangRuntimeException("Unable to read input " + input, e); - } - } -} diff --git a/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java b/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java index a6e059d..afc44c5 100644 --- a/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java +++ b/ownlang-parser/src/test/java/com/annimon/ownlang/parser/ProgramsTest.java @@ -9,6 +9,7 @@ import com.annimon.ownlang.parser.ast.Visitor; import com.annimon.ownlang.parser.optimization.OptimizationStage; import com.annimon.ownlang.parser.visitors.AbstractVisitor; import com.annimon.ownlang.stages.*; +import com.annimon.ownlang.util.SourceLoaderStage; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; diff --git a/ownlang-utils/src/main/java/com/annimon/ownlang/utils/Sandbox.java b/ownlang-utils/src/main/java/com/annimon/ownlang/utils/Sandbox.java index 7d23ed6..27f75ed 100644 --- a/ownlang-utils/src/main/java/com/annimon/ownlang/utils/Sandbox.java +++ b/ownlang-utils/src/main/java/com/annimon/ownlang/utils/Sandbox.java @@ -46,7 +46,7 @@ public final class Sandbox { } catch (Exception ex) { // ownlang call stack to stdout System.out.format("%s in %s%n", ex.getMessage(), Thread.currentThread().getName()); - CallStack.getCalls().forEach(call -> System.out.format("\tat %s%n", call)); + System.out.println(CallStack.getFormattedCalls()); // java stack trace to stderr Console.handleException(Thread.currentThread(), ex); }