diff --git a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java
index 9ac9bfbf7..1e47938c7 100644
--- a/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java
+++ b/build-time-compiler/src/main/java/run/endive/build/time/compiler/Generator.java
@@ -37,22 +37,17 @@
import run.endive.codegen.ModuleInterfaceCodegen;
import run.endive.compiler.internal.ByteClassCollector;
import run.endive.compiler.internal.Compiler;
-import run.endive.runtime.ByteBufferMemory;
import run.endive.runtime.CompiledModule;
import run.endive.runtime.Instance;
import run.endive.runtime.Machine;
-import run.endive.runtime.Memory;
-import run.endive.runtime.TableInstance;
import run.endive.wasm.MalformedException;
import run.endive.wasm.Parser;
import run.endive.wasm.WasmModule;
import run.endive.wasm.WasmWriter;
import run.endive.wasm.types.ExternalType;
-import run.endive.wasm.types.MemoryLimits;
import run.endive.wasm.types.OpCode;
import run.endive.wasm.types.RawSection;
import run.endive.wasm.types.SectionId;
-import run.endive.wasm.types.Table;
public class Generator {
@@ -115,9 +110,6 @@ public void generateSources() throws IOException {
generateLoadMethod(cu, type);
generateMachineFactoryMethod(cu, type, moduleName);
generateWasmModuleMethod(cu, type, moduleName);
- generateBuilderMethod(cu, type, moduleName);
- generateSafeBuilderMethod(cu, type, moduleName);
- generateImportFactoryMethods(cu, type);
dest.add(packageName, moduleName + ".java", cu);
dest.saveAll();
@@ -241,105 +233,6 @@ private static void generateCreateMethod(
method.addStatement(new ReturnStmt(constructorInvocation));
}
- /**
- * Generates:
- *
- * public static Instance.Builder builder() {
- * return Instance.builder(load()).withMachineFactory(<moduleName>::create);
- * }
- *
- *
- *
Every generated module has this, so the same user code compiles whether or
- * not a backend that replaces it is in play. Redline overwrites the body to pick
- * native code when it can, and leaves this one as the fallback.
- */
- private static void generateBuilderMethod(
- CompilationUnit cu, ClassOrInterfaceDeclaration type, String moduleName) {
- cu.addImport(Instance.class);
- type.addMethod("builder", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC)
- .setType(parseClassOrInterfaceType("Instance.Builder"))
- .createBody()
- .addStatement(
- new ReturnStmt(compiledBuilder(new MethodCallExpr("load"), moduleName)));
- }
-
- /**
- * Generates the same builder under a name no backend replaces, so there is always
- * a way to ask for the bytecode specifically.
- */
- private static void generateSafeBuilderMethod(
- CompilationUnit cu, ClassOrInterfaceDeclaration type, String moduleName) {
- cu.addImport(Instance.class);
- type.addMethod("safeBuilder", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC)
- .setType(parseClassOrInterfaceType("Instance.Builder"))
- .createBody()
- .addStatement(
- new ReturnStmt(compiledBuilder(new MethodCallExpr("load"), moduleName)));
- }
-
- /** {@code Instance.builder().withMachineFactory(::create)} */
- private static MethodCallExpr compiledBuilder(
- com.github.javaparser.ast.expr.Expression module, String moduleName) {
- return new MethodCallExpr(
- new MethodCallExpr(new NameExpr("Instance"), "builder", new NodeList<>(module)),
- "withMachineFactory",
- new NodeList<>(
- new MethodReferenceExpr()
- .setScope(new NameExpr(moduleName))
- .setIdentifier("create")));
- }
-
- /**
- * Generates:
- *
- * public static Memory createMemory(MemoryLimits limits) {
- * return new ByteBufferMemory(limits);
- * }
- *
- * public static TableInstance createTable(Table table, int initValue) {
- * return new TableInstance(table, initValue);
- * }
- *
- *
- * A backend whose compiled code reaches into a memory or table directly
- * cannot accept one built any other way, so it replaces these. Going through
- * the module rather than naming a type keeps the calling code the same either
- * way.
- */
- private static void generateImportFactoryMethods(
- CompilationUnit cu, ClassOrInterfaceDeclaration type) {
- cu.addImport(Memory.class);
- cu.addImport(ByteBufferMemory.class);
- cu.addImport(TableInstance.class);
- cu.addImport(MemoryLimits.class);
- cu.addImport(Table.class);
-
- type.addMethod("createMemory", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC)
- .addParameter(parseType("MemoryLimits"), "limits")
- .setType(Memory.class)
- .createBody()
- .addStatement(
- new ReturnStmt(
- new ObjectCreationExpr(
- null,
- parseClassOrInterfaceType("ByteBufferMemory"),
- NodeList.nodeList(new NameExpr("limits")))));
-
- type.addMethod("createTable", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC)
- .addParameter(parseType("Table"), "table")
- .addParameter(parseType("int"), "initValue")
- .setType(TableInstance.class)
- .createBody()
- .addStatement(
- new ReturnStmt(
- new ObjectCreationExpr(
- null,
- parseClassOrInterfaceType("TableInstance"),
- NodeList.nodeList(
- new NameExpr("table"),
- new NameExpr("initValue")))));
- }
-
private static void generateWasmModuleHolderInnerClass(
ClassOrInterfaceDeclaration type, String moduleName, String wasmName) {
diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/CtxBuffer.java b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/CtxBuffer.java
index 7462e7896..7d37d076a 100644
--- a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/CtxBuffer.java
+++ b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/CtxBuffer.java
@@ -73,12 +73,6 @@ private CtxBuffer() {}
public static final int TRAP_UNALIGNED_ATOMIC = 12;
public static final int TRAP_INTERRUPTED = 13;
- /**
- * A host function threw. The throwable itself is held by the runner; this only
- * marks the context so compiled code unwinds instead of running on.
- */
- public static final int TRAP_HOST_EXCEPTION = 14;
-
public static final int TABLE_SIZE_OFFSET = 0;
public static final int TABLE_MAX_OFFSET = 4;
public static final int TABLE_ENTRIES_OFFSET = 8;
diff --git a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java
index 4505231f5..4e6357fd4 100644
--- a/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java
+++ b/redline/build-time-compiler/src/main/java/run/endive/redline/experimental/build/RedlineGenerator.java
@@ -5,7 +5,6 @@
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.Modifier;
-import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.Parameter;
@@ -104,7 +103,7 @@ public void extendGeneratedSources() throws IOException {
generateLoadNativeCodeMethod(type);
generateNativeProviderMethod(type);
generateBuilderMethod(type, baseName);
- generateImportFactoryMethods(type);
+ generateSafeBuilderMethod(type, baseName);
Files.writeString(sourceFile, cu.toString());
}
@@ -311,88 +310,6 @@ private static void generateNativeProviderMethod(ClassOrInterfaceDeclaration typ
new NameExpr("NativeMachineFactoryProvider"), "discover")));
}
- /**
- * Replaces the plain import factories with ones that go through the native
- * provider when there is one. Compiled code reaches into a memory or table
- * through a raw base address, so an imported one has to come from the same
- * backend that will run the module. Falling back leaves the plain types,
- * which is what the bytecode path expects.
- *
- *
Generates:
- *
- * public static Memory createMemory(MemoryLimits limits) {
- * var provider = nativeProvider();
- * if (provider.isPresent()) {
- * return provider.get().createMemory(limits);
- * }
- * return new ByteBufferMemory(limits);
- * }
- *
- * and the same shape for createTable.
- */
- private static void generateImportFactoryMethods(ClassOrInterfaceDeclaration type) {
- // addMethod appends, so the base generator's versions have to go first.
- type.getMethodsByName("createMemory").forEach(Node::remove);
- type.getMethodsByName("createTable").forEach(Node::remove);
-
- var memory =
- type.addMethod("createMemory", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC)
- .addParameter(parseType("MemoryLimits"), "limits")
- .setType(parseType("Memory"));
- memory.createBody()
- .addStatement(providerVar())
- .addStatement(
- ifProviderPresent(
- new MethodCallExpr(
- new MethodCallExpr(new NameExpr("provider"), "get"),
- "createMemory",
- new NodeList<>(new NameExpr("limits")))))
- .addStatement(
- new ReturnStmt(
- new com.github.javaparser.ast.expr.ObjectCreationExpr(
- null,
- parseClassOrInterfaceType("ByteBufferMemory"),
- new NodeList<>(new NameExpr("limits")))));
-
- var table =
- type.addMethod("createTable", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC)
- .addParameter(parseType("Table"), "table")
- .addParameter(parseType("int"), "initValue")
- .setType(parseType("TableInstance"));
- table.createBody()
- .addStatement(providerVar())
- .addStatement(
- ifProviderPresent(
- new MethodCallExpr(
- new MethodCallExpr(new NameExpr("provider"), "get"),
- "createImportTable",
- new NodeList<>(
- new NameExpr("table"), new NameExpr("initValue")))))
- .addStatement(
- new ReturnStmt(
- new com.github.javaparser.ast.expr.ObjectCreationExpr(
- null,
- parseClassOrInterfaceType("TableInstance"),
- new NodeList<>(
- new NameExpr("table"),
- new NameExpr("initValue")))));
- }
-
- /** {@code var provider = nativeProvider();} */
- private static ExpressionStmt providerVar() {
- return new ExpressionStmt(
- new VariableDeclarationExpr(
- new VariableDeclarator(
- new VarType(), "provider", new MethodCallExpr("nativeProvider"))));
- }
-
- /** {@code if (provider.isPresent()) { return ; }} */
- private static IfStmt ifProviderPresent(MethodCallExpr call) {
- return new IfStmt()
- .setCondition(new MethodCallExpr(new NameExpr("provider"), "isPresent"))
- .setThenStmt(new BlockStmt(new NodeList<>(new ReturnStmt(call))));
- }
-
private static void generateBuilderMethod(ClassOrInterfaceDeclaration type, String moduleName) {
// Generates:
//
@@ -409,9 +326,6 @@ private static void generateBuilderMethod(ClassOrInterfaceDeclaration type, Stri
// The native path is selected through nativeProvider() so that callers
// checking it see exactly the decision this method makes. Falling back means
// the build-time compiled bytecode, not the interpreter.
- // The base generator already emitted a builder(); addMethod appends rather
- // than replaces, so it has to go before this one is added.
- type.getMethodsByName("builder").forEach(Node::remove);
var method =
type.addMethod("builder", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC)
.setType(parseClassOrInterfaceType("Instance.Builder"));
@@ -454,6 +368,23 @@ private static void generateBuilderMethod(ClassOrInterfaceDeclaration type, Stri
body.addStatement(new ReturnStmt(compiledBuilder(new NameExpr("module"), moduleName)));
}
+ private static void generateSafeBuilderMethod(
+ ClassOrInterfaceDeclaration type, String moduleName) {
+ // Generates:
+ //
+ // public static Instance.Builder safeBuilder() {
+ // return Instance.builder(load()).withMachineFactory(::create);
+ // }
+ //
+ var method =
+ type.addMethod("safeBuilder", Modifier.Keyword.PUBLIC, Modifier.Keyword.STATIC)
+ .setType(parseClassOrInterfaceType("Instance.Builder"));
+
+ method.createBody()
+ .addStatement(
+ new ReturnStmt(compiledBuilder(new MethodCallExpr("load"), moduleName)));
+ }
+
/**
* {@code Instance.builder().withMachineFactory(::create)}
*
diff --git a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/EmitContext.java b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/EmitContext.java
index 4245469ac..a89be5daf 100644
--- a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/EmitContext.java
+++ b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/EmitContext.java
@@ -140,14 +140,16 @@ ValType resolveGlobalType(int globalIdx) {
return module.globalSection().getGlobal(moduleGlobalIdx).valueType();
}
- /**
- * Widens a value for the argument buffer a host import reads. i32 is
- * sign-extended so a host handed -1 sees -1, matching the interpreter, rather
- * than 4294967295.
- */
int widenToI64(int valId, ValType type) {
if (type.equals(ValType.I32)) {
- return bridge.exports().emitSextendI64(valId);
+ return bridge.exports().emitUextendI64(valId);
+ }
+ return valId;
+ }
+
+ int narrowFromI64(int valId, ValType type) {
+ if (type.equals(ValType.I32)) {
+ return bridge.exports().emitIreduceI32(valId);
}
return valId;
}
diff --git a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeEmitters.java b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeEmitters.java
index 9e336a93f..a03df5ef1 100644
--- a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeEmitters.java
+++ b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeEmitters.java
@@ -361,24 +361,6 @@ static void fillTrapBlock(EmitContext ctx, int trapBlock, int trapCode) {
ctx.emitReturnForFuncType();
}
- /**
- * A callee that traps records its code and returns like any other call, so the
- * caller has to look for it. Without this check the caller runs on to
- * completion after the trap: its stores land and its host imports fire.
- */
- static void emitTrapCheck(EmitContext ctx) {
- var b = ctx.bridge.exports();
- int zero = b.emitIconst32(0);
- int trapCode = b.emitLoadI32(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAP_CODE);
- int trapped = b.emitIcmp(1, trapCode, b.emitIconst32(0));
- int propagateBlock = b.createBlock();
- int continueBlock = b.createBlock();
- b.emitBrif(trapped, propagateBlock, continueBlock);
- b.switchToBlock(propagateBlock);
- ctx.emitReturnForFuncType();
- b.switchToBlock(continueBlock);
- }
-
// --- Extensions ---
static void emitI32Extend8S(EmitContext ctx) {
@@ -786,7 +768,6 @@ static void emitCall(EmitContext ctx, AnnotatedInstruction ins) {
}
int rawResult = ctx.bridge.exports().emitCallIndirect(sigRef, funcPtr);
- emitTrapCheck(ctx);
if (calleeMultiReturn) {
// Read return values from argsBuffer
@@ -906,7 +887,6 @@ static void emitCallIndirect(EmitContext ctx, AnnotatedInstruction ins) {
}
int rawResult = b.emitCallIndirect(sigRef, funcPtr);
- emitTrapCheck(ctx);
// 9. Handle results
if (calleeMultiReturn) {
diff --git a/redline/runner-jffi-tests/pom.xml b/redline/runner-jffi-tests/pom.xml
index 3c4a56fcc..c31225208 100644
--- a/redline/runner-jffi-tests/pom.xml
+++ b/redline/runner-jffi-tests/pom.xml
@@ -186,7 +186,8 @@
SpecV1ImportsTest.test118, SpecV1ImportsTest.test119, SpecV1ImportsTest.test120,
SpecV1ImportsTest.test123, SpecV1ImportsTest.test124, SpecV1ImportsTest.test125,
SpecV1ImportsTest.test127, SpecV1ImportsTest.test128, SpecV1ImportsTest.test129,
- SpecV1LinkingTest.test130, SpecV1LinkingTest.test131,
+ SpecV1LinkingTest.test129, SpecV1LinkingTest.test130, SpecV1LinkingTest.test131,
+ SpecV1StartTest.test18,
SpecV1FuncTest.test85,
SpecV1ThreadsImportsTest.test64, SpecV1ThreadsImportsTest.test65,
SpecV1ThreadsImportsTest.test66,
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/HostImportRoundTripTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/HostImportRoundTripTest.java
deleted file mode 100644
index 1ff78e08a..000000000
--- a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/HostImportRoundTripTest.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package run.endive.redline.experimental.runner.jffi.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
-import run.endive.runtime.HostFunction;
-import run.endive.runtime.ImportValues;
-import run.endive.runtime.Instance;
-import run.endive.wasm.Parser;
-import run.endive.wasm.types.FunctionType;
-import run.endive.wasm.types.ValType;
-import run.endive.wasm.types.Value;
-
-/**
- * Values crossing the host boundary are marshalled by hand in each runner, so each
- * conversion is a place they can be mangled. The spec suite does not reach these:
- * it drives modules that are self-contained rather than calling back into Java.
- */
-public class HostImportRoundTripTest {
-
- @Test
- public void floatResultKeepsItsBitPattern() {
- try (var instance = buildInstance()) {
- assertEquals(
- 1.5f,
- Value.longToFloat(instance.export("callRetF32").apply()[0]),
- "a float result must be reinterpreted, not converted numerically");
- }
- }
-
- @Test
- public void doubleResultKeepsItsBitPattern() {
- try (var instance = buildInstance()) {
- assertEquals(
- 2.5d,
- Value.longToDouble(instance.export("callRetF64").apply()[0]),
- "a double result must be reinterpreted, not converted numerically");
- }
- }
-
- @Test
- public void negativeI32ArgumentArrivesSignExtended() {
- try (var instance = buildInstance()) {
- assertEquals(
- 1,
- (int) instance.export("callTakeI32").apply()[0],
- "the host must be handed -1, not 4294967295");
- }
- }
-
- @Test
- public void multiValueResultKeepsEveryValue() {
- try (var instance = buildInstance()) {
- assertEquals(
- 30,
- (int) instance.export("callRetPairSum").apply()[0],
- "both results of a multi-value host import must arrive");
- }
- }
-
- private static Instance buildInstance() {
- var module =
- Parser.parse(
- CorpusResources.getResource("compiled/host-import-roundtrip.wat.wasm"));
-
- var imports =
- ImportValues.builder()
- .addFunction(
- new HostFunction(
- "host",
- "retF32",
- FunctionType.of(
- java.util.List.of(),
- java.util.List.of(ValType.F32)),
- (inst, args) -> new long[] {Value.floatToLong(1.5f)}),
- new HostFunction(
- "host",
- "retF64",
- FunctionType.of(
- java.util.List.of(),
- java.util.List.of(ValType.F64)),
- (inst, args) -> new long[] {Value.doubleToLong(2.5d)}),
- new HostFunction(
- "host",
- "takeI32",
- FunctionType.of(
- java.util.List.of(ValType.I32),
- java.util.List.of(ValType.I32)),
- // Reports on the raw long it was handed rather
- // than echoing it: an echo would be truncated
- // back to -1 on the way out and hide a
- // zero-extended argument.
- (inst, args) -> new long[] {args[0] == -1L ? 1 : 0}),
- new HostFunction(
- "host",
- "retPair",
- FunctionType.of(
- java.util.List.of(),
- java.util.List.of(ValType.I32, ValType.I32)),
- (inst, args) -> new long[] {10, 20}))
- .build();
-
- return JffiNativeMachineFactory.builder(module)
- .withImportValues(imports)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(), m))
- .build();
- }
-}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/HostThrowPropagationTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/HostThrowPropagationTest.java
deleted file mode 100644
index be14266a4..000000000
--- a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/HostThrowPropagationTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package run.endive.redline.experimental.runner.jffi.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
-import run.endive.runtime.HostFunction;
-import run.endive.runtime.ImportValues;
-import run.endive.wasm.Parser;
-import run.endive.wasm.types.FunctionType;
-
-/**
- * An exception from a host function has to abandon the module the same way a trap
- * does, otherwise the module keeps running on state the host has already rejected.
- */
-public class HostThrowPropagationTest {
-
- private static final class Boom extends RuntimeException {
- Boom() {
- super("boom");
- }
- }
-
- @Test
- public void moduleStopsWhenAHostFunctionThrows() {
- var module =
- Parser.parse(
- CorpusResources.getResource(
- "compiled/host-throw-stops-execution.wat.wasm"));
-
- var imports =
- ImportValues.builder()
- .addFunction(
- new HostFunction(
- "host",
- "boom",
- FunctionType.of(java.util.List.of(), java.util.List.of()),
- (inst, args) -> {
- throw new Boom();
- }))
- .build();
-
- try (var instance =
- JffiNativeMachineFactory.builder(module)
- .withImportValues(imports)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(),
- m))
- .build()) {
- assertThrows(Boom.class, () -> instance.export("callBoom").apply());
- assertEquals(
- 0,
- instance.memory().readInt(0),
- "the store after the throwing host call must never run");
- }
- }
-}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/InterruptFlagTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/InterruptFlagTest.java
deleted file mode 100644
index 55c8fdf3c..000000000
--- a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/InterruptFlagTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package run.endive.redline.experimental.runner.jffi.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
-import run.endive.runtime.HostFunction;
-import run.endive.runtime.ImportValues;
-import run.endive.wasm.Parser;
-import run.endive.wasm.types.FunctionType;
-
-/**
- * The watchdog raises the interrupt flag from another thread, so it can land after
- * the call it was meant to stop has passed its last check. The flag must not then
- * sit in the context and stop a later call that nobody interrupted.
- */
-public class InterruptFlagTest {
-
- @AfterEach
- public void clearInterruptStatus() {
- // Keeps a failure from leaking an interrupt into the rest of the suite.
- Thread.interrupted();
- }
-
- @Test
- public void aFlagRaisedMidCallDoesNotStopTheNextCall() {
- var module =
- Parser.parse(CorpusResources.getResource("compiled/interrupt-midcall.wat.wasm"));
-
- var machineRef = new JffiNativeMachine[1];
- var imports =
- ImportValues.builder()
- .addFunction(
- new HostFunction(
- "host",
- "raiseFlag",
- FunctionType.of(java.util.List.of(), java.util.List.of()),
- (inst, args) -> {
- machineRef[0].requestInterrupt();
- return null;
- }))
- .build();
-
- try (var instance =
- JffiNativeMachineFactory.builder(module)
- .withImportValues(imports)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(),
- m))
- .build()) {
- machineRef[0] = (JffiNativeMachine) instance.getMachine();
-
- // Returns normally: the entry check ran before the flag was raised.
- instance.export("callHost").apply();
-
- assertEquals(
- 42,
- (int) instance.export("answer").apply()[0],
- "a flag left over from the previous call must not stop this one");
- assertFalse(
- Thread.currentThread().isInterrupted(),
- "no interrupt happened, so the caller must not be left interrupted");
- }
- }
-}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/LifecycleTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/LifecycleTest.java
deleted file mode 100644
index a160367b2..000000000
--- a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/LifecycleTest.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package run.endive.redline.experimental.runner.jffi.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
-import run.endive.wasm.Parser;
-import run.endive.wasm.WasmModule;
-
-/**
- * Everything a machine releases on close is an off-heap free, so closing twice
- * has to be a no-op rather than a double free, and a memory the instance only
- * borrowed has to survive it.
- */
-public class LifecycleTest {
-
- @Test
- public void closingTwiceIsSafe() {
- var instance = build(parse());
- instance.close();
- instance.close();
- }
-
- @Test
- public void aMemoryTheModuleDefinesStillWorksBeforeClose() {
- try (var instance = build(parse())) {
- instance.memory().writeI32(0, 0x5A5A5A5A);
- assertEquals(0x5A5A5A5A, instance.memory().readInt(0));
- }
- }
-
- private static WasmModule parse() {
- return Parser.parse(CorpusResources.getResource("compiled/trap-stops-execution.wat.wasm"));
- }
-
- private static run.endive.runtime.Instance build(WasmModule module) {
- return JffiNativeMachineFactory.builder(module)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(), m))
- .build();
- }
-}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/MemoryBoundsTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/MemoryBoundsTest.java
deleted file mode 100644
index b567d70aa..000000000
--- a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/MemoryBoundsTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package run.endive.redline.experimental.runner.jffi.internal;
-
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import org.junit.jupiter.api.Test;
-import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
-import run.endive.runtime.WasmRuntimeException;
-import run.endive.wasm.types.MemoryLimits;
-
-/**
- * A host reading past the end of a Wasm memory has to trap the same way it would
- * on any other backend. The spec suite drives memory from inside the module, so it
- * never exercises these accessors.
- */
-public class MemoryBoundsTest {
-
- private static final int PAGE = 65536;
-
- @Test
- public void readPastTheEndTraps() {
- var memory = JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 2));
- assertThrows(WasmRuntimeException.class, () -> memory.readInt(PAGE));
- assertThrows(WasmRuntimeException.class, () -> memory.readLong(PAGE - 4));
- assertThrows(WasmRuntimeException.class, () -> memory.read(PAGE));
- assertThrows(WasmRuntimeException.class, () -> memory.readShort(PAGE - 1));
- assertThrows(WasmRuntimeException.class, () -> memory.readBytes(PAGE - 1, 8));
- }
-
- @Test
- public void writePastTheEndTraps() {
- var memory = JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 2));
- assertThrows(WasmRuntimeException.class, () -> memory.writeI32(PAGE, 1));
- assertThrows(WasmRuntimeException.class, () -> memory.writeLong(PAGE - 4, 1L));
- assertThrows(WasmRuntimeException.class, () -> memory.writeByte(PAGE, (byte) 1));
- assertThrows(WasmRuntimeException.class, () -> memory.writeShort(PAGE - 1, (short) 1));
- }
-
- @Test
- public void insideTheMemoryIsUntouched() {
- var memory = JffiNativeMachineFactory.createMemory(new MemoryLimits(1, 2));
- memory.writeI32(PAGE - 4, 0x11223344);
- org.junit.jupiter.api.Assertions.assertEquals(0x11223344, memory.readInt(PAGE - 4));
- }
-}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/ReentrantStackGuardTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/ReentrantStackGuardTest.java
deleted file mode 100644
index 527adceec..000000000
--- a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/ReentrantStackGuardTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package run.endive.redline.experimental.runner.jffi.internal;
-
-import static org.junit.jupiter.api.Assertions.assertInstanceOf;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
-import run.endive.runtime.HostFunction;
-import run.endive.runtime.ImportValues;
-import run.endive.wasm.Parser;
-import run.endive.wasm.WasmEngineException;
-import run.endive.wasm.types.FunctionType;
-
-/**
- * Recursion that goes back through the host re-enters the machine from the top
- * every time. The stack guard has to stay anchored where the outermost call
- * started: re-anchoring per call moves the limit deeper on every level, so it
- * never fires and the JVM raises StackOverflowError instead. That is an Error,
- * which callers guarding against runaway modules do not catch.
- */
-public class ReentrantStackGuardTest {
-
- /** Only a backstop: the guard is expected to fire long before this. */
- private static final int CAP = 20_000;
-
- @Test
- public void theGuardStillFiresWhenRecursionGoesThroughTheHost() {
- var thrown = assertThrows(Throwable.class, () -> recurseThroughHost(true));
- assertInstanceOf(
- WasmEngineException.class,
- thrown,
- "must trap rather than let the JVM raise StackOverflowError");
- assertTrue(
- String.valueOf(thrown.getMessage()).contains("call stack exhausted"),
- "expected a call stack exhausted trap, got: " + thrown.getMessage());
- }
-
- @Test
- public void matchesTheInterpreter() {
- var reference = assertThrows(Throwable.class, () -> recurseThroughHost(false));
- assertInstanceOf(WasmEngineException.class, reference);
-
- var actual = assertThrows(Throwable.class, () -> recurseThroughHost(true));
- assertInstanceOf(
- reference.getClass(),
- actual,
- "redline must end this the same way the interpreter does");
- }
-
- private static void recurseThroughHost(boolean native_) {
- var module =
- Parser.parse(CorpusResources.getResource("compiled/reentrant-recursion.wat.wasm"));
-
- int[] depth = {0};
- var imports =
- ImportValues.builder()
- .addFunction(
- new HostFunction(
- "host",
- "reenter",
- FunctionType.of(java.util.List.of(), java.util.List.of()),
- (inst, args) -> {
- if (depth[0]++ < CAP) {
- inst.export("recurse").apply();
- }
- return null;
- }))
- .build();
-
- if (!native_) {
- run.endive.runtime.Instance.builder(module)
- .withImportValues(imports)
- .build()
- .export("recurse")
- .apply();
- return;
- }
-
- try (var instance =
- JffiNativeMachineFactory.builder(module)
- .withImportValues(imports)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(),
- m))
- .build()) {
- instance.export("recurse").apply();
- }
- }
-}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/TableInitExprTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/TableInitExprTest.java
deleted file mode 100644
index b593ccf5b..000000000
--- a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/TableInitExprTest.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package run.endive.redline.experimental.runner.jffi.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
-import run.endive.wasm.Parser;
-
-/**
- * A table declared with an initialiser has to come up holding it. The spec suite
- * does not cover this: its tables are filled by element segments, which take a
- * different path.
- */
-public class TableInitExprTest {
-
- @Test
- public void tableComesUpHoldingItsInitialiser() {
- var module = Parser.parse(CorpusResources.getResource("compiled/table-init-expr.wat.wasm"));
-
- try (var instance =
- JffiNativeMachineFactory.builder(module)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(),
- m))
- .build()) {
- assertEquals(
- 42,
- (int) instance.export("callInitialised").apply()[0],
- "a slot filled only by the table initialiser must be callable");
- }
- }
-}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/TableReleaseTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/TableReleaseTest.java
deleted file mode 100644
index d571fb732..000000000
--- a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/TableReleaseTest.java
+++ /dev/null
@@ -1,77 +0,0 @@
-package run.endive.redline.experimental.runner.jffi.internal;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
-import run.endive.runtime.ImportTable;
-import run.endive.runtime.ImportValues;
-import run.endive.wasm.Parser;
-import run.endive.wasm.WasmModule;
-import run.endive.wasm.types.Table;
-import run.endive.wasm.types.TableLimits;
-import run.endive.wasm.types.ValType;
-import run.endive.wasm.types.Value;
-
-/**
- * A table's buffer is off-heap, so the garbage collector never reclaims it and
- * closing the instance has to. Which tables that covers is the whole question: one
- * the module declares belongs to the instance, and one it borrowed through an
- * import belongs to whoever created it and may still back something else.
- */
-public class TableReleaseTest {
-
- @Test
- public void closingReleasesATableTheModuleDeclares() {
- var module = Parser.parse(CorpusResources.getResource("compiled/big-table.wat.wasm"));
-
- JffiNativeTable table;
- try (var instance = build(module, null)) {
- instance.export("noop").apply();
- table = (JffiNativeTable) instance.table(0);
- assertFalse(table.isFreed(), "still in use");
- }
-
- assertTrue(table.isFreed(), "a table the module declares dies with the instance");
- }
-
- @Test
- public void closingKeepsATableTheModuleImported() {
- var module = Parser.parse(CorpusResources.getResource("compiled/imported-table.wat.wasm"));
-
- var borrowed =
- (JffiNativeTable)
- JffiNativeMachineFactory.createImportTable(
- new Table(ValType.FuncRef, new TableLimits(4, 4)),
- Value.REF_NULL_VALUE);
- var imports =
- ImportValues.builder().addTable(new ImportTable("env", "table", borrowed)).build();
-
- try (var instance = build(module, imports)) {
- instance.export("noop").apply();
- }
-
- assertFalse(
- borrowed.isFreed(),
- "an imported table belongs to its creator and may back other instances");
- borrowed.free();
- }
-
- private static run.endive.runtime.Instance build(WasmModule module, ImportValues imports) {
- var builder =
- JffiNativeMachineFactory.builder(module)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(),
- m));
- if (imports != null) {
- builder.withImportValues(imports);
- }
- return builder.build();
- }
-}
diff --git a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/TrapPropagationTest.java b/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/TrapPropagationTest.java
deleted file mode 100644
index 2fc4e480a..000000000
--- a/redline/runner-jffi-tests/src/test/java/run/endive/redline/experimental/runner/jffi/internal/TrapPropagationTest.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package run.endive.redline.experimental.runner.jffi.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.jffi.JffiNativeMachineFactory;
-import run.endive.wasm.Parser;
-import run.endive.wasm.WasmEngineException;
-import run.endive.wasm.WasmModule;
-
-/**
- * A trap has to abandon the caller, not just the frame it happened in. The spec
- * suite asserts on the exception a call ends with, which is reported correctly
- * either way, so it never noticed execution carrying on past the trap.
- */
-public class TrapPropagationTest {
-
- @Test
- public void callerStopsWhenItsCalleeTraps() {
- var module = parse();
- try (var instance = build(module)) {
- assertThrows(
- WasmEngineException.class, () -> instance.export("storeAfterTrap").apply());
- assertEquals(
- 0,
- instance.memory().readInt(0),
- "the store after the trapping call must never run");
- }
- }
-
- @Test
- public void matchesTheInterpreter() {
- var module = parse();
- var interpreter = run.endive.runtime.Instance.builder(module).build();
- assertThrows(WasmEngineException.class, () -> interpreter.export("loopAfterTrap").apply());
- int reference = interpreter.memory().readInt(4);
-
- try (var instance = build(module)) {
- assertThrows(WasmEngineException.class, () -> instance.export("loopAfterTrap").apply());
- assertEquals(reference, instance.memory().readInt(4), "must match the interpreter");
- }
- }
-
- private static WasmModule parse() {
- return Parser.parse(CorpusResources.getResource("compiled/trap-stops-execution.wat.wasm"));
- }
-
- private static run.endive.runtime.Instance build(WasmModule module) {
- return JffiNativeMachineFactory.builder(module)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(), m))
- .build();
- }
-}
diff --git a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiNativeMachineFactory.java b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiNativeMachineFactory.java
index c64febb42..979d0e9c0 100644
--- a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiNativeMachineFactory.java
+++ b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/JffiNativeMachineFactory.java
@@ -62,13 +62,13 @@ public static Builder builder(WasmModule module) {
}
public TableInstance createTable(Table table, int initValue) {
- var nativeTable = new JffiNativeTable(table, initValue);
+ var nativeTable = new JffiNativeTable(table);
nativeTables.add(nativeTable);
return nativeTable;
}
public static TableInstance createImportTable(Table table, int initValue) {
- return new JffiNativeTable(table, initValue);
+ return new JffiNativeTable(table);
}
public GlobalInstance createGlobal(
diff --git a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeGlobalInstance.java b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeGlobalInstance.java
index d7eeb6c6c..0506ee620 100644
--- a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeGlobalInstance.java
+++ b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeGlobalInstance.java
@@ -47,7 +47,6 @@ public void setValue(long value) {
@Override
public void setValue(Value value) {
- checkType(value);
MEM.putLong(bufferAddress + offset, value.raw());
}
diff --git a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java
index 1d5b84e0d..40ed18b63 100644
--- a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java
+++ b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMachine.java
@@ -22,8 +22,6 @@
import run.endive.redline.experimental.bridge.internal.CraneliftBridge;
import run.endive.runtime.Instance;
import run.endive.runtime.Machine;
-import run.endive.runtime.TrapException;
-import run.endive.runtime.WasmRuntimeException;
import run.endive.wasm.WasmEngineException;
import run.endive.wasm.types.FunctionType;
import run.endive.wasm.types.ValType;
@@ -101,10 +99,7 @@ public final class JffiNativeMachine implements Machine {
private final long funcTypesArraySize; // byte size
private long tablePtrsArrayAddr;
private JffiNativeTable[] nativeTables;
- private boolean[] ownsTable;
private boolean tablesInitialized;
- private boolean ownsMemory;
- private boolean closed;
private final int numImports;
private final int globalCount;
private boolean importGlobalsInitialized;
@@ -112,7 +107,6 @@ public final class JffiNativeMachine implements Machine {
private boolean memBaseInitialized;
private JffiNativeMemory nativeMemory;
private volatile Throwable pendingException;
- private int callDepth;
// Keep closure handles alive to prevent GC
private final Closure.Handle trampolineHandle;
@@ -390,27 +384,11 @@ public JffiNativeMachine(
instance.memory() instanceof JffiNativeMemory
? (JffiNativeMemory) instance.memory()
: null;
- // An imported memory outlives this instance and may back others, so only
- // a memory this module defines is ours to close.
- this.ownsMemory = instance.imports().memoryCount() == 0;
}
@Override
public void close() {
- if (closed) {
- // Every free below is a native one, so a second close would be a
- // double free rather than a no-op.
- return;
- }
- closed = true;
- if (ownsTable != null) {
- for (int i = 0; i < ownsTable.length; i++) {
- if (ownsTable[i]) {
- nativeTables[i].free();
- }
- }
- }
- if (nativeMemory != null && ownsMemory) {
+ if (nativeMemory != null) {
nativeMemory.close();
}
if (tablePtrsArrayAddr != 0) {
@@ -496,10 +474,6 @@ private Closure.Handle createImportStub(int funcId, FunctionType funcType) {
Type returnType;
if (funcType.returns().isEmpty()) {
returnType = Type.VOID;
- } else if (funcType.returns().size() > 1) {
- // Multi-return follows the compiled convention: results go through
- // argsBuffer and the call itself returns a dummy i64.
- returnType = Type.SINT64;
} else {
returnType = valTypeToJffiType(funcType.returns().get(0));
}
@@ -526,10 +500,6 @@ private static void setClosureReturn(Closure.Buffer buf, long result, FunctionTy
if (funcType.returns().isEmpty()) {
return;
}
- if (funcType.returns().size() > 1) {
- buf.setLongReturn(result);
- return;
- }
ValType retType = funcType.returns().get(0);
if (retType.equals(ValType.I32)) {
buf.setIntReturn((int) result);
@@ -552,22 +522,11 @@ private long importDispatchDirect(int funcId) {
if (funcId < numImports) {
var importFunc = instance.imports().function(funcId);
long[] result = importFunc.handle().apply(instance, args);
- if (result == null || result.length == 0) {
- return 0L;
- }
- if (importFunc.functionType().returns().size() > 1) {
- // Multi-return convention: the caller reads the results back
- // out of argsBuffer and ignores the returned value.
- for (int i = 0; i < result.length; i++) {
- MEM.putLong(argsBufferAddr + CtxBuffer.argOffset(i), result[i]);
- }
- return 0L;
- }
- return result[0];
+ return (result != null && result.length > 0) ? result[0] : 0L;
}
throw new WasmEngineException("Function " + funcId + " not compiled");
} catch (Throwable t) {
- recordHostException(t);
+ pendingException = t;
return 0L;
}
}
@@ -595,9 +554,28 @@ private long callIndirectTrampoline(long ctxAddr) {
if (argCount < 0) {
return handleTableOperation(argCount);
}
- throw new WasmEngineException("Unexpected trampoline call: argCount " + argCount);
+
+ // Normal call_indirect path
+ int typeId = MEM.getInt(ctxAddr + CtxBuffer.TYPE_ID);
+ int tableIdx = MEM.getInt(ctxAddr + CtxBuffer.TABLE_IDX);
+ int elemIdx = MEM.getInt(ctxAddr + CtxBuffer.ELEM_IDX);
+
+ int funcId = nativeTables[tableIdx].requiredRef(elemIdx);
+
+ int actualTypeIdx = instance.functionType(funcId);
+ if (actualTypeIdx != typeId) {
+ throw new WasmEngineException("indirect call type mismatch");
+ }
+
+ long[] args = new long[argCount];
+ for (int i = 0; i < argCount; i++) {
+ args[i] = MEM.getLong(argsBufferAddr + CtxBuffer.argOffset(i));
+ }
+
+ long[] result = this.call(funcId, args);
+ return result.length > 0 ? result[0] : 0L;
} catch (Throwable t) {
- recordHostException(t);
+ pendingException = t;
return 0L;
}
}
@@ -776,7 +754,7 @@ private long memoryGrowHandler(long ctxAddr) {
}
return oldPages;
} catch (Throwable t) {
- recordHostException(t);
+ pendingException = t;
return -1L;
}
}
@@ -822,22 +800,16 @@ private void initializeNativeTables() {
this.nativeTables = new JffiNativeTable[tableCount];
boolean[] owned = new boolean[tableCount];
- this.ownsTable = owned;
this.tablePtrsArrayAddr = MEM.allocateMemory((long) tableCount * 8, true);
for (int i = 0; i < tableCount; i++) {
var table = instance.table(i);
if (table instanceof JffiNativeTable) {
- var nt = (JffiNativeTable) table;
- nt.resolvePendingRefs(instance);
- nativeTables[i] = nt;
- // A table this module defines came from our factory and dies with
- // the instance. An imported one belongs to whoever created it.
- owned[i] = i >= importedTableCount;
+ nativeTables[i] = (JffiNativeTable) table;
} else {
// Imported table not created by our factory — wrap it
var tableDef = new run.endive.wasm.types.Table(table.elementType(), table.limits());
- var nt = new JffiNativeTable(tableDef, run.endive.wasm.types.Value.REF_NULL_VALUE);
+ var nt = new JffiNativeTable(tableDef);
for (int j = 0; j < table.size(); j++) {
nt.setRef(j, table.ref(j), instance);
}
@@ -860,57 +832,45 @@ long getFuncTypesArrayAddress() {
return funcTypesArrayAddr;
}
- /**
- * Marks the context so compiled code unwinds at its next trap check rather
- * than running on. The first throwable wins: it is the one that stopped
- * execution, so a later one would be a symptom of it.
- */
- private void recordHostException(Throwable t) {
- if (pendingException == null) {
- pendingException = t;
- }
- MEM.putInt(ctxBufferAddr + CtxBuffer.TRAP_CODE, CtxBuffer.TRAP_HOST_EXCEPTION);
- }
-
private static WasmEngineException trapException(int trapCode) {
if (trapCode == CtxBuffer.TRAP_DIV_BY_ZERO) {
- return new TrapException("integer divide by zero");
+ return new WasmEngineException("integer divide by zero");
}
if (trapCode == CtxBuffer.TRAP_INT_OVERFLOW) {
- return new TrapException("integer overflow");
+ return new WasmEngineException("integer overflow");
}
if (trapCode == CtxBuffer.TRAP_UNREACHABLE) {
- return new TrapException("unreachable");
+ return new WasmEngineException("unreachable");
}
if (trapCode == CtxBuffer.TRAP_TRUNC_OVERFLOW) {
- return new TrapException("integer overflow");
+ return new WasmEngineException("integer overflow");
}
if (trapCode == CtxBuffer.TRAP_TRUNC_NAN) {
- return new TrapException("invalid conversion to integer");
+ return new WasmEngineException("invalid conversion to integer");
}
if (trapCode == CtxBuffer.TRAP_OOB) {
- return new WasmRuntimeException("out of bounds memory access");
+ return new WasmEngineException("out of bounds memory access");
}
if (trapCode == CtxBuffer.TRAP_CALL_STACK_EXHAUSTED) {
- return new TrapException("call stack exhausted");
+ return new WasmEngineException("call stack exhausted");
}
if (trapCode == CtxBuffer.TRAP_TABLE_OOB) {
- return new TrapException("out of bounds table access");
+ return new WasmEngineException("out of bounds table access");
}
if (trapCode == CtxBuffer.TRAP_UNDEFINED_ELEMENT) {
- return new TrapException("undefined element");
+ return new WasmEngineException("undefined element");
}
if (trapCode == CtxBuffer.TRAP_UNINITIALIZED_ELEMENT) {
- return new TrapException("uninitialized element");
+ return new WasmEngineException("uninitialized element");
}
if (trapCode == CtxBuffer.TRAP_INDIRECT_CALL_TYPE_MISMATCH) {
- return new TrapException("indirect call type mismatch");
+ return new WasmEngineException("indirect call type mismatch");
}
if (trapCode == CtxBuffer.TRAP_UNALIGNED_ATOMIC) {
- return new TrapException("unaligned atomic");
+ return new WasmEngineException("unaligned atomic");
}
if (trapCode == CtxBuffer.TRAP_INTERRUPTED) {
- return new TrapException("interrupted");
+ return new WasmEngineException("interrupted");
}
return new WasmEngineException("trap: unknown code " + trapCode);
}
@@ -1025,17 +985,11 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
var trampolineCallCtx = entryTrampolineCallCtxs[funcId];
try {
- boolean outermostCall = callDepth++ == 0;
initializeImportGlobals();
initializeNativeTables();
- // Re-anchor the stack guard only for a call that starts on this
- // stack. A host function calling back in has to keep measuring
- // against where the outer call began, or every level moves the
- // limit deeper and the guard stops firing.
- if (outermostCall) {
- MEM.putLong(ctxBufferAddr + CtxBuffer.STACK_LIMIT, 0L);
- }
+ // Reset stack limit so native code re-initializes from calling thread's RSP
+ MEM.putLong(ctxBufferAddr + CtxBuffer.STACK_LIMIT, 0L);
if (!memBaseInitialized) {
var mem = instance.memory();
@@ -1060,19 +1014,17 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
}
if (Thread.interrupted()) {
- throw new TrapException("interrupted");
+ throw new WasmEngineException("interrupted");
}
Thread caller = Thread.currentThread();
Thread watchdog =
new Thread(
() -> {
- // Keeps raising rather than returning after the
- // first: a nested call clears the flag when it
- // finishes, and the outer call still needs it.
while (!Thread.currentThread().isInterrupted()) {
if (caller.isInterrupted()) {
requestInterrupt();
+ return;
}
try {
Thread.sleep(1);
@@ -1096,9 +1048,6 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
args);
} finally {
watchdog.interrupt();
- // The flag only ever means "stop this call". Left set it would
- // trap the next one on a thread nobody interrupted.
- clearInterrupt();
}
// Check for exceptions from upcall stubs first — a host function
@@ -1144,7 +1093,6 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
sneakyThrow(e);
throw new AssertionError("unreachable");
} finally {
- callDepth--;
// Prevent the JIT from considering this machine unreachable during
// the native call, which would let GC collect and close() free
// native memory while code is executing.
diff --git a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMemory.java b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMemory.java
index 5db8bc26b..a79677275 100644
--- a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMemory.java
+++ b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeMemory.java
@@ -342,16 +342,6 @@ private int sizeInBytes() {
return PAGE_SIZE * nPages;
}
- /**
- * jffi dereferences the address without checking it, so an out of bounds
- * host read would take the JVM down with a SIGSEGV rather than trap.
- */
- private void checkBounds(int addr, int size) {
- if (Integer.toUnsignedLong(addr) + Integer.toUnsignedLong(size) > sizeInBytes()) {
- throw new run.endive.runtime.WasmRuntimeException("out of bounds memory access");
- }
- }
-
@Override
public void write(int addr, byte[] data, int offset, int size) {
long limit = sizeInBytes();
@@ -364,13 +354,11 @@ public void write(int addr, byte[] data, int offset, int size) {
@Override
public byte read(int addr) {
- checkBounds(addr, 1);
return MEM.getByte(reservedAddress + addr);
}
@Override
public byte[] readBytes(int addr, int len) {
- checkBounds(addr, len);
byte[] result = new byte[len];
MEM.getByteArray(reservedAddress + addr, result, 0, len);
return result;
@@ -378,37 +366,31 @@ public byte[] readBytes(int addr, int len) {
@Override
public void writeI32(int addr, int data) {
- checkBounds(addr, 4);
MEM.putInt(reservedAddress + addr, data);
}
@Override
public int readInt(int addr) {
- checkBounds(addr, 4);
return MEM.getInt(reservedAddress + addr);
}
@Override
public void writeLong(int addr, long data) {
- checkBounds(addr, 8);
MEM.putLong(reservedAddress + addr, data);
}
@Override
public long readLong(int addr) {
- checkBounds(addr, 8);
return MEM.getLong(reservedAddress + addr);
}
@Override
public void writeShort(int addr, short data) {
- checkBounds(addr, 2);
MEM.putShort(reservedAddress + addr, data);
}
@Override
public short readShort(int addr) {
- checkBounds(addr, 2);
return MEM.getShort(reservedAddress + addr);
}
@@ -419,7 +401,6 @@ public long readU16(int addr) {
@Override
public void writeByte(int addr, byte data) {
- checkBounds(addr, 1);
MEM.putByte(reservedAddress + addr, data);
}
diff --git a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeTable.java b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeTable.java
index 765f38064..bf7ec52a1 100644
--- a/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeTable.java
+++ b/redline/runner-jffi/src/main/java/run/endive/redline/experimental/runner/jffi/internal/JffiNativeTable.java
@@ -33,8 +33,8 @@ public final class JffiNativeTable extends TableInstance {
private final boolean isExternRef;
private boolean freed;
- public JffiNativeTable(Table table, int initValue) {
- super(table, initValue);
+ public JffiNativeTable(Table table) {
+ super(table, REF_NULL_VALUE);
this.isExternRef = table.elementType().equals(ValType.ExternRef);
int initial = (int) table.limits().min();
int max = (int) table.limits().max();
@@ -47,47 +47,10 @@ public JffiNativeTable(Table table, int initValue) {
MEM.putInt(bufferAddress + CtxBuffer.TABLE_SIZE_OFFSET, initial);
MEM.putInt(bufferAddress + CtxBuffer.TABLE_MAX_OFFSET, max > 0 ? max : capacity);
- // Only the entries below size are reachable: every access bounds-checks
- // against the size field, and grow fills the slots it exposes. Filling
- // the whole capacity here would make the entire pre-allocation resident.
- for (int i = 0; i < initial; i++) {
+ // Fill all entries with null (funcId=-1, funcPtr=0, typeIdx=0)
+ for (int i = 0; i < capacity; i++) {
writeNullEntry(i);
}
-
- if (initValue != REF_NULL_VALUE) {
- // The instance has no machine yet, so funcPtr cannot be resolved
- // here. resolvePendingRefs fills it in once there is one.
- for (int i = 0; i < initial; i++) {
- writeUnresolvedEntry(i, initValue);
- }
- }
- }
-
- /** A funcref whose native address is not known yet. */
- private void writeUnresolvedEntry(int index, int value) {
- long base = bufferAddress + entryBase(index);
- MEM.putInt(base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0);
- MEM.putInt(base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, value);
- MEM.putLong(base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L);
- }
-
- /**
- * Fills in the native address of entries written before the instance had a
- * machine, which is the case for a table initialiser.
- */
- void resolvePendingRefs(Instance instance) {
- if (isExternRef) {
- return;
- }
- int sz = size();
- for (int i = 0; i < sz; i++) {
- long base = bufferAddress + entryBase(i);
- int funcId = MEM.getInt(base + CtxBuffer.ENTRY_FUNC_ID_OFFSET);
- long funcPtr = MEM.getLong(base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET);
- if (funcId != REF_NULL_VALUE && funcPtr == 0L) {
- resolveFromInstance(i, funcId, instance);
- }
- }
}
private long entryBase(int index) {
@@ -140,11 +103,6 @@ boolean isExternRef() {
return isExternRef;
}
- /** Whether the off-heap buffer has been released. */
- boolean isFreed() {
- return freed;
- }
-
/** Free the off-heap buffer. Idempotent — safe to call multiple times. */
public void free() {
if (!freed && bufferAddress != 0) {
@@ -193,8 +151,14 @@ public void setRef(int index, int value, Instance instance) {
}
if (value == REF_NULL_VALUE) {
writeNullEntry(index);
- } else if (!resolveFromInstance(index, value, instance)) {
- writeUnresolvedEntry(index, value);
+ } else if (resolveFromInstance(index, value, instance)) {
+ // Resolved using the calling module's NativeMachine
+ } else {
+ // No NativeMachine available — store funcId only (externref or non-native)
+ long base = bufferAddress + entryBase(index);
+ MEM.putInt(base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0);
+ MEM.putInt(base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, value);
+ MEM.putLong(base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L);
}
}
@@ -211,7 +175,10 @@ public int grow(int delta, int value, Instance instance) {
if (value == REF_NULL_VALUE) {
writeNullEntry(i);
} else if (!resolveFromInstance(i, value, instance)) {
- writeUnresolvedEntry(i, value);
+ long base = bufferAddress + entryBase(i);
+ MEM.putInt(base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0);
+ MEM.putInt(base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, value);
+ MEM.putLong(base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L);
}
}
// Update size
diff --git a/redline/runner-tests/pom.xml b/redline/runner-tests/pom.xml
index 95a6b06be..f191c42de 100644
--- a/redline/runner-tests/pom.xml
+++ b/redline/runner-tests/pom.xml
@@ -187,7 +187,8 @@
SpecV1ImportsTest.test118, SpecV1ImportsTest.test119, SpecV1ImportsTest.test120,
SpecV1ImportsTest.test123, SpecV1ImportsTest.test124, SpecV1ImportsTest.test125,
SpecV1ImportsTest.test127, SpecV1ImportsTest.test128, SpecV1ImportsTest.test129,
- SpecV1LinkingTest.test130, SpecV1LinkingTest.test131,
+ SpecV1LinkingTest.test129, SpecV1LinkingTest.test130, SpecV1LinkingTest.test131,
+ SpecV1StartTest.test18,
SpecV1FuncTest.test85,
SpecV1ThreadsImportsTest.test64, SpecV1ThreadsImportsTest.test65,
SpecV1ThreadsImportsTest.test66,
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/HostImportRoundTripTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/HostImportRoundTripTest.java
deleted file mode 100644
index 35c7c06ac..000000000
--- a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/HostImportRoundTripTest.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package run.endive.redline.experimental.runner.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.NativeMachineFactory;
-import run.endive.runtime.HostFunction;
-import run.endive.runtime.ImportValues;
-import run.endive.runtime.Instance;
-import run.endive.wasm.Parser;
-import run.endive.wasm.types.FunctionType;
-import run.endive.wasm.types.ValType;
-import run.endive.wasm.types.Value;
-
-/**
- * Values crossing the host boundary are marshalled by hand in each runner, so each
- * conversion is a place they can be mangled. The spec suite does not reach these:
- * it drives modules that are self-contained rather than calling back into Java.
- */
-public class HostImportRoundTripTest {
-
- @Test
- public void floatResultKeepsItsBitPattern() {
- try (var instance = buildInstance()) {
- assertEquals(
- 1.5f,
- Value.longToFloat(instance.export("callRetF32").apply()[0]),
- "a float result must be reinterpreted, not converted numerically");
- }
- }
-
- @Test
- public void doubleResultKeepsItsBitPattern() {
- try (var instance = buildInstance()) {
- assertEquals(
- 2.5d,
- Value.longToDouble(instance.export("callRetF64").apply()[0]),
- "a double result must be reinterpreted, not converted numerically");
- }
- }
-
- @Test
- public void negativeI32ArgumentArrivesSignExtended() {
- try (var instance = buildInstance()) {
- assertEquals(
- 1,
- (int) instance.export("callTakeI32").apply()[0],
- "the host must be handed -1, not 4294967295");
- }
- }
-
- @Test
- public void multiValueResultKeepsEveryValue() {
- try (var instance = buildInstance()) {
- assertEquals(
- 30,
- (int) instance.export("callRetPairSum").apply()[0],
- "both results of a multi-value host import must arrive");
- }
- }
-
- private static Instance buildInstance() {
- var module =
- Parser.parse(
- CorpusResources.getResource("compiled/host-import-roundtrip.wat.wasm"));
-
- var imports =
- ImportValues.builder()
- .addFunction(
- new HostFunction(
- "host",
- "retF32",
- FunctionType.of(
- java.util.List.of(),
- java.util.List.of(ValType.F32)),
- (inst, args) -> new long[] {Value.floatToLong(1.5f)}),
- new HostFunction(
- "host",
- "retF64",
- FunctionType.of(
- java.util.List.of(),
- java.util.List.of(ValType.F64)),
- (inst, args) -> new long[] {Value.doubleToLong(2.5d)}),
- new HostFunction(
- "host",
- "takeI32",
- FunctionType.of(
- java.util.List.of(ValType.I32),
- java.util.List.of(ValType.I32)),
- // Reports on the raw long it was handed rather
- // than echoing it: an echo would be truncated
- // back to -1 on the way out and hide a
- // zero-extended argument.
- (inst, args) -> new long[] {args[0] == -1L ? 1 : 0}),
- new HostFunction(
- "host",
- "retPair",
- FunctionType.of(
- java.util.List.of(),
- java.util.List.of(ValType.I32, ValType.I32)),
- (inst, args) -> new long[] {10, 20}))
- .build();
-
- return NativeMachineFactory.builder(module)
- .withImportValues(imports)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(), m))
- .build();
- }
-}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/HostThrowPropagationTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/HostThrowPropagationTest.java
deleted file mode 100644
index fc519f1d1..000000000
--- a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/HostThrowPropagationTest.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package run.endive.redline.experimental.runner.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.NativeMachineFactory;
-import run.endive.runtime.HostFunction;
-import run.endive.runtime.ImportValues;
-import run.endive.wasm.Parser;
-import run.endive.wasm.types.FunctionType;
-
-/**
- * An exception from a host function has to abandon the module the same way a trap
- * does, otherwise the module keeps running on state the host has already rejected.
- */
-public class HostThrowPropagationTest {
-
- private static final class Boom extends RuntimeException {
- Boom() {
- super("boom");
- }
- }
-
- @Test
- public void moduleStopsWhenAHostFunctionThrows() {
- var module =
- Parser.parse(
- CorpusResources.getResource(
- "compiled/host-throw-stops-execution.wat.wasm"));
-
- var imports =
- ImportValues.builder()
- .addFunction(
- new HostFunction(
- "host",
- "boom",
- FunctionType.of(java.util.List.of(), java.util.List.of()),
- (inst, args) -> {
- throw new Boom();
- }))
- .build();
-
- try (var instance =
- NativeMachineFactory.builder(module)
- .withImportValues(imports)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(),
- m))
- .build()) {
- assertThrows(Boom.class, () -> instance.export("callBoom").apply());
- assertEquals(
- 0,
- instance.memory().readInt(0),
- "the store after the throwing host call must never run");
- }
- }
-}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/InterruptFlagTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/InterruptFlagTest.java
deleted file mode 100644
index d81115e90..000000000
--- a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/InterruptFlagTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package run.endive.redline.experimental.runner.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.NativeMachineFactory;
-import run.endive.runtime.HostFunction;
-import run.endive.runtime.ImportValues;
-import run.endive.wasm.Parser;
-import run.endive.wasm.types.FunctionType;
-
-/**
- * The watchdog raises the interrupt flag from another thread, so it can land after
- * the call it was meant to stop has passed its last check. The flag must not then
- * sit in the context and stop a later call that nobody interrupted.
- */
-public class InterruptFlagTest {
-
- @AfterEach
- public void clearInterruptStatus() {
- // Keeps a failure from leaking an interrupt into the rest of the suite.
- Thread.interrupted();
- }
-
- @Test
- public void aFlagRaisedMidCallDoesNotStopTheNextCall() {
- var module =
- Parser.parse(CorpusResources.getResource("compiled/interrupt-midcall.wat.wasm"));
-
- var machineRef = new NativeMachine[1];
- var imports =
- ImportValues.builder()
- .addFunction(
- new HostFunction(
- "host",
- "raiseFlag",
- FunctionType.of(java.util.List.of(), java.util.List.of()),
- (inst, args) -> {
- machineRef[0].requestInterrupt();
- return null;
- }))
- .build();
-
- try (var instance =
- NativeMachineFactory.builder(module)
- .withImportValues(imports)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(),
- m))
- .build()) {
- machineRef[0] = (NativeMachine) instance.getMachine();
-
- // Returns normally: the entry check ran before the flag was raised.
- instance.export("callHost").apply();
-
- assertEquals(
- 42,
- (int) instance.export("answer").apply()[0],
- "a flag left over from the previous call must not stop this one");
- assertFalse(
- Thread.currentThread().isInterrupted(),
- "no interrupt happened, so the caller must not be left interrupted");
- }
- }
-}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/LifecycleTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/LifecycleTest.java
deleted file mode 100644
index 202b3b871..000000000
--- a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/LifecycleTest.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package run.endive.redline.experimental.runner.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.NativeMachineFactory;
-import run.endive.wasm.Parser;
-import run.endive.wasm.WasmModule;
-
-/**
- * Everything a machine releases on close is an off-heap free, so closing twice
- * has to be a no-op rather than a double free, and a memory the instance only
- * borrowed has to survive it.
- */
-public class LifecycleTest {
-
- @Test
- public void closingTwiceIsSafe() {
- var instance = build(parse());
- instance.close();
- instance.close();
- }
-
- @Test
- public void aMemoryTheModuleDefinesStillWorksBeforeClose() {
- try (var instance = build(parse())) {
- instance.memory().writeI32(0, 0x5A5A5A5A);
- assertEquals(0x5A5A5A5A, instance.memory().readInt(0));
- }
- }
-
- private static WasmModule parse() {
- return Parser.parse(CorpusResources.getResource("compiled/trap-stops-execution.wat.wasm"));
- }
-
- private static run.endive.runtime.Instance build(WasmModule module) {
- return NativeMachineFactory.builder(module)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(), m))
- .build();
- }
-}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/MemoryBoundsTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/MemoryBoundsTest.java
deleted file mode 100644
index 855e18d25..000000000
--- a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/MemoryBoundsTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package run.endive.redline.experimental.runner.internal;
-
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import org.junit.jupiter.api.Test;
-import run.endive.redline.experimental.runner.NativeMachineFactory;
-import run.endive.runtime.WasmRuntimeException;
-import run.endive.wasm.types.MemoryLimits;
-
-/**
- * A host reading past the end of a Wasm memory has to trap the same way it would
- * on any other backend. The spec suite drives memory from inside the module, so it
- * never exercises these accessors.
- */
-public class MemoryBoundsTest {
-
- private static final int PAGE = 65536;
-
- @Test
- public void readPastTheEndTraps() {
- var memory = NativeMachineFactory.createMemory(new MemoryLimits(1, 2));
- assertThrows(WasmRuntimeException.class, () -> memory.readInt(PAGE));
- assertThrows(WasmRuntimeException.class, () -> memory.readLong(PAGE - 4));
- assertThrows(WasmRuntimeException.class, () -> memory.read(PAGE));
- assertThrows(WasmRuntimeException.class, () -> memory.readShort(PAGE - 1));
- assertThrows(WasmRuntimeException.class, () -> memory.readBytes(PAGE - 1, 8));
- }
-
- @Test
- public void writePastTheEndTraps() {
- var memory = NativeMachineFactory.createMemory(new MemoryLimits(1, 2));
- assertThrows(WasmRuntimeException.class, () -> memory.writeI32(PAGE, 1));
- assertThrows(WasmRuntimeException.class, () -> memory.writeLong(PAGE - 4, 1L));
- assertThrows(WasmRuntimeException.class, () -> memory.writeByte(PAGE, (byte) 1));
- assertThrows(WasmRuntimeException.class, () -> memory.writeShort(PAGE - 1, (short) 1));
- }
-
- @Test
- public void insideTheMemoryIsUntouched() {
- var memory = NativeMachineFactory.createMemory(new MemoryLimits(1, 2));
- memory.writeI32(PAGE - 4, 0x11223344);
- org.junit.jupiter.api.Assertions.assertEquals(0x11223344, memory.readInt(PAGE - 4));
- }
-}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/ReentrantStackGuardTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/ReentrantStackGuardTest.java
deleted file mode 100644
index 3e766bc5a..000000000
--- a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/ReentrantStackGuardTest.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package run.endive.redline.experimental.runner.internal;
-
-import static org.junit.jupiter.api.Assertions.assertInstanceOf;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.NativeMachineFactory;
-import run.endive.runtime.HostFunction;
-import run.endive.runtime.ImportValues;
-import run.endive.wasm.Parser;
-import run.endive.wasm.WasmEngineException;
-import run.endive.wasm.types.FunctionType;
-
-/**
- * Recursion that goes back through the host re-enters the machine from the top
- * every time. The stack guard has to stay anchored where the outermost call
- * started: re-anchoring per call moves the limit deeper on every level, so it
- * never fires and the JVM raises StackOverflowError instead. That is an Error,
- * which callers guarding against runaway modules do not catch.
- */
-public class ReentrantStackGuardTest {
-
- /** Only a backstop: the guard is expected to fire long before this. */
- private static final int CAP = 20_000;
-
- @Test
- public void theGuardStillFiresWhenRecursionGoesThroughTheHost() {
- var thrown = assertThrows(Throwable.class, () -> recurseThroughHost(true));
- assertInstanceOf(
- WasmEngineException.class,
- thrown,
- "must trap rather than let the JVM raise StackOverflowError");
- assertTrue(
- String.valueOf(thrown.getMessage()).contains("call stack exhausted"),
- "expected a call stack exhausted trap, got: " + thrown.getMessage());
- }
-
- @Test
- public void matchesTheInterpreter() {
- var reference = assertThrows(Throwable.class, () -> recurseThroughHost(false));
- assertInstanceOf(WasmEngineException.class, reference);
-
- var actual = assertThrows(Throwable.class, () -> recurseThroughHost(true));
- assertInstanceOf(
- reference.getClass(),
- actual,
- "redline must end this the same way the interpreter does");
- }
-
- private static void recurseThroughHost(boolean native_) {
- var module =
- Parser.parse(CorpusResources.getResource("compiled/reentrant-recursion.wat.wasm"));
-
- int[] depth = {0};
- var imports =
- ImportValues.builder()
- .addFunction(
- new HostFunction(
- "host",
- "reenter",
- FunctionType.of(java.util.List.of(), java.util.List.of()),
- (inst, args) -> {
- if (depth[0]++ < CAP) {
- inst.export("recurse").apply();
- }
- return null;
- }))
- .build();
-
- if (!native_) {
- run.endive.runtime.Instance.builder(module)
- .withImportValues(imports)
- .build()
- .export("recurse")
- .apply();
- return;
- }
-
- try (var instance =
- NativeMachineFactory.builder(module)
- .withImportValues(imports)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(),
- m))
- .build()) {
- instance.export("recurse").apply();
- }
- }
-}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/TableInitExprTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/TableInitExprTest.java
deleted file mode 100644
index 4657db2d2..000000000
--- a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/TableInitExprTest.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package run.endive.redline.experimental.runner.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.NativeMachineFactory;
-import run.endive.wasm.Parser;
-
-/**
- * A table declared with an initialiser has to come up holding it. The spec suite
- * does not cover this: its tables are filled by element segments, which take a
- * different path.
- */
-public class TableInitExprTest {
-
- @Test
- public void tableComesUpHoldingItsInitialiser() {
- var module = Parser.parse(CorpusResources.getResource("compiled/table-init-expr.wat.wasm"));
-
- try (var instance =
- NativeMachineFactory.builder(module)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(),
- m))
- .build()) {
- assertEquals(
- 42,
- (int) instance.export("callInitialised").apply()[0],
- "a slot filled only by the table initialiser must be callable");
- }
- }
-}
diff --git a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/TrapPropagationTest.java b/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/TrapPropagationTest.java
deleted file mode 100644
index f6a7677bb..000000000
--- a/redline/runner-tests/src/test/java/run/endive/redline/experimental/runner/internal/TrapPropagationTest.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package run.endive.redline.experimental.runner.internal;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import org.junit.jupiter.api.Test;
-import run.endive.corpus.CorpusResources;
-import run.endive.redline.experimental.api.internal.RedlineTarget;
-import run.endive.redline.experimental.compiler.internal.NativeCompiler;
-import run.endive.redline.experimental.runner.NativeMachineFactory;
-import run.endive.wasm.Parser;
-import run.endive.wasm.WasmEngineException;
-import run.endive.wasm.WasmModule;
-
-/**
- * A trap has to abandon the caller, not just the frame it happened in. The spec
- * suite asserts on the exception a call ends with, which is reported correctly
- * either way, so it never noticed execution carrying on past the trap.
- */
-public class TrapPropagationTest {
-
- @Test
- public void callerStopsWhenItsCalleeTraps() {
- var module = parse();
- try (var instance = build(module)) {
- assertThrows(
- WasmEngineException.class, () -> instance.export("storeAfterTrap").apply());
- assertEquals(
- 0,
- instance.memory().readInt(0),
- "the store after the trapping call must never run");
- }
- }
-
- @Test
- public void matchesTheInterpreter() {
- var module = parse();
- var interpreter = run.endive.runtime.Instance.builder(module).build();
- assertThrows(WasmEngineException.class, () -> interpreter.export("loopAfterTrap").apply());
- int reference = interpreter.memory().readInt(4);
-
- try (var instance = build(module)) {
- assertThrows(WasmEngineException.class, () -> instance.export("loopAfterTrap").apply());
- assertEquals(reference, instance.memory().readInt(4), "must match the interpreter");
- }
- }
-
- private static WasmModule parse() {
- return Parser.parse(CorpusResources.getResource("compiled/trap-stops-execution.wat.wasm"));
- }
-
- private static run.endive.runtime.Instance build(WasmModule module) {
- return NativeMachineFactory.builder(module)
- .withCompilerFunction(
- m ->
- NativeCompiler.compileAll(
- RedlineTarget.detectHost().orElseThrow().triple(), m))
- .build();
- }
-}
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/NativeMachineFactory.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/NativeMachineFactory.java
index 8c67a82a6..b5370f03d 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/NativeMachineFactory.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/NativeMachineFactory.java
@@ -62,13 +62,13 @@ public static Builder builder(WasmModule module) {
}
public TableInstance createTable(Table table, int initValue) {
- var nativeTable = new NativeTable(table, initValue, arena);
+ var nativeTable = new NativeTable(table, arena);
nativeTables.add(nativeTable);
return nativeTable;
}
public static TableInstance createImportTable(Table table, int initValue) {
- return new NativeTable(table, initValue, Arena.ofAuto());
+ return new NativeTable(table, Arena.ofAuto());
}
public GlobalInstance createGlobal(
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeGlobalInstance.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeGlobalInstance.java
index 3910d5048..3a97f0b93 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeGlobalInstance.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeGlobalInstance.java
@@ -46,7 +46,6 @@ public void setValue(long value) {
@Override
public void setValue(Value value) {
- checkType(value);
buffer.set(ValueLayout.JAVA_LONG, offset, value.raw());
}
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java
index 4578b2f1d..adbb53e39 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java
@@ -19,8 +19,6 @@
import run.endive.redline.experimental.bridge.internal.CraneliftBridge;
import run.endive.runtime.Instance;
import run.endive.runtime.Machine;
-import run.endive.runtime.TrapException;
-import run.endive.runtime.WasmRuntimeException;
import run.endive.wasm.WasmEngineException;
import run.endive.wasm.types.FunctionType;
import run.endive.wasm.types.ValType;
@@ -101,9 +99,6 @@ public final class NativeMachine implements Machine {
private boolean memBaseInitialized;
private NativeMemory nativeMemory;
private volatile Throwable pendingException;
- private int callDepth;
- private boolean ownsMemory;
- private boolean closed;
public NativeMachine(
Instance instance,
@@ -356,20 +351,11 @@ public NativeMachine(
}
this.nativeMemory = instance.memory() instanceof NativeMemory nm ? nm : null;
- // An imported memory outlives this instance and may back others, so only
- // a memory this module defines is ours to close.
- this.ownsMemory = instance.imports().memoryCount() == 0;
}
@Override
public void close() {
- if (closed) {
- // munmap below is a native free, so a second close would unmap a
- // region that may already have been handed back out.
- return;
- }
- closed = true;
- if (nativeMemory != null && ownsMemory) {
+ if (nativeMemory != null) {
nativeMemory.close();
}
try {
@@ -502,14 +488,8 @@ private MemorySegment createImportStub(int funcId, FunctionType funcType) {
layouts.add(valTypeToLayout(param));
}
- // Multi-return follows the compiled convention: results go through
- // argsBuffer and the call itself returns a dummy i64.
- boolean multiReturn = funcType.returns().size() > 1;
-
ValueLayout returnLayout = null;
- if (multiReturn) {
- returnLayout = ValueLayout.JAVA_LONG;
- } else if (!funcType.returns().isEmpty()) {
+ if (!funcType.returns().isEmpty()) {
returnLayout = valTypeToLayout(funcType.returns().get(0));
}
@@ -549,17 +529,9 @@ private MemorySegment createImportStub(int funcId, FunctionType funcType) {
var voidType =
MethodType.methodType(void.class, targetParamTypes.toArray(new Class[0]));
dropper = dropper.asType(voidType);
- } else if (!multiReturn && !funcType.returns().isEmpty()) {
+ } else if (!funcType.returns().isEmpty()) {
var retClass = valTypeToJavaClass(funcType.returns().get(0));
- if (retClass.equals(float.class)) {
- // The long carries the f32 bit pattern, so it has to be
- // reinterpreted. A cast would convert numerically and turn the
- // bits of 1.5f into 1.06954752E9f.
- dropper = MethodHandles.filterReturnValue(dropper, LONG_TO_FLOAT);
- } else if (retClass.equals(double.class)) {
- dropper = MethodHandles.filterReturnValue(dropper, LONG_TO_DOUBLE);
- } else if (!retClass.equals(long.class)) {
- // i32: the value is the low 32 bits, so truncation is correct.
+ if (!retClass.equals(long.class)) {
dropper =
MethodHandles.explicitCastArguments(
dropper,
@@ -589,22 +561,11 @@ private long importDispatchDirect(int funcId) {
if (funcId < numImports) {
var importFunc = instance.imports().function(funcId);
long[] result = importFunc.handle().apply(instance, args);
- if (result == null || result.length == 0) {
- return 0L;
- }
- if (importFunc.functionType().returns().size() > 1) {
- // Multi-return convention: the caller reads the results back out
- // of argsBuffer and ignores the returned value.
- for (int i = 0; i < result.length; i++) {
- argsBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(i), result[i]);
- }
- return 0L;
- }
- return result[0];
+ return (result != null && result.length > 0) ? result[0] : 0L;
}
throw new WasmEngineException("Function " + funcId + " not compiled");
} catch (Throwable t) {
- recordHostException(t);
+ pendingException = t;
return 0L;
}
}
@@ -626,23 +587,39 @@ private MemorySegment createTrampolineStub() {
}
}
- /**
- * Compiled code only reaches this with a table operation sentinel: it emits
- * call_indirect inline and never writes TYPE_ID, TABLE_IDX or ELEM_IDX, so the
- * call_indirect path this used to carry could only ever have dispatched on
- * whatever those fields happened to hold.
- */
@SuppressWarnings("unused")
private long callIndirectTrampoline(long ctxAddr) {
try {
var ctx = MemorySegment.ofAddress(ctxAddr).reinterpret(CTX_SIZE);
int argCount = ctx.get(ValueLayout.JAVA_INT, CtxBuffer.ARG_COUNT);
+
+ // Negative argCount = table operation sentinel
if (argCount < 0) {
return handleTableOperation(argCount);
}
- throw new WasmEngineException("Unexpected trampoline call: argCount " + argCount);
+
+ // Normal call_indirect path (fallback, rarely used now)
+ int typeId = ctx.get(ValueLayout.JAVA_INT, CtxBuffer.TYPE_ID);
+ int tableIdx = ctx.get(ValueLayout.JAVA_INT, CtxBuffer.TABLE_IDX);
+ int elemIdx = ctx.get(ValueLayout.JAVA_INT, CtxBuffer.ELEM_IDX);
+
+ int funcId = nativeTables[tableIdx].requiredRef(elemIdx);
+
+ // Type check
+ int actualTypeIdx = instance.functionType(funcId);
+ if (actualTypeIdx != typeId) {
+ throw new WasmEngineException("indirect call type mismatch");
+ }
+
+ long[] args = new long[argCount];
+ for (int i = 0; i < argCount; i++) {
+ args[i] = argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(i));
+ }
+
+ long[] result = this.call(funcId, args);
+ return result.length > 0 ? result[0] : 0L;
} catch (Throwable t) {
- recordHostException(t);
+ pendingException = t;
return 0L;
}
}
@@ -832,7 +809,7 @@ private long memoryGrowHandler(long ctxAddr) {
}
return oldPages;
} catch (Throwable t) {
- recordHostException(t);
+ pendingException = t;
return -1L;
}
}
@@ -894,14 +871,11 @@ private void initializeNativeTables() {
for (int i = 0; i < tableCount; i++) {
var table = instance.table(i);
if (table instanceof NativeTable nt) {
- nt.resolvePendingRefs(instance);
nativeTables[i] = nt;
} else {
// Imported table not created by our factory — wrap it
var tableDef = new run.endive.wasm.types.Table(table.elementType(), table.limits());
- var nt =
- new NativeTable(
- tableDef, run.endive.wasm.types.Value.REF_NULL_VALUE, arena);
+ var nt = new NativeTable(tableDef, arena);
for (int j = 0; j < table.size(); j++) {
nt.setRef(j, table.ref(j), instance);
}
@@ -925,36 +899,25 @@ MemorySegment getFuncTypesArray() {
return funcTypesArray;
}
- /**
- * Marks the context so compiled code unwinds at its next trap check rather
- * than running on. The first throwable wins: it is the one that stopped
- * execution, so a later one would be a symptom of it.
- */
- private void recordHostException(Throwable t) {
- if (pendingException == null) {
- pendingException = t;
- }
- ctxBuffer.set(ValueLayout.JAVA_INT, CtxBuffer.TRAP_CODE, CtxBuffer.TRAP_HOST_EXCEPTION);
- }
-
private static WasmEngineException trapException(int trapCode) {
- // TrapException, not the WasmEngineException parent: Instance catches
- // TrapException to report a trapping start function as uninstantiable.
return switch (trapCode) {
- case CtxBuffer.TRAP_DIV_BY_ZERO -> new TrapException("integer divide by zero");
- case CtxBuffer.TRAP_INT_OVERFLOW -> new TrapException("integer overflow");
- case CtxBuffer.TRAP_UNREACHABLE -> new TrapException("unreachable");
- case CtxBuffer.TRAP_TRUNC_OVERFLOW -> new TrapException("integer overflow");
- case CtxBuffer.TRAP_TRUNC_NAN -> new TrapException("invalid conversion to integer");
- case CtxBuffer.TRAP_OOB -> new WasmRuntimeException("out of bounds memory access");
- case CtxBuffer.TRAP_CALL_STACK_EXHAUSTED -> new TrapException("call stack exhausted");
- case CtxBuffer.TRAP_TABLE_OOB -> new TrapException("out of bounds table access");
- case CtxBuffer.TRAP_UNDEFINED_ELEMENT -> new TrapException("undefined element");
- case CtxBuffer.TRAP_UNINITIALIZED_ELEMENT -> new TrapException("uninitialized element");
+ case CtxBuffer.TRAP_DIV_BY_ZERO -> new WasmEngineException("integer divide by zero");
+ case CtxBuffer.TRAP_INT_OVERFLOW -> new WasmEngineException("integer overflow");
+ case CtxBuffer.TRAP_UNREACHABLE -> new WasmEngineException("unreachable");
+ case CtxBuffer.TRAP_TRUNC_OVERFLOW -> new WasmEngineException("integer overflow");
+ case CtxBuffer.TRAP_TRUNC_NAN ->
+ new WasmEngineException("invalid conversion to integer");
+ case CtxBuffer.TRAP_OOB -> new WasmEngineException("out of bounds memory access");
+ case CtxBuffer.TRAP_CALL_STACK_EXHAUSTED ->
+ new WasmEngineException("call stack exhausted");
+ case CtxBuffer.TRAP_TABLE_OOB -> new WasmEngineException("out of bounds table access");
+ case CtxBuffer.TRAP_UNDEFINED_ELEMENT -> new WasmEngineException("undefined element");
+ case CtxBuffer.TRAP_UNINITIALIZED_ELEMENT ->
+ new WasmEngineException("uninitialized element");
case CtxBuffer.TRAP_INDIRECT_CALL_TYPE_MISMATCH ->
- new TrapException("indirect call type mismatch");
- case CtxBuffer.TRAP_UNALIGNED_ATOMIC -> new TrapException("unaligned atomic");
- case CtxBuffer.TRAP_INTERRUPTED -> new TrapException("interrupted");
+ new WasmEngineException("indirect call type mismatch");
+ case CtxBuffer.TRAP_UNALIGNED_ATOMIC -> new WasmEngineException("unaligned atomic");
+ case CtxBuffer.TRAP_INTERRUPTED -> new WasmEngineException("interrupted");
default -> new WasmEngineException("trap: unknown code " + trapCode);
};
}
@@ -1026,19 +989,13 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
var handle = downcalls[funcId];
try {
- boolean outermostCall = callDepth++ == 0;
var funcType = (FunctionType) instance.type(instance.functionType(funcId));
initializeImportGlobals();
initializeNativeTables();
- // Re-anchor the stack guard only for a call that starts on this
- // stack. A host function calling back in has to keep measuring
- // against where the outer call began, or every level moves the
- // limit deeper and the guard stops firing.
- if (outermostCall) {
- ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.STACK_LIMIT, 0L);
- }
+ // Reset stack limit so native code re-initializes from calling thread's RSP
+ ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.STACK_LIMIT, 0L);
if (!memBaseInitialized) {
var mem = instance.memory();
@@ -1066,19 +1023,17 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
}
if (Thread.interrupted()) {
- throw new TrapException("interrupted");
+ throw new WasmEngineException("interrupted");
}
Thread caller = Thread.currentThread();
Thread watchdog =
new Thread(
() -> {
- // Keeps raising rather than returning after the
- // first: a nested call clears the flag when it
- // finishes, and the outer call still needs it.
while (!Thread.currentThread().isInterrupted()) {
if (caller.isInterrupted()) {
requestInterrupt();
+ return;
}
try {
Thread.sleep(1);
@@ -1094,9 +1049,6 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
result = (long) handle.invokeExact(cachedMemBase, ctxBuffer, args);
} finally {
watchdog.interrupt();
- // The flag only ever means "stop this call". Left set it would
- // trap the next one on a thread nobody interrupted.
- clearInterrupt();
}
// Check for exceptions from upcall stubs first — a host function
@@ -1142,7 +1094,6 @@ public long[] call(int funcId, long[] args) throws WasmEngineException {
sneakyThrow(e);
throw new AssertionError("unreachable");
} finally {
- callDepth--;
// Prevent the JIT from considering this machine unreachable during
// the native call, which would let GC collect and close() free
// native memory while code is executing.
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMemory.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMemory.java
index cb907bafa..4c3828e1f 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMemory.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMemory.java
@@ -250,94 +250,47 @@ public void write(int addr, byte[] data, int offset, int size) {
MemorySegment.copy(MemorySegment.ofArray(data), offset, segment, addr, size);
}
- /**
- * A MemorySegment reports an out of bounds access its own way, but a host
- * reading past the end of a Wasm memory has to see the same trap it would
- * from any other backend.
- */
- private static run.endive.runtime.WasmRuntimeException outOfBounds(int addr) {
- return new run.endive.runtime.WasmRuntimeException(
- "out of bounds memory access: attempted to access address: " + addr);
- }
-
@Override
public byte read(int addr) {
- try {
- return segment.get(ValueLayout.JAVA_BYTE, addr);
- } catch (IndexOutOfBoundsException e) {
- throw outOfBounds(addr);
- }
+ return segment.get(ValueLayout.JAVA_BYTE, addr);
}
@Override
public byte[] readBytes(int addr, int len) {
- try {
- return segment.asSlice(addr, len).toArray(ValueLayout.JAVA_BYTE);
- } catch (IndexOutOfBoundsException e) {
- throw outOfBounds(addr);
- }
+ return segment.asSlice(addr, len).toArray(ValueLayout.JAVA_BYTE);
}
@Override
public void writeI32(int addr, int data) {
- try {
- segment.set(
- ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr, data);
- } catch (IndexOutOfBoundsException e) {
- throw outOfBounds(addr);
- }
+ segment.set(ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr, data);
}
@Override
public int readInt(int addr) {
- try {
- return segment.get(
- ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr);
- } catch (IndexOutOfBoundsException e) {
- throw outOfBounds(addr);
- }
+ return segment.get(ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr);
}
@Override
public void writeLong(int addr, long data) {
- try {
- segment.set(
- ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr, data);
- } catch (IndexOutOfBoundsException e) {
- throw outOfBounds(addr);
- }
+ segment.set(ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr, data);
}
@Override
public long readLong(int addr) {
- try {
- return segment.get(
- ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr);
- } catch (IndexOutOfBoundsException e) {
- throw outOfBounds(addr);
- }
+ return segment.get(
+ ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr);
}
@Override
public void writeShort(int addr, short data) {
- try {
- segment.set(
- ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN),
- addr,
- data);
- } catch (IndexOutOfBoundsException e) {
- throw outOfBounds(addr);
- }
+ segment.set(
+ ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr, data);
}
@Override
public short readShort(int addr) {
- try {
- return segment.get(
- ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr);
- } catch (IndexOutOfBoundsException e) {
- throw outOfBounds(addr);
- }
+ return segment.get(
+ ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr);
}
@Override
@@ -347,11 +300,7 @@ public long readU16(int addr) {
@Override
public void writeByte(int addr, byte data) {
- try {
- segment.set(ValueLayout.JAVA_BYTE, addr, data);
- } catch (IndexOutOfBoundsException e) {
- throw outOfBounds(addr);
- }
+ segment.set(ValueLayout.JAVA_BYTE, addr, data);
}
@Override
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeTable.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeTable.java
index 2831ec6a3..bdb1de3cb 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeTable.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeTable.java
@@ -45,8 +45,8 @@ public final class NativeTable extends TableInstance {
private final int capacity;
private final boolean isExternRef;
- public NativeTable(Table table, int initValue, Arena arena) {
- super(table, initValue);
+ public NativeTable(Table table, Arena arena) {
+ super(table, REF_NULL_VALUE);
this.isExternRef = table.elementType().equals(ValType.ExternRef);
int initial = (int) table.limits().min();
int max = (int) table.limits().max();
@@ -60,20 +60,10 @@ public NativeTable(Table table, int initValue, Arena arena) {
buffer.set(ValueLayout.JAVA_INT, CtxBuffer.TABLE_SIZE_OFFSET, initial);
buffer.set(ValueLayout.JAVA_INT, CtxBuffer.TABLE_MAX_OFFSET, max > 0 ? max : capacity);
- // Only the entries below size are reachable: every access bounds-checks
- // against the size field, and grow fills the slots it exposes. Filling
- // the whole capacity here would make the entire pre-allocation resident.
- for (int i = 0; i < initial; i++) {
+ // Fill all entries with null (funcId=-1, funcPtr=0, typeIdx=0)
+ for (int i = 0; i < capacity; i++) {
writeNullEntry(i);
}
-
- if (initValue != REF_NULL_VALUE) {
- // The instance has no machine yet, so funcPtr cannot be resolved
- // here. resolvePendingRefs fills it in once there is one.
- for (int i = 0; i < initial; i++) {
- writeUnresolvedEntry(i, initValue);
- }
- }
}
private long entryBase(int index) {
@@ -87,34 +77,6 @@ private void writeNullEntry(int index) {
buffer.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L);
}
- /** A funcref whose native address is not known yet. */
- private void writeUnresolvedEntry(int index, int value) {
- long base = entryBase(index);
- buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0);
- buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, value);
- buffer.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L);
- }
-
- /**
- * Fills in the native address of entries written before the instance had a
- * machine, which is the case for a table initialiser.
- */
- void resolvePendingRefs(Instance instance) {
- if (isExternRef) {
- return;
- }
- int sz = size();
- for (int i = 0; i < sz; i++) {
- long base = entryBase(i);
- int funcId = buffer.get(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET);
- long funcPtr =
- buffer.get(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET);
- if (funcId != REF_NULL_VALUE && funcPtr == 0L) {
- resolveFromInstance(i, funcId, instance);
- }
- }
- }
-
private void writeOpaqueEntry(int index, int value) {
long base = entryBase(index);
buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0);
@@ -197,8 +159,14 @@ public void setRef(int index, int value, Instance instance) {
}
if (value == REF_NULL_VALUE) {
writeNullEntry(index);
- } else if (!resolveFromInstance(index, value, instance)) {
- writeUnresolvedEntry(index, value);
+ } else if (resolveFromInstance(index, value, instance)) {
+ // Resolved using the calling module's NativeMachine
+ } else {
+ // No NativeMachine available — store funcId only (externref or non-native)
+ long base = entryBase(index);
+ buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0);
+ buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, value);
+ buffer.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L);
}
}
@@ -215,7 +183,10 @@ public int grow(int delta, int value, Instance instance) {
if (value == REF_NULL_VALUE) {
writeNullEntry(i);
} else if (!resolveFromInstance(i, value, instance)) {
- writeUnresolvedEntry(i, value);
+ long base = entryBase(i);
+ buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0);
+ buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, value);
+ buffer.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L);
}
}
// Update size
diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/PanamaExecutor.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/PanamaExecutor.java
index fca696c58..16ace607d 100644
--- a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/PanamaExecutor.java
+++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/PanamaExecutor.java
@@ -35,7 +35,6 @@ private PanamaExecutor() {}
private static final int PROT_WRITE = 0x2;
private static final int PROT_EXEC = 0x4;
private static final int MAP_PRIVATE = 0x02;
- private static final long MAP_FAILED = -1L;
private static final int MAP_ANONYMOUS;
// --- Windows handles (null on POSIX) ---
@@ -153,21 +152,9 @@ static MemorySegment mmapCode(long size) throws Throwable {
MAP_PRIVATE | MAP_ANONYMOUS,
-1,
0L);
- checkMapped(addr);
return addr.reinterpret(size);
}
- /**
- * mmap reports failure by returning MAP_FAILED, not null, so an unchecked
- * result would be reinterpreted as a segment at 0xFFFF...FFFF and crash on
- * first use rather than throw.
- */
- private static void checkMapped(MemorySegment addr) {
- if (addr.address() == MAP_FAILED) {
- throw new OutOfMemoryError("mmap failed");
- }
- }
-
/** Make a previously mmapped region executable (and remove write). */
static void mprotectExec(MemorySegment addr, long size) throws Throwable {
if (IS_WINDOWS) {
@@ -213,7 +200,6 @@ static MemorySegment mmapNoAccess(long size) throws Throwable {
MAP_PRIVATE | MAP_ANONYMOUS,
-1,
0L);
- checkMapped(addr);
return addr.reinterpret(size);
}
diff --git a/runtime/src/main/java/run/endive/runtime/GlobalInstance.java b/runtime/src/main/java/run/endive/runtime/GlobalInstance.java
index 81d256f30..aec4ca19e 100644
--- a/runtime/src/main/java/run/endive/runtime/GlobalInstance.java
+++ b/runtime/src/main/java/run/endive/runtime/GlobalInstance.java
@@ -89,7 +89,10 @@ public ValType getType() {
}
public void setValue(Value value) {
- checkType(value);
+ if (value.type() != valType) {
+ throw new IllegalArgumentException(
+ "Value has wrong type; expected " + valType + " got " + value.type());
+ }
this.valueLow = value.raw();
}
@@ -97,14 +100,6 @@ public void setValue(long value) {
this.valueLow = value;
}
- /** For subclasses that store the value elsewhere but still owe the same check. */
- protected final void checkType(Value value) {
- if (value.type() != valType) {
- throw new IllegalArgumentException(
- "Value has wrong type; expected " + valType + " got " + value.type());
- }
- }
-
public void setValueLow(long value) {
this.valueLow = value;
}
diff --git a/wasm-corpus/src/main/resources/compiled/big-table.wat.wasm b/wasm-corpus/src/main/resources/compiled/big-table.wat.wasm
deleted file mode 100644
index 87056af4b..000000000
Binary files a/wasm-corpus/src/main/resources/compiled/big-table.wat.wasm and /dev/null differ
diff --git a/wasm-corpus/src/main/resources/compiled/host-import-roundtrip.wat.wasm b/wasm-corpus/src/main/resources/compiled/host-import-roundtrip.wat.wasm
deleted file mode 100644
index 51d697fb0..000000000
Binary files a/wasm-corpus/src/main/resources/compiled/host-import-roundtrip.wat.wasm and /dev/null differ
diff --git a/wasm-corpus/src/main/resources/compiled/host-throw-stops-execution.wat.wasm b/wasm-corpus/src/main/resources/compiled/host-throw-stops-execution.wat.wasm
deleted file mode 100644
index 90d17a5ea..000000000
Binary files a/wasm-corpus/src/main/resources/compiled/host-throw-stops-execution.wat.wasm and /dev/null differ
diff --git a/wasm-corpus/src/main/resources/compiled/imported-table.wat.wasm b/wasm-corpus/src/main/resources/compiled/imported-table.wat.wasm
deleted file mode 100644
index f2fe78cb6..000000000
Binary files a/wasm-corpus/src/main/resources/compiled/imported-table.wat.wasm and /dev/null differ
diff --git a/wasm-corpus/src/main/resources/compiled/interrupt-midcall.wat.wasm b/wasm-corpus/src/main/resources/compiled/interrupt-midcall.wat.wasm
deleted file mode 100644
index 90d3e3e6a..000000000
Binary files a/wasm-corpus/src/main/resources/compiled/interrupt-midcall.wat.wasm and /dev/null differ
diff --git a/wasm-corpus/src/main/resources/compiled/reentrant-recursion.wat.wasm b/wasm-corpus/src/main/resources/compiled/reentrant-recursion.wat.wasm
deleted file mode 100644
index 68a16bdb3..000000000
Binary files a/wasm-corpus/src/main/resources/compiled/reentrant-recursion.wat.wasm and /dev/null differ
diff --git a/wasm-corpus/src/main/resources/compiled/table-init-expr.wat.wasm b/wasm-corpus/src/main/resources/compiled/table-init-expr.wat.wasm
deleted file mode 100644
index 75346f064..000000000
Binary files a/wasm-corpus/src/main/resources/compiled/table-init-expr.wat.wasm and /dev/null differ
diff --git a/wasm-corpus/src/main/resources/compiled/trap-stops-execution.wat.wasm b/wasm-corpus/src/main/resources/compiled/trap-stops-execution.wat.wasm
deleted file mode 100644
index 0117e4d91..000000000
Binary files a/wasm-corpus/src/main/resources/compiled/trap-stops-execution.wat.wasm and /dev/null differ
diff --git a/wasm-corpus/src/main/resources/wat/big-table.wat b/wasm-corpus/src/main/resources/wat/big-table.wat
deleted file mode 100644
index 87b20b5ea..000000000
--- a/wasm-corpus/src/main/resources/wat/big-table.wat
+++ /dev/null
@@ -1,7 +0,0 @@
-;; A table large enough that failing to release it shows up as real memory.
-;; Bounded so the whole thing is allocated and touched up front.
-(module
- (table 100000 100000 funcref)
-
- (func (export "noop"))
-)
diff --git a/wasm-corpus/src/main/resources/wat/host-import-roundtrip.wat b/wasm-corpus/src/main/resources/wat/host-import-roundtrip.wat
deleted file mode 100644
index da00b4ae2..000000000
--- a/wasm-corpus/src/main/resources/wat/host-import-roundtrip.wat
+++ /dev/null
@@ -1,24 +0,0 @@
-;; Exercises what crosses the boundary to and from a host function: float bit
-;; patterns, negative i32 arguments, and multi-value results. Compiled backends
-;; marshal these by hand, so each one is a place the value can be mangled.
-(module
- (import "host" "retF32" (func $retF32 (result f32)))
- (import "host" "retF64" (func $retF64 (result f64)))
- (import "host" "takeI32" (func $takeI32 (param i32) (result i32)))
- (import "host" "retPair" (func $retPair (result i32 i32)))
-
- (func (export "callRetF32") (result f32)
- (call $retF32))
-
- (func (export "callRetF64") (result f64)
- (call $retF64))
-
- ;; passes -1 straight through to the host
- (func (export "callTakeI32") (result i32)
- (call $takeI32 (i32.const -1)))
-
- ;; the host returns (10, 20); summing proves both results arrived
- (func (export "callRetPairSum") (result i32)
- (call $retPair)
- (i32.add))
-)
diff --git a/wasm-corpus/src/main/resources/wat/host-throw-stops-execution.wat b/wasm-corpus/src/main/resources/wat/host-throw-stops-execution.wat
deleted file mode 100644
index 6a09a2cd2..000000000
--- a/wasm-corpus/src/main/resources/wat/host-throw-stops-execution.wat
+++ /dev/null
@@ -1,10 +0,0 @@
-;; A host function that throws has to abandon the module the same way a trap
-;; does. mem[0] stays 0 unless execution carried on after the exception.
-(module
- (import "host" "boom" (func $boom))
- (memory (export "mem") 1)
-
- (func (export "callBoom")
- (call $boom)
- (i32.store (i32.const 0) (i32.const 42)))
-)
diff --git a/wasm-corpus/src/main/resources/wat/imported-table.wat b/wasm-corpus/src/main/resources/wat/imported-table.wat
deleted file mode 100644
index 41a29a15c..000000000
--- a/wasm-corpus/src/main/resources/wat/imported-table.wat
+++ /dev/null
@@ -1,6 +0,0 @@
-;; Borrows its table from the host, so the instance must not release it on close.
-(module
- (import "env" "table" (table 4 funcref))
-
- (func (export "noop"))
-)
diff --git a/wasm-corpus/src/main/resources/wat/interrupt-midcall.wat b/wasm-corpus/src/main/resources/wat/interrupt-midcall.wat
deleted file mode 100644
index 5d6b83cfe..000000000
--- a/wasm-corpus/src/main/resources/wat/interrupt-midcall.wat
+++ /dev/null
@@ -1,13 +0,0 @@
-;; The host function is called after the entry interrupt check has already
-;; passed, and nothing loops afterwards, so a flag raised from inside it is
-;; still set when the call returns normally. That is what the watchdog thread
-;; does when it observes an interrupt near the end of a call.
-(module
- (import "host" "raiseFlag" (func $raiseFlag))
-
- (func (export "callHost")
- (call $raiseFlag))
-
- (func (export "answer") (result i32)
- (i32.const 42))
-)
diff --git a/wasm-corpus/src/main/resources/wat/reentrant-recursion.wat b/wasm-corpus/src/main/resources/wat/reentrant-recursion.wat
deleted file mode 100644
index 25944a886..000000000
--- a/wasm-corpus/src/main/resources/wat/reentrant-recursion.wat
+++ /dev/null
@@ -1,10 +0,0 @@
-;; Recurses back into itself through the host rather than through a wasm call,
-;; so every level re-enters the machine from the top. The stack guard has to
-;; keep measuring against where the first call started, not where the latest
-;; one did, or the budget grows by a frame on every level.
-(module
- (import "host" "reenter" (func $reenter))
-
- (func (export "recurse")
- (call $reenter))
-)
diff --git a/wasm-corpus/src/main/resources/wat/table-init-expr.wat b/wasm-corpus/src/main/resources/wat/table-init-expr.wat
deleted file mode 100644
index acc3a5488..000000000
--- a/wasm-corpus/src/main/resources/wat/table-init-expr.wat
+++ /dev/null
@@ -1,15 +0,0 @@
-;; A table declared with a non-null initialiser: every slot starts out holding
-;; $f rather than null. Nothing writes to the table afterwards, so a backend
-;; that null-fills instead comes up empty and traps on call_indirect.
-(module
- (type $ft (func (result i32)))
-
- (func $f (type $ft)
- (i32.const 42))
-
- (table $t 2 2 funcref (ref.func $f))
-
- ;; index 1 is only reachable through the initialiser
- (func (export "callInitialised") (result i32)
- (call_indirect (type $ft) (i32.const 1)))
-)
diff --git a/wasm-corpus/src/main/resources/wat/trap-stops-execution.wat b/wasm-corpus/src/main/resources/wat/trap-stops-execution.wat
deleted file mode 100644
index 3ae5bca53..000000000
--- a/wasm-corpus/src/main/resources/wat/trap-stops-execution.wat
+++ /dev/null
@@ -1,19 +0,0 @@
-;; A trap in a callee has to abandon the caller too. Each export below leaves an
-;; observable mark after the call that traps, so if execution carried on past the
-;; trap the mark is still there once the exception surfaces.
-(module
- (memory (export "mem") 1)
-
- (func $trapper (result i32)
- (i32.div_s (i32.const 1) (i32.const 0)))
-
- ;; mem[0] stays 0 unless execution continued past the trap
- (func (export "storeAfterTrap")
- (drop (call $trapper))
- (i32.store (i32.const 0) (i32.const 42)))
-
- ;; mem[4] counts loop iterations that ran after the trap
- (func (export "loopAfterTrap")
- (drop (call $trapper))
- (i32.store (i32.const 4) (i32.const 7)))
-)