diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/StandardLib.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/StandardLib.java index 7e45fea..9d3cf1c 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/StandardLib.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/StandardLib.java @@ -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) diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Bind.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Bind.java index 52380ed..45d04d5 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Bind.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Bind.java @@ -29,7 +29,7 @@ public class Bind extends DynamicExpr { .asCallable() .getValue() .apply(DFinal.of(argsWithParameter)); - }, this::toString); + }, this::toString, ""); } @Override diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Call.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Call.java index 81d7373..5af53e7 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Call.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/Call.java @@ -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) { diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/assign/DynamicAssign.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/assign/DynamicAssign.java index ec505a9..3a68d9a 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/assign/DynamicAssign.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/ast/dynamic/assign/DynamicAssign.java @@ -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; } diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/data/Script.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/data/Script.java index fcd9c56..e3a9f56 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/data/Script.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/data/Script.java @@ -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}()", ""); } public DynamicExpr asExpr() { diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/DCallable.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/DCallable.java index 31d0b57..13c6ffb 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/DCallable.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/DCallable.java @@ -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 default Dynamic call(Dynamic... args) { return call(DFinal.of(args)); } + + default String getName() { + return ""; + } + + default DCallable named(String name) { + return new NamedDCallable(this, name); + } } diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/additional/DFinal.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/additional/DFinal.java index c51018e..dd29385 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/additional/DFinal.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/additional/DFinal.java @@ -38,7 +38,15 @@ public class DFinal { } public static DCallable of(Function> b, Supplier serialized) { - return new FCallable((Function>) b, new LazySupplier<>(serialized)); + return of(b, serialized, null); + } + + public static DCallable of(Function> b, String name) { + return of(b, () -> name, name); + } + + public static DCallable of(Function> b, Supplier serialized, String name) { + return new FCallable((Function>) b, new LazySupplier<>(serialized), name); } /** @@ -107,7 +115,7 @@ public class DFinal { } } - private record FCallable(Function> value, Supplier string) implements DCallable, FImpl { + private record FCallable(Function> value, Supplier 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); + } } } diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/additional/NamedDCallable.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/additional/NamedDCallable.java new file mode 100644 index 0000000..0ad1500 --- /dev/null +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/data/dynamic/additional/NamedDCallable.java @@ -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> getValue() { + return inner.getValue(); + } +} diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalError.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalError.java index e83d643..3f1b9b7 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalError.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalError.java @@ -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 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 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 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"; } } diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalException.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalException.java index a853682..36a27c3 100644 --- a/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalException.java +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/error/LocationalException.java @@ -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 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; } } diff --git a/muscript/src/main/java/io/gitlab/jfronny/muscript/error/StackFrame.java b/muscript/src/main/java/io/gitlab/jfronny/muscript/error/StackFrame.java new file mode 100644 index 0000000..588cea8 --- /dev/null +++ b/muscript/src/main/java/io/gitlab/jfronny/muscript/error/StackFrame.java @@ -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 + ")"; + } + } +} diff --git a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/StackTraceTest.java b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/StackTraceTest.java new file mode 100644 index 0000000..362ab79 --- /dev/null +++ b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/StackTraceTest.java @@ -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()); + } +} diff --git a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/util/MuTestUtil.java b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/util/MuTestUtil.java index 7fc6c1f..10aff17 100644 --- a/muscript/src/test/java/io/gitlab/jfronny/muscript/test/util/MuTestUtil.java +++ b/muscript/src/test/java/io/gitlab/jfronny/muscript/test/util/MuTestUtil.java @@ -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)))