diff --git a/AGENT.md b/AGENT.md index 120a3e16b..f0557e221 100644 --- a/AGENT.md +++ b/AGENT.md @@ -122,7 +122,6 @@ mvn surefire:test -pl runtime-tests -Dtest=SpecV1GcStructTest - `ImportFunction.java` — imported function representation with cross-module type validation - `ConstantEvaluators.java` — constant expression evaluation (globals, element/data segments) - `WasmStruct.java`, `WasmArray.java`, `WasmI31Ref.java` — GC object types -- `internal/GcRefStore.java` — auto-keyed store for Wasm GC references with mark-sweep collection ### `compiler` module - `MachineFactoryCompiler.java` — entry point for the JVM bytecode compiler diff --git a/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java b/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java index 5228f6f1c..baae22163 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java +++ b/compiler/src/main/java/run/endive/compiler/internal/CompilerUtil.java @@ -58,7 +58,6 @@ private CompilerUtil() {} public static Class jvmType(ValType type) { switch (type.opcode()) { case ValType.ID.I32: - case ValType.ID.ExnRef: return int.class; case ValType.ID.Ref: case ValType.ID.RefNull: @@ -79,7 +78,6 @@ public static Class jvmType(ValType type) { public static Type asmType(ValType type) { switch (type.opcode()) { case ValType.ID.I32: - case ValType.ID.ExnRef: return INT_TYPE; case ValType.ID.Ref: case ValType.ID.RefNull: @@ -106,7 +104,6 @@ public static ValType localType(FunctionType type, FunctionBody body, int localI public static void emitLongToJvm(MethodVisitor asm, ValType type) { switch (type.opcode()) { case ValType.ID.I32: - case ValType.ID.ExnRef: asm.visitInsn(Opcodes.L2I); return; case ValType.ID.Ref: @@ -134,7 +131,6 @@ public static void emitLongToJvm(MethodVisitor asm, ValType type) { public static void emitJvmToLong(MethodVisitor asm, ValType type) { switch (type.opcode()) { case ValType.ID.I32: - case ValType.ID.ExnRef: asm.visitInsn(Opcodes.I2L); return; case ValType.ID.Ref: @@ -225,8 +221,6 @@ public static Object defaultValue(ValType type) { return null; // GC refs use null as their default } return REF_NULL_VALUE; - case ValType.ID.ExnRef: - return REF_NULL_VALUE; default: throw new IllegalArgumentException("Unsupported ValType: " + type); } diff --git a/compiler/src/main/java/run/endive/compiler/internal/Emitters.java b/compiler/src/main/java/run/endive/compiler/internal/Emitters.java index 5d5ae6481..c4e767ad0 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Emitters.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Emitters.java @@ -35,7 +35,6 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.commons.InstructionAdapter; -import run.endive.runtime.Instance; import run.endive.runtime.OpCodeIdentifier; import run.endive.runtime.WasmException; import run.endive.wasm.WasmEngineException; @@ -1565,11 +1564,8 @@ public static void THROW(Context ctx, CompilerInstruction ins, InstructionAdapte } public static void THROW_REF(Context ctx, CompilerInstruction ins, InstructionAdapter asm) { - // The exception reference is already on the stack as an integer - // Get the instance and retrieve the exception - asm.load(ctx.instanceSlot(), OBJECT_TYPE); - asm.swap(); // Swap instance and exception reference - emitInvokeVirtual(asm, ShadedRefs.INSTANCE_GET_EXCEPTION); + // Narrow the exnref on the stack, trapping if it is null + emitInvokeStatic(asm, ShadedRefs.WASM_EXCEPTION_CHECKED); asm.athrow(); } @@ -1703,15 +1699,8 @@ public static void CATCH_COMPARE_TAG( public static void CATCH_REGISTER_EXCEPTION( Context ctx, CompilerInstruction ins, InstructionAdapter asm) { - // Register exception and push its - // index - asm.load(ctx.instanceSlot(), OBJECT_TYPE); + // Push the caught exception, already stored as an object by CATCH_START asm.load(ctx.tempSlot(), OBJECT_TYPE); - asm.invokevirtual( - getInternalName(Instance.class), - "registerException", - getMethodDescriptor(INT_TYPE, getType(WasmException.class)), - false); } // ========= GC Operations ========= diff --git a/compiler/src/main/java/run/endive/compiler/internal/Shaded.java b/compiler/src/main/java/run/endive/compiler/internal/Shaded.java index 9de122acd..fd09e432e 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/Shaded.java +++ b/compiler/src/main/java/run/endive/compiler/internal/Shaded.java @@ -522,7 +522,6 @@ public static WasmException createWasmException(long[] args, int tagNumber, Inst } WasmException e = WasmException.builder().instance(instance).tagIdx(tagNumber).args(args).build(); - instance.registerException(e); return e; } @@ -538,7 +537,6 @@ public static WasmException createWasmExceptionGc( .args(args) .refArgs(refArgs) .build(); - instance.registerException(e); return e; } diff --git a/compiler/src/main/java/run/endive/compiler/internal/ShadedRefs.java b/compiler/src/main/java/run/endive/compiler/internal/ShadedRefs.java index 5921703fd..c4ea3c1ce 100644 --- a/compiler/src/main/java/run/endive/compiler/internal/ShadedRefs.java +++ b/compiler/src/main/java/run/endive/compiler/internal/ShadedRefs.java @@ -82,7 +82,7 @@ public final class ShadedRefs { // Exception handling methods static final Method CREATE_WASM_EXCEPTION; static final Method CREATE_WASM_EXCEPTION_GC; - static final Method INSTANCE_GET_EXCEPTION; + static final Method WASM_EXCEPTION_CHECKED; static final Method EXCEPTION_MATCHES; static final Method MEMORY_ATOMIC_INT_WRITE; @@ -398,7 +398,7 @@ public final class ShadedRefs { Object[].class, int.class, Instance.class); - INSTANCE_GET_EXCEPTION = Instance.class.getMethod("exn", int.class); + WASM_EXCEPTION_CHECKED = WasmException.class.getMethod("checked", Object.class); EXCEPTION_MATCHES = Shaded.class.getMethod( "exceptionMatches", WasmException.class, int.class, Instance.class); diff --git a/compiler/src/test/java/run/endive/approvals/ApprovalTest.java b/compiler/src/test/java/run/endive/approvals/ApprovalTest.java index 3c5f2ed1f..9645ca101 100644 --- a/compiler/src/test/java/run/endive/approvals/ApprovalTest.java +++ b/compiler/src/test/java/run/endive/approvals/ApprovalTest.java @@ -103,6 +103,13 @@ public void verifyExceptions() { verifyGeneratedBytecode("exceptions.wat.wasm", (name) -> !name.contains("FuncGroup")); } + /** Pins the bytecode for catch_ref / catch_all_ref / throw_ref. */ + @Test + public void verifyExceptionRefs() { + verifyGeneratedBytecode( + "catch_ref_non_null.wat.wasm", (name) -> !name.contains("FuncGroup")); + } + @Test public void verifyTailCall() { verifyGeneratedBytecode("tail_call_return_call.wat.wasm"); diff --git a/compiler/src/test/resources/run/endive/approvals/ApprovalTest.verifyExceptionRefs.approved.txt b/compiler/src/test/resources/run/endive/approvals/ApprovalTest.verifyExceptionRefs.approved.txt new file mode 100644 index 000000000..f6233ca73 --- /dev/null +++ b/compiler/src/test/resources/run/endive/approvals/ApprovalTest.verifyExceptionRefs.approved.txt @@ -0,0 +1,258 @@ +final class run/endive/$gen/CompiledMachineFuncGroup_0 { + + public static func_0(ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)V + ILOAD 0 + ISTORE 3 + ICONST_1 + NEWARRAY T_LONG + DUP + ICONST_0 + ILOAD 3 + I2L + LASTORE + ICONST_0 + ALOAD 2 + INVOKESTATIC run/endive/$gen/CompiledMachineShaded.createWasmException ([JILrun/endive/runtime/Instance;)Lrun/endive/runtime/WasmException; + ATHROW + L0 + ATHROW + + public static call_0(Lrun/endive/runtime/Instance;Lrun/endive/runtime/Memory;[J[Ljava/lang/Object;)[J + ALOAD 2 + ICONST_0 + LALOAD + L2I + ALOAD 1 + ALOAD 0 + INVOKESTATIC run/endive/$gen/CompiledMachineFuncGroup_0.func_0 (ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)V + ACONST_NULL + ARETURN + + public static func_1(ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)I + TRYCATCHBLOCK L0 L1 L2 run/endive/runtime/WasmException + L0 + ILOAD 0 + INVOKESTATIC run/endive/$gen/CompiledMachineShaded.checkInterruption ()V + ALOAD 1 + ALOAD 2 + INVOKESTATIC run/endive/$gen/CompiledMachineFuncGroup_0.func_0 (ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)V + ICONST_0 + L1 + GOTO L3 + L2 + ASTORE 3 + ALOAD 3 + ICONST_0 + ALOAD 2 + INVOKESTATIC run/endive/$gen/CompiledMachineShaded.exceptionMatches (Lrun/endive/runtime/WasmException;ILrun/endive/runtime/Instance;)Z + IFEQ L4 + ALOAD 3 + INVOKEVIRTUAL run/endive/runtime/WasmException.args ()[J + ASTORE 4 + ALOAD 4 + ICONST_0 + LALOAD + L2I + ALOAD 3 + GOTO L5 + L4 + ALOAD 3 + ATHROW + L3 + IRETURN + L5 + POP + IRETURN + + public static call_1(Lrun/endive/runtime/Instance;Lrun/endive/runtime/Memory;[J[Ljava/lang/Object;)[J + ALOAD 2 + ICONST_0 + LALOAD + L2I + ALOAD 1 + ALOAD 0 + INVOKESTATIC run/endive/$gen/CompiledMachineFuncGroup_0.func_1 (ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)I + I2L + LSTORE 4 + ICONST_1 + NEWARRAY T_LONG + DUP + ICONST_0 + LLOAD 4 + LASTORE + ARETURN + + public static func_2(ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)I + TRYCATCHBLOCK L0 L1 L2 run/endive/runtime/WasmException + L0 + ILOAD 0 + INVOKESTATIC run/endive/$gen/CompiledMachineShaded.checkInterruption ()V + ALOAD 1 + ALOAD 2 + INVOKESTATIC run/endive/$gen/CompiledMachineFuncGroup_0.func_0 (ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)V + L1 + GOTO L3 + L2 + ASTORE 3 + ALOAD 3 + GOTO L4 + L5 + NOP + ATHROW + L3 + ICONST_0 + IRETURN + L4 + POP + ILOAD 0 + IRETURN + + public static call_2(Lrun/endive/runtime/Instance;Lrun/endive/runtime/Memory;[J[Ljava/lang/Object;)[J + ALOAD 2 + ICONST_0 + LALOAD + L2I + ALOAD 1 + ALOAD 0 + INVOKESTATIC run/endive/$gen/CompiledMachineFuncGroup_0.func_2 (ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)I + I2L + LSTORE 4 + ICONST_1 + NEWARRAY T_LONG + DUP + ICONST_0 + LLOAD 4 + LASTORE + ARETURN + + public static func_3(ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)I + TRYCATCHBLOCK L0 L1 L2 run/endive/runtime/WasmException + TRYCATCHBLOCK L3 L4 L5 run/endive/runtime/WasmException + ACONST_NULL + ASTORE 3 + L3 + ILOAD 0 + INVOKESTATIC run/endive/$gen/CompiledMachineShaded.checkInterruption ()V + ALOAD 1 + ALOAD 2 + INVOKESTATIC run/endive/$gen/CompiledMachineFuncGroup_0.func_0 (ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)V + L4 + GOTO L6 + L5 + ASTORE 4 + ALOAD 4 + GOTO L7 + L8 + NOP + NOP + ATHROW + L6 + ICONST_0 + IRETURN + L7 + ASTORE 3 + L0 + ALOAD 3 + INVOKESTATIC run/endive/runtime/WasmException.checked (Ljava/lang/Object;)Lrun/endive/runtime/WasmException; + ATHROW + L1 + NOP + NOP + ATHROW + L2 + ASTORE 4 + ALOAD 4 + ICONST_0 + ALOAD 2 + INVOKESTATIC run/endive/$gen/CompiledMachineShaded.exceptionMatches (Lrun/endive/runtime/WasmException;ILrun/endive/runtime/Instance;)Z + IFEQ L9 + ALOAD 4 + INVOKEVIRTUAL run/endive/runtime/WasmException.args ()[J + ASTORE 5 + ALOAD 5 + ICONST_0 + LALOAD + L2I + ALOAD 4 + GOTO L10 + L9 + ALOAD 4 + ATHROW + L11 + ATHROW + L10 + POP + IRETURN + + public static call_3(Lrun/endive/runtime/Instance;Lrun/endive/runtime/Memory;[J[Ljava/lang/Object;)[J + ALOAD 2 + ICONST_0 + LALOAD + L2I + ALOAD 1 + ALOAD 0 + INVOKESTATIC run/endive/$gen/CompiledMachineFuncGroup_0.func_3 (ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)I + I2L + LSTORE 4 + ICONST_1 + NEWARRAY T_LONG + DUP + ICONST_0 + LLOAD 4 + LASTORE + ARETURN + + public static func_4(ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)I + TRYCATCHBLOCK L0 L1 L2 run/endive/runtime/WasmException + L0 + ILOAD 0 + INVOKESTATIC run/endive/$gen/CompiledMachineShaded.checkInterruption ()V + ALOAD 1 + ALOAD 2 + INVOKESTATIC run/endive/$gen/CompiledMachineFuncGroup_0.func_0 (ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)V + ICONST_0 + L1 + GOTO L3 + L2 + ASTORE 3 + ALOAD 3 + ICONST_0 + ALOAD 2 + INVOKESTATIC run/endive/$gen/CompiledMachineShaded.exceptionMatches (Lrun/endive/runtime/WasmException;ILrun/endive/runtime/Instance;)Z + IFEQ L4 + ALOAD 3 + INVOKEVIRTUAL run/endive/runtime/WasmException.args ()[J + ASTORE 4 + ALOAD 4 + ICONST_0 + LALOAD + L2I + ALOAD 3 + GOTO L5 + L4 + ALOAD 3 + ATHROW + L3 + IRETURN + L5 + POP + IRETURN + + public static call_4(Lrun/endive/runtime/Instance;Lrun/endive/runtime/Memory;[J[Ljava/lang/Object;)[J + ALOAD 2 + ICONST_0 + LALOAD + L2I + ALOAD 1 + ALOAD 0 + INVOKESTATIC run/endive/$gen/CompiledMachineFuncGroup_0.func_4 (ILrun/endive/runtime/Memory;Lrun/endive/runtime/Instance;)I + I2L + LSTORE 4 + ICONST_1 + NEWARRAY T_LONG + DUP + ICONST_0 + LLOAD 4 + LASTORE + ARETURN +} diff --git a/machine-tests/src/test/java/run/endive/testing/ExnRefHostTest.java b/machine-tests/src/test/java/run/endive/testing/ExnRefHostTest.java new file mode 100644 index 000000000..539b281b3 --- /dev/null +++ b/machine-tests/src/test/java/run/endive/testing/ExnRefHostTest.java @@ -0,0 +1,74 @@ +package run.endive.testing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +import java.util.List; +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.CallResult; +import run.endive.runtime.HostFunction; +import run.endive.runtime.ImportValues; +import run.endive.runtime.Instance; +import run.endive.runtime.InterpreterMachine; +import run.endive.runtime.WasmException; +import run.endive.wasm.Parser; +import run.endive.wasm.WasmModule; +import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.ValType; + +/** An exnref crossing to a host function and back keeps its identity. */ +public class ExnRefHostTest { + + private static final WasmModule MODULE = + Parser.parse(CorpusResources.getResource("compiled/exnref_host.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) { + var roundtrip = + new HostFunction( + "host", + "roundtrip", + FunctionType.of(List.of(ValType.ExnRef), List.of(ValType.ExnRef)), + new run.endive.runtime.WasmFunctionHandle() { + @Override + public long[] apply(Instance instance, long... args) { + throw new UnsupportedOperationException("use applyWithRefs"); + } + + @Override + public CallResult applyWithRefs( + Instance instance, long[] args, Object[] refArgs) { + // the host sees the real exception object, not an index + assertInstanceOf(WasmException.class, refArgs[0]); + return CallResult.of(new long[1], new Object[] {refArgs[0]}); + } + }); + return machineInject + .apply( + Instance.builder(MODULE) + .withImportValues( + ImportValues.builder().addFunction(roundtrip).build())) + .build(); + } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void roundTripThroughHost(Function machineInject) { + assertEquals(42, instance(machineInject).export("roundtrip-payload").apply(42)[0]); + } +} diff --git a/machine-tests/src/test/java/run/endive/testing/ExnRefIdentityTest.java b/machine-tests/src/test/java/run/endive/testing/ExnRefIdentityTest.java new file mode 100644 index 000000000..a267ac7e8 --- /dev/null +++ b/machine-tests/src/test/java/run/endive/testing/ExnRefIdentityTest.java @@ -0,0 +1,68 @@ +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; + +/** + * Two live exceptions sharing a tag must stay distinct. Expected values cross-checked against + * wasmtime 50.0.0-dev. + */ +public class ExnRefIdentityTest { + + private static final WasmModule MODULE = + Parser.parse(CorpusResources.getResource("compiled/exnref_identity.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 locals(Function machineInject) { + assertEquals(12, instance(machineInject).export("locals").apply()[0]); + } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void globals(Function machineInject) { + assertEquals(12, instance(machineInject).export("globals").apply()[0]); + } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void table(Function machineInject) { + assertEquals(12, instance(machineInject).export("table").apply()[0]); + } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void select(Function machineInject) { + var instance = instance(machineInject); + assertEquals(1, instance.export("select").apply(1)[0]); + assertEquals(2, instance.export("select").apply(0)[0]); + } +} diff --git a/machine-tests/src/test/java/run/endive/testing/ExnRefOpsTest.java b/machine-tests/src/test/java/run/endive/testing/ExnRefOpsTest.java new file mode 100644 index 000000000..88465cded --- /dev/null +++ b/machine-tests/src/test/java/run/endive/testing/ExnRefOpsTest.java @@ -0,0 +1,66 @@ +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; + +/** + * `ref.test` against the exception hierarchy and `br_table` carrying an exnref. Expected values + * cross-checked against wasmtime 50.0.0-dev. + */ +public class ExnRefOpsTest { + + private static final WasmModule MODULE = + Parser.parse(CorpusResources.getResource("compiled/exnref_ops.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 refTestExn(Function machineInject) { + var instance = instance(machineInject); + assertEquals(1, instance.export("test-exn-nonnull").apply()[0]); + assertEquals(0, instance.export("test-exn-null").apply()[0]); + assertEquals(1, instance.export("test-nullable-exn-null").apply()[0]); + } + + /** noexn is uninhabited, so a real exception is never an instance of it. */ + @ParameterizedTest + @MethodSource("machineImplementations") + public void refTestNoExn(Function machineInject) { + assertEquals(0, instance(machineInject).export("test-noexn-nonnull").apply()[0]); + } + + @ParameterizedTest + @MethodSource("machineImplementations") + public void brTableCarriesExnRef(Function machineInject) { + var instance = instance(machineInject); + assertEquals(10, instance.export("br-table").apply(0)[0]); + assertEquals(20, instance.export("br-table").apply(1)[0]); + } +} diff --git a/machine-tests/src/test/java/run/endive/testing/NoExnTest.java b/machine-tests/src/test/java/run/endive/testing/NoExnTest.java index 7f33c057d..5e2873d1a 100644 --- a/machine-tests/src/test/java/run/endive/testing/NoExnTest.java +++ b/machine-tests/src/test/java/run/endive/testing/NoExnTest.java @@ -1,6 +1,8 @@ package run.endive.testing; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.function.Function; import java.util.stream.Stream; @@ -14,7 +16,6 @@ import run.endive.runtime.InterpreterMachine; import run.endive.wasm.Parser; import run.endive.wasm.WasmModule; -import run.endive.wasm.types.Value; /** Tests for the `noexn` bottom type of the exception hierarchy. */ public class NoExnTest { @@ -42,7 +43,18 @@ private static Instance instance(Function ma @MethodSource("machineImplementations") public void nullToExnRef(Function machineInject) { var instance = instance(machineInject); - assertEquals(Value.REF_NULL_VALUE, instance.export("null-to-exnref").apply()[0]); + var result = instance.export("null-to-exnref").applyWithRefs(new long[0], new Object[0]); + assertNull(result.refResult(0)); + } + + /** An exnref signature is an object-ref signature, so the flat path is rejected. */ + @ParameterizedTest + @MethodSource("machineImplementations") + public void exnRefRejectsApply(Function machineInject) { + var instance = instance(machineInject); + assertThrows( + UnsupportedOperationException.class, + () -> instance.export("null-to-exnref").apply()); } @ParameterizedTest diff --git a/runtime/src/main/java/run/endive/runtime/Instance.java b/runtime/src/main/java/run/endive/runtime/Instance.java index 9ea6ae292..9fed504c2 100644 --- a/runtime/src/main/java/run/endive/runtime/Instance.java +++ b/runtime/src/main/java/run/endive/runtime/Instance.java @@ -72,8 +72,6 @@ public class Instance implements AutoCloseable { private final ExecutionListener listener; private final Exports fluentExports; - private final Map exnRefs; - private TailCallPending tailCallPending; static final class TailCallPending { @@ -135,8 +133,6 @@ static final class TailCallPending { this.globalFactory = globalFactory; this.fluentExports = new Exports(this); - this.exnRefs = new HashMap<>(); - for (int i = 0; i < tables.length; i++) { var result = computeConstant(this, tables[i].initialize()); int initValue = (int) result.longValue(); @@ -298,7 +294,7 @@ public long[] apply(long... args) { throw new UnsupportedOperationException( "Function '" + name - + "' uses GC reference types." + + "' uses reference types carried as objects." + " Use applyWithRefs()."); } return instance.machine.call(export.index(), args); @@ -459,19 +455,6 @@ public int tagCount() { return tags.length; } - public int registerException(WasmException ex) { - exnRefs.put(ex.tagIdx(), ex); - return ex.tagIdx(); - } - - public WasmException exn(int idx) { - var exn = exnRefs.get(idx); - if (exn == null) { - throw new TrapException("Trapped on throw_ref on null reference"); - } - return exn; - } - @Deprecated public long[] array(int idx) { throw new UnsupportedOperationException( @@ -542,6 +525,9 @@ public boolean heapTypeMatchRef( || targetHeapType == ValType.TypeIdxCode.EXTERN.code()) { return true; } + if (targetHeapType == ValType.TypeIdxCode.EXN.code()) { + return ref instanceof WasmException; + } if (ref instanceof WasmGcRef) { return heapTypeSubOf(((WasmGcRef) ref).typeIdx(), targetHeapType); } diff --git a/runtime/src/main/java/run/endive/runtime/InterpreterMachine.java b/runtime/src/main/java/run/endive/runtime/InterpreterMachine.java index 889a175e6..7cf78e3f5 100644 --- a/runtime/src/main/java/run/endive/runtime/InterpreterMachine.java +++ b/runtime/src/main/java/run/endive/runtime/InterpreterMachine.java @@ -88,12 +88,14 @@ protected long[] call( // When called via call(int, long[]) with no refArgs and the function // has externref params, populate refArgs from longs so the ref stack // is set up correctly. Uses WasmExternRef (the proper externref type). + // Only externref has a meaningful long encoding here; any other object + // ref stays null rather than being wrapped in the wrong type. if (refArgs == null && type.hasObjectRefParams()) { refArgs = new Object[args.length]; int slot = 0; for (int pi = 0; pi < type.params().size(); pi++) { var param = type.params().get(pi); - if (param.isObjectRef()) { + if (param.isObjectRef() && isExternHeapType(param.typeIdx())) { long val = args[slot]; refArgs[slot] = (val == REF_NULL_VALUE) ? null : new WasmExternRef(val); } @@ -159,7 +161,7 @@ protected long[] call( } } } catch (WasmException e) { - THROW_REF(instance, instance.registerException(e), stack, stackFrame, callStack); + THROW_REF(instance, e, stack, stackFrame, callStack); } catch (StackOverflowError e) { throw new WasmEngineException("call stack exhausted", e); } finally { @@ -348,14 +350,12 @@ protected void eval(MStack stack, Instance instance, Deque callStack .args(args) .refArgs(refArgs) .build(); - var exceptionIdx = instance.registerException(exception); - frame = THROW_REF(instance, exceptionIdx, stack, frame, callStack); + frame = THROW_REF(instance, exception, stack, frame, callStack); break; } case THROW_REF: { - var exceptionIdx = (int) stack.pop(); - frame = THROW_REF(instance, exceptionIdx, stack, frame, callStack); + frame = THROW_REF(instance, stack.popRef(), stack, frame, callStack); break; } case CALL_INDIRECT: @@ -1874,9 +1874,7 @@ private static void F32_CONVERT_I64_S(MStack stack) { private static void REF_NULL(MStack stack, Operands operands) { var heapType = (int) operands.get(0); if (heapType == ValType.TypeIdxCode.FUNC.code() - || heapType == ValType.TypeIdxCode.NOFUNC.code() - || heapType == ValType.TypeIdxCode.EXN.code() - || heapType == ValType.TypeIdxCode.NOEXN.code()) { + || heapType == ValType.TypeIdxCode.NOFUNC.code()) { stack.push(REF_NULL_VALUE); } else { // GC refs, externref, noexternref all use Object null @@ -2906,7 +2904,7 @@ private static StackFrame RETURN_CALL( } } } catch (WasmException e) { - THROW_REF(instance, instance.registerException(e), stack, newFrame, callStack); + THROW_REF(instance, e, stack, newFrame, callStack); } if (fromCallStack) { callStack.push(newFrame); @@ -3003,7 +3001,7 @@ private static StackFrame RETURN_CALL_INDIRECT( } } } catch (WasmException e) { - THROW_REF(instance, instance.registerException(e), stack, newFrame, callStack); + THROW_REF(instance, e, stack, newFrame, callStack); } if (fromCallStack) { callStack.push(newFrame); @@ -3135,11 +3133,11 @@ private static int numberOfValuesToReturn(Instance instance, AnnotatedInstructio protected static StackFrame THROW_REF( Instance instance, - int exceptionIdx, + Object exnRef, MStack stack, StackFrame frame, Deque callStack) { - var exception = instance.exn(exceptionIdx); + var exception = WasmException.checked(exnRef); boolean found = false; while (!found) { while (frame.ctrlStackSize() > 0) { @@ -3184,7 +3182,7 @@ protected static StackFrame THROW_REF( if (currentCatch.tag() == exception.tagIdx() || compatibleImport) { found = true; pushExceptionArgs(exception, stack); - stack.push(exceptionIdx); + stack.pushRef(exception); } break; case CATCH_ALL: @@ -3192,7 +3190,7 @@ protected static StackFrame THROW_REF( break; case CATCH_ALL_REF: found = true; - stack.push(exceptionIdx); + stack.pushRef(exception); break; } @@ -3980,13 +3978,16 @@ private static void pushExceptionArgs(WasmException exception, MStack stack) { } } + private static boolean isExternHeapType(int heapType) { + return heapType == ValType.TypeIdxCode.EXTERN.code() + || heapType == ValType.TypeIdxCode.NOEXTERN.code(); + } + private static boolean isSourceGcRef(int sourceHeapType) { return sourceHeapType != ValType.TypeIdxCode.FUNC.code() && sourceHeapType != ValType.TypeIdxCode.NOFUNC.code() && sourceHeapType != ValType.TypeIdxCode.EXTERN.code() - && sourceHeapType != ValType.TypeIdxCode.NOEXTERN.code() - && sourceHeapType != ValType.TypeIdxCode.EXN.code() - && sourceHeapType != ValType.TypeIdxCode.NOEXN.code(); + && sourceHeapType != ValType.TypeIdxCode.NOEXTERN.code(); } private static void REF_TEST( diff --git a/runtime/src/main/java/run/endive/runtime/WasmException.java b/runtime/src/main/java/run/endive/runtime/WasmException.java index da0b6a36c..bb38cba47 100644 --- a/runtime/src/main/java/run/endive/runtime/WasmException.java +++ b/runtime/src/main/java/run/endive/runtime/WasmException.java @@ -35,6 +35,14 @@ private WasmException(Builder b) { (b.refArgs != null) ? b.refArgs.clone() : null); } + /** Narrows an exnref value, trapping on null as {@code throw_ref} requires. */ + public static WasmException checked(Object exnref) { + if (exnref == null) { + throw new TrapException("Trapped on throw_ref on null reference"); + } + return (WasmException) exnref; + } + public Instance instance() { return instance; } diff --git a/runtime/src/main/java/run/endive/runtime/internal/CompilerInterpreterMachine.java b/runtime/src/main/java/run/endive/runtime/internal/CompilerInterpreterMachine.java index ef3649513..805d3bac6 100644 --- a/runtime/src/main/java/run/endive/runtime/internal/CompilerInterpreterMachine.java +++ b/runtime/src/main/java/run/endive/runtime/internal/CompilerInterpreterMachine.java @@ -107,7 +107,7 @@ protected void CALL(Operands operands) { } catch (WasmException e) { // we need at least an empty frame var stackFrame = new StackFrame(instance, funcId, args); - THROW_REF(instance, instance.registerException(e), stack, stackFrame, callStack); + THROW_REF(instance, e, stack, stackFrame, callStack); } } } diff --git a/test-gen-lib/src/main/java/run/endive/testgen/wast/WasmValue.java b/test-gen-lib/src/main/java/run/endive/testgen/wast/WasmValue.java index c408f93e9..8f4df031f 100644 --- a/test-gen-lib/src/main/java/run/endive/testgen/wast/WasmValue.java +++ b/test-gen-lib/src/main/java/run/endive/testgen/wast/WasmValue.java @@ -156,10 +156,11 @@ public NameExpr toAssertion(String resultVar, String moduleName) { case NULL_REF: case NULL_FUNC_REF: case NULL_EXTERN_REF: - case NULL_EXN_REF: - case EXN_REF: return new NameExpr( "assertEquals(" + resultVar + ", " + "REF_NULL_VALUE" + ")"); + case NULL_EXN_REF: + case EXN_REF: + return new NameExpr("assertNull(" + resultVar + ")"); case STRUCT_REF: case ANY_REF: case I31_REF: @@ -306,12 +307,15 @@ public NameExpr toRefAssertion(String resultVar, String moduleName) { return new NameExpr("assertNotNull(" + resultVar + ")"); case EXTERN_REF: return new NameExpr("assertNotNull(" + resultVar + ")"); + case EXN_REF: + return new NameExpr("assertNotNull(" + resultVar + ")"); case REF_NULL: case NULL_REF: // These are GC null types -> Java null from popRef() return new NameExpr("assertNull(" + resultVar + ")"); case NULL_FUNC_REF: case NULL_EXTERN_REF: + case NULL_EXN_REF: return new NameExpr("assertNull(" + resultVar + ")"); case STRUCT_REF: case ANY_REF: diff --git a/test-gen-lib/src/main/java/run/endive/testgen/wast/WasmValueType.java b/test-gen-lib/src/main/java/run/endive/testgen/wast/WasmValueType.java index 6aa92c1b8..fc72eb1fc 100644 --- a/test-gen-lib/src/main/java/run/endive/testgen/wast/WasmValueType.java +++ b/test-gen-lib/src/main/java/run/endive/testgen/wast/WasmValueType.java @@ -63,6 +63,8 @@ public boolean isObjectRef() { case REF_NULL: case EXTERN_REF: case NULL_EXTERN_REF: + case EXN_REF: + case NULL_EXN_REF: return true; default: return false; diff --git a/wasm-corpus/src/main/resources/compiled/exnref_host.wat.wasm b/wasm-corpus/src/main/resources/compiled/exnref_host.wat.wasm new file mode 100644 index 000000000..9f27c92c3 Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/exnref_host.wat.wasm differ diff --git a/wasm-corpus/src/main/resources/compiled/exnref_identity.wat.wasm b/wasm-corpus/src/main/resources/compiled/exnref_identity.wat.wasm new file mode 100644 index 000000000..ebadf89f4 Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/exnref_identity.wat.wasm differ diff --git a/wasm-corpus/src/main/resources/compiled/exnref_ops.wat.wasm b/wasm-corpus/src/main/resources/compiled/exnref_ops.wat.wasm new file mode 100644 index 000000000..7b3eaf32d Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/exnref_ops.wat.wasm differ diff --git a/wasm-corpus/src/main/resources/wat/exnref_host.wat b/wasm-corpus/src/main/resources/wat/exnref_host.wat new file mode 100644 index 000000000..573eda446 --- /dev/null +++ b/wasm-corpus/src/main/resources/wat/exnref_host.wat @@ -0,0 +1,20 @@ +(module + ;; An exnref handed to the host and handed straight back must survive the round trip. + (import "host" "roundtrip" (func $roundtrip (param exnref) (result exnref))) + + (tag $e (param i32)) + (func $throw (param i32) (throw $e (local.get 0))) + + (func $capture (param $val i32) (result (ref exn)) + (block $h (result (ref exn)) + (try_table (catch_all_ref $h) (call $throw (local.get $val))) + (unreachable))) + + (func (export "roundtrip-payload") (param $val i32) (result i32) + (block $h (result i32 (ref exn)) + (try_table (result i32) (catch_ref $e $h) + (call $roundtrip (call $capture (local.get $val))) + (throw_ref)) + (return)) + (drop)) +) diff --git a/wasm-corpus/src/main/resources/wat/exnref_identity.wat b/wasm-corpus/src/main/resources/wat/exnref_identity.wat new file mode 100644 index 000000000..53e85a73d --- /dev/null +++ b/wasm-corpus/src/main/resources/wat/exnref_identity.wat @@ -0,0 +1,71 @@ +(module + ;; Each `throw` creates a distinct exception. Two live exceptions sharing a tag + ;; must not alias, whether they are held in locals, globals or a table. + + (tag $e (param i32)) + (func $throw (param i32) (throw $e (local.get 0))) + + (global $ga (mut exnref) (ref.null exn)) + (global $gb (mut exnref) (ref.null exn)) + (table $t 2 exnref) + + ;; catch one exception and hand it back + (func $capture (param $val i32) (result (ref exn)) + (block $h (result (ref exn)) + (try_table (catch_all_ref $h) (call $throw (local.get $val))) + (unreachable) + ) + ) + + ;; rethrow the given exnref and report the payload the handler observes + (func $payload_of (param $x exnref) (result i32) + (block $h (result i32 (ref exn)) + (try_table (result i32) (catch_ref $e $h) + (local.get $x) + (throw_ref) + ) + (return) + ) + (drop) + ) + + ;; two exceptions on one tag, held in locals + (func (export "locals") (result i32) + (local $a exnref) + (local $b exnref) + (local.set $a (call $capture (i32.const 1))) + (local.set $b (call $capture (i32.const 2))) + (i32.add + (i32.mul (call $payload_of (local.get $a)) (i32.const 10)) + (call $payload_of (local.get $b))) + ) + + ;; the same, held in two globals + (func (export "globals") (result i32) + (global.set $ga (call $capture (i32.const 1))) + (global.set $gb (call $capture (i32.const 2))) + (i32.add + (i32.mul (call $payload_of (global.get $ga)) (i32.const 10)) + (call $payload_of (global.get $gb))) + ) + + ;; the same, held in a table + (func (export "table") (result i32) + (table.set $t (i32.const 0) (call $capture (i32.const 1))) + (table.set $t (i32.const 1) (call $capture (i32.const 2))) + (i32.add + (i32.mul (call $payload_of (table.get $t (i32.const 0))) (i32.const 10)) + (call $payload_of (table.get $t (i32.const 1)))) + ) + + ;; select between two live exceptions -- exercises the un-normalised + ;; VEC_VALUE_TYPE operand path in the validator/interpreter + (func (export "select") (param $pick i32) (result i32) + (local $a exnref) + (local $b exnref) + (local.set $a (call $capture (i32.const 1))) + (local.set $b (call $capture (i32.const 2))) + (call $payload_of + (select (result exnref) (local.get $a) (local.get $b) (local.get $pick))) + ) +) diff --git a/wasm-corpus/src/main/resources/wat/exnref_ops.wat b/wasm-corpus/src/main/resources/wat/exnref_ops.wat new file mode 100644 index 000000000..e26030f56 --- /dev/null +++ b/wasm-corpus/src/main/resources/wat/exnref_ops.wat @@ -0,0 +1,33 @@ +(module + (tag $e (param i32)) + (func $throw (param i32) (throw $e (local.get 0))) + + (func $capture (param $val i32) (result (ref exn)) + (block $h (result (ref exn)) + (try_table (catch_all_ref $h) (call $throw (local.get $val))) + (unreachable))) + + ;; ref.test against the exn hierarchy + (func (export "test-exn-nonnull") (result i32) + (ref.test (ref exn) (call $capture (i32.const 7)))) + (func (export "test-exn-null") (result i32) + (ref.test (ref exn) (ref.null exn))) + (func (export "test-nullable-exn-null") (result i32) + (ref.test (ref null exn) (ref.null exn))) + (func (export "test-noexn-nonnull") (result i32) + (ref.test (ref null noexn) (call $capture (i32.const 7)))) + + ;; br_table carrying an exnref through two different label depths + (func (export "br-table") (param $pick i32) (result i32) + (local $a exnref) + (local.set $a (call $capture (i32.const 5))) + (block $outer (result i32) + (block $l1 (result exnref) + (block $l0 (result exnref) + (local.get $a) + (br_table $l0 $l1 (local.get $pick))) + (drop) + (br $outer (i32.const 10))) + (drop) + (i32.const 20))) +) 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 e568a5203..b1e2688a3 100644 --- a/wasm/src/main/java/run/endive/wasm/types/ValType.java +++ b/wasm/src/main/java/run/endive/wasm/types/ValType.java @@ -116,7 +116,7 @@ private ValType( // Pre-compute gcReference and objectRef for well-known types // (these don't need a TypeSection) this.gcReference = computeIsGcReference(null); - this.objectRef = this.gcReference || computeIsExternRef(); + this.objectRef = this.gcReference || computeIsExternRef() || computeIsExnRef(); } public ValType resolve(TypeSection typeSection) { @@ -129,7 +129,7 @@ public ValType resolve(TypeSection typeSection) { } } this.gcReference = computeIsGcReference(typeSection); - this.objectRef = gcReference || computeIsExternRef(); + this.objectRef = gcReference || computeIsExternRef() || computeIsExnRef(); return this; } @@ -293,8 +293,8 @@ public boolean isGcReference() { } /** - * Returns true if this type is an Object reference on the JVM: GC refs AND externref, - * but NOT funcref (which stays as int for call_indirect dispatch). + * Returns true if this type is an Object reference on the JVM: GC refs, externref and + * exnref, but NOT funcref (which stays as int for call_indirect dispatch). */ public boolean isObjectRef() { return objectRef; @@ -304,6 +304,15 @@ public static boolean isObjectRef(long valTypeId, TypeSection typeSection) { return builder().fromId(valTypeId).isObjectRef(typeSection); } + private boolean computeIsExnRef() { + int op = opcode(); + if (op != ID.Ref && op != ID.RefNull) { + return false; + } + int ht = typeIdx(); + return ht == TypeIdxCode.EXN.code() || ht == TypeIdxCode.NOEXN.code(); + } + private boolean computeIsExternRef() { int op = opcode(); if (op != ID.Ref && op != ID.RefNull) { @@ -805,7 +814,20 @@ public boolean isObjectRef() { } public boolean isObjectRef(TypeSection ts) { - return isGcReference(ts) || isExternRef(); + return isGcReference(ts) || isExternRef() || isExnRef(); + } + + private boolean isExnRef() { + if (!isReference()) { + return false; + } + if (opcode == ID.ExnRef || opcode == ID.NoExnRef) { + return true; + } + if (opcode == ID.Ref || opcode == ID.RefNull) { + return typeIdx == TypeIdxCode.EXN.code() || typeIdx == TypeIdxCode.NOEXN.code(); + } + return false; } private boolean isExternRef() { diff --git a/wasm/src/main/java/run/endive/wasm/types/Value.java b/wasm/src/main/java/run/endive/wasm/types/Value.java index a0627d843..5d7d8a8fd 100644 --- a/wasm/src/main/java/run/endive/wasm/types/Value.java +++ b/wasm/src/main/java/run/endive/wasm/types/Value.java @@ -312,7 +312,6 @@ public static long zero(ValType valType) { case ValType.ID.I64: case ValType.ID.F64: return 0L; - case ValType.ID.ExnRef: case ValType.ID.Ref: case ValType.ID.RefNull: return REF_NULL_VALUE;