reconstruct call stack on exception
This commit is contained in:
parent
b66332f615
commit
51cc7e41ab
|
@ -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)
|
||||
|
|
|
@ -29,7 +29,7 @@ public class Bind extends DynamicExpr {
|
|||
.asCallable()
|
||||
.getValue()
|
||||
.apply(DFinal.of(argsWithParameter));
|
||||
}, this::toString);
|
||||
}, this::toString, "<bind>");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 + ")";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
|
|
Loading…
Reference in New Issue