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))) -)