From d72a617290235d6dd321816214ba6f8ed33a271b Mon Sep 17 00:00:00 2001 From: gnembon Date: Tue, 9 Mar 2021 04:57:23 -0400 Subject: [PATCH] added trace information to thrown exceptions and clean some of the docs --- .../language/FunctionsAndControlFlow.md | 104 ++++++++++-------- src/main/java/carpet/script/Tokenizer.java | 1 + .../script/exception/ExpressionException.java | 10 +- .../carpet/script/exception/Throwables.java | 32 +++--- .../carpet/script/language/ControlFlow.java | 31 +++++- 5 files changed, 111 insertions(+), 67 deletions(-) diff --git a/docs/scarpet/language/FunctionsAndControlFlow.md b/docs/scarpet/language/FunctionsAndControlFlow.md index 79c931a3..73d5d1fc 100644 --- a/docs/scarpet/language/FunctionsAndControlFlow.md +++ b/docs/scarpet/language/FunctionsAndControlFlow.md @@ -234,61 +234,71 @@ returns everywhere, but it would often lead to a messy code. It terminates entire program passing `expr` as the result of the program execution, or null if omitted. -### `try(expr, id_filter, catch_expr(_, _msg), id_filter2?, catch_expr2(_, _msg), ...)` +### `try(expr)` `try(expr, user_catch_expr)` `try(expr, type, catch_expr, type?, catch_expr?, ...)` -`try` function evaluates expression, and continues further unless an exception is thrown anywhere inside `expr`, -it being produced by `throw` or a catchable exception being thrown by Carpet. In that case the `catch_expr` is evaluated with -`_` set to the id of the exception and `_msg` set to the exception message. Only exceptions with their or their parent's id(recursively) equal -to a provided `id_filter` will be caught, and any other exception will continue up the stack. You can specify multiple filters -and their respective `catch_expr` to be able to catch different exceptions within the same code. When an exception occurs, the script -will try to find an equality with every provided filter in order. Therefore, even if the caught exception matches multiple filters, only -the first matching block will be executed. It is recommended to filter the exceptions to catch to be able to distinguish from an exception -that is known that may happen from one that shouldn't. +`try` evaluates expression, allowing capturing exceptions that would be thrown inside `expr` statement. The exceptions can be +thrown explicitly using `throw()` or internally by scarpet where code is correct but detects illegal state. The 2-argument form +catches only user-thrown exceptions and one argument call `try(expr)` is equivalent to `try(expr, null)`, +or `try(expr, 'user_exception', null)`. If multiple `type-catch` pairs are defined, the execution terminates on the first +applicable type for the exception thrown. Therefore, even if the caught exception matches multiple filters, only +the first matching block will be executed. -This mechanism allows to terminate large portion of a convoluted call stack and continue program execution. +Catch expressions are evaluated with +`_` set to the value associated with the exception and `_trace` set to contain details about point of error (token, and line and +column positions), call stack and local +variables at the time of failure. The `type` will catch any exception of that type and any subtype of this type. + -The `try` function allows you to catch some Scarpet exceptions for those cases when trying to get things like items, -blocks, biomes or dimensions from registries, that may have been modified by datapacks, resourcepacks or other mods, or when an error that -is out of the scope of the programmer occurs, such as problems when reading files. +You can use `try` mechanism to exit from large portion of a convoluted call stack and continue program execution, although catching +exceptions is typically much more expensive comparing to not throwing them. -Deprecated usages: -- `try(expr)`: Filter the exception you are looking for, and provide something as the `catch_expr` (use `null` if you have nothing to provide). - Example replacement: `try(expr, 'exception', null)` -- `try(expr, catch_expr)`: Filter the exception you are looking for. +The `try` function allows you to catch some scarpet exceptions for cases covering invalid data, like invalid +blocks, biomes, dimensions and other things, that may have been modified by datapacks, resourcepacks or other mods, +or when an error is outside of the programmers scope, such as problems when reading or decoding files. -In this documentation, those are documented in at least most of the functions that throw them, and the current exception -hierarchy is the following: -- `exception`: This is the base exception. Every exception has this as its top-level parent - - `value_exception`: This is the parent for any exception that occurs due to a provided value for something not being present in the game - - `unknown_item`: Happens when a specified item doesn't exist - - `unknown_block`: Happens when a specified block doesn't exist - - `unknown_biome`: Happens when a specified biome doesn't exist - - `unknown_sound`: Happens when a specified sound doesn't exist - - `unknown_particle`: Happens when a specified particle doesn't exist - - `unknown_poi_type`: Happens when a specified POI type doesn't exist - - `unknown_dimension`: Happens when a specified dimension doesn't exist - - `unknown_structure`: Happens when a specified structure doesn't exist - - `unknown_criterion`: Happens when a specified scoreboard criterion doesn't exist - - `file_read_exception`: This is the parent for any exception that occurs due to an error while reading a file, including appdata load - - `nbt_read_exception`: Happens when there is an exception while reading an NBT file - - `json_read_exception`: Happens when there is an exception while reading a JSON file - - `user_defined_exception`: This is the parent for any exception thrown by the below `throw` function, except for those that explicitly declare - their parent to be one of the above +This is the hierarchy of the exceptions that could be thrown/caught in the with the `try` function: +- `exception`: This is the base exception. Catching `'exception'` allows to catch everything that can be caught, +but like everywhere else, doing that sounds like a bad idea. + - `value_exception`: This is the parent for any exception that occurs due to an + incorrect argument value provided to a built-in function + - `unknown_item`, `unknown_block`, `unknown_biome`, `unknown_sound`, `unknown_particle`, + `unknown_poi_type`, `unknown_dimension`, `unknown_structure`, `unknown_criterion`: Specific + errors thrown when a specified internal name does not exist or is invalid. + - `io_exception`: This is the parent for any exception that occurs due to an error handling external data. + - `nbt_read_error`: Incorrect input NBT file. + - `json_read_error`: Incorrect input JSON data. + - `user_exception`: Exception thrown by default with `throw` function. + +Synopsis: +
+inner_call() ->
+(
+   aaa = 'booyah';
+   try(
+      for (range(10), item_tags('stick'+_*'k'));
+   ,
+      print(_trace) // not caught, only catching user_exceptions
+   )
+);
 
-### `throw(id, message?, parent?)`
+outer_call() -> 
+( 
+   try(
+      inner_call()
+   , 'exception', // catching everything
+      print(_trace)
+   ) 
+);
+
+Producing: +``` +{stack: [[, inner_call, 1, 14]], locals: {_a: 0, aaa: booyah, _: 1, _y: 0, _i: 1, _x: 0, _z: 0}, token: [item_tags, 5, 23]} +``` -Throws an exception that can be caught in a `try` block (see above). If ran without arguments, it will pass `null` -as the value to the `catch_expr`. `message` is the message to show to chat or console in case the `throw` is -unhandled. +### `throw(value?)` `throw(type, value)` -By default, the parent of any exception thrown by this function is `user_defined_exception`. -You can specify the `parent` to be either an already existing exception from the list above, in which case your exception -will not inherit from `user_defined_exception`, or specify a custom one, in which case your exception will inherit from -`exception` > `user_defined_exception` > `parent` > `id`. - -If you don't want your exception to be handled, you can use `error` as the `parent` to throw an error which cannot be handled. - -Deprecated usage: `throw()` (without id): Specify at least the id of your exception +Throws an exception that can be caught in a `try` block (see above). If ran without arguments, it will throw `user_exception` +passing `null` as the value to the `catch_expr`. With two arguments you can mimic any other exception type thrown in scarpet. ### `if(cond, expr, cond?, expr?, ..., default?)` diff --git a/src/main/java/carpet/script/Tokenizer.java b/src/main/java/carpet/script/Tokenizer.java index 30fac871..24a92df1 100644 --- a/src/main/java/carpet/script/Tokenizer.java +++ b/src/main/java/carpet/script/Tokenizer.java @@ -406,6 +406,7 @@ public class Tokenizer implements Iterator public int pos; public int linepos; public int lineno; + public static final Token NONE = new Token(); public Token morphedInto(TokenType newType, String newSurface) { Token created = new Token(); diff --git a/src/main/java/carpet/script/exception/ExpressionException.java b/src/main/java/carpet/script/exception/ExpressionException.java index ab9ec2e9..c62e3db7 100644 --- a/src/main/java/carpet/script/exception/ExpressionException.java +++ b/src/main/java/carpet/script/exception/ExpressionException.java @@ -14,26 +14,29 @@ import java.util.function.Supplier; public class ExpressionException extends RuntimeException implements ResolvedException { public final Context context; + public final Tokenizer.Token token; public final List stack = new ArrayList<>(); private final Supplier lazyStacktrace; private String cachedMessage = null; public ExpressionException(Context c, Expression e, String message) { - this(c, e, null, message); + this(c, e, Tokenizer.Token.NONE, message); } public ExpressionException(Context c, Expression e, Tokenizer.Token t, String message) { super("Error"); lazyStacktrace = () -> makeMessage(c, e, t, message); + token = t; context = c; } public ExpressionException(Context c, Expression e, Tokenizer.Token t, String message, List stack) { super("Error"); this.stack.addAll(stack); - lazyStacktrace = () -> makeMessage(c, e, t, message); + lazyStacktrace = () -> makeMessage(c, e, t, message); + token = t; context = c; } @@ -41,7 +44,8 @@ public class ExpressionException extends RuntimeException implements ResolvedExc { super("Error"); this.stack.addAll(stack); - lazyStacktrace = messageSupplier; + lazyStacktrace = () -> makeMessage(c, e, t, messageSupplier.get()); + token = t; context = c; } diff --git a/src/main/java/carpet/script/exception/Throwables.java b/src/main/java/carpet/script/exception/Throwables.java index 136aa09e..589ea740 100644 --- a/src/main/java/carpet/script/exception/Throwables.java +++ b/src/main/java/carpet/script/exception/Throwables.java @@ -13,24 +13,24 @@ public class Throwables { private final String id; private final Throwables parent; - private static final Map byValue = new HashMap<>(); + private static final Map byId = new HashMap<>(); private static final Map> handles = new HashMap<>(); public static final Throwables THROWN_EXCEPTION_TYPE = register("exception", null); public static final Throwables VALUE_EXCEPTION = register("value_exception", THROWN_EXCEPTION_TYPE); - public static final Throwables UNKNOWN_ITEM = register("unknown_item", VALUE_EXCEPTION); - public static final Throwables UNKNOWN_BLOCK = register("unknown_block", VALUE_EXCEPTION); - public static final Throwables UNKNOWN_BIOME = register("unknown_biome", VALUE_EXCEPTION); - public static final Throwables UNKNOWN_SOUND = register("unknown_sound", VALUE_EXCEPTION); - public static final Throwables UNKNOWN_PARTICLE = register("unknown_particle", VALUE_EXCEPTION); - public static final Throwables UNKNOWN_POI_TYPE = register("unknown_poi_type", VALUE_EXCEPTION); - public static final Throwables UNKNOWN_DIMENSION = register("unknown_dimension", VALUE_EXCEPTION); - public static final Throwables UNKNOWN_STRUCTURE = register("unknown_structure", VALUE_EXCEPTION); - public static final Throwables UNKNOWN_CRITERION = register("unknown_criterion", VALUE_EXCEPTION); - public static final Throwables FILE_READ_EXCEPTION = register("file_read_exception", THROWN_EXCEPTION_TYPE); - public static final Throwables NBT_READ = register("nbt_read_exception", FILE_READ_EXCEPTION); - public static final Throwables JSON_READ = register("json_read_exception", FILE_READ_EXCEPTION); - public static final Throwables USER_DEFINED = register("user_defined_exception", THROWN_EXCEPTION_TYPE); + public static final Throwables UNKNOWN_ITEM = register("unknown_item", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_BLOCK = register("unknown_block", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_BIOME = register("unknown_biome", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_SOUND = register("unknown_sound", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_PARTICLE = register("unknown_particle", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_POI_TYPE = register("unknown_poi_type", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_DIMENSION = register("unknown_dimension", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_STRUCTURE = register("unknown_structure", VALUE_EXCEPTION); + public static final Throwables UNKNOWN_CRITERION = register("unknown_criterion", VALUE_EXCEPTION); + public static final Throwables IO_EXCEPTION = register("io_exception", THROWN_EXCEPTION_TYPE); + public static final Throwables NBT_READ = register("nbt_read_error", IO_EXCEPTION); + public static final Throwables JSON_READ = register("json_read_error", IO_EXCEPTION); + public static final Throwables USER_DEFINED = register("user_exception", THROWN_EXCEPTION_TYPE); /** * Creates an exception and registers it to be used as parent for @@ -43,7 +43,7 @@ public class Throwables { public static Throwables register(String id, Throwables parent) { Throwables exc = new Throwables(id, parent); - byValue.put(id, exc); + byId.put(id, exc); Set itHandles = new HashSet<>(); itHandles.add(exc); while (parent!=null) @@ -69,7 +69,7 @@ public class Throwables { public static Throwables getTypeForException(String type) { - Throwables properType = byValue.get(type); + Throwables properType = byId.get(type); if (properType == null) throw new InternalExpressionException("Unknown exception type: "+type); return properType; } diff --git a/src/main/java/carpet/script/language/ControlFlow.java b/src/main/java/carpet/script/language/ControlFlow.java index f5173f26..7972d582 100644 --- a/src/main/java/carpet/script/language/ControlFlow.java +++ b/src/main/java/carpet/script/language/ControlFlow.java @@ -8,7 +8,14 @@ import carpet.script.exception.InternalExpressionException; import carpet.script.exception.ProcessedThrowStatement; import carpet.script.exception.ThrowStatement; import carpet.script.exception.Throwables; +import carpet.script.value.ListValue; +import carpet.script.value.MapValue; +import carpet.script.value.NumericValue; +import carpet.script.value.StringValue; import carpet.script.value.Value; +import com.google.common.collect.ImmutableMap; + +import java.util.stream.Collectors; public class ControlFlow { public static void apply(Expression expression) // public just to get the javadoc right @@ -85,6 +92,25 @@ public class ControlFlow { LazyValue __ = c.getVariable("_"); c.setVariable("_", (__c, __t) -> ret.data.reboundedTo("_")); + LazyValue _trace = c.getVariable("_trace"); + c.setVariable("_trace", (__c, __t) -> MapValue.wrap(ImmutableMap.of( + StringValue.of("stack"), ListValue.wrap(ret.stack.stream().map(f -> ListValue.of( + StringValue.of(f.getModule().getName()), + StringValue.of(f.getString()), + NumericValue.of(f.getToken().lineno+1), + NumericValue.of(f.getToken().linepos+1) + )).collect(Collectors.toList())), + + StringValue.of("locals"), MapValue.wrap(ret.context.variables.entrySet().stream().collect(Collectors.toMap( + e -> StringValue.of(e.getKey()), + e -> e.getValue().evalValue(ret.context) + ))), + StringValue.of("token"), ListValue.of( + StringValue.of(ret.token.surface), + NumericValue.of(ret.token.lineno+1), + NumericValue.of(ret.token.linepos+1) + ) + ))); if (lv.size() == 2) { @@ -105,7 +131,10 @@ public class ControlFlow { } } c.setVariable("_", __); - + if (_trace != null) + c.setVariable("_trace", _trace); + else + c.delVariable("_trace"); if (val == null) // not handled { throw ret;