Generalized way to show source located errors (parse and runtime errors)

This commit is contained in:
aNNiMON 2023-10-04 19:30:35 +03:00 committed by Victor Melnik
parent 1fb9c8b3c5
commit 02e9d1f6c5
19 changed files with 274 additions and 95 deletions

View File

@ -3,10 +3,15 @@ package com.annimon.ownlang;
import com.annimon.ownlang.lib.CallStack; import com.annimon.ownlang.lib.CallStack;
import com.annimon.ownlang.outputsettings.ConsoleOutputSettings; import com.annimon.ownlang.outputsettings.ConsoleOutputSettings;
import com.annimon.ownlang.outputsettings.OutputSettings; 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.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.PrintStream; import java.io.PrintStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List;
public class Console { public class Console {
@ -58,6 +63,17 @@ public class Console {
outputSettings.error(value); 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) { public static void handleException(Thread thread, Throwable throwable) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream(); final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try(final PrintStream ps = new PrintStream(baos)) { try(final PrintStream ps = new PrintStream(baos)) {

View File

@ -1,19 +1,36 @@
package com.annimon.ownlang.exceptions; package com.annimon.ownlang.exceptions;
import com.annimon.ownlang.util.Range;
import com.annimon.ownlang.util.SourceLocatedError;
/** /**
* Base type for all runtime exceptions * Base type for all runtime exceptions
*/ */
public class OwnLangRuntimeException extends RuntimeException { public class OwnLangRuntimeException extends RuntimeException implements SourceLocatedError {
private final Range range;
public OwnLangRuntimeException() { public OwnLangRuntimeException() {
super(); super();
this.range = null;
} }
public OwnLangRuntimeException(String message) { public OwnLangRuntimeException(String message) {
this(message, (Range) null);
}
public OwnLangRuntimeException(String message, Range range) {
super(message); super(message);
this.range = range;
} }
public OwnLangRuntimeException(String message, Throwable ex) { public OwnLangRuntimeException(String message, Throwable ex) {
super(message, ex); super(message, ex);
this.range = null;
}
@Override
public Range getRange() {
return range;
} }
} }

View File

@ -1,7 +1,9 @@
package com.annimon.ownlang.lib; package com.annimon.ownlang.lib;
import com.annimon.ownlang.util.Range;
import java.util.Deque; import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.stream.Collectors;
public final class CallStack { public final class CallStack {
@ -13,7 +15,7 @@ public final class CallStack {
calls.clear(); 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(); String func = function.toString();
if (func.contains("com.annimon.ownlang.modules")) { if (func.contains("com.annimon.ownlang.modules")) {
func = func.replaceAll( func = func.replaceAll(
@ -22,7 +24,7 @@ public final class CallStack {
if (func.contains("\n")) { if (func.contains("\n")) {
func = func.substring(0, func.indexOf("\n")).trim(); 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() { public static synchronized void exit() {
@ -33,13 +35,23 @@ public final class CallStack {
return calls; return calls;
} }
public record CallInfo(String name, String function, String position) { public static String getFormattedCalls() {
return calls.stream()
.map(CallInfo::format)
.collect(Collectors.joining("\n"));
}
public record CallInfo(String name, String function, Range range) {
String format() {
return "\tat " + this;
}
@Override @Override
public String toString() { public String toString() {
if (position == null) { if (range == null) {
return String.format("%s: %s", name, function); return String.format("%s: %s", name, function);
} else { } else {
return String.format("%s: %s %s", name, function, position); return String.format("%s: %s %s", name, function, range.format());
} }
} }
} }

View File

@ -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());
}
}
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -5,9 +5,4 @@ public interface SourceLocation {
default Range getRange() { default Range getRange() {
return null; return null;
} }
default String formatRange() {
final var range = getRange();
return range == null ? "" : range.format();
}
} }

View File

@ -2,7 +2,9 @@ package com.annimon.ownlang;
import com.annimon.ownlang.exceptions.OwnLangParserException; import com.annimon.ownlang.exceptions.OwnLangParserException;
import com.annimon.ownlang.exceptions.StoppedException; 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.ast.Statement;
import com.annimon.ownlang.parser.error.ParseErrorsFormatterStage; import com.annimon.ownlang.parser.error.ParseErrorsFormatterStage;
import com.annimon.ownlang.parser.linters.LinterStage; import com.annimon.ownlang.parser.linters.LinterStage;
@ -163,7 +165,7 @@ public final class Main {
} catch (StoppedException ex) { } catch (StoppedException ex) {
// skip // skip
} catch (Exception ex) { } catch (Exception ex) {
Console.handleException(Thread.currentThread(), ex); Console.handleException(stagesData, Thread.currentThread(), ex);
} finally { } finally {
if (options.showTokens) { if (options.showTokens) {
final List<Token> tokens = stagesData.get(LexerStage.TAG_TOKENS); final List<Token> tokens = stagesData.get(LexerStage.TAG_TOKENS);

View File

@ -81,7 +81,7 @@ public final class Parser {
parseErrors.add(new ParseError(ex.getMessage(), ex.getRange())); parseErrors.add(new ParseError(ex.getMessage(), ex.getRange()));
recover(); recover();
} catch (Exception ex) { } catch (Exception ex) {
parseErrors.add(new ParseError(ex.getMessage(), getRange(), List.of(ex.getStackTrace()))); parseErrors.add(new ParseError(ex.getMessage(), getRange(), ex.getStackTrace()));
recover(); recover();
} }
} }

View File

@ -51,7 +51,7 @@ public final class FunctionalExpression extends InterruptableNode
values[i] = arguments.get(i).eval(); values[i] = arguments.get(i).eval();
} }
final Function f = consumeFunction(functionExpr); final Function f = consumeFunction(functionExpr);
CallStack.enter(functionExpr.toString(), f, formatRange()); CallStack.enter(functionExpr.toString(), f, range);
final Value result = f.execute(values); final Value result = f.execute(values);
CallStack.exit(); CallStack.exit();
return result; return result;

View File

@ -1,16 +1,31 @@
package com.annimon.ownlang.parser.error; package com.annimon.ownlang.parser.error;
import com.annimon.ownlang.util.Range; import com.annimon.ownlang.util.Range;
import java.util.Collections; import com.annimon.ownlang.util.SourceLocatedError;
import java.util.List;
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) { public ParseError(String message, Range range) {
this(message, range, Collections.emptyList()); this(message, range, new StackTraceElement[0]);
} }
public boolean hasStackTrace() { @Override
return !stackTraceElements.isEmpty(); public String getMessage() {
return message;
}
@Override
public Range getRange() {
return range;
}
@Override
public StackTraceElement[] getStackTrace() {
return stackTraceElements;
} }
@Override @Override

View File

@ -1,63 +1,18 @@
package com.annimon.ownlang.parser.error; 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.Stage;
import com.annimon.ownlang.stages.StagesData; 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> { public class ParseErrorsFormatterStage implements Stage<ParseErrors, String> {
@Override @Override
public String perform(StagesData stagesData, ParseErrors input) { public String perform(StagesData stagesData, ParseErrors input) {
final var sb = new StringBuilder(); String error = new ErrorsLocationFormatterStage()
final String source = stagesData.get(SourceLoaderStage.TAG_SOURCE); .perform(stagesData, input);
final var lines = source.split("\r?\n"); String stackTrace = new ErrorsStackTraceFormatterStage()
for (ParseError parseError : input) { .perform(stagesData, input);
sb.append(Console.newline()); return error + "\n" + stackTrace;
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());
}
}
} }
} }

View File

@ -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);
}
}
}

View File

@ -9,6 +9,7 @@ import com.annimon.ownlang.parser.ast.Visitor;
import com.annimon.ownlang.parser.optimization.OptimizationStage; import com.annimon.ownlang.parser.optimization.OptimizationStage;
import com.annimon.ownlang.parser.visitors.AbstractVisitor; import com.annimon.ownlang.parser.visitors.AbstractVisitor;
import com.annimon.ownlang.stages.*; import com.annimon.ownlang.stages.*;
import com.annimon.ownlang.util.SourceLoaderStage;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;

View File

@ -46,7 +46,7 @@ public final class Sandbox {
} catch (Exception ex) { } catch (Exception ex) {
// ownlang call stack to stdout // ownlang call stack to stdout
System.out.format("%s in %s%n", ex.getMessage(), Thread.currentThread().getName()); 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 // java stack trace to stderr
Console.handleException(Thread.currentThread(), ex); Console.handleException(Thread.currentThread(), ex);
} }