Skip to content

Commit 02e9d1f

Browse files
committed
Generalized way to show source located errors (parse and runtime errors)
1 parent 1fb9c8b commit 02e9d1f

File tree

19 files changed

+274
-95
lines changed

19 files changed

+274
-95
lines changed

ownlang-core/src/main/java/com/annimon/ownlang/Console.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
import com.annimon.ownlang.lib.CallStack;
44
import com.annimon.ownlang.outputsettings.ConsoleOutputSettings;
55
import com.annimon.ownlang.outputsettings.OutputSettings;
6+
import com.annimon.ownlang.stages.StagesData;
7+
import com.annimon.ownlang.util.ErrorsLocationFormatterStage;
8+
import com.annimon.ownlang.util.ExceptionConverterStage;
9+
import com.annimon.ownlang.util.ExceptionStackTraceToStringStage;
610
import java.io.ByteArrayOutputStream;
711
import java.io.File;
812
import java.io.PrintStream;
913
import java.nio.charset.StandardCharsets;
14+
import java.util.List;
1015

1116
public class Console {
1217

@@ -58,6 +63,17 @@ public static void error(CharSequence value) {
5863
outputSettings.error(value);
5964
}
6065

66+
public static void handleException(StagesData stagesData, Thread thread, Exception exception) {
67+
String mainError = new ExceptionConverterStage()
68+
.then((data, error) -> List.of(error))
69+
.then(new ErrorsLocationFormatterStage())
70+
.perform(stagesData, exception);
71+
String callStack = CallStack.getFormattedCalls();
72+
String stackTrace = new ExceptionStackTraceToStringStage()
73+
.perform(stagesData, exception);
74+
error(String.join("\n", mainError, "Thread: " + thread.getName(), callStack, stackTrace));
75+
}
76+
6177
public static void handleException(Thread thread, Throwable throwable) {
6278
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
6379
try(final PrintStream ps = new PrintStream(baos)) {
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
package com.annimon.ownlang.exceptions;
22

3+
import com.annimon.ownlang.util.Range;
4+
import com.annimon.ownlang.util.SourceLocatedError;
5+
36
/**
47
* Base type for all runtime exceptions
58
*/
6-
public class OwnLangRuntimeException extends RuntimeException {
9+
public class OwnLangRuntimeException extends RuntimeException implements SourceLocatedError {
10+
11+
private final Range range;
712

813
public OwnLangRuntimeException() {
914
super();
15+
this.range = null;
1016
}
1117

1218
public OwnLangRuntimeException(String message) {
19+
this(message, (Range) null);
20+
}
21+
22+
public OwnLangRuntimeException(String message, Range range) {
1323
super(message);
24+
this.range = range;
1425
}
1526

1627
public OwnLangRuntimeException(String message, Throwable ex) {
1728
super(message, ex);
29+
this.range = null;
30+
}
31+
32+
@Override
33+
public Range getRange() {
34+
return range;
1835
}
1936
}

ownlang-core/src/main/java/com/annimon/ownlang/lib/CallStack.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.annimon.ownlang.lib;
22

3+
import com.annimon.ownlang.util.Range;
34
import java.util.Deque;
45
import java.util.concurrent.ConcurrentLinkedDeque;
6+
import java.util.stream.Collectors;
57

68
public final class CallStack {
79

@@ -13,7 +15,7 @@ public static synchronized void clear() {
1315
calls.clear();
1416
}
1517

16-
public static synchronized void enter(String name, Function function, String position) {
18+
public static synchronized void enter(String name, Function function, Range range) {
1719
String func = function.toString();
1820
if (func.contains("com.annimon.ownlang.modules")) {
1921
func = func.replaceAll(
@@ -22,7 +24,7 @@ public static synchronized void enter(String name, Function function, String pos
2224
if (func.contains("\n")) {
2325
func = func.substring(0, func.indexOf("\n")).trim();
2426
}
25-
calls.push(new CallInfo(name, func, position));
27+
calls.push(new CallInfo(name, func, range));
2628
}
2729

2830
public static synchronized void exit() {
@@ -32,14 +34,24 @@ public static synchronized void exit() {
3234
public static synchronized Deque<CallInfo> getCalls() {
3335
return calls;
3436
}
37+
38+
public static String getFormattedCalls() {
39+
return calls.stream()
40+
.map(CallInfo::format)
41+
.collect(Collectors.joining("\n"));
42+
}
3543

36-
public record CallInfo(String name, String function, String position) {
44+
public record CallInfo(String name, String function, Range range) {
45+
String format() {
46+
return "\tat " + this;
47+
}
48+
3749
@Override
3850
public String toString() {
39-
if (position == null) {
51+
if (range == null) {
4052
return String.format("%s: %s", name, function);
4153
} else {
42-
return String.format("%s: %s %s", name, function, position);
54+
return String.format("%s: %s %s", name, function, range.format());
4355
}
4456
}
4557
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.annimon.ownlang.util;
2+
3+
import com.annimon.ownlang.Console;
4+
import com.annimon.ownlang.stages.Stage;
5+
import com.annimon.ownlang.stages.StagesData;
6+
7+
public class ErrorsLocationFormatterStage implements Stage<Iterable<? extends SourceLocatedError>, String> {
8+
9+
@Override
10+
public String perform(StagesData stagesData, Iterable<? extends SourceLocatedError> input) {
11+
final var sb = new StringBuilder();
12+
final String source = stagesData.get(SourceLoaderStage.TAG_SOURCE);
13+
final var lines = source.split("\r?\n");
14+
for (SourceLocatedError error : input) {
15+
sb.append(Console.newline());
16+
sb.append(error);
17+
sb.append(Console.newline());
18+
final Range range = error.getRange();
19+
if (range != null) {
20+
printPosition(sb, range.normalize(), lines);
21+
}
22+
}
23+
return sb.toString();
24+
}
25+
26+
private static void printPosition(StringBuilder sb, Range range, String[] lines) {
27+
final Pos start = range.start();
28+
final int linesCount = lines.length;;
29+
if (range.isOnSameLine()) {
30+
if (start.row() < linesCount) {
31+
sb.append(lines[start.row()]);
32+
sb.append(Console.newline());
33+
sb.append(" ".repeat(start.col()));
34+
sb.append("^".repeat(range.end().col() - start.col() + 1));
35+
sb.append(Console.newline());
36+
}
37+
} else {
38+
if (start.row() < linesCount) {
39+
String line = lines[start.row()];
40+
sb.append(line);
41+
sb.append(Console.newline());
42+
sb.append(" ".repeat(start.col()));
43+
sb.append("^".repeat(Math.max(1, line.length() - start.col())));
44+
sb.append(Console.newline());
45+
}
46+
final Pos end = range.end();
47+
if (end.row() < linesCount) {
48+
sb.append(lines[end.row()]);
49+
sb.append(Console.newline());
50+
sb.append("^".repeat(end.col()));
51+
sb.append(Console.newline());
52+
}
53+
}
54+
}
55+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.annimon.ownlang.util;
2+
3+
import com.annimon.ownlang.Console;
4+
import com.annimon.ownlang.stages.Stage;
5+
import com.annimon.ownlang.stages.StagesData;
6+
7+
public class ErrorsStackTraceFormatterStage implements Stage<Iterable<? extends SourceLocatedError>, String> {
8+
9+
@Override
10+
public String perform(StagesData stagesData, Iterable<? extends SourceLocatedError> input) {
11+
final var sb = new StringBuilder();
12+
for (SourceLocatedError error : input) {
13+
if (!error.hasStackTrace()) continue;
14+
for (StackTraceElement el : error.getStackTrace()) {
15+
sb.append("\t").append(el).append(Console.newline());
16+
}
17+
}
18+
return sb.toString();
19+
}
20+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.annimon.ownlang.util;
2+
3+
import com.annimon.ownlang.stages.Stage;
4+
import com.annimon.ownlang.stages.StagesData;
5+
6+
public class ExceptionConverterStage implements Stage<Exception, SourceLocatedError> {
7+
@Override
8+
public SourceLocatedError perform(StagesData stagesData, Exception ex) {
9+
if (ex instanceof SourceLocatedError sle) return sle;
10+
return new SimpleError(ex.getMessage());
11+
}
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.annimon.ownlang.util;
2+
3+
import com.annimon.ownlang.stages.Stage;
4+
import com.annimon.ownlang.stages.StagesData;
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.PrintStream;
7+
import java.nio.charset.StandardCharsets;
8+
9+
public class ExceptionStackTraceToStringStage implements Stage<Exception, String> {
10+
@Override
11+
public String perform(StagesData stagesData, Exception ex) {
12+
final var baos = new ByteArrayOutputStream();
13+
try (final PrintStream ps = new PrintStream(baos)) {
14+
for (StackTraceElement traceElement : ex.getStackTrace()) {
15+
ps.println("\tat " + traceElement);
16+
}
17+
}
18+
return baos.toString(StandardCharsets.UTF_8);
19+
}
20+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.annimon.ownlang.util;
2+
3+
public record SimpleError(String message) implements SourceLocatedError {
4+
@Override
5+
public String getMessage() {
6+
return message;
7+
}
8+
9+
@Override
10+
public String toString() {
11+
return message;
12+
}
13+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.annimon.ownlang.util;
2+
3+
import com.annimon.ownlang.exceptions.OwnLangRuntimeException;
4+
import com.annimon.ownlang.stages.Stage;
5+
import com.annimon.ownlang.stages.StagesData;
6+
import java.io.ByteArrayOutputStream;
7+
import java.io.FileInputStream;
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.nio.charset.StandardCharsets;
11+
12+
public class SourceLoaderStage implements Stage<String, String> {
13+
14+
public static final String TAG_SOURCE = "source";
15+
16+
@Override
17+
public String perform(StagesData stagesData, String name) {
18+
try {
19+
String result = readSource(name);
20+
stagesData.put(TAG_SOURCE, result);
21+
return result;
22+
} catch (IOException e) {
23+
throw new OwnLangRuntimeException("Unable to read input " + name, e);
24+
}
25+
}
26+
27+
private String readSource(String name) throws IOException {
28+
try (InputStream is = getClass().getResourceAsStream("/" + name)) {
29+
if (is != null) {
30+
return readStream(is);
31+
}
32+
}
33+
try (InputStream is = new FileInputStream(name)) {
34+
return readStream(is);
35+
}
36+
}
37+
38+
public static String readStream(InputStream is) throws IOException {
39+
final ByteArrayOutputStream result = new ByteArrayOutputStream();
40+
final int bufferSize = 1024;
41+
final byte[] buffer = new byte[bufferSize];
42+
int read;
43+
while ((read = is.read(buffer)) != -1) {
44+
result.write(buffer, 0, read);
45+
}
46+
return result.toString(StandardCharsets.UTF_8);
47+
}
48+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.annimon.ownlang.util;
2+
3+
public interface SourceLocatedError extends SourceLocation {
4+
5+
String getMessage();
6+
7+
default StackTraceElement[] getStackTrace() {
8+
return new StackTraceElement[0];
9+
}
10+
11+
default boolean hasStackTrace() {
12+
return !stackTraceIsEmpty();
13+
}
14+
15+
private boolean stackTraceIsEmpty() {
16+
final var st = getStackTrace();
17+
return st == null || st.length == 0;
18+
}
19+
}

0 commit comments

Comments
 (0)