mirror of
https://github.com/aNNiMON/Own-Programming-Language-Tutorial.git
synced 2024-09-20 00:34:20 +03:00
Generalized way to show source located errors (parse and runtime errors)
This commit is contained in:
parent
1fb9c8b3c5
commit
02e9d1f6c5
@ -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)) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<CallInfo> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<Iterable<? extends SourceLocatedError>, String> {
|
||||
|
||||
@Override
|
||||
public String perform(StagesData stagesData, Iterable<? extends SourceLocatedError> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<Iterable<? extends SourceLocatedError>, String> {
|
||||
|
||||
@Override
|
||||
public String perform(StagesData stagesData, Iterable<? extends SourceLocatedError> 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();
|
||||
}
|
||||
}
|
@ -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<Exception, SourceLocatedError> {
|
||||
@Override
|
||||
public SourceLocatedError perform(StagesData stagesData, Exception ex) {
|
||||
if (ex instanceof SourceLocatedError sle) return sle;
|
||||
return new SimpleError(ex.getMessage());
|
||||
}
|
||||
}
|
@ -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<Exception, String> {
|
||||
@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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<String, String> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -5,9 +5,4 @@ public interface SourceLocation {
|
||||
default Range getRange() {
|
||||
return null;
|
||||
}
|
||||
|
||||
default String formatRange() {
|
||||
final var range = getRange();
|
||||
return range == null ? "" : range.format();
|
||||
}
|
||||
}
|
||||
|
@ -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<Token> tokens = stagesData.get(LexerStage.TAG_TOKENS);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<StackTraceElement> 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
|
||||
|
@ -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<ParseErrors, String> {
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
@ -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<String, String> {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user