reconstruct call stack on exception

This commit is contained in:
Johannes Frohnmeyer 2023-04-18 10:36:49 +02:00
parent b66332f615
commit 51cc7e41ab
Signed by: Johannes
GPG Key ID: E76429612C2929F4
13 changed files with 234 additions and 35 deletions

View File

@ -32,7 +32,7 @@ public class StandardLib {
int a1 = args.get(1).asNumber().getValue().intValue();
int a2 = args.get(2).asNumber().getValue().intValue();
return new DDate(() -> LocalDate.of(a0, a1, a2));
}, () -> "date")))
}, "date")))
.set("time", new DCallableObject(Map.of(
"now", new DTime(LocalTime::now)
), DFinal.of(args -> {
@ -43,7 +43,7 @@ public class StandardLib {
int a1 = args.get(1).asNumber().getValue().intValue();
int a2 = args.get(2).asNumber().getValue().intValue();
return new DTime(() -> LocalTime.of(a0, a1, a2));
}, () -> "time")))
}, "time")))
.set("round", StandardLib::round)
.set("floor", StandardLib::floor)

View File

@ -29,7 +29,7 @@ public class Bind extends DynamicExpr {
.asCallable()
.getValue()
.apply(DFinal.of(argsWithParameter));
}, this::toString);
}, this::toString, "<bind>");
}
@Override

View File

@ -9,6 +9,7 @@ import io.gitlab.jfronny.muscript.data.Scope;
import io.gitlab.jfronny.muscript.data.dynamic.*;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal;
import io.gitlab.jfronny.muscript.error.LocationalException;
import io.gitlab.jfronny.muscript.error.StackFrame;
import java.io.IOException;
import java.util.*;
@ -28,11 +29,21 @@ public class Call extends DynamicExpr {
@Override
public Dynamic<?> get(Scope dataRoot) {
DCallable dc;
DList arg;
try {
return left.get(dataRoot)
.asCallable()
.getValue()
.apply(DFinal.of(args.stream().flatMap(e -> e.get(dataRoot)).toArray(Dynamic[]::new)));
dc = left.get(dataRoot)
.asCallable();
arg = DFinal.of(args.stream().flatMap(e -> e.get(dataRoot)).toArray(Dynamic[]::new));
} catch (DynamicTypeConversionException e) {
throw e.locational(chStart, chEnd);
} catch (RuntimeException e) {
throw new LocationalException(chStart, chEnd, "Could not perform call successfully", e);
}
try {
return dc.getValue().apply(arg);
} catch (LocationalException le) {
throw le.appendStack(new StackFrame.Raw(dc.getName(), left.chStart));
} catch (DynamicTypeConversionException e) {
throw e.locational(chStart, chEnd);
} catch (RuntimeException e) {

View File

@ -3,6 +3,7 @@ package io.gitlab.jfronny.muscript.ast.dynamic.assign;
import io.gitlab.jfronny.muscript.ast.*;
import io.gitlab.jfronny.muscript.compiler.*;
import io.gitlab.jfronny.muscript.data.Scope;
import io.gitlab.jfronny.muscript.data.dynamic.DCallable;
import io.gitlab.jfronny.muscript.data.dynamic.Dynamic;
import io.gitlab.jfronny.muscript.error.LocationalException;
@ -22,7 +23,7 @@ public class DynamicAssign extends DynamicExpr {
@Override
public Dynamic<?> get(Scope dataRoot) {
Dynamic<?> data = value.get(dataRoot);
dataRoot.set(name, data);
dataRoot.set(name, data instanceof DCallable callable ? callable.named(name) : data);
return data;
}

View File

@ -41,7 +41,7 @@ public class Script extends Decompilable {
return DFinal.of(args -> {
scope.set("args", args);
return run(scope);
}, () -> "{->\n" + this + "\n}()");
}, () -> "{->\n" + this + "\n}()", "<root>");
}
public DynamicExpr asExpr() {

View File

@ -1,6 +1,7 @@
package io.gitlab.jfronny.muscript.data.dynamic;
import io.gitlab.jfronny.muscript.data.dynamic.additional.DFinal;
import io.gitlab.jfronny.muscript.data.dynamic.additional.NamedDCallable;
import java.util.function.Function;
@ -12,4 +13,12 @@ public non-sealed interface DCallable extends Dynamic<Function<DList, Dynamic<?>
default Dynamic<?> call(Dynamic<?>... args) {
return call(DFinal.of(args));
}
default String getName() {
return "<unnamed>";
}
default DCallable named(String name) {
return new NamedDCallable(this, name);
}
}

View File

@ -38,7 +38,15 @@ public class DFinal {
}
public static DCallable of(Function<DList, ? extends Dynamic<?>> b, Supplier<String> serialized) {
return new FCallable((Function<DList, Dynamic<?>>) b, new LazySupplier<>(serialized));
return of(b, serialized, null);
}
public static DCallable of(Function<DList, ? extends Dynamic<?>> b, String name) {
return of(b, () -> name, name);
}
public static DCallable of(Function<DList, ? extends Dynamic<?>> b, Supplier<String> serialized, String name) {
return new FCallable((Function<DList, Dynamic<?>>) b, new LazySupplier<>(serialized), name);
}
/**
@ -107,7 +115,7 @@ public class DFinal {
}
}
private record FCallable(Function<DList, Dynamic<?>> value, Supplier<String> string) implements DCallable, FImpl {
private record FCallable(Function<DList, Dynamic<?>> value, Supplier<String> string, String name) implements DCallable, FImpl {
@Override
public void serialize(ExprWriter writer) throws IOException {
writer.append(toString());
@ -122,5 +130,15 @@ public class DFinal {
public String toString() {
return string.get();
}
@Override
public String getName() {
return name == null ? DCallable.super.getName() : name;
}
@Override
public DCallable named(String name) {
return new FCallable(value, string, name);
}
}
}

View File

@ -0,0 +1,29 @@
package io.gitlab.jfronny.muscript.data.dynamic.additional;
import io.gitlab.jfronny.muscript.compiler.ExprWriter;
import io.gitlab.jfronny.muscript.data.dynamic.*;
import java.io.IOException;
import java.util.function.Function;
public record NamedDCallable(DCallable inner, String name) implements DCallable {
@Override
public String getName() {
return name;
}
@Override
public DCallable named(String name) {
return new NamedDCallable(inner, name);
}
@Override
public void serialize(ExprWriter writer) throws IOException {
inner.serialize(writer);
}
@Override
public Function<DList, Dynamic<?>> getValue() {
return inner.getValue();
}
}

View File

@ -2,6 +2,8 @@ package io.gitlab.jfronny.muscript.error;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* Class for storing errors with code context
* Can be generated from a LocationalException with asPrintable
@ -10,7 +12,11 @@ import org.jetbrains.annotations.Nullable;
* @param start The location of the start of this error. Both it and its components must not be null.
* @param end The location of the end of this error. May be null.
*/
public record LocationalError(String message, Location start, @Nullable Location end) {
public record LocationalError(String message, Location start, @Nullable Location end, List<? extends StackFrame> callStack) {
public LocationalError(String message, Location start, @Nullable Location end) {
this(message, start, end, List.of());
}
/**
* A location in the source code
*
@ -54,7 +60,7 @@ public record LocationalError(String message, Location start, @Nullable Location
* @return An error using the provided information
*/
public static LocationalError create(String source, int start, String message) {
return new LocationalError(message, Location.create(source, start), null);
return builder().setSource(source).setLocation(start).setMessage(message).build();
}
/**
@ -67,13 +73,7 @@ public record LocationalError(String message, Location start, @Nullable Location
* @return An error using the provided information
*/
public static LocationalError create(String source, int start, int end, String message) {
if (end == start) return create(source, start, message);
if (end < start) {
int a = end;
end = start;
start = a;
}
return new LocationalError(message, Location.create(source, start), Location.create(source, end));
return builder().setSource(source).setLocation(start, end).setMessage(message).build();
}
/**
@ -83,18 +83,72 @@ public record LocationalError(String message, Location start, @Nullable Location
*/
@Override
public String toString() {
if (start.column < 0) return "Error at unknown location: " + message;
if (end == null) return "Error at " + start + ": " + message + "\n" + start.prettyPrint() + "Here";
if (start.row == end.row) {
String linePrefix = String.format("%1$6d", start.row) + " | ";
return "Error at " + start + ": " + message + "\n"
+ linePrefix + start.line + "\n"
+ " ".repeat(linePrefix.length() + start.column) + "^"
+ "-".repeat(end.column - start.column - 1)
+ "^-- Here";
StringBuilder sb = new StringBuilder();
if (start.column < 0) sb.append("Error at unknown location: ").append(message);
else {
sb.append("Error at ").append(start).append(": ").append(message).append("\n");
if (end == null) sb.append(start.prettyPrint()).append("Here");
else if (start.row == end.row) {
String linePrefix = String.format("%1$6d", start.row) + " | ";
sb.append(linePrefix).append(start.line).append("\n").append(" ".repeat(linePrefix.length() + start.column)).append("^").append("-".repeat(end.column - start.column - 1)).append("^-- Here");
}
else sb.append(start.prettyPrint()).append("From here").append(end.prettyPrint()).append("to here");
}
callStack.forEach(frame -> sb.append("\n at ").append(frame));
return sb.toString();
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String message;
private Location start;
private @Nullable Location end;
private List<? extends StackFrame> callStack = List.of();
private String source;
public Builder setSource(String source) {
this.source = source;
return this;
}
public Builder setMessage(String message) {
this.message = message;
return this;
}
public Builder setLocation(int chStart) {
start = Location.create(Objects.requireNonNull(source, "source is required to set location"), chStart);
end = null;
return this;
}
public Builder setLocation(int chStart, int chEnd) {
if (chEnd == chStart) return setLocation(chStart);
Objects.requireNonNull(source, "source is required to set location");
if (chEnd < chStart) {
int a = chEnd;
chEnd = chStart;
chStart = a;
}
start = Location.create(source, chStart);
end = Location.create(source, chEnd);
return this;
}
public Builder setCallStack(List<StackFrame> callStack) {
this.callStack = callStack.stream()
.map(frame -> frame instanceof StackFrame.Lined lined
? lined
: ((StackFrame.Raw) frame).lined(Objects.requireNonNull(source, "source is required to set call stack")))
.toList();
return this;
}
public LocationalError build() {
return new LocationalError(message, start, end, callStack);
}
return "Error at " + start + ": " + message + "\n"
+ start.prettyPrint() + "From here"
+ end.prettyPrint() + "to here";
}
}

View File

@ -1,11 +1,15 @@
package io.gitlab.jfronny.muscript.error;
import java.util.LinkedList;
import java.util.List;
/**
* An exception type with a location
* For use in MuScript, can be converted to a pretty LocationalError with asPrintable
*/
public class LocationalException extends RuntimeException {
private final int start, end;
private final List<StackFrame> callStack = new LinkedList<>();
public LocationalException(int start, int end) {
super();
@ -32,6 +36,16 @@ public class LocationalException extends RuntimeException {
}
public LocationalError asPrintable(String source) {
return LocationalError.create(source, start, end, getLocalizedMessage());
return LocationalError.builder()
.setSource(source)
.setLocation(start, end)
.setMessage(getLocalizedMessage())
.setCallStack(callStack)
.build();
}
public LocationalException appendStack(StackFrame frame) {
callStack.add(frame);
return this;
}
}

View File

@ -0,0 +1,23 @@
package io.gitlab.jfronny.muscript.error;
public sealed interface StackFrame {
record Raw(String name, int chStart) implements StackFrame {
@Override
public String toString() {
return name + " (call: character " + chStart + ")";
}
public Lined lined(String source) {
int lineStart = source.lastIndexOf('\n', chStart);
int lineIndex = lineStart > 0 ? (int) source.substring(0, lineStart).chars().filter(c -> c == '\n').count() : 0;
return new Lined(name, lineIndex + 1);
}
}
record Lined(String name, int row) implements StackFrame {
@Override
public String toString() {
return name + " (call: line " + row + ")";
}
}
}

View File

@ -0,0 +1,40 @@
package io.gitlab.jfronny.muscript.test;
import io.gitlab.jfronny.muscript.StandardLib;
import io.gitlab.jfronny.muscript.compiler.Parser;
import io.gitlab.jfronny.muscript.error.LocationalException;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class StackTraceTest {
@Test
void stackTrace() {
String source = """
someInner = { ->
throw()
}
someOuter = { ->
someInner()
}
someOuter()
""";
assertEquals("""
Error at '(' (character 8): Could not perform call successfully
1 | throw()
^-- Here
at someInner (call: line 5)
at someOuter (call: line 8)""",
assertThrows(LocationalException.class, () -> Parser.parseScript(source)
.run(StandardLib.createScope()
.set("throw", args -> {
throw new IllegalArgumentException("No");
})
))
.asPrintable(source).toString());
}
}

View File

@ -40,7 +40,7 @@ public class MuTestUtil {
.set("string", "Value")
.set("object", Map.of(
"subvalue", of(1024),
"subfunc", of(v -> of(v.get(1).asNumber().getValue() * v.size()), () -> "object.subfunc"),
"subfunc", of(v -> of(v.get(1).asNumber().getValue() * v.size()), "object.subfunc"),
"1", of("One")
))
.set("object2", Map.of(
@ -48,7 +48,7 @@ public class MuTestUtil {
"sub", of(Map.of(
"val", of(10)
)),
"stringfunc", of(v -> of(v.get(0).asString().getValue()), () -> "object2.stringfunc")
"stringfunc", of(v -> of(v.get(0).asString().getValue()), "object2.stringfunc")
))
.set("list", of(of(true), of(2), of("3")))
.set("numbers", of(of(1), of(2), of(3), of(4), of(5), of(6), of(7), of(8), of(9), of(10)))