diff --git a/codegen/src/main/java/run/endive/codegen/ModuleInterfaceCodegen.java b/codegen/src/main/java/run/endive/codegen/ModuleInterfaceCodegen.java index ea3878c3e..bb24223ed 100644 --- a/codegen/src/main/java/run/endive/codegen/ModuleInterfaceCodegen.java +++ b/codegen/src/main/java/run/endive/codegen/ModuleInterfaceCodegen.java @@ -830,31 +830,42 @@ private Expression listOfValueTypes(List valTypes) { return new MethodCallExpr(new NameExpr("List"), "of", NodeList.nodeList(values)); } + /** The {@code ValType} shorthand constant for an abstract heap type, or null if there is none. */ + private static String abstractRefShorthand(int typeIdx) { + if (typeIdx == ValType.TypeIdxCode.EXTERN.code()) { + return "ExternRef"; + } else if (typeIdx == ValType.TypeIdxCode.ANY.code()) { + return "AnyRef"; + } else if (typeIdx == ValType.TypeIdxCode.EQ.code()) { + return "EqRef"; + } else if (typeIdx == ValType.TypeIdxCode.I31.code()) { + return "I31Ref"; + } else if (typeIdx == ValType.TypeIdxCode.STRUCT.code()) { + return "StructRef"; + } else if (typeIdx == ValType.TypeIdxCode.ARRAY.code()) { + return "ArrayRef"; + } else if (typeIdx == ValType.TypeIdxCode.NONE.code()) { + return "NoneRef"; + } else if (typeIdx == ValType.TypeIdxCode.FUNC.code()) { + return "FuncRef"; + } else if (typeIdx == ValType.TypeIdxCode.EXN.code()) { + return "ExnRef"; + } else if (typeIdx == ValType.TypeIdxCode.NOFUNC.code()) { + return "NoFuncRef"; + } else if (typeIdx == ValType.TypeIdxCode.NOEXTERN.code()) { + return "NoExternRef"; + } else { + return null; + } + } + private static Expression valTypeRefExpr(ValType vt) { int ti = vt.typeIdx(); - if (ti == ValType.TypeIdxCode.EXTERN.code()) { - return new FieldAccessExpr(new NameExpr("ValType"), "ExternRef"); - } else if (ti == ValType.TypeIdxCode.ANY.code()) { - return new FieldAccessExpr(new NameExpr("ValType"), "AnyRef"); - } else if (ti == ValType.TypeIdxCode.EQ.code()) { - return new FieldAccessExpr(new NameExpr("ValType"), "EqRef"); - } else if (ti == ValType.TypeIdxCode.I31.code()) { - return new FieldAccessExpr(new NameExpr("ValType"), "I31Ref"); - } else if (ti == ValType.TypeIdxCode.STRUCT.code()) { - return new FieldAccessExpr(new NameExpr("ValType"), "StructRef"); - } else if (ti == ValType.TypeIdxCode.ARRAY.code()) { - return new FieldAccessExpr(new NameExpr("ValType"), "ArrayRef"); - } else if (ti == ValType.TypeIdxCode.NONE.code()) { - return new FieldAccessExpr(new NameExpr("ValType"), "NoneRef"); - } else if (ti == ValType.TypeIdxCode.FUNC.code()) { - return new FieldAccessExpr(new NameExpr("ValType"), "FuncRef"); - } else if (ti == ValType.TypeIdxCode.EXN.code()) { - return new FieldAccessExpr(new NameExpr("ValType"), "ExnRef"); - } else if (ti == ValType.TypeIdxCode.NOFUNC.code()) { - return new FieldAccessExpr(new NameExpr("ValType"), "NoFuncRef"); - } else if (ti == ValType.TypeIdxCode.NOEXTERN.code()) { - return new FieldAccessExpr(new NameExpr("ValType"), "NoExternRef"); - } else if (ti >= 0) { + String shorthand = abstractRefShorthand(ti); + // the shorthands are all nullable, a non-nullable ref has to be built explicitly + if (shorthand != null && vt.opcode() == ValType.ID.RefNull) { + return new FieldAccessExpr(new NameExpr("ValType"), shorthand); + } else if (shorthand != null || ti >= 0) { String opcName = vt.opcode() == ValType.ID.Ref ? "Ref" : "RefNull"; return new MethodCallExpr( new MethodCallExpr( diff --git a/compiler-tests/pom.xml b/compiler-tests/pom.xml index 188e1c202..122221b97 100644 --- a/compiler-tests/pom.xml +++ b/compiler-tests/pom.xml @@ -262,6 +262,7 @@ proposals/threads/exports.wast proposals/threads/imports.wast proposals/threads/memory.wast + proposals/wasm-3.0/try_table.wast ref_func.wast ref_is_null.wast ref_null.wast diff --git a/machine-tests/src/test/java/run/endive/testing/CatchRefNonNullTest.java b/machine-tests/src/test/java/run/endive/testing/CatchRefNonNullTest.java new file mode 100644 index 000000000..84f0900e5 --- /dev/null +++ b/machine-tests/src/test/java/run/endive/testing/CatchRefNonNullTest.java @@ -0,0 +1,67 @@ +package run.endive.testing; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import run.endive.compiler.MachineFactoryCompiler; +import run.endive.corpus.CorpusResources; +import run.endive.runtime.ImportValues; +import run.endive.runtime.Instance; +import run.endive.runtime.InterpreterMachine; +import run.endive.wasm.Parser; +import run.endive.wasm.WasmModule; + +/** Tests for `catch_ref` and `catch_all_ref` sending a non-nullable `(ref exn)`. */ +public class CatchRefNonNullTest { + + private static final WasmModule MODULE = + Parser.parse(CorpusResources.getResource("compiled/catch_ref_non_null.wat.wasm")); + + private static Stream machineImplementations() { + return Stream.of( + Arguments.of( + (Function) + (b) -> b.withMachineFactory(InterpreterMachine::new)), + Arguments.of( + (Function) + (b) -> b.withMachineFactory(MachineFactoryCompiler::compile))); + } + + private static Instance instance(Function machineInject) { + return machineInject + .apply(Instance.builder(MODULE).withImportValues(ImportValues.builder().build())) + .build(); + } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void catchRefNonNull(Function machineInject) { + var instance = instance(machineInject); + assertEquals(42, instance.export("catch-ref-non-null").apply(42)[0]); + } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void catchAllRefNonNull(Function machineInject) { + var instance = instance(machineInject); + assertEquals(42, instance.export("catch-all-ref-non-null").apply(42)[0]); + } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void rethrowNonNull(Function machineInject) { + var instance = instance(machineInject); + assertEquals(42, instance.export("rethrow-non-null").apply(42)[0]); + } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void catchRefNullable(Function machineInject) { + var instance = instance(machineInject); + assertEquals(42, instance.export("catch-ref-nullable").apply(42)[0]); + } +} diff --git a/runtime-tests/pom.xml b/runtime-tests/pom.xml index 1541847c7..31354d968 100644 --- a/runtime-tests/pom.xml +++ b/runtime-tests/pom.xml @@ -267,6 +267,7 @@ proposals/threads/exports.wast proposals/threads/imports.wast proposals/threads/memory.wast + proposals/wasm-3.0/try_table.wast ref_func.wast ref_is_null.wast ref_null.wast @@ -737,6 +738,7 @@ proposals/threads/exports.wast proposals/threads/imports.wast proposals/threads/memory.wast + proposals/wasm-3.0/try_table.wast ref_func.wast ref_is_null.wast ref_null.wast diff --git a/wasm-corpus/src/main/resources/compiled/catch_ref_non_null.wat.wasm b/wasm-corpus/src/main/resources/compiled/catch_ref_non_null.wat.wasm new file mode 100644 index 000000000..5d5316724 Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/catch_ref_non_null.wat.wasm differ diff --git a/wasm-corpus/src/main/resources/wat/catch_ref_non_null.wat b/wasm-corpus/src/main/resources/wat/catch_ref_non_null.wat new file mode 100644 index 000000000..1603567a2 --- /dev/null +++ b/wasm-corpus/src/main/resources/wat/catch_ref_non_null.wat @@ -0,0 +1,63 @@ +(module + ;; `catch_ref` and `catch_all_ref` send a non-nullable `(ref exn)` to the handler label. + + (tag $e (param i32)) + + (func $throw (param $val i32) + (throw $e (local.get $val)) + ) + + (func (export "catch-ref-non-null") (param $val i32) (result i32) + (block $h (result i32 (ref exn)) + (try_table (result i32) (catch_ref $e $h) + (call $throw (local.get $val)) + (i32.const 0) + ) + (return) + ) + (drop) + ) + + (func (export "catch-all-ref-non-null") (param $val i32) (result i32) + (block $h (result (ref exn)) + (try_table (catch_all_ref $h) + (call $throw (local.get $val)) + ) + (return (i32.const 0)) + ) + (drop) + (local.get $val) + ) + + ;; the caught `(ref exn)` is usable: store it in a nullable local and rethrow it + (func (export "rethrow-non-null") (param $val i32) (result i32) + (local $exn exnref) + (block $h (result (ref exn)) + (try_table (catch_all_ref $h) + (call $throw (local.get $val)) + ) + (return (i32.const 0)) + ) + (local.set $exn) + (block $h2 (result i32 (ref exn)) + (try_table (result i32) (catch_ref $e $h2) + (local.get $exn) + (throw_ref) + ) + (return) + ) + (drop) + ) + + ;; the nullable spelling has to keep working, `(ref exn)` is a subtype of `exnref` + (func (export "catch-ref-nullable") (param $val i32) (result i32) + (block $h (result i32 exnref) + (try_table (result i32) (catch_ref $e $h) + (call $throw (local.get $val)) + (i32.const 0) + ) + (return) + ) + (drop) + ) +) diff --git a/wasm/src/main/java/run/endive/wasm/Validator.java b/wasm/src/main/java/run/endive/wasm/Validator.java index 994b44c1b..417a01123 100644 --- a/wasm/src/main/java/run/endive/wasm/Validator.java +++ b/wasm/src/main/java/run/endive/wasm/Validator.java @@ -1009,13 +1009,13 @@ void validateFunction(int funcIdx, FunctionBody body, FunctionType functionType) getTagType(currentCatch.tag()) .typeIdx()); pushVals(tagType.params()); - pushVal(ValType.ExnRef); + pushVal(ValType.RefExn); break; } case CATCH_ALL: break; case CATCH_ALL_REF: - pushVal(ValType.ExnRef); + pushVal(ValType.RefExn); break; } popCtrl(); diff --git a/wasm/src/main/java/run/endive/wasm/types/ValType.java b/wasm/src/main/java/run/endive/wasm/types/ValType.java index e66f04a10..40a49a64f 100644 --- a/wasm/src/main/java/run/endive/wasm/types/ValType.java +++ b/wasm/src/main/java/run/endive/wasm/types/ValType.java @@ -37,6 +37,10 @@ public final class ValType { public static final ValType RefBot = new ValType(ValType.ID.Ref, ValType.TypeIdxCode.BOT.code()); + /** The non-nullable {@code (ref exn)}. */ + public static final ValType RefExn = + new ValType(ValType.ID.Ref, ValType.TypeIdxCode.EXN.code()); + private final long id; // defined function type. This is not representable in the binary or textual representation