diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b5b1afe4e..06a550cdd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,6 @@ jobs: ci: name: CI runs-on: ${{ matrix.os }} - continue-on-error: true strategy: fail-fast: false matrix: @@ -44,7 +43,8 @@ jobs: with: repository: WebAssembly/wasi-testsuite path: wasi-testsuite - ref: prod/testsuite-base + # Keep in sync with the default ref in wasi-test-gen-plugin. + ref: 6345da2237c562d9a94e281332c653b5528fdd52 - name: Set up Java uses: actions/setup-java@v5 with: @@ -55,6 +55,23 @@ jobs: run: mvn -B clean install env: MAVEN_OPTS: "-ea" + - name: Collect ARM JVM diagnostics + if: failure() && runner.os == 'Linux' && runner.arch == 'ARM64' + run: | + lscpu > ci-cpu.txt + java -XX:+PrintFlagsFinal -version > ci-jvm.txt 2>&1 + - name: Upload failure diagnostics + if: failure() + uses: actions/upload-artifact@v7 + with: + name: ci-failure-${{ matrix.os }}-java-${{ matrix.version }} + path: | + **/target/surefire-reports/ + runtime-tests/target/generated-resources/compiled-wast/comments/ + runtime/target/runtime-*.jar + wasm-tools/target/wasm-tools-*.jar + ci-cpu.txt + ci-jvm.txt docs-ci: runs-on: ubuntu-latest @@ -84,7 +101,6 @@ jobs: test-results: name: Test Results runs-on: ubuntu-latest - continue-on-error: true strategy: fail-fast: false matrix: @@ -104,7 +120,8 @@ jobs: with: repository: WebAssembly/wasi-testsuite path: wasi-testsuite - ref: prod/testsuite-base + # Keep in sync with the default ref in wasi-test-gen-plugin. + ref: 6345da2237c562d9a94e281332c653b5528fdd52 - name: Set up Java uses: actions/setup-java@v5 with: diff --git a/.github/workflows/teavm.yaml b/.github/workflows/teavm.yaml new file mode 100644 index 000000000..b0888c450 --- /dev/null +++ b/.github/workflows/teavm.yaml @@ -0,0 +1,40 @@ +name: NGE TeaVM Runtime + +on: + push: + branches: [ main ] + pull_request: + +permissions: + contents: read + +jobs: + runtime: + name: Runtime and build-time modules (${{ matrix.backend }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + backend: [javascript, wasm-gc] + steps: + - name: Checkout sources + uses: actions/checkout@v6 + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '25' + cache: maven + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + - name: Compile and execute with the NGE TeaVM fork + run: mvn -B -ntp -U -Dteavm-tests -Dteavm.backend=${{ matrix.backend }} -pl teavm-tests -am verify + - name: Upload generated TeaVM application + if: always() + uses: actions/upload-artifact@v7 + with: + name: teavm-runtime-${{ matrix.backend }} + path: teavm-tests/target/${{ matrix.backend }}/ diff --git a/compiler-tests/src/test/java/com/dylibso/chicory/testing/TestModule.java b/compiler-tests/src/test/java/com/dylibso/chicory/testing/TestModule.java index 181f3ea27..0ea01b3c1 100644 --- a/compiler-tests/src/test/java/com/dylibso/chicory/testing/TestModule.java +++ b/compiler-tests/src/test/java/com/dylibso/chicory/testing/TestModule.java @@ -49,7 +49,7 @@ public static TestModule of(String classpath) { parsed = Wat2Wasm.parse(is); } catch (RuntimeException e) { throw new MalformedException( - e.getMessage() + HACK_MATCH_ALL_MALFORMED_EXCEPTION_TEXT); + e.getMessage() + HACK_MATCH_ALL_MALFORMED_EXCEPTION_TEXT, e); } return of(Parser.parse(parsed)); } diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Compiler.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Compiler.java index 9c8e58035..deeffaae8 100644 --- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Compiler.java +++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/Compiler.java @@ -100,7 +100,22 @@ public final class Compiler { private static final MethodType MACHINE_CALL_METHOD_TYPE = methodType(long[].class, Instance.class, Memory.class, int.class, long[].class); - private static final int MAX_MACHINE_CALL_METHODS = 1024; // must be power of two + // C2 JIT's HugeMethodLimit (default 8KB) — methods exceeding this get degraded optimization. + // Dispatch chunks are sized to stay under this limit for full C2 compilation. + // Estimated upper bound: tableswitch offset (4) + label + invokestatic (~5) + areturn (1) + + // overhead + private static final int ESTIMATED_BYTES_PER_DISPATCH_ENTRY = 40; + private static final int MAX_DISPATCH_METHODS = + dispatchChunkSize(Integer.getInteger("chicory.hugeMethodLimit", 8000)); + private static final int MAX_CALL_INDIRECT_METHODS = 1024; // must be power of two + + static int dispatchChunkSize(int hugeMethodLimit) { + // Keep chunking valid even for zero, negative or very large property values. + int entries = + Math.max(1, Math.min(1024, hugeMethodLimit / ESTIMATED_BYTES_PER_DISPATCH_ENTRY)); + return Integer.highestOneBit(entries); + } + // 1024*12 was empirically determined to work for the 50K small wasm functions. // So lets start there and halve it until we find a size that works. // This should give us the biggest class size possible. @@ -547,15 +562,23 @@ private byte[] compileClass() { } // value_xxx() bridges for multi-value return + // Deduplicate by method name, not by type list, because different + // reference types (e.g. RefNull(funcref) vs RefNull(externref)) produce + // the same JVM method name and signature. + var seenValueMethods = new java.util.HashSet(); var returnTypes = functionTypes.stream() .map(FunctionType::returns) .filter(types -> types.size() > 1) .collect(toSet()); for (var types : returnTypes) { + var methodName = valueMethodName(types); + if (!seenValueMethods.add(methodName)) { + continue; + } emitFunction( classWriter, - valueMethodName(types), + methodName, valueMethodType(types), true, asm -> { @@ -773,11 +796,11 @@ private void compileMachineCallClass() { // static implementation for Machine.call() Consumer callMethod; - if (functionTypes.size() < MAX_MACHINE_CALL_METHODS) { + if (functionTypes.size() < MAX_DISPATCH_METHODS) { callMethod = asm -> compileMachineCallInvoke(asm, 0, functionTypes.size()); } else { // Best value that worked with the 50K small wasm functions - var maxMachineCallMethods = MAX_MACHINE_CALL_METHODS << 2; + var maxMachineCallMethods = MAX_DISPATCH_METHODS << 2; maxMachineCallMethods = loadChunkedClass( functionTypes.size(), @@ -1034,7 +1057,7 @@ private void compileCallIndirect( asm.load(instance, OBJECT_TYPE); // Can we fit the impl in a single method? - if (validIds.size() <= MAX_MACHINE_CALL_METHODS) { + if (validIds.size() <= MAX_CALL_INDIRECT_METHODS) { int[] keys = validIds.stream().mapToInt(x -> x).toArray(); Label[] labels = validIds.stream().map(x -> new Label()).toArray(Label[]::new); @@ -1061,10 +1084,10 @@ private void compileCallIndirect( .appendParameterTypes(Memory.class, Instance.class, int.class); // Best value that worked with the 50K small wasm functions - var maxMachineCallMethods = MAX_MACHINE_CALL_METHODS << 2; + var maxCallIndirectMethods = MAX_CALL_INDIRECT_METHODS << 2; loadChunkedClass( functionTypes.size(), - maxMachineCallMethods, + maxCallIndirectMethods, (collector, start, end, chunkSize) -> compileExtraClass( collector, @@ -1085,8 +1108,8 @@ private void compileCallIndirect( end)); })); - assert Integer.bitCount(maxMachineCallMethods) == 1; // power of two - int shift = Integer.numberOfTrailingZeros(maxMachineCallMethods); + assert Integer.bitCount(maxCallIndirectMethods) == 1; // power of two + int shift = Integer.numberOfTrailingZeros(maxCallIndirectMethods); // switch (funcId >> shift) Label[] labels = new Label[((functionTypes.size() - 1) >> shift) + 1]; diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/TypeStack.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/TypeStack.java index 733e1eab9..60bb8d390 100644 --- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/TypeStack.java +++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/TypeStack.java @@ -81,7 +81,7 @@ public void scopeRestore() { types.push(restore.getFirst()); } - public int scopeStackSize(Instruction scope) { + public Integer scopeStackSize(Instruction scope) { return scopes.get(scope); } diff --git a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmAnalyzer.java b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmAnalyzer.java index 35faff124..c2f4a2830 100644 --- a/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmAnalyzer.java +++ b/compiler/src/main/java/com/dylibso/chicory/compiler/internal/WasmAnalyzer.java @@ -88,6 +88,8 @@ public static class TryCatchBlock { // types of values below the try scope that need to be saved/restored int savedStackSlotBase; List savedStackTypes; + // set to true when TRY_CATCH_BLOCK was emitted (i.e., block is reachable) + boolean registered; public TryCatchBlock( AnnotatedInstruction ins, @@ -113,6 +115,12 @@ public AnalysisResult analyze(int funcId) { int nextLabel = body.instructions().size(); int trySaveSlotOffset = 0; List result = new ArrayList<>(); + // Collect TRY_CATCH_BLOCK instructions during the forward pass and + // prepend them in reverse order at the end. JVM tries exception handlers + // in registration order, so inner (nested) handlers must come before + // outer ones. The forward pass encounters outer try_tables first, so we + // reverse the collected list to get inner-first order. + List tryCatchBlockInstructions = new ArrayList<>(); // find label targets Set labels = new HashSet<>(); @@ -152,12 +160,6 @@ public AnalysisResult analyze(int funcId) { var block = new TryCatchBlock(ins, start, end, handle, after, afterCatchLabels); tryCatchBlocks.put(ins.address(), block); - result.add( - new CompilerInstruction( - CompilerOpCode.TRY_CATCH_BLOCK, - block.start, - block.end, - block.handler)); } } @@ -181,6 +183,19 @@ public AnalysisResult analyze(int funcId) { exitBlockDepth = -1; if (ins.opcode() == OpCode.END) { + // Emit try-catch handler labels even in unreachable code, + // otherwise ASM's computeAllFrames fails with NPE because + // visitTryCatchBlock was called but the handler label was + // never visited. + if (ins.scope().opcode() == OpCode.TRY_TABLE) { + var tryCatchBlock = tryCatchBlocks.remove(ins.scope().address()); + if (tryCatchBlock != null && tryCatchBlock.registered) { + // TRY_TABLE was reachable but END is unreachable — + // still need to emit handler labels since the + // try-catch range was already registered with ASM + analyzeTryCatchEnd(result, tryCatchBlock); + } + } stack.scopeRestore(); } } @@ -357,6 +372,16 @@ public AnalysisResult analyze(int funcId) { } stack.enterScope(ins.scope(), blockType(ins)); + // Collect TRY_CATCH_BLOCK (emitted at end in reverse order). + // Only register reachable TRY_TABLE blocks — unreachable ones + // are skipped, avoiding ASM NPE from unvisited labels. + tryCatchBlock.registered = true; + tryCatchBlockInstructions.add( + new CompilerInstruction( + CompilerOpCode.TRY_CATCH_BLOCK, + tryCatchBlock.start, + tryCatchBlock.end, + tryCatchBlock.handler)); result.add( new CompilerInstruction(CompilerOpCode.LABEL, tryCatchBlock.start)); break; @@ -656,6 +681,12 @@ public AnalysisResult analyze(int funcId) { result.add(new CompilerInstruction(CompilerOpCode.RETURN, ids(functionType.returns()))); stack.verifyEmpty(); + + // Prepend TRY_CATCH_BLOCK instructions in reverse order so that inner + // (nested) handlers are registered before outer ones in the JVM. + reverse(tryCatchBlockInstructions); + result.addAll(0, tryCatchBlockInstructions); + return new AnalysisResult(result, computeMaxTempSlots(result)); } @@ -1513,8 +1544,14 @@ private Optional unwindStack( var types = forward ? blockType.returns() : blockType.params(); int keep = types.size(); + // scope may have been exited already (unreachable code after unconditional branch) + var scopeSize = stack.scopeStackSize(scope); + if (scopeSize == null) { + return Optional.empty(); + } + // for a backward jump, the initial loop parameters are dropped - int drop = stack.types().size() - stack.scopeStackSize(scope); + int drop = stack.types().size() - scopeSize; // do not drop the return values for a forward jump if (forward) { diff --git a/compiler/src/test/java/com/dylibso/chicory/compiler/internal/CallTest.java b/compiler/src/test/java/com/dylibso/chicory/compiler/internal/CallTest.java index 00c86c4f2..07e83ac8e 100644 --- a/compiler/src/test/java/com/dylibso/chicory/compiler/internal/CallTest.java +++ b/compiler/src/test/java/com/dylibso/chicory/compiler/internal/CallTest.java @@ -10,6 +10,26 @@ public class CallTest { + @Test + public void indirectTailCallCanTargetAnotherInstance() { + var module = + Parser.parse( + CorpusResources.getResource("compiled/cross-instance-tail-call.wat.wasm")); + for (boolean compiledTarget : new boolean[] {false, true}) { + var targetBuilder = Instance.builder(module); + if (compiledTarget) { + targetBuilder.withMachineFactory(MachineFactoryCompiler::compile); + } + var target = targetBuilder.build(); + var caller = + Instance.builder(module) + .withMachineFactory(MachineFactoryCompiler::compile) + .build(); + caller.table(0).setRef(0, 0, target); + assertArrayEquals(new long[] {42}, caller.export("run").apply(41)); + } + } + @Test public void callLotsOfArgs() throws InterruptedException { var module = Parser.parse(CorpusResources.getResource("compiled/lots-of-args.wat.wasm")); diff --git a/compiler/src/test/java/com/dylibso/chicory/compiler/internal/DispatchChunkTest.java b/compiler/src/test/java/com/dylibso/chicory/compiler/internal/DispatchChunkTest.java new file mode 100644 index 000000000..c8e8d24b1 --- /dev/null +++ b/compiler/src/test/java/com/dylibso/chicory/compiler/internal/DispatchChunkTest.java @@ -0,0 +1,129 @@ +package com.dylibso.chicory.compiler.internal; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.dylibso.chicory.compiler.MachineFactoryCompiler; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasm.Parser; +import java.io.ByteArrayOutputStream; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.commons.CodeSizeEvaluator; + +public class DispatchChunkTest { + @Test + public void propertyBoundsAlwaysProduceSupportedPowerOfTwo() { + assertEquals(128, Compiler.dispatchChunkSize(8000)); + for (int limit : new int[] {Integer.MIN_VALUE, -1, 0, 1, 39, 40}) { + assertEquals(1, Compiler.dispatchChunkSize(limit)); + } + assertEquals(1024, Compiler.dispatchChunkSize(Integer.MAX_VALUE)); + } + + @Test + public void largeModuleDispatchesAcrossChunksWithinDefaultJitBudget() { + var module = Parser.parse(module(2000)); + var result = Compiler.builder(module).build().compile(); + AtomicInteger dispatchMethods = new AtomicInteger(); + result.classBytes() + .forEach( + (name, bytes) -> { + if (!name.contains("Dispatch_")) { + return; + } + new ClassReader(bytes) + .accept( + new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod( + int access, + String method, + String descriptor, + String signature, + String[] exceptions) { + if (!method.startsWith("call_dispatch_")) { + return null; + } + dispatchMethods.incrementAndGet(); + return new CodeSizeEvaluator(null) { + @Override + public void visitEnd() { + assertTrue( + getMaxSize() < 8000, + name + + "." + + method + + ": " + + getMaxSize()); + } + }; + } + }, + 0); + }); + assertTrue(dispatchMethods.get() > 1); + var instance = + Instance.builder(module) + .withMachineFactory(MachineFactoryCompiler::compile) + .build(); + for (int index : new int[] {0, 127, 128, 511, 512, 1023, 1024, 1999}) { + assertArrayEquals( + new long[] {index + 1}, instance.getMachine().call(index, new long[0])); + } + } + + private static byte[] module(int functions) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.writeBytes(new byte[] {0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}); + section(out, 1, new byte[] {1, 0x60, 0, 1, 0x7f}); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + unsigned(data, functions); + for (int i = 0; i < functions; i++) { + data.write(0); + } + section(out, 3, data.toByteArray()); + data.reset(); + unsigned(data, functions); + for (int i = 0; i < functions; i++) { + ByteArrayOutputStream body = new ByteArrayOutputStream(); + body.write(0); + body.write(0x41); + int value = i + 1; + int part; + do { + part = value & 127; + value >>= 7; + boolean done = value == 0 && (part & 64) == 0; + body.write(done ? part : part | 128); + if (done) { + break; + } + } while (true); + body.write(0x0b); + unsigned(data, body.size()); + data.writeBytes(body.toByteArray()); + } + section(out, 10, data.toByteArray()); + return out.toByteArray(); + } + + private static void section(ByteArrayOutputStream out, int id, byte[] bytes) { + out.write(id); + unsigned(out, bytes.length); + out.writeBytes(bytes); + } + + private static void unsigned(ByteArrayOutputStream out, int value) { + do { + int part = value & 127; + value >>>= 7; + out.write(value == 0 ? part : part | 128); + } while (value != 0); + } +} diff --git a/compiler/src/test/java/com/dylibso/chicory/compiler/internal/FuzzRegressionTest.java b/compiler/src/test/java/com/dylibso/chicory/compiler/internal/FuzzRegressionTest.java new file mode 100644 index 000000000..08fa7a6aa --- /dev/null +++ b/compiler/src/test/java/com/dylibso/chicory/compiler/internal/FuzzRegressionTest.java @@ -0,0 +1,44 @@ +package com.dylibso.chicory.compiler.internal; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import com.dylibso.chicory.compiler.MachineFactoryCompiler; +import com.dylibso.chicory.corpus.CorpusResources; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wasm.Parser; +import org.junit.jupiter.api.Test; + +public class FuzzRegressionTest { + @Test + public void referenceBridgesMatchInterpreter() { + verify( + "compiler-fuzz-regressions", + new String[] {"function-reference", "external-reference"}, + new long[] {17, 23}); + } + + @Test + public void nestedAndUnreachableHandlersMatchInterpreter() { + verify( + "compiler-exception-regressions", + new String[] {"nested-handlers", "unreachable-handler"}, + new long[] {42, 7}); + } + + private static void verify(String fixture, String[] exports, long[] expected) { + var module = Parser.parse(CorpusResources.getResource("compiled/" + fixture + ".wat.wasm")); + var interpreted = Instance.builder(module).build(); + var compiled = + Instance.builder(module) + .withMachineFactory(MachineFactoryCompiler::compile) + .build(); + for (int i = 0; i < exports.length; i++) { + long[] reference = interpreted.export(exports[i]).apply(); + assertArrayEquals(reference, compiled.export(exports[i]).apply(), exports[i]); + assertArrayEquals( + new long[] {expected[i]}, + new long[] {reference[reference.length - 1]}, + exports[i]); + } + } +} diff --git a/docs/docs/usage/runtime-compiler.md b/docs/docs/usage/runtime-compiler.md index 2eb0ba323..fda65e589 100644 --- a/docs/docs/usage/runtime-compiler.md +++ b/docs/docs/usage/runtime-compiler.md @@ -110,6 +110,21 @@ var instance = Instance.builder(module). Typically, you can obtain the list of the functions by running the compiler once with `InterpreterFallback.WARN` +### JVM Dispatch Sizing + +For large modules, the compiler splits direct function dispatch into smaller JVM methods +to help the JIT optimize them. The bytecode budget defaults to 8000 bytes. If your JVM +uses a different JIT method-size limit, set `chicory.hugeMethodLimit` before the compiler +is first used: + +```sh +java -Dchicory.hugeMethodLimit=8000 -jar application.jar +``` + +Chunk sizes are powers of two. Very small or nonpositive budgets use the minimum chunk +size, and very large budgets are capped at a bounded maximum. +The default budget is appropriate for the usual HotSpot configuration. + ### Caveats Please note that compiling and executing Wasm modules at runtime requires: diff --git a/jmh/src/main/java/com/dylibso/chicory/bench/BenchmarkDispatchChunkSize.java b/jmh/src/main/java/com/dylibso/chicory/bench/BenchmarkDispatchChunkSize.java new file mode 100644 index 000000000..c355eea83 --- /dev/null +++ b/jmh/src/main/java/com/dylibso/chicory/bench/BenchmarkDispatchChunkSize.java @@ -0,0 +1,67 @@ +package com.dylibso.chicory.bench; + +import com.dylibso.chicory.compiler.MachineFactoryCompiler; +import com.dylibso.chicory.runtime.ExportFunction; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.wabt.Wat2Wasm; +import com.dylibso.chicory.wasm.Parser; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +// Measures dispatch overhead for large wasm modules (2000 functions). +// Compare the default JIT budget with the previous, larger dispatch chunks: +// java -jar benchmarks.jar BenchmarkDispatchChunkSize +// java -Dchicory.hugeMethodLimit=40960 -jar benchmarks.jar BenchmarkDispatchChunkSize +@State(Scope.Benchmark) +@Warmup(iterations = 3, time = 2) +@Measurement(iterations = 5, time = 3) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@BenchmarkMode(Mode.AverageTime) +@Fork(1) +public class BenchmarkDispatchChunkSize { + + private static final int NUM_FUNCTIONS = 2000; + + @Param({"0", "999", "1999"}) + private int targetFunc; + + private ExportFunction exportFunc; + + @Setup + public void setup() { + StringBuilder wat = new StringBuilder(); + wat.append("(module\n"); + for (int i = 0; i < NUM_FUNCTIONS; i++) { + wat.append(" (func $f").append(i); + wat.append(" (export \"f").append(i).append("\")"); + wat.append(" (param i32) (result i32)\n"); + wat.append(" local.get 0\n"); + wat.append(" i32.const ").append(i + 1).append("\n"); + wat.append(" i32.add)\n"); + } + wat.append(")\n"); + + byte[] wasm = Wat2Wasm.parse(wat.toString()); + Instance instance = + Instance.builder(Parser.parse(wasm)) + .withMachineFactory(MachineFactoryCompiler::compile) + .build(); + exportFunc = instance.export("f" + targetFunc); + } + + @Benchmark + public void dispatch(Blackhole bh) { + bh.consume(exportFunc.apply(42)); + } +} diff --git a/pom.xml b/pom.xml index c31c53fc2..789fe71ab 100644 --- a/pom.xml +++ b/pom.xml @@ -730,6 +730,18 @@ + + teavm-tests + + + teavm-tests + + + + teavm-tests + + + dev diff --git a/runtime-tests/src/test/java/com/dylibso/chicory/testing/TestModule.java b/runtime-tests/src/test/java/com/dylibso/chicory/testing/TestModule.java index d31ec3246..2e6ae9392 100644 --- a/runtime-tests/src/test/java/com/dylibso/chicory/testing/TestModule.java +++ b/runtime-tests/src/test/java/com/dylibso/chicory/testing/TestModule.java @@ -51,7 +51,7 @@ public static TestModule of(String classpath) { parsed = Wat2Wasm.parse(is); } catch (RuntimeException e) { throw new MalformedException( - e.getMessage() + HACK_MATCH_ALL_MALFORMED_EXCEPTION_TEXT); + e.getMessage() + HACK_MATCH_ALL_MALFORMED_EXCEPTION_TEXT, e); } return of(Parser.parse(parsed)); } diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/ByteBufferMemory.java b/runtime/src/main/java/com/dylibso/chicory/runtime/ByteBufferMemory.java index e3163de50..ea0917696 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/ByteBufferMemory.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/ByteBufferMemory.java @@ -345,10 +345,26 @@ public void write(int addr, byte[] data, int offset, int size) { } } + private ByteBuffer pageForAddress(int addr, int accessSize) { + int pageIndex = addr >>> PAGE_SHIFT; + // TeaVM can omit JVM array/null checks; validate against allocated pages explicitly. + if (pageIndex >= nPages) { + // Throw directly; rechecking the range can miscompile on older ARM JVMs. + throw new WasmRuntimeException( + "out of bounds memory access: attempted to access address: " + + addr + + " but limit is: " + + sizeInBytes() + + " and size: " + + accessSize); + } + return pages[pageIndex]; + } + @Override public byte read(int addr) { try { - return pages[addr >>> PAGE_SHIFT].get(addr & PAGE_MASK); + return pageForAddress(addr, 1).get(addr & PAGE_MASK); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 1); } @@ -379,7 +395,7 @@ public void writeI32(int addr, int data) { int off = addr & PAGE_MASK; if (off + 4 <= PAGE_SIZE) { try { - pages[addr >>> PAGE_SHIFT].putInt(off, data); + pageForAddress(addr, 4).putInt(off, data); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 4); } @@ -401,7 +417,7 @@ public int readInt(int addr) { int off = addr & PAGE_MASK; if (off + 4 <= PAGE_SIZE) { try { - return pages[addr >>> PAGE_SHIFT].getInt(off); + return pageForAddress(addr, 4).getInt(off); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 4); } @@ -422,7 +438,7 @@ public void writeLong(int addr, long data) { int off = addr & PAGE_MASK; if (off + 8 <= PAGE_SIZE) { try { - pages[addr >>> PAGE_SHIFT].putLong(off, data); + pageForAddress(addr, 8).putLong(off, data); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 8); } @@ -448,7 +464,7 @@ public long readLong(int addr) { int off = addr & PAGE_MASK; if (off + 8 <= PAGE_SIZE) { try { - return pages[addr >>> PAGE_SHIFT].getLong(off); + return pageForAddress(addr, 8).getLong(off); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 8); } @@ -473,7 +489,7 @@ public void writeShort(int addr, short data) { int off = addr & PAGE_MASK; if (off + 2 <= PAGE_SIZE) { try { - pages[addr >>> PAGE_SHIFT].putShort(off, data); + pageForAddress(addr, 2).putShort(off, data); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 2); } @@ -493,7 +509,7 @@ public short readShort(int addr) { int off = addr & PAGE_MASK; if (off + 2 <= PAGE_SIZE) { try { - return pages[addr >>> PAGE_SHIFT].getShort(off); + return pageForAddress(addr, 2).getShort(off); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 2); } @@ -514,7 +530,7 @@ public long readU16(int addr) { @Override public void writeByte(int addr, byte data) { try { - pages[addr >>> PAGE_SHIFT].put(addr & PAGE_MASK, data); + pageForAddress(addr, 1).put(addr & PAGE_MASK, data); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 1); } @@ -525,7 +541,7 @@ public void writeF32(int addr, float data) { int off = addr & PAGE_MASK; if (off + 4 <= PAGE_SIZE) { try { - pages[addr >>> PAGE_SHIFT].putFloat(off, data); + pageForAddress(addr, 4).putFloat(off, data); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 4); } @@ -544,7 +560,7 @@ public float readFloat(int addr) { int off = addr & PAGE_MASK; if (off + 4 <= PAGE_SIZE) { try { - return pages[addr >>> PAGE_SHIFT].getFloat(off); + return pageForAddress(addr, 4).getFloat(off); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 4); } @@ -558,7 +574,7 @@ public void writeF64(int addr, double data) { int off = addr & PAGE_MASK; if (off + 8 <= PAGE_SIZE) { try { - pages[addr >>> PAGE_SHIFT].putDouble(off, data); + pageForAddress(addr, 8).putDouble(off, data); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 8); } @@ -572,7 +588,7 @@ public double readDouble(int addr) { int off = addr & PAGE_MASK; if (off + 8 <= PAGE_SIZE) { try { - return pages[addr >>> PAGE_SHIFT].getDouble(off); + return pageForAddress(addr, 8).getDouble(off); } catch (RuntimeException e) { throw outOfBoundsException(e, addr, 8); } diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java b/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java index 72c6e8acb..74a2fb8f6 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java @@ -179,7 +179,9 @@ protected void eval(MStack stack, Instance instance, Deque callStack // + " stack=" // + stack); var opcode = instruction.opcode(); - Operands operands = instruction::operand; + // Built-in handlers read the existing instruction directly. Creating a bound + // method reference here allocates an adapter for every executed opcode. + Instruction operands = instruction; instance.onExecution(instruction, stack); switch (opcode) { case UNREACHABLE: @@ -248,7 +250,7 @@ protected void eval(MStack stack, Instance instance, Deque callStack break; case THROW: { - int tagNumber = (int) operands.get(0); + int tagNumber = (int) operands.operand(0); var tag = instance.tag(tagNumber); var type = instance.type(tag.tagType().typeIdx()); @@ -374,16 +376,16 @@ protected void eval(MStack stack, Instance instance, Deque callStack break; // TODO 32bit and 64 bit operations are the same for now case I32_CONST: - stack.push(operands.get(0)); + stack.push(operands.operand(0)); break; case I64_CONST: - stack.push(operands.get(0)); + stack.push(operands.operand(0)); break; case F32_CONST: - stack.push(operands.get(0)); + stack.push(operands.operand(0)); break; case F64_CONST: - stack.push(operands.get(0)); + stack.push(operands.operand(0)); break; case I32_EQ: I32_EQ(stack); @@ -548,7 +550,7 @@ protected void eval(MStack stack, Instance instance, Deque callStack F64_NEG(stack); break; case CALL: - CALL(operands); + CALL(instruction::operand); break; case CALL_REF: CALL_REF(); @@ -827,7 +829,7 @@ protected void eval(MStack stack, Instance instance, Deque callStack TABLE_GROW(stack, instance, operands); break; case REF_FUNC: - stack.push(operands.get(0)); + stack.push(operands.operand(0)); break; case REF_NULL: REF_NULL(stack); @@ -1097,7 +1099,7 @@ protected void eval(MStack stack, Instance instance, Deque callStack break; default: { - evalDefault(stack, instance, callStack, instruction, operands); + evalDefault(stack, instance, callStack, instruction, instruction::operand); break; } } @@ -1608,21 +1610,21 @@ private static void I32_EQ(MStack stack) { stack.push(OpcodeImpl.I32_EQ(a, b)); } - private static void MEMORY_SIZE(MStack stack, Instance instance, Operands operands) { - var sz = instance.memory((int) operands.get(0)).pages(); + private static void MEMORY_SIZE(MStack stack, Instance instance, Instruction operands) { + var sz = instance.memory((int) operands.operand(0)).pages(); stack.push(sz); } - private static void I64_STORE32(MStack stack, Instance instance, Operands operands) { + private static void I64_STORE32(MStack stack, Instance instance, Instruction operands) { var value = stack.pop(); - var ptr = (int) (operands.get(1) + (int) stack.pop()); - instance.memory((int) operands.get(2)).writeI32(ptr, (int) value); + var ptr = (int) (operands.operand(1) + (int) stack.pop()); + instance.memory((int) operands.operand(2)).writeI32(ptr, (int) value); } - private static void I64_STORE8(MStack stack, Instance instance, Operands operands) { + private static void I64_STORE8(MStack stack, Instance instance, Instruction operands) { var value = (byte) stack.pop(); - var ptr = (int) (operands.get(1) + (int) stack.pop()); - instance.memory((int) operands.get(2)).writeByte(ptr, value); + var ptr = (int) (operands.operand(1) + (int) stack.pop()); + instance.memory((int) operands.operand(2)).writeByte(ptr, value); } private static void F64_PROMOTE_F32(MStack stack) { @@ -1776,8 +1778,8 @@ private static void REF_NULL(MStack stack) { stack.push(REF_NULL_VALUE); } - private static void ELEM_DROP(Instance instance, Operands operands) { - var x = (int) operands.get(0); + private static void ELEM_DROP(Instance instance, Instruction operands) { + var x = (int) operands.operand(0); instance.setElement(x, null); } @@ -1794,8 +1796,8 @@ private static void REF_AS_NON_NULL(MStack stack) { stack.push(val); } - private static void DATA_DROP(Instance instance, Operands operands) { - var segment = (int) operands.get(0); + private static void DATA_DROP(Instance instance, Instruction operands) { + var segment = (int) operands.operand(0); instance.dropDataSegment(segment); } @@ -1804,8 +1806,8 @@ private static void F64_CONVERT_I64_S(MStack stack) { stack.push(Value.doubleToLong(OpcodeImpl.F64_CONVERT_I64_S(tos))); } - private static void TABLE_GROW(MStack stack, Instance instance, Operands operands) { - var tableidx = (int) operands.get(0); + private static void TABLE_GROW(MStack stack, Instance instance, Instruction operands) { + var tableidx = (int) operands.operand(0); var table = instance.table(tableidx); var size = (int) stack.pop(); @@ -1815,15 +1817,15 @@ private static void TABLE_GROW(MStack stack, Instance instance, Operands operand stack.push(res); } - private static void TABLE_SIZE(MStack stack, Instance instance, Operands operands) { - var tableidx = (int) operands.get(0); + private static void TABLE_SIZE(MStack stack, Instance instance, Instruction operands) { + var tableidx = (int) operands.operand(0); var table = instance.table(tableidx); stack.push(table.size()); } - private static void TABLE_FILL(MStack stack, Instance instance, Operands operands) { - var tableidx = (int) operands.get(0); + private static void TABLE_FILL(MStack stack, Instance instance, Instruction operands) { + var tableidx = (int) operands.operand(0); var size = (int) stack.pop(); var val = OpcodeImpl.boxForTable(stack.pop(), instance); @@ -1832,9 +1834,9 @@ private static void TABLE_FILL(MStack stack, Instance instance, Operands operand OpcodeImpl.TABLE_FILL(instance, tableidx, size, val, offset); } - private static void TABLE_COPY(MStack stack, Instance instance, Operands operands) { - var tableidxSrc = (int) operands.get(1); - var tableidxDst = (int) operands.get(0); + private static void TABLE_COPY(MStack stack, Instance instance, Instruction operands) { + var tableidxSrc = (int) operands.operand(1); + var tableidxDst = (int) operands.operand(0); var size = (int) stack.pop(); var s = (int) stack.pop(); @@ -1843,12 +1845,12 @@ private static void TABLE_COPY(MStack stack, Instance instance, Operands operand OpcodeImpl.TABLE_COPY(instance, tableidxSrc, tableidxDst, size, s, d); } - private static void MEMORY_COPY(MStack stack, Instance instance, Operands operands) { + private static void MEMORY_COPY(MStack stack, Instance instance, Instruction operands) { var size = (int) stack.pop(); var offset = (int) stack.pop(); var destination = (int) stack.pop(); - var dstMem = instance.memory((int) operands.get(0)); - var srcMem = instance.memory((int) operands.get(1)); + var dstMem = instance.memory((int) operands.operand(0)); + var srcMem = instance.memory((int) operands.operand(1)); if (dstMem == srcMem) { dstMem.copy(destination, offset, size); } else { @@ -1856,9 +1858,9 @@ private static void MEMORY_COPY(MStack stack, Instance instance, Operands operan } } - private static void TABLE_INIT(MStack stack, Instance instance, Operands operands) { - var tableidx = (int) operands.get(1); - var elementidx = (int) operands.get(0); + private static void TABLE_INIT(MStack stack, Instance instance, Instruction operands) { + var tableidx = (int) operands.operand(1); + var elementidx = (int) operands.operand(0); var size = (int) stack.pop(); var elemidx = (int) stack.pop(); @@ -1867,9 +1869,9 @@ private static void TABLE_INIT(MStack stack, Instance instance, Operands operand OpcodeImpl.TABLE_INIT(instance, tableidx, elementidx, size, elemidx, offset); } - private static void MEMORY_INIT(MStack stack, Instance instance, Operands operands) { - var segmentId = (int) operands.get(0); - var memidx = (int) operands.get(1); + private static void MEMORY_INIT(MStack stack, Instance instance, Instruction operands) { + var segmentId = (int) operands.operand(0); + var memidx = (int) operands.operand(1); var size = (int) stack.pop(); var offset = (int) stack.pop(); var destination = (int) stack.pop(); @@ -2000,17 +2002,17 @@ private static void F32_NEG(MStack stack) { stack.push(Value.floatToLong(-tos)); } - private static void MEMORY_FILL(MStack stack, Instance instance, Operands operands) { + private static void MEMORY_FILL(MStack stack, Instance instance, Instruction operands) { var size = (int) stack.pop(); var val = (byte) stack.pop(); var offset = (int) stack.pop(); var end = (size + offset); - instance.memory((int) operands.get(0)).fill(val, offset, end); + instance.memory((int) operands.operand(0)).fill(val, offset, end); } - private static void MEMORY_GROW(MStack stack, Instance instance, Operands operands) { + private static void MEMORY_GROW(MStack stack, Instance instance, Instruction operands) { var size = (int) stack.pop(); - var nPages = instance.memory((int) operands.get(0)).grow(size); + var nPages = instance.memory((int) operands.operand(0)).grow(size); stack.push(nPages); } @@ -2023,123 +2025,132 @@ protected static int readMemPtr(MStack stack, Operands operands) { return (int) (operands.get(1) + address); } - private static void F64_STORE(MStack stack, Instance instance, Operands operands) { + private static int readMemPtr(MStack stack, Instruction operands) { + int address = (int) stack.pop(); + if (operands.operand(1) < 0 || operands.operand(1) >= Integer.MAX_VALUE || address < 0) { + throw new WasmRuntimeException("out of bounds memory access"); + } + + return (int) (operands.operand(1) + address); + } + + private static void F64_STORE(MStack stack, Instance instance, Instruction operands) { var value = Value.longToDouble(stack.pop()); var ptr = readMemPtr(stack, operands); - instance.memory((int) operands.get(2)).writeF64(ptr, value); + instance.memory((int) operands.operand(2)).writeF64(ptr, value); } - private static void F32_STORE(MStack stack, Instance instance, Operands operands) { + private static void F32_STORE(MStack stack, Instance instance, Instruction operands) { var value = Value.longToFloat(stack.pop()); var ptr = readMemPtr(stack, operands); - instance.memory((int) operands.get(2)).writeF32(ptr, value); + instance.memory((int) operands.operand(2)).writeF32(ptr, value); } - private static void I64_STORE(MStack stack, Instance instance, Operands operands) { + private static void I64_STORE(MStack stack, Instance instance, Instruction operands) { var value = stack.pop(); var ptr = readMemPtr(stack, operands); - instance.memory((int) operands.get(2)).writeLong(ptr, value); + instance.memory((int) operands.operand(2)).writeLong(ptr, value); } - private static void I64_STORE16(MStack stack, Instance instance, Operands operands) { + private static void I64_STORE16(MStack stack, Instance instance, Instruction operands) { var value = (short) stack.pop(); var ptr = readMemPtr(stack, operands); - instance.memory((int) operands.get(2)).writeShort(ptr, value); + instance.memory((int) operands.operand(2)).writeShort(ptr, value); } - private static void I32_STORE(MStack stack, Instance instance, Operands operands) { + private static void I32_STORE(MStack stack, Instance instance, Instruction operands) { var value = (int) stack.pop(); var ptr = readMemPtr(stack, operands); - instance.memory((int) operands.get(2)).writeI32(ptr, value); + instance.memory((int) operands.operand(2)).writeI32(ptr, value); } - private static void I64_LOAD32_U(MStack stack, Instance instance, Operands operands) { + private static void I64_LOAD32_U(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); // TODO: make all the memory.readThings to return long - var val = instance.memory((int) operands.get(2)).readU32(ptr); + var val = instance.memory((int) operands.operand(2)).readU32(ptr); stack.push(val); } - private static void I64_LOAD32_S(MStack stack, Instance instance, Operands operands) { + private static void I64_LOAD32_S(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readI32(ptr); + var val = instance.memory((int) operands.operand(2)).readI32(ptr); stack.push(val); } - private static void I64_LOAD16_U(MStack stack, Instance instance, Operands operands) { + private static void I64_LOAD16_U(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readU16(ptr); + var val = instance.memory((int) operands.operand(2)).readU16(ptr); stack.push(val); } - private static void I32_LOAD16_U(MStack stack, Instance instance, Operands operands) { + private static void I32_LOAD16_U(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readU16(ptr); + var val = instance.memory((int) operands.operand(2)).readU16(ptr); stack.push(val); } - private static void I64_LOAD16_S(MStack stack, Instance instance, Operands operands) { + private static void I64_LOAD16_S(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readI16(ptr); + var val = instance.memory((int) operands.operand(2)).readI16(ptr); stack.push(val); } - private static void I32_LOAD16_S(MStack stack, Instance instance, Operands operands) { + private static void I32_LOAD16_S(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readI16(ptr); + var val = instance.memory((int) operands.operand(2)).readI16(ptr); stack.push(val); } - private static void I64_LOAD8_U(MStack stack, Instance instance, Operands operands) { + private static void I64_LOAD8_U(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readU8(ptr); + var val = instance.memory((int) operands.operand(2)).readU8(ptr); stack.push(val); } - private static void I32_LOAD8_U(MStack stack, Instance instance, Operands operands) { + private static void I32_LOAD8_U(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readU8(ptr); + var val = instance.memory((int) operands.operand(2)).readU8(ptr); stack.push(val); } - private static void I64_LOAD8_S(MStack stack, Instance instance, Operands operands) { + private static void I64_LOAD8_S(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readI8(ptr); + var val = instance.memory((int) operands.operand(2)).readI8(ptr); stack.push(val); } - private static void I32_LOAD8_S(MStack stack, Instance instance, Operands operands) { + private static void I32_LOAD8_S(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readI8(ptr); + var val = instance.memory((int) operands.operand(2)).readI8(ptr); stack.push(val); } - private static void F64_LOAD(MStack stack, Instance instance, Operands operands) { + private static void F64_LOAD(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readF64(ptr); + var val = instance.memory((int) operands.operand(2)).readF64(ptr); stack.push(val); } - private static void F32_LOAD(MStack stack, Instance instance, Operands operands) { + private static void F32_LOAD(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readF32(ptr); + var val = instance.memory((int) operands.operand(2)).readF32(ptr); stack.push(val); } - private static void I64_LOAD(MStack stack, Instance instance, Operands operands) { + private static void I64_LOAD(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readI64(ptr); + var val = instance.memory((int) operands.operand(2)).readI64(ptr); stack.push(val); } - private static void I32_LOAD(MStack stack, Instance instance, Operands operands) { + private static void I32_LOAD(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).readI32(ptr); + var val = instance.memory((int) operands.operand(2)).readI32(ptr); stack.push(val); } - private static void TABLE_SET(MStack stack, Instance instance, Operands operands) { - var idx = (int) operands.get(0); + private static void TABLE_SET(MStack stack, Instance instance, Instruction operands) { + var idx = (int) operands.operand(0); var table = instance.table(idx); var value = OpcodeImpl.boxForTable(stack.pop(), instance); @@ -2147,16 +2158,16 @@ private static void TABLE_SET(MStack stack, Instance instance, Operands operands table.setRef(i, value, instance); } - private static void TABLE_GET(MStack stack, Instance instance, Operands operands) { - var idx = (int) operands.get(0); + private static void TABLE_GET(MStack stack, Instance instance, Instruction operands) { + var idx = (int) operands.operand(0); var table = instance.table(idx); var i = (int) stack.pop(); var ref = OpcodeImpl.TABLE_GET(instance, idx, i); stack.push(OpcodeImpl.unboxFromTable(ref, instance, table.elementType())); } - private static void GLOBAL_SET(MStack stack, Instance instance, Operands operands) { - var id = (int) operands.get(0); + private static void GLOBAL_SET(MStack stack, Instance instance, Instruction operands) { + var id = (int) operands.operand(0); if (!instance.global(id).getType().equals(ValType.V128)) { var val = stack.pop(); instance.global(id).setValue(val); @@ -2168,8 +2179,8 @@ private static void GLOBAL_SET(MStack stack, Instance instance, Operands operand } } - private static void GLOBAL_GET(MStack stack, Instance instance, Operands operands) { - int idx = (int) operands.get(0); + private static void GLOBAL_GET(MStack stack, Instance instance, Instruction operands) { + int idx = (int) operands.operand(0); stack.push(instance.global(idx).getValueLow()); if (instance.global(idx).getType().equals(ValType.V128)) { @@ -2177,16 +2188,16 @@ private static void GLOBAL_GET(MStack stack, Instance instance, Operands operand } } - private static void DROP(MStack stack, Operands operands) { - if (operands.get(0) == ValType.ID.V128) { + private static void DROP(MStack stack, Instruction operands) { + if (operands.operand(0) == ValType.ID.V128) { stack.pop(); } stack.pop(); } - private static void SELECT(MStack stack, Operands operands) { + private static void SELECT(MStack stack, Instruction operands) { var pred = (int) stack.pop(); - if (operands.get(0) == ValType.ID.V128) { + if (operands.operand(0) == ValType.ID.V128) { var b1 = stack.pop(); var b2 = stack.pop(); var a1 = stack.pop(); @@ -2209,9 +2220,9 @@ private static void SELECT(MStack stack, Operands operands) { } } - private static void SELECT_T(MStack stack, Operands operands) { + private static void SELECT_T(MStack stack, Instruction operands) { var pred = (int) stack.pop(); - var typeId = operands.get(0); + var typeId = operands.operand(0); if (typeId == ValType.V128.id()) { var b1 = stack.pop(); @@ -2236,8 +2247,9 @@ private static void SELECT_T(MStack stack, Operands operands) { } } - private static void LOCAL_GET(MStack stack, Operands operands, StackFrame currentStackFrame) { - var idx = (int) operands.get(0); + private static void LOCAL_GET( + MStack stack, Instruction operands, StackFrame currentStackFrame) { + var idx = (int) operands.operand(0); var i = currentStackFrame.localIndexOf(idx); if (currentStackFrame.localType(idx).equals(ValType.V128)) { stack.push(currentStackFrame.local(i)); @@ -2247,8 +2259,9 @@ private static void LOCAL_GET(MStack stack, Operands operands, StackFrame curren } } - private static void LOCAL_SET(MStack stack, Operands operands, StackFrame currentStackFrame) { - var idx = (int) operands.get(0); + private static void LOCAL_SET( + MStack stack, Instruction operands, StackFrame currentStackFrame) { + var idx = (int) operands.operand(0); var i = currentStackFrame.localIndexOf(idx); if (currentStackFrame.localType(idx).equals(ValType.V128)) { currentStackFrame.setLocal(i, stack.pop()); @@ -2258,9 +2271,10 @@ private static void LOCAL_SET(MStack stack, Operands operands, StackFrame curren } } - private static void LOCAL_TEE(MStack stack, Operands operands, StackFrame currentStackFrame) { + private static void LOCAL_TEE( + MStack stack, Instruction operands, StackFrame currentStackFrame) { // here we peek instead of pop, leaving it on the stack - var idx = (int) operands.get(0); + var idx = (int) operands.operand(0); var i = currentStackFrame.localIndexOf(idx); if (currentStackFrame.localType(idx).equals(ValType.V128)) { var tmp = stack.pop(); @@ -2272,107 +2286,107 @@ private static void LOCAL_TEE(MStack stack, Operands operands, StackFrame curren } } - private static void I32_ATOMIC_LOAD(MStack stack, Instance instance, Operands operands) { + private static void I32_ATOMIC_LOAD(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); if (ptr % 4 != 0) { throw new InvalidException("unaligned atomic"); } - var val = instance.memory((int) operands.get(2)).atomicReadInt(ptr); + var val = instance.memory((int) operands.operand(2)).atomicReadInt(ptr); stack.push(val); } - private static void I64_ATOMIC_LOAD(MStack stack, Instance instance, Operands operands) { + private static void I64_ATOMIC_LOAD(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); if (ptr % 8 != 0) { throw new InvalidException("unaligned atomic"); } - var val = instance.memory((int) operands.get(2)).atomicReadLong(ptr); + var val = instance.memory((int) operands.operand(2)).atomicReadLong(ptr); stack.push(val); } - private static void I64_ATOMIC_LOAD8_U(MStack stack, Instance instance, Operands operands) { + private static void I64_ATOMIC_LOAD8_U(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).atomicReadByte(ptr); + var val = instance.memory((int) operands.operand(2)).atomicReadByte(ptr); stack.push(Byte.toUnsignedLong(val)); } - private static void I32_ATOMIC_LOAD8_U(MStack stack, Instance instance, Operands operands) { + private static void I32_ATOMIC_LOAD8_U(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); - var val = instance.memory((int) operands.get(2)).atomicReadByte(ptr); + var val = instance.memory((int) operands.operand(2)).atomicReadByte(ptr); stack.push(Byte.toUnsignedLong(val)); } - private static void I32_ATOMIC_LOAD16_U(MStack stack, Instance instance, Operands operands) { + private static void I32_ATOMIC_LOAD16_U(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); if (ptr % 2 != 0) { throw new InvalidException("unaligned atomic"); } - var val = instance.memory((int) operands.get(2)).atomicReadShort(ptr); + var val = instance.memory((int) operands.operand(2)).atomicReadShort(ptr); stack.push(Short.toUnsignedLong(val)); } - private static void I64_ATOMIC_LOAD16_U(MStack stack, Instance instance, Operands operands) { + private static void I64_ATOMIC_LOAD16_U(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); if (ptr % 2 != 0) { throw new InvalidException("unaligned atomic"); } - var val = instance.memory((int) operands.get(2)).atomicReadShort(ptr); + var val = instance.memory((int) operands.operand(2)).atomicReadShort(ptr); stack.push(Short.toUnsignedLong(val)); } - private static void I64_ATOMIC_LOAD32_U(MStack stack, Instance instance, Operands operands) { + private static void I64_ATOMIC_LOAD32_U(MStack stack, Instance instance, Instruction operands) { var ptr = readMemPtr(stack, operands); if (ptr % 4 != 0) { throw new InvalidException("unaligned atomic"); } - var val = instance.memory((int) operands.get(2)).atomicReadInt(ptr); + var val = instance.memory((int) operands.operand(2)).atomicReadInt(ptr); stack.push(val & 0xffff_ffffL); } - private static void I32_ATOMIC_STORE(MStack stack, Instance instance, Operands operands) { + private static void I32_ATOMIC_STORE(MStack stack, Instance instance, Instruction operands) { var value = (int) stack.pop(); var ptr = readMemPtr(stack, operands); if (ptr % 4 != 0) { throw new InvalidException("unaligned atomic"); } - instance.memory((int) operands.get(2)).atomicWriteInt(ptr, value); + instance.memory((int) operands.operand(2)).atomicWriteInt(ptr, value); } - private static void I64_ATOMIC_STORE8(MStack stack, Instance instance, Operands operands) { + private static void I64_ATOMIC_STORE8(MStack stack, Instance instance, Instruction operands) { var value = (byte) stack.pop(); var ptr = readMemPtr(stack, operands); - instance.memory((int) operands.get(2)).atomicWriteByte(ptr, value); + instance.memory((int) operands.operand(2)).atomicWriteByte(ptr, value); } - private static void I64_ATOMIC_STORE16(MStack stack, Instance instance, Operands operands) { + private static void I64_ATOMIC_STORE16(MStack stack, Instance instance, Instruction operands) { var value = (short) stack.pop(); var ptr = readMemPtr(stack, operands); if (ptr % 2 != 0) { throw new InvalidException("unaligned atomic"); } - instance.memory((int) operands.get(2)).atomicWriteShort(ptr, value); + instance.memory((int) operands.operand(2)).atomicWriteShort(ptr, value); } - private static void I64_ATOMIC_STORE32(MStack stack, Instance instance, Operands operands) { + private static void I64_ATOMIC_STORE32(MStack stack, Instance instance, Instruction operands) { var value = stack.pop(); var ptr = readMemPtr(stack, operands); if (ptr % 4 != 0) { throw new InvalidException("unaligned atomic"); } - instance.memory((int) operands.get(2)).atomicWriteInt(ptr, (int) value); + instance.memory((int) operands.operand(2)).atomicWriteInt(ptr, (int) value); } - private static void I64_ATOMIC_STORE(MStack stack, Instance instance, Operands operands) { + private static void I64_ATOMIC_STORE(MStack stack, Instance instance, Instruction operands) { var value = stack.pop(); var ptr = readMemPtr(stack, operands); if (ptr % 8 != 0) { throw new InvalidException("unaligned atomic"); } - instance.memory((int) operands.get(2)).atomicWriteLong(ptr, value); + instance.memory((int) operands.operand(2)).atomicWriteLong(ptr, value); } private static void I32_ATOMIC_RMW( - MStack stack, Instance instance, Operands operands, AtomicOp op) { + MStack stack, Instance instance, Instruction operands, AtomicOp op) { var operand = (int) stack.pop(); var ptr = readMemPtr(stack, operands); if (ptr % 4 != 0) { @@ -2381,22 +2395,22 @@ private static void I32_ATOMIC_RMW( int oldVal; switch (op) { case ADD: - oldVal = instance.memory((int) operands.get(2)).atomicAddInt(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAddInt(ptr, operand); break; case SUB: - oldVal = instance.memory((int) operands.get(2)).atomicAddInt(ptr, -operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAddInt(ptr, -operand); break; case AND: - oldVal = instance.memory((int) operands.get(2)).atomicAndInt(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAndInt(ptr, operand); break; case OR: - oldVal = instance.memory((int) operands.get(2)).atomicOrInt(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicOrInt(ptr, operand); break; case XOR: - oldVal = instance.memory((int) operands.get(2)).atomicXorInt(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicXorInt(ptr, operand); break; case XCHG: - oldVal = instance.memory((int) operands.get(2)).atomicXchgInt(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicXchgInt(ptr, operand); break; default: throw new IllegalStateException("Unexpected atomic op: " + op); @@ -2404,7 +2418,8 @@ private static void I32_ATOMIC_RMW( stack.push(oldVal); } - private static void I32_ATOMIC_RMW_CMPXCHG(MStack stack, Instance instance, Operands operands) { + private static void I32_ATOMIC_RMW_CMPXCHG( + MStack stack, Instance instance, Instruction operands) { var replacement = (int) stack.pop(); // c3 var expected = (int) stack.pop(); // c2 var ptr = readMemPtr(stack, operands); // i @@ -2412,12 +2427,13 @@ private static void I32_ATOMIC_RMW_CMPXCHG(MStack stack, Instance instance, Oper throw new InvalidException("unaligned atomic"); } var oldVal = - instance.memory((int) operands.get(2)).atomicCmpxchgInt(ptr, expected, replacement); + instance.memory((int) operands.operand(2)) + .atomicCmpxchgInt(ptr, expected, replacement); stack.push(oldVal); } private static void I64_ATOMIC_RMW( - MStack stack, Instance instance, Operands operands, AtomicOp op) { + MStack stack, Instance instance, Instruction operands, AtomicOp op) { var operand = stack.pop(); var ptr = readMemPtr(stack, operands); if (ptr % 8 != 0) { @@ -2426,22 +2442,22 @@ private static void I64_ATOMIC_RMW( long oldVal; switch (op) { case ADD: - oldVal = instance.memory((int) operands.get(2)).atomicAddLong(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAddLong(ptr, operand); break; case SUB: - oldVal = instance.memory((int) operands.get(2)).atomicAddLong(ptr, -operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAddLong(ptr, -operand); break; case AND: - oldVal = instance.memory((int) operands.get(2)).atomicAndLong(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAndLong(ptr, operand); break; case OR: - oldVal = instance.memory((int) operands.get(2)).atomicOrLong(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicOrLong(ptr, operand); break; case XOR: - oldVal = instance.memory((int) operands.get(2)).atomicXorLong(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicXorLong(ptr, operand); break; case XCHG: - oldVal = instance.memory((int) operands.get(2)).atomicXchgLong(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicXchgLong(ptr, operand); break; default: throw new IllegalStateException("Unexpected atomic op: " + op); @@ -2449,7 +2465,8 @@ private static void I64_ATOMIC_RMW( stack.push(oldVal); } - private static void I64_ATOMIC_RMW_CMPXCHG(MStack stack, Instance instance, Operands operands) { + private static void I64_ATOMIC_RMW_CMPXCHG( + MStack stack, Instance instance, Instruction operands) { var replacement = stack.pop(); var expected = stack.pop(); var ptr = readMemPtr(stack, operands); @@ -2457,34 +2474,36 @@ private static void I64_ATOMIC_RMW_CMPXCHG(MStack stack, Instance instance, Oper throw new InvalidException("unaligned atomic"); } var oldVal = - instance.memory((int) operands.get(2)) + instance.memory((int) operands.operand(2)) .atomicCmpxchgLong(ptr, expected, replacement); stack.push(oldVal); } private static void I64_ATOMIC_RMW8_U( - MStack stack, Instance instance, Operands operands, AtomicOp op) { + MStack stack, Instance instance, Instruction operands, AtomicOp op) { var operand = (byte) stack.pop(); var ptr = readMemPtr(stack, operands); byte oldVal; switch (op) { case ADD: - oldVal = instance.memory((int) operands.get(2)).atomicAddByte(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAddByte(ptr, operand); break; case SUB: - oldVal = instance.memory((int) operands.get(2)).atomicAddByte(ptr, (byte) -operand); + oldVal = + instance.memory((int) operands.operand(2)) + .atomicAddByte(ptr, (byte) -operand); break; case AND: - oldVal = instance.memory((int) operands.get(2)).atomicAndByte(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAndByte(ptr, operand); break; case OR: - oldVal = instance.memory((int) operands.get(2)).atomicOrByte(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicOrByte(ptr, operand); break; case XOR: - oldVal = instance.memory((int) operands.get(2)).atomicXorByte(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicXorByte(ptr, operand); break; case XCHG: - oldVal = instance.memory((int) operands.get(2)).atomicXchgByte(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicXchgByte(ptr, operand); break; default: throw new IllegalStateException("Unexpected atomic op: " + op); @@ -2493,18 +2512,18 @@ private static void I64_ATOMIC_RMW8_U( } private static void I64_ATOMIC_RMW8_CMPXCHG_U( - MStack stack, Instance instance, Operands operands) { + MStack stack, Instance instance, Instruction operands) { var replacement = (byte) stack.pop(); var expected = (byte) stack.pop(); var ptr = readMemPtr(stack, operands); var oldVal = - instance.memory((int) operands.get(2)) + instance.memory((int) operands.operand(2)) .atomicCmpxchgByte(ptr, expected, replacement); stack.push(Byte.toUnsignedLong(oldVal)); } private static void I64_ATOMIC_RMW16_U( - MStack stack, Instance instance, Operands operands, AtomicOp op) { + MStack stack, Instance instance, Instruction operands, AtomicOp op) { var operand = (short) stack.pop(); var ptr = readMemPtr(stack, operands); if (ptr % 2 != 0) { @@ -2513,24 +2532,24 @@ private static void I64_ATOMIC_RMW16_U( short oldVal; switch (op) { case ADD: - oldVal = instance.memory((int) operands.get(2)).atomicAddShort(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAddShort(ptr, operand); break; case SUB: oldVal = - instance.memory((int) operands.get(2)) + instance.memory((int) operands.operand(2)) .atomicAddShort(ptr, (short) -operand); break; case AND: - oldVal = instance.memory((int) operands.get(2)).atomicAndShort(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAndShort(ptr, operand); break; case OR: - oldVal = instance.memory((int) operands.get(2)).atomicOrShort(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicOrShort(ptr, operand); break; case XOR: - oldVal = instance.memory((int) operands.get(2)).atomicXorShort(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicXorShort(ptr, operand); break; case XCHG: - oldVal = instance.memory((int) operands.get(2)).atomicXchgShort(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicXchgShort(ptr, operand); break; default: throw new IllegalStateException("Unexpected atomic op: " + op); @@ -2539,7 +2558,7 @@ private static void I64_ATOMIC_RMW16_U( } private static void I64_ATOMIC_RMW16_CMPXCHG_U( - MStack stack, Instance instance, Operands operands) { + MStack stack, Instance instance, Instruction operands) { var replacement = (short) stack.pop(); var expected = (short) stack.pop(); var ptr = readMemPtr(stack, operands); @@ -2547,13 +2566,13 @@ private static void I64_ATOMIC_RMW16_CMPXCHG_U( throw new InvalidException("unaligned atomic"); } var oldVal = - instance.memory((int) operands.get(2)) + instance.memory((int) operands.operand(2)) .atomicCmpxchgShort(ptr, expected, replacement); stack.push(Short.toUnsignedLong(oldVal)); } private static void I64_ATOMIC_RMW32_U( - MStack stack, Instance instance, Operands operands, AtomicOp op) { + MStack stack, Instance instance, Instruction operands, AtomicOp op) { var operand = (int) stack.pop(); var ptr = readMemPtr(stack, operands); if (ptr % 4 != 0) { @@ -2562,22 +2581,22 @@ private static void I64_ATOMIC_RMW32_U( int oldVal; switch (op) { case ADD: - oldVal = instance.memory((int) operands.get(2)).atomicAddInt(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAddInt(ptr, operand); break; case SUB: - oldVal = instance.memory((int) operands.get(2)).atomicAddInt(ptr, -operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAddInt(ptr, -operand); break; case AND: - oldVal = instance.memory((int) operands.get(2)).atomicAndInt(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicAndInt(ptr, operand); break; case OR: - oldVal = instance.memory((int) operands.get(2)).atomicOrInt(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicOrInt(ptr, operand); break; case XOR: - oldVal = instance.memory((int) operands.get(2)).atomicXorInt(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicXorInt(ptr, operand); break; case XCHG: - oldVal = instance.memory((int) operands.get(2)).atomicXchgInt(ptr, operand); + oldVal = instance.memory((int) operands.operand(2)).atomicXchgInt(ptr, operand); break; default: throw new IllegalStateException("Unexpected atomic op: " + op); @@ -2586,7 +2605,7 @@ private static void I64_ATOMIC_RMW32_U( } private static void I64_ATOMIC_RMW32_CMPXCHG_U( - MStack stack, Instance instance, Operands operands) { + MStack stack, Instance instance, Instruction operands) { var replacement = (int) stack.pop(); var expected = (int) stack.pop(); var ptr = readMemPtr(stack, operands); @@ -2594,36 +2613,37 @@ private static void I64_ATOMIC_RMW32_CMPXCHG_U( throw new InvalidException("unaligned atomic"); } var oldVal = - instance.memory((int) operands.get(2)).atomicCmpxchgInt(ptr, expected, replacement); + instance.memory((int) operands.operand(2)) + .atomicCmpxchgInt(ptr, expected, replacement); stack.push(oldVal & 0xffff_ffffL); } - private static void MEM_ATOMIC_WAIT32(MStack stack, Instance instance, Operands operands) { + private static void MEM_ATOMIC_WAIT32(MStack stack, Instance instance, Instruction operands) { long timeout = stack.pop(); int expected = (int) stack.pop(); var ptr = readMemPtr(stack, operands); if (ptr % 4 != 0) { throw new InvalidException("unaligned atomic"); } - var result = instance.memory((int) operands.get(2)).atomicWait(ptr, expected, timeout); + var result = instance.memory((int) operands.operand(2)).atomicWait(ptr, expected, timeout); stack.push(result); } - private static void MEM_ATOMIC_WAIT64(MStack stack, Instance instance, Operands operands) { + private static void MEM_ATOMIC_WAIT64(MStack stack, Instance instance, Instruction operands) { long timeout = stack.pop(); long expected = stack.pop(); var ptr = readMemPtr(stack, operands); if (ptr % 8 != 0) { throw new InvalidException("unaligned atomic"); } - var result = instance.memory((int) operands.get(2)).atomicWait(ptr, expected, timeout); + var result = instance.memory((int) operands.operand(2)).atomicWait(ptr, expected, timeout); stack.push(result); } - private static void MEM_ATOMIC_NOTIFY(MStack stack, Instance instance, Operands operands) { + private static void MEM_ATOMIC_NOTIFY(MStack stack, Instance instance, Instruction operands) { int maxThreads = (int) stack.pop(); var ptr = readMemPtr(stack, operands); - var result = instance.memory((int) operands.get(2)).atomicNotify(ptr, maxThreads); + var result = instance.memory((int) operands.operand(2)).atomicNotify(ptr, maxThreads); stack.push(result); } @@ -2635,9 +2655,9 @@ private static StackFrame RETURN_CALL( MStack stack, Instance instance, Deque callStack, - Operands operands, + Instruction operands, StackFrame currentStackFrame) { - var funcId = (int) operands.get(0); + var funcId = (int) operands.operand(0); var typeId = instance.functionType(funcId); var type = instance.type(typeId); var func = instance.function(funcId); @@ -2703,12 +2723,12 @@ private static StackFrame RETURN_CALL_INDIRECT( MStack stack, Instance instance, Deque callStack, - Operands operands, + Instruction operands, StackFrame currentStackFrame) { - var tableIdx = (int) operands.get(1); + var tableIdx = (int) operands.operand(1); var table = instance.table(tableIdx); - var typeId = (int) operands.get(0); + var typeId = (int) operands.operand(0); int funcTableIdx = (int) stack.pop(); int funcId = table.requiredRef(funcTableIdx); @@ -2826,11 +2846,11 @@ private static StackFrame RETURN_CALL_REF( } private void CALL_INDIRECT( - MStack stack, Instance instance, Deque callStack, Operands operands) { - var tableIdx = (int) operands.get(1); + MStack stack, Instance instance, Deque callStack, Instruction operands) { + var tableIdx = (int) operands.operand(1); var table = instance.table(tableIdx); - var typeId = (int) operands.get(0); + var typeId = (int) operands.operand(0); int funcTableIdx = (int) stack.pop(); int funcId = table.requiredRef(funcTableIdx); @@ -3030,6 +3050,7 @@ private static void BR(StackFrame frame, MStack stack, AnnotatedInstruction inst } private static void BR_TABLE(StackFrame frame, MStack stack, AnnotatedInstruction instruction) { + checkInterruption(); var pred = (int) stack.pop(); var defaultIdx = instruction.operandCount() - 1; @@ -3044,6 +3065,7 @@ private static void BR_TABLE(StackFrame frame, MStack stack, AnnotatedInstructio } private static void BR_IF(StackFrame frame, MStack stack, AnnotatedInstruction instruction) { + checkInterruption(); var pred = (int) stack.pop(); if (pred == 0) { @@ -3172,8 +3194,8 @@ private static void I31_GET_U(MStack stack) { stack.push(Value.decodeI31U(ref)); } - private static void STRUCT_NEW(MStack stack, Instance instance, Operands operands) { - var typeIdx = (int) operands.get(0); + private static void STRUCT_NEW(MStack stack, Instance instance, Instruction operands) { + var typeIdx = (int) operands.operand(0); var st = instance.module().typeSection().getSubType(typeIdx).compType().structType(); var fields = new long[st.fieldTypes().length]; // Pop fields in reverse order (last field on top) @@ -3184,8 +3206,8 @@ private static void STRUCT_NEW(MStack stack, Instance instance, Operands operand stack.push(instance.registerGcRef(struct)); } - private static void STRUCT_NEW_DEFAULT(MStack stack, Instance instance, Operands operands) { - var typeIdx = (int) operands.get(0); + private static void STRUCT_NEW_DEFAULT(MStack stack, Instance instance, Instruction operands) { + var typeIdx = (int) operands.operand(0); var st = instance.module().typeSection().getSubType(typeIdx).compType().structType(); var fields = new long[st.fieldTypes().length]; // Default values: 0 for numeric, REF_NULL_VALUE for references @@ -3201,9 +3223,9 @@ private static void STRUCT_NEW_DEFAULT(MStack stack, Instance instance, Operands } private static void STRUCT_GET( - MStack stack, Instance instance, Operands operands, OpCode opcode) { - var typeIdx = (int) operands.get(0); - var fieldIdx = (int) operands.get(1); + MStack stack, Instance instance, Instruction operands, OpCode opcode) { + var typeIdx = (int) operands.operand(0); + var fieldIdx = (int) operands.operand(1); var ref = (int) stack.pop(); if (ref == REF_NULL_VALUE) { throw new TrapException("null structure reference"); @@ -3222,9 +3244,9 @@ private static void STRUCT_GET( stack.push(val); } - private static void STRUCT_SET(MStack stack, Instance instance, Operands operands) { - var typeIdx = (int) operands.get(0); - var fieldIdx = (int) operands.get(1); + private static void STRUCT_SET(MStack stack, Instance instance, Instruction operands) { + var typeIdx = (int) operands.operand(0); + var fieldIdx = (int) operands.operand(1); var val = stack.pop(); var ref = (int) stack.pop(); if (ref == REF_NULL_VALUE) { @@ -3239,8 +3261,8 @@ private static void STRUCT_SET(MStack stack, Instance instance, Operands operand struct.setField(fieldIdx, val); } - private static void ARRAY_NEW(MStack stack, Instance instance, Operands operands) { - var typeIdx = (int) operands.get(0); + private static void ARRAY_NEW(MStack stack, Instance instance, Instruction operands) { + var typeIdx = (int) operands.operand(0); var len = (int) stack.pop(); var initVal = stack.pop(); var elems = new long[len]; @@ -3249,8 +3271,8 @@ private static void ARRAY_NEW(MStack stack, Instance instance, Operands operands stack.push(instance.registerGcRef(arr)); } - private static void ARRAY_NEW_DEFAULT(MStack stack, Instance instance, Operands operands) { - var typeIdx = (int) operands.get(0); + private static void ARRAY_NEW_DEFAULT(MStack stack, Instance instance, Instruction operands) { + var typeIdx = (int) operands.operand(0); var len = (int) stack.pop(); var at = instance.module().typeSection().getSubType(typeIdx).compType().arrayType(); var elems = new long[len]; @@ -3262,9 +3284,9 @@ private static void ARRAY_NEW_DEFAULT(MStack stack, Instance instance, Operands stack.push(instance.registerGcRef(arr)); } - private static void ARRAY_NEW_FIXED(MStack stack, Instance instance, Operands operands) { - var typeIdx = (int) operands.get(0); - var len = (int) operands.get(1); + private static void ARRAY_NEW_FIXED(MStack stack, Instance instance, Instruction operands) { + var typeIdx = (int) operands.operand(0); + var len = (int) operands.operand(1); var elems = new long[len]; for (int i = len - 1; i >= 0; i--) { elems[i] = stack.pop(); @@ -3273,9 +3295,9 @@ private static void ARRAY_NEW_FIXED(MStack stack, Instance instance, Operands op stack.push(instance.registerGcRef(arr)); } - private static void ARRAY_NEW_DATA(MStack stack, Instance instance, Operands operands) { - var typeIdx = (int) operands.get(0); - var dataIdx = (int) operands.get(1); + private static void ARRAY_NEW_DATA(MStack stack, Instance instance, Instruction operands) { + var typeIdx = (int) operands.operand(0); + var dataIdx = (int) operands.operand(1); var len = (int) stack.pop(); var offset = (int) stack.pop(); var at = instance.module().typeSection().getSubType(typeIdx).compType().arrayType(); @@ -3293,9 +3315,9 @@ private static void ARRAY_NEW_DATA(MStack stack, Instance instance, Operands ope stack.push(instance.registerGcRef(arr)); } - private static void ARRAY_NEW_ELEM(MStack stack, Instance instance, Operands operands) { - var typeIdx = (int) operands.get(0); - var elemIdx = (int) operands.get(1); + private static void ARRAY_NEW_ELEM(MStack stack, Instance instance, Instruction operands) { + var typeIdx = (int) operands.operand(0); + var elemIdx = (int) operands.operand(1); var len = (int) stack.pop(); var offset = (int) stack.pop(); var element = instance.element(elemIdx); @@ -3312,8 +3334,8 @@ private static void ARRAY_NEW_ELEM(MStack stack, Instance instance, Operands ope } private static void ARRAY_GET( - MStack stack, Instance instance, Operands operands, OpCode opcode) { - var typeIdx = (int) operands.get(0); + MStack stack, Instance instance, Instruction operands, OpCode opcode) { + var typeIdx = (int) operands.operand(0); var idx = (int) stack.pop(); var ref = (int) stack.pop(); if (ref == REF_NULL_VALUE) { @@ -3335,8 +3357,8 @@ private static void ARRAY_GET( stack.push(val); } - private static void ARRAY_SET(MStack stack, Instance instance, Operands operands) { - var typeIdx = (int) operands.get(0); + private static void ARRAY_SET(MStack stack, Instance instance, Instruction operands) { + var typeIdx = (int) operands.operand(0); var val = stack.pop(); var idx = (int) stack.pop(); var ref = (int) stack.pop(); @@ -3363,8 +3385,8 @@ private static void ARRAY_LEN(MStack stack, Instance instance) { stack.push(arr.length()); } - private static void ARRAY_FILL(MStack stack, Instance instance, Operands operands) { - var typeIdx = (int) operands.get(0); + private static void ARRAY_FILL(MStack stack, Instance instance, Instruction operands) { + var typeIdx = (int) operands.operand(0); var len = (int) stack.pop(); var val = stack.pop(); var offset = (int) stack.pop(); @@ -3412,9 +3434,9 @@ private static void ARRAY_COPY(MStack stack, Instance instance) { } } - private static void ARRAY_INIT_DATA(MStack stack, Instance instance, Operands operands) { - var typeIdx = (int) operands.get(0); - var dataIdx = (int) operands.get(1); + private static void ARRAY_INIT_DATA(MStack stack, Instance instance, Instruction operands) { + var typeIdx = (int) operands.operand(0); + var dataIdx = (int) operands.operand(1); var len = (int) stack.pop(); var srcOffset = (int) stack.pop(); var dstOffset = (int) stack.pop(); @@ -3438,9 +3460,9 @@ private static void ARRAY_INIT_DATA(MStack stack, Instance instance, Operands op } } - private static void ARRAY_INIT_ELEM(MStack stack, Instance instance, Operands operands) { + private static void ARRAY_INIT_ELEM(MStack stack, Instance instance, Instruction operands) { // operand 0 is the type index (used for validation, not needed at runtime) - var elemIdx = (int) operands.get(1); + var elemIdx = (int) operands.operand(1); var len = (int) stack.pop(); var srcOffset = (int) stack.pop(); var dstOffset = (int) stack.pop(); @@ -3468,9 +3490,9 @@ private static void ARRAY_INIT_ELEM(MStack stack, Instance instance, Operands op } private static void REF_TEST( - MStack stack, Instance instance, Operands operands, OpCode opcode) { - var heapType = (int) operands.get(0); - var sourceHeapType = (int) operands.get(1); + MStack stack, Instance instance, Instruction operands, OpCode opcode) { + var heapType = (int) operands.operand(0); + var sourceHeapType = (int) operands.operand(1); var ref = stack.pop(); boolean nullable = (opcode == OpCode.REF_TEST_NULL); stack.push( @@ -3480,9 +3502,9 @@ private static void REF_TEST( } private static void CAST_TEST( - MStack stack, Instance instance, Operands operands, OpCode opcode) { - var heapType = (int) operands.get(0); - var sourceHeapType = (int) operands.get(1); + MStack stack, Instance instance, Instruction operands, OpCode opcode) { + var heapType = (int) operands.operand(0); + var sourceHeapType = (int) operands.operand(1); var ref = stack.pop(); boolean nullable = (opcode == OpCode.CAST_TEST_NULL); if (!instance.heapTypeMatch(ref, nullable, heapType, sourceHeapType)) { @@ -3496,15 +3518,15 @@ private static void BR_ON_CAST( Instance instance, StackFrame frame, AnnotatedInstruction instruction, - Operands operands) { - var flags = (int) operands.get(0); - var ht2 = (int) operands.get(3); - var sourceHeapType = (int) operands.get(4); + Instruction operands) { + var flags = (int) operands.operand(0); + var ht2 = (int) operands.operand(3); + var sourceHeapType = (int) operands.operand(4); boolean null2 = (flags & 2) != 0; var ref = stack.pop(); if (instance.heapTypeMatch(ref, null2, ht2, sourceHeapType)) { stack.push(ref); - ctrlJump(frame, stack, (int) operands.get(1)); + ctrlJump(frame, stack, (int) operands.operand(1)); frame.jumpTo(instruction.labelTrue()); } else { stack.push(ref); @@ -3516,15 +3538,15 @@ private static void BR_ON_CAST_FAIL( Instance instance, StackFrame frame, AnnotatedInstruction instruction, - Operands operands) { - var flags = (int) operands.get(0); - var ht2 = (int) operands.get(3); - var sourceHeapType = (int) operands.get(4); + Instruction operands) { + var flags = (int) operands.operand(0); + var ht2 = (int) operands.operand(3); + var sourceHeapType = (int) operands.operand(4); boolean null2 = (flags & 2) != 0; var ref = stack.pop(); if (!instance.heapTypeMatch(ref, null2, ht2, sourceHeapType)) { stack.push(ref); - ctrlJump(frame, stack, (int) operands.get(1)); + ctrlJump(frame, stack, (int) operands.operand(1)); frame.jumpTo(instruction.labelTrue()); } else { stack.push(ref); diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java b/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java index 4287fb584..37f97a103 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/StackFrame.java @@ -31,7 +31,11 @@ public class StackFrame { private final int[] localIdx; private final Instance instance; + // Reuse only immutable values at a previously active depth. This retains at most + // 64 inactive frames per activation; deeper active control flow is unchanged. + private static final int MAX_CACHED_CONTROL_FRAMES = 64; private final List ctrlStack = new ArrayList<>(); + private int ctrlStackSize; public StackFrame(Instance instance, int funcId, long[] args) { this( @@ -149,36 +153,58 @@ boolean terminated() { } void pushCtrl(CtrlFrame ctrlFrame) { - ctrlStack.add(ctrlFrame); + if (ctrlStackSize == ctrlStack.size()) { + ctrlStack.add(ctrlFrame); + } else { + ctrlStack.set(ctrlStackSize, ctrlFrame); + } + ctrlStackSize++; } void pushCtrl(OpCode opcode, int startValues, int returnValues, int height) { - ctrlStack.add(new CtrlFrame(opcode, startValues, returnValues, height)); + pushCtrl(opcode, startValues, returnValues, height, 0); } void pushCtrl(OpCode opcode, int startValues, int returnValues, int height, int pc) { - ctrlStack.add(new CtrlFrame(opcode, startValues, returnValues, height, pc)); + if (ctrlStackSize < ctrlStack.size()) { + CtrlFrame cached = ctrlStack.get(ctrlStackSize); + if (cached != null + && cached.opCode == opcode + && cached.startValues == startValues + && cached.endValues == returnValues + && cached.height == height + && cached.pc == pc) { + ctrlStackSize++; + return; + } + } + pushCtrl(new CtrlFrame(opcode, startValues, returnValues, height, pc)); } int ctrlStackSize() { - return ctrlStack.size(); + return ctrlStackSize; } CtrlFrame popCtrl() { - var ctrlFrame = ctrlStack.remove(ctrlStack.size() - 1); + int index = ctrlStackSize - 1; + CtrlFrame ctrlFrame = ctrlStack.get(index); + if (index >= MAX_CACHED_CONTROL_FRAMES) { + ctrlStack.remove(index); + } + ctrlStackSize = index; return ctrlFrame; } CtrlFrame popCtrl(int n) { - int mostRecentCallHeight = ctrlStack.size(); + int mostRecentCallHeight = ctrlStackSize; while (true) { if (ctrlStack.get(--mostRecentCallHeight).opCode == OpCode.CALL) { break; } } - var finalHeight = ctrlStack.size() - (mostRecentCallHeight + n + 1); + var finalHeight = ctrlStackSize - (mostRecentCallHeight + n + 1); CtrlFrame ctrlFrame = null; - while (ctrlStack.size() > finalHeight) { + while (ctrlStackSize > finalHeight) { ctrlFrame = popCtrl(); } return ctrlFrame; diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/ConditionalBranchInterruptionTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/ConditionalBranchInterruptionTest.java new file mode 100644 index 000000000..c0050d44c --- /dev/null +++ b/runtime/src/test/java/com/dylibso/chicory/runtime/ConditionalBranchInterruptionTest.java @@ -0,0 +1,104 @@ +package com.dylibso.chicory.runtime; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.dylibso.chicory.wasm.Parser; +import com.dylibso.chicory.wasm.types.OpCode; +import java.io.ByteArrayOutputStream; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +/** + * Real binary loops whose only backedge is conditional or table-based, with bounded cleanup. + */ +public class ConditionalBranchInterruptionTest { + @Test + @Timeout(10) + public void shouldInterruptConditionalOnlyBackedge() throws InterruptedException { + // (loop (i32.const 1) (br_if 0)) -- no ordinary BR or host call can hide the bug. + assertInterrupted(OpCode.BR_IF, new int[] {0, 3, 0x40, 0x41, 1, 0x0d, 0, 0x0b, 0x0b}); + } + + @Test + @Timeout(10) + public void shouldInterruptTableOnlyBackedge() throws InterruptedException { + // (loop (i32.const 0) (br_table 0)) -- empty label vector, default targets the loop. + assertInterrupted(OpCode.BR_TABLE, new int[] {0, 3, 0x40, 0x41, 0, 0x0e, 0, 0, 0x0b, 0x0b}); + } + + private static void assertInterrupted(OpCode backedge, int[] body) throws InterruptedException { + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch returned = new CountDownLatch(1); + AtomicBoolean cleanup = new AtomicBoolean(); + AtomicReference failure = new AtomicReference<>(); + RuntimeException cleanupOnly = new RuntimeException("Controlled failed-test cleanup"); + Instance instance = + Instance.builder(Parser.parse(module(body))) + .withUnsafeExecutionListener( + (instruction, stack) -> { + if (cleanup.get()) { + throw cleanupOnly; + } + if (instruction.opcode() == backedge) { + entered.countDown(); + } + }) + .build(); + Thread worker = + new Thread( + () -> { + try { + instance.exports().function("run").apply(); + } catch (Throwable aborted) { + failure.set(aborted); + } finally { + returned.countDown(); + } + }, + "conditional-branch-interruption-test"); + worker.start(); + try { + assertTrue(entered.await(2, SECONDS), "The actual loop must start before interruption"); + worker.interrupt(); + assertTrue(returned.await(2, SECONDS), "The branch backedge must observe interruption"); + assertNotSame(cleanupOnly, failure.get()); + assertInstanceOf(ChicoryInterruptedException.class, failure.get()); + } finally { + // The original implementation must also leave no live thread after its failed + // assertion. This listener-only escape is cleanup, never the passing outcome. + cleanup.set(true); + worker.interrupt(); + worker.join(2000); + assertFalse(worker.isAlive(), "The owned test worker must physically return"); + } + } + + private static byte[] module(int[] body) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(new byte[] {0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}, 0, 8); + section(bytes, 1, new int[] {1, 0x60, 0, 0}); + section(bytes, 3, new int[] {1, 0}); + section(bytes, 7, new int[] {1, 3, 'r', 'u', 'n', 0, 0}); + int[] code = new int[body.length + 2]; + code[0] = 1; + code[1] = body.length; + System.arraycopy(body, 0, code, 2, body.length); + section(bytes, 10, code); + return bytes.toByteArray(); + } + + private static void section(ByteArrayOutputStream bytes, int id, int[] body) { + bytes.write(id); + bytes.write(body.length); // These fixed test sections fit one canonical ULEB byte. + for (int value : body) { + bytes.write(value); + } + } +} diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/ImmutableControlFramesTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/ImmutableControlFramesTest.java new file mode 100644 index 000000000..61cf5a41f --- /dev/null +++ b/runtime/src/test/java/com/dylibso/chicory/runtime/ImmutableControlFramesTest.java @@ -0,0 +1,312 @@ +package com.dylibso.chicory.runtime; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.dylibso.chicory.wasm.Parser; +import com.dylibso.chicory.wasm.types.OpCode; +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +/** + * Immutable popped values must survive later control-stack activity, including real guest unwind. + */ +@Timeout(10) +public class ImmutableControlFramesTest { + @Test + public void retainedTargetAndCallUnwindStayIndependentOfInactiveCache() { + StackFrame frame = new StackFrame(emptyInstance(), 0, new long[0]); + frame.pushCtrl(OpCode.CALL, 0, 0, 0); + frame.pushCtrl(OpCode.TRY_TABLE, 0, 2, 2, 17); + CtrlFrame retained = frame.popCtrl(); + frame.pushCtrl(OpCode.BLOCK, 1, 0, 9, 23); + frame.pushCtrl(OpCode.LOOP, 0, 1, 11); + assertEquals(OpCode.CALL, frame.popCtrlTillCall().opCode); + assertEquals(0, frame.ctrlStackSize()); + assertThrows(IndexOutOfBoundsException.class, frame::popCtrl); + assertEquals(0, frame.ctrlStackSize()); + assertFrame(retained, OpCode.TRY_TABLE, 0, 2, 2, 17); + frame.pushCtrl(retained); + MStack values = new MStack(); + for (long value : new long[] {99, 88, 77, 31, 37}) { + values.push(value); + } + StackFrame.doControlTransfer(frame.popCtrl(), values); + assertEquals(4, values.size()); + assertArrayEquals( + new long[] {37, 31, 88, 99}, + new long[] {values.pop(), values.pop(), values.pop(), values.pop()}); + assertFrame(retained, OpCode.TRY_TABLE, 0, 2, 2, 17); + } + + @Test + public void immutableReuseRequiresAllFieldsAndDeepCacheIsBounded() { + StackFrame frame = new StackFrame(emptyInstance(), 0, new long[0]); + for (int changed = 0; changed < 5; changed++) { + frame.pushCtrl(OpCode.BLOCK, 1, 2, 3, 4); + CtrlFrame retained = frame.popCtrl(); + frame.pushCtrl(OpCode.BLOCK, 1, 2, 3, 4); + assertSame(retained, frame.popCtrl()); + OpCode op = changed == 0 ? OpCode.LOOP : OpCode.BLOCK; + int start = changed == 1 ? 2 : 1; + int end = changed == 2 ? 3 : 2; + int height = changed == 3 ? 4 : 3; + int pc = changed == 4 ? 5 : 4; + frame.pushCtrl(op, start, end, height, pc); + CtrlFrame replaced = frame.popCtrl(); + assertNotSame(retained, replaced); + assertFrame(replaced, op, start, end, height, pc); + assertFrame(retained, OpCode.BLOCK, 1, 2, 3, 4); + } + List retained = new ArrayList<>(); + for (int index = 0; index < 257; index++) { + frame.pushCtrl(OpCode.BLOCK, 0, 0, index); + } + for (int index = 0; index < 257; index++) { + retained.add(0, frame.popCtrl()); + } + assertEquals(0, frame.ctrlStackSize()); + for (int index = 0; index < 257; index++) { + frame.pushCtrl(OpCode.BLOCK, 0, 0, index); + CtrlFrame current = frame.popCtrl(); + if (index < 64) { + assertSame(retained.get(index), current); + } else { + assertNotSame(retained.get(index), current); + } + frame.pushCtrl(current); + } + assertEquals(257, frame.ctrlStackSize()); + } + + @Test + public void branchTableLoopAndMultiValueTransferKeepOriginalResults() { + List trace = new ArrayList<>(); + Instance loop = + Instance.builder( + Parser.parse( + module( + new byte[][] {type(new int[0], 0x7f)}, + new int[] {0}, + false, + body( + 1, 0x41, 0, 0x21, 0, 0x02, 0x40, 0x03, 0x40, + 0x02, 0x7f, 0x41, 7, 0x41, 0, 0x0e, 0, 0, 0, + 0x0b, 0x1a, 0x20, 0, 0x41, 1, 0x6a, 0x22, 0, + 0x41, 32, 0x49, 0x0d, 0, 0x0b, 0x0b, 0x20, + 0, 0x0b)))) + .withUnsafeExecutionListener( + (instruction, stack) -> trace.add(instruction.opcode())) + .build(); + assertArrayEquals(new long[] {32}, loop.export("run").apply()); + List first = List.copyOf(trace); + trace.clear(); + assertArrayEquals(new long[] {32}, loop.export("run").apply()); + assertEquals(first, trace); + assertEquals(32, trace.stream().filter(op -> op == OpCode.BR_TABLE).count()); + assertEquals(32, trace.stream().filter(op -> op == OpCode.BR_IF).count()); + Instance values = + Instance.builder( + Parser.parse( + module( + new byte[][] { + type(new int[0], 0x7f), + type(new int[0], 0x7f, 0x7e) + }, + new int[] {0}, + false, + body( + 0, 0x02, 1, 0x41, 42, 0x42, 9, 0x0c, 0, + 0x0b, 0x1a, 0x0b)))) + .build(); + assertArrayEquals(new long[] {42}, values.export("run").apply()); + } + + @Test + public void sameFunctionTailCallsReuseDetachedCallTargetsWithoutGrowingDepth() { + AtomicReference machine = new AtomicReference<>(); + AtomicInteger deepest = new AtomicInteger(); + AtomicInteger tails = new AtomicInteger(); + Instance instance = + Instance.builder( + Parser.parse( + module( + new byte[][] {type(new int[] {0x7f}, 0x7f)}, + new int[] {0}, + false, + body( + 0, 0x20, 0, 0x45, 0x04, 0x7f, 0x41, 37, + 0x05, 0x20, 0, 0x41, 1, 0x6b, 0x12, 0, 0x0b, + 0x0b)))) + .withMachineFactory( + owner -> { + DepthMachine value = new DepthMachine(owner); + machine.set(value); + return value; + }) + .withUnsafeExecutionListener( + (instruction, stack) -> { + deepest.accumulateAndGet(machine.get().depth(), Math::max); + if (instruction.opcode() == OpCode.RETURN_CALL) { + tails.incrementAndGet(); + } + }) + .build(); + assertArrayEquals(new long[] {37}, instance.export("run").apply(128)); + assertEquals(128, tails.get()); + assertEquals(1, deepest.get()); + assertEquals(0, machine.get().depth()); + } + + @Test + public void repeatedTryTableUnwindKeepsHandlerAndFatalIdentity() { + byte[] binary = + module( + new byte[][] {type(new int[0]), type(new int[0], 0x7f)}, + new int[] {1}, + true, + body( + 1, 0x03, 0x40, 0x02, 0x40, 0x1f, 0x40, 1, 2, 0, 0x02, 0x40, 0x20, 0, + 0x41, 1, 0x6a, 0x21, 0, 0x08, 0, 0x0b, 0x0b, 0x0b, 0x20, 0, 0x41, + 32, 0x49, 0x0d, 0, 0x0b, 0x20, 0, 0x0b)); + AtomicInteger handlers = new AtomicInteger(); + Instance guest = + Instance.builder(Parser.parse(binary)) + .withUnsafeExecutionListener( + (instruction, stack) -> { + if (instruction.opcode() == OpCode.TRY_TABLE) { + handlers.incrementAndGet(); + } + }) + .build(); + assertArrayEquals(new long[] {32}, guest.export("run").apply()); + assertArrayEquals(new long[] {32}, guest.export("run").apply()); + assertEquals(64, handlers.get()); + CancellationException fatal = new CancellationException("Owned control-stack cancellation"); + Instance cancelled = + Instance.builder(Parser.parse(binary)) + .withUnsafeExecutionListener( + (instruction, stack) -> { + if (instruction.opcode() == OpCode.THROW) { + throw fatal; + } + }) + .build(); + assertSame( + fatal, + assertThrows(CancellationException.class, () -> cancelled.export("run").apply())); + } + + private static final class DepthMachine extends InterpreterMachine { + DepthMachine(Instance instance) { + super(instance); + } + + int depth() { + return callStack.size(); + } + } + + private static void assertFrame( + CtrlFrame value, OpCode op, int start, int end, int height, int pc) { + assertEquals(op, value.opCode); + assertEquals(start, value.startValues); + assertEquals(end, value.endValues); + assertEquals(height, value.height); + assertEquals(pc, value.pc); + } + + private static Instance emptyInstance() { + return Instance.builder( + Parser.parse( + module( + new byte[][] {type(new int[0])}, + new int[] {0}, + false, + body(0, 0x0b)))) + .build(); + } + + private static byte[] type(int[] parameters, int... results) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(0x60); + unsigned(out, parameters.length); + for (int value : parameters) { + out.write(value); + } + unsigned(out, results.length); + for (int value : results) { + out.write(value); + } + return out.toByteArray(); + } + + private static byte[] body(int locals, int... instructions) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + unsigned(out, locals == 0 ? 0 : 1); + if (locals != 0) { + unsigned(out, locals); + out.write(0x7f); + } + for (int value : instructions) { + out.write(value); + } + return out.toByteArray(); + } + + private static byte[] module(byte[][] types, int[] functions, boolean tag, byte[]... bodies) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(new byte[] {0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}, 0, 8); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + unsigned(data, types.length); + for (byte[] value : types) { + data.write(value, 0, value.length); + } + section(out, 1, data); + data = new ByteArrayOutputStream(); + unsigned(data, functions.length); + for (int value : functions) { + unsigned(data, value); + } + section(out, 3, data); + if (tag) { + data = new ByteArrayOutputStream(); + data.write(new byte[] {1, 0, 0}, 0, 3); + section(out, 13, data); + } + data = new ByteArrayOutputStream(); + data.write(new byte[] {1, 3, 'r', 'u', 'n', 0, 0}, 0, 7); + section(out, 7, data); + data = new ByteArrayOutputStream(); + unsigned(data, bodies.length); + for (byte[] value : bodies) { + unsigned(data, value.length); + data.write(value, 0, value.length); + } + section(out, 10, data); + return out.toByteArray(); + } + + private static void section(ByteArrayOutputStream out, int id, ByteArrayOutputStream data) { + out.write(id); + unsigned(out, data.size()); + out.write(data.toByteArray(), 0, data.size()); + } + + private static void unsigned(ByteArrayOutputStream out, int value) { + do { + int next = value & 127; + value >>>= 7; + out.write(value == 0 ? next : next | 128); + } while (value != 0); + } +} diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/InstructionOperandsTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/InstructionOperandsTest.java new file mode 100644 index 000000000..124cd05a1 --- /dev/null +++ b/runtime/src/test/java/com/dylibso/chicory/runtime/InstructionOperandsTest.java @@ -0,0 +1,338 @@ +package com.dylibso.chicory.runtime; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.dylibso.chicory.wasm.Parser; +import com.dylibso.chicory.wasm.types.Instruction; +import com.dylibso.chicory.wasm.types.OpCode; +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; + +/** + * Real binary programs cover built-in direct operands and unchanged extension boundaries. + */ +@Timeout(10) +public class InstructionOperandsTest { + @Test + public void builtInOperandsAndListenerBeforeMemoryStoreRemainExact() { + AtomicReference owner = new AtomicReference<>(); + List observed = new ArrayList<>(); + Instance instance = + Instance.builder( + Parser.parse( + module( + 0x7f, + true, + new int[] { + 0, 0x41, 8, 0x41, 37, 0x36, 2, 4, 0x41, 8, 0x28, + 2, 4, 0x23, 0, 0x6a, 0x0b + }))) + .withUnsafeExecutionListener( + (instruction, stack) -> { + observed.add(instruction.opcode()); + if (instruction.opcode() == OpCode.I32_STORE) { + assertEquals(0, owner.get().memory().readInt(12)); + } + }) + .build(); + owner.set(instance); + assertArrayEquals(new long[] {46}, instance.exports().function("run").apply()); + assertEquals(37, instance.memory().readInt(12)); + assertEquals( + Arrays.asList( + OpCode.I32_CONST, + OpCode.I32_CONST, + OpCode.I32_STORE, + OpCode.I32_CONST, + OpCode.I32_LOAD, + OpCode.GLOBAL_GET, + OpCode.I32_ADD, + OpCode.END), + observed); + } + + @Test + public void customDefaultAdaptersRetainOriginalInstructionValuesAcrossLaterCalls() { + List observed = new ArrayList<>(); + AtomicReference machine = new AtomicReference<>(); + int[] first = vectorConstant(17, 29); + int[] second = vectorConstant(43, 61); + Instance instance = + Instance.builder( + Parser.parse( + module( + 0x7b, + false, + join( + new int[] {0}, + first, + new int[] {0x1a}, + second, + new int[] {0x0b})))) + .withUnsafeExecutionListener( + (instruction, stack) -> observed.add(instruction)) + .withMachineFactory( + context -> { + ExtensionMachine extension = + new ExtensionMachine(context, observed); + machine.set(extension); + return extension; + }) + .build(); + assertArrayEquals(new long[] {43, 61}, instance.exports().function("run").apply()); + assertArrayEquals(new long[] {43, 61}, instance.exports().function("run").apply()); + assertEquals(4, machine.get().defaults.size()); + for (int index = 0; index < 4; index++) { + assertEquals(index % 2 == 0 ? 17 : 43, machine.get().defaults.get(index).get(0)); + assertEquals(index % 2 == 0 ? 29 : 61, machine.get().defaults.get(index).get(1)); + } + } + + @Test + public void directCallStillInvokesProtectedOverrideWithStableAdapters() { + AtomicReference machine = new AtomicReference<>(); + List observed = new ArrayList<>(); + Instance instance = + Instance.builder( + Parser.parse( + module( + 0x7f, + false, + new int[] {0, 0x10, 1, 0x10, 2, 0x6a, 0x0b}, + new int[] {0, 0x41, 17, 0x0b}, + new int[] {0, 0x41, 23, 0x0b}))) + .withUnsafeExecutionListener( + (instruction, stack) -> observed.add(instruction)) + .withMachineFactory( + context -> { + ExtensionMachine extension = + new ExtensionMachine(context, observed); + machine.set(extension); + return extension; + }) + .build(); + assertArrayEquals(new long[] {40}, instance.exports().function("run").apply()); + assertArrayEquals(new long[] {40}, instance.exports().function("run").apply()); + assertEquals(4, machine.get().calls.size()); + for (int index = 0; index < 4; index++) { + assertEquals(index % 2 + 1, machine.get().calls.get(index).get(0)); + } + } + + @Test + public void listenerAbortEscapesByIdentityBeforeAnyStoreSideEffect() { + RuntimeException fatal = new CancellationException("Owned fixture cancellation"); + List observed = new ArrayList<>(); + Instance instance = + Instance.builder( + Parser.parse( + module( + 0x7f, + true, + new int[] { + 0, 0x41, 8, 0x41, 37, 0x36, 2, 4, 0x41, 0, 0x0b + }))) + .withUnsafeExecutionListener( + (instruction, stack) -> { + observed.add(instruction.opcode()); + if (instruction.opcode() == OpCode.I32_STORE) { + throw fatal; + } + }) + .build(); + assertSame( + fatal, + assertThrows( + CancellationException.class, + () -> instance.exports().function("run").apply())); + assertEquals(0, instance.memory().readInt(12)); + assertEquals(Arrays.asList(OpCode.I32_CONST, OpCode.I32_CONST, OpCode.I32_STORE), observed); + } + + @Test + public void everyLoopInstructionStillReachesFiniteFuelAndCurrentnessListeners() { + for (boolean cancellation : Arrays.asList(false, true)) { + AtomicInteger executed = new AtomicInteger(); + RuntimeException fatal = + cancellation + ? new CancellationException("Owned fixture currentness exhausted") + : new IllegalStateException("Owned fixture instruction fuel exhausted"); + Instance instance = + Instance.builder( + Parser.parse( + module( + -1, + false, + new int[] { + 0, 0x03, 0x40, 0x41, 1, 0x1a, 0x0c, 0, 0x0b, + 0x0b + }))) + .withUnsafeExecutionListener( + (instruction, stack) -> { + int count = executed.incrementAndGet(); + OpCode expected = + count == 1 + ? OpCode.LOOP + : (count - 2) % 3 == 0 + ? OpCode.I32_CONST + : (count - 2) % 3 == 1 + ? OpCode.DROP + : OpCode.BR; + assertEquals(expected, instruction.opcode()); + if (count == 130) { + throw fatal; + } + }) + .build(); + assertSame( + fatal, + assertThrows( + fatal.getClass(), () -> instance.exports().function("run").apply())); + assertEquals(130, executed.get()); + } + } + + @Test + public void memoryTrapAndProtectedStaticPointerAdapterRemainUnchanged() { + List observed = new ArrayList<>(); + Instance instance = + Instance.builder( + Parser.parse( + module( + 0x7f, + true, + new int[] { + 0, 0x41, 0x7f, 0x41, 37, 0x36, 2, 4, 0x41, 0, + 0x0b + }))) + .withUnsafeExecutionListener( + (instruction, stack) -> observed.add(instruction.opcode())) + .build(); + assertThrows(WasmRuntimeException.class, () -> instance.exports().function("run").apply()); + assertEquals(0, instance.memory().readInt(0)); + assertEquals(Arrays.asList(OpCode.I32_CONST, OpCode.I32_CONST, OpCode.I32_STORE), observed); + MStack stack = new MStack(); + stack.push(8); + assertEquals(12, ExtensionMachine.pointer(stack, 4)); + stack.push(-1); + assertThrows(WasmRuntimeException.class, () -> ExtensionMachine.pointer(stack, 4)); + } + + private static final class ExtensionMachine extends InterpreterMachine { + final List defaults = new ArrayList<>(); + final List calls = new ArrayList<>(); + private final List observed; + + ExtensionMachine(Instance instance, List observed) { + super(instance); + this.observed = observed; + } + + @Override + protected void evalDefault( + MStack stack, + Instance instance, + Deque frames, + Instruction instruction, + Operands operands) { + assertSame(instruction, observed.get(observed.size() - 1)); + assertEquals(OpCode.V128_CONST, instruction.opcode()); + defaults.add(operands); + stack.push(operands.get(0)); + stack.push(operands.get(1)); + } + + @Override + protected void CALL(Operands operands) { + assertEquals(OpCode.CALL, observed.get(observed.size() - 1).opcode()); + calls.add(operands); + super.CALL(operands); + } + + static int pointer(MStack stack, long offset) { + return readMemPtr(stack, index -> index == 1 ? offset : 0); + } + } + + private static int[] vectorConstant(long low, long high) { + int[] bytes = new int[18]; + bytes[0] = 0xfd; + bytes[1] = 12; + for (int index = 0; index < 8; index++) { + bytes[index + 2] = (int) (low >>> (index * 8)) & 255; + bytes[index + 10] = (int) (high >>> (index * 8)) & 255; + } + return bytes; + } + + private static int[] join(int[]... parts) { + int size = 0; + for (int[] part : parts) { + size += part.length; + } + int[] result = new int[size]; + int at = 0; + for (int[] part : parts) { + System.arraycopy(part, 0, result, at, part.length); + at += part.length; + } + return result; + } + + private static byte[] module(int result, boolean memory, int[]... bodies) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + out.write(new byte[] {0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}, 0, 8); + section(out, 1, result < 0 ? new int[] {1, 0x60, 0, 0} : new int[] {1, 0x60, 0, 1, result}); + int[] functions = new int[bodies.length + 1]; + functions[0] = bodies.length; + section(out, 3, functions); + if (memory) { + section(out, 5, new int[] {1, 0, 1}); + section(out, 6, new int[] {1, 0x7f, 1, 0x41, 9, 0x0b}); + } + section(out, 7, new int[] {1, 3, 'r', 'u', 'n', 0, 0}); + ByteArrayOutputStream code = new ByteArrayOutputStream(); + unsigned(code, bodies.length); + for (int[] body : bodies) { + unsigned(code, body.length); + for (int value : body) { + code.write(value); + } + } + section(out, 10, code.toByteArray()); + return out.toByteArray(); + } + + private static void section(ByteArrayOutputStream out, int id, int[] values) { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + for (int value : values) { + bytes.write(value); + } + section(out, id, bytes.toByteArray()); + } + + private static void section(ByteArrayOutputStream out, int id, byte[] values) { + out.write(id); + unsigned(out, values.length); + out.write(values, 0, values.length); + } + + private static void unsigned(ByteArrayOutputStream out, int value) { + do { + int next = value & 127; + value >>>= 7; + out.write(value == 0 ? next : next | 128); + } while (value != 0); + } +} diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/MemoryTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/MemoryTest.java index a82d9eda1..d89d0c499 100644 --- a/runtime/src/test/java/com/dylibso/chicory/runtime/MemoryTest.java +++ b/runtime/src/test/java/com/dylibso/chicory/runtime/MemoryTest.java @@ -1,6 +1,8 @@ package com.dylibso.chicory.runtime; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import com.dylibso.chicory.wasm.types.MemoryLimits; import java.util.concurrent.CompletableFuture; @@ -13,6 +15,34 @@ public class MemoryTest { + @ParameterizedTest(name = "{0}") + @MethodSource("memoryImplementations") + public void scalarBoundsTrapAndCrossPageAccessRemainExact( + String name, Supplier memorySupplier) { + var memory = memorySupplier.get(); + int limit = memory.pages() * Memory.PAGE_SIZE; + for (int address : new int[] {-1, Integer.MIN_VALUE, Integer.MAX_VALUE, limit}) { + assertThrows(WasmRuntimeException.class, () -> memory.read(address)); + assertThrows(WasmRuntimeException.class, () -> memory.readShort(address)); + assertThrows(WasmRuntimeException.class, () -> memory.readInt(address)); + assertThrows(WasmRuntimeException.class, () -> memory.readLong(address)); + assertThrows(WasmRuntimeException.class, () -> memory.readFloat(address)); + assertThrows(WasmRuntimeException.class, () -> memory.readDouble(address)); + assertThrows(WasmRuntimeException.class, () -> memory.writeByte(address, (byte) 1)); + assertThrows(WasmRuntimeException.class, () -> memory.writeShort(address, (short) 1)); + assertThrows(WasmRuntimeException.class, () -> memory.writeI32(address, 1)); + assertThrows(WasmRuntimeException.class, () -> memory.writeLong(address, 1)); + assertThrows(WasmRuntimeException.class, () -> memory.writeF32(address, 1)); + assertThrows(WasmRuntimeException.class, () -> memory.writeF64(address, 1)); + } + memory.writeLong(Memory.PAGE_SIZE - 3, 0x0123456789ABCDEFL); + assertEquals(0x0123456789ABCDEFL, memory.readLong(Memory.PAGE_SIZE - 3)); + memory.writeByte(limit - 1, (byte) 42); + assertThrows(WasmRuntimeException.class, () -> memory.writeI32(limit - 1, 0)); + assertEquals(42, memory.read(limit - 1)); + assertThrows(WasmRuntimeException.class, () -> memory.readLong(limit - 1)); + } + private static Stream memoryImplementations() { return Stream.of( Arguments.of( diff --git a/teavm-tests/README.md b/teavm-tests/README.md new file mode 100644 index 000000000..36fec98df --- /dev/null +++ b/teavm-tests/README.md @@ -0,0 +1,65 @@ +# NGE TeaVM runtime tests + +With JDK 25 and Node.js 22 installed, run from the repository root: + +```sh +# TeaVM JavaScript +mvn -B -ntp -U -Dteavm-tests -pl teavm-tests -am verify +# TeaVM WebAssembly GC +mvn -B -ntp -U -Dteavm-tests -Dteavm.backend=wasm-gc -pl teavm-tests -am verify +``` + +Both builds use [`NostrGameEngine/teavm`](https://github.com/NostrGameEngine/teavm): +`org.ngengine:teavm-maven-plugin`, `org.ngengine:teavm-classlib` and +`org.ngengine:teavm-extension-spi` version `0.16.0-SNAPSHOT` from Maven Central +snapshots. Override the version with `-Dteavm.version=...` to test another +published fork version. The Wasm GC test runner enables Node's +`--experimental-wasm-exnref` option for the exception handling instructions +emitted by this TeaVM version. + +Each backend runs two execution modes: + +| TeaVM backend | Runtime-loaded Wasm | Build-time compiled Wasm | +| --- | --- | --- | +| JavaScript | `Parser.parse(byte[])` and interpreter | Generated `PrecompiledSmoke` machine | +| WebAssembly GC | `Parser.parse(byte[])` and interpreter | Generated `PrecompiledSmoke` machine | + +For runtime loading, the Node.js runner reads `runtime-smoke.wasm` after the +TeaVM build and passes its bytes to the application. For precompiled loading, +the Chicory Maven plugin generates JVM classes during `generate-sources`, and +TeaVM compiles those classes into the target application. `SmokeResources` +uses the NGE TeaVM resource policy SPI to bundle the generated `.meta` resource, +which `PrecompiledSmoke.load()` reads through the normal classpath API. + +Both modes check conditional/table loops, tail calls, active element function +references, 32/64-bit memory access, growth and bounds traps with the default +memory and `ByteArrayMemory`. Interpreter tail-call depth is 10,000; precompiled +tail-call depth is 128 because the current compiler uses the host call stack. +This is a compatibility smoke test, not the complete WebAssembly specification +suite. Runtime JVM bytecode generation is not used in the TeaVM application. + +`verify` fails on compilation errors, failed assertions or missing completion. +Generated applications are in `target/javascript/` and `target/wasm-gc/`. +Regenerate the binary fixture from `src/main/resources/runtime-smoke.wat` with +`wat2wasm --enable-tail-call` when changing it. + +In an application, runtime-loaded modules use the ordinary API: + +```java +var instance = Instance.builder(Parser.parse(downloadedWasmBytes)).build(); +``` + +For build-time modules, run `org.ngengine.chicory:chicory-compiler-maven-plugin` +before TeaVM, bundle the generated classes and `.meta` file, and instantiate +with the generated factory: + +```java +var instance = Instance.builder(MyCompiledModule.load()) + .withMachineFactory(MyCompiledModule::create) + .build(); +``` + +Keep the JVM compiler/ASM dependencies in the build plugin. The TeaVM application +only needs Chicory's runtime/parser and the generated classes. For resource +bundling, adapt `SmokeResources` and its `META-INF/services` registration to the +generated module name; both TeaVM targets use the same resource policy. diff --git a/teavm-tests/pom.xml b/teavm-tests/pom.xml new file mode 100644 index 000000000..956386e27 --- /dev/null +++ b/teavm-tests/pom.xml @@ -0,0 +1,197 @@ + + + 4.0.0 + + + org.ngengine.chicory + chicory + 999-SNAPSHOT + ../pom.xml + + teavm-tests + Chicory - NGE TeaVM Runtime Tests + Run interpreted and precompiled modules in NGE TeaVM JavaScript and Wasm GC + + + + 9.10.1 + 0.16.0-SNAPSHOT + javascript + JAVASCRIPT + runtime-smoke.mjs + true + true + + + + + org.ngengine.chicory + runtime + + + org.ngengine.chicory + wasm + + + org.ngengine + teavm-classlib + ${teavm.version} + provided + + + org.ngengine + teavm-extension-spi + ${teavm.version} + provided + + + + + + + false + + + true + + nge-teavm-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + + + + + false + + + true + + nge-teavm-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.ngengine:teavm-classlib + + + + + org.codehaus.mojo + exec-maven-plugin + + + execute-javascript + + exec + + integration-test + + node + ${project.basedir} + + run-smoke.mjs + ${teavm.backend} + + 90000 + + + + + + org.ngengine + teavm-maven-plugin + ${teavm.version} + + com.dylibso.chicory.teavm.RuntimeSmoke + ${teavm.targetType} + ES2015 + ${project.build.directory}/${teavm.backend} + ${teavm.targetFileName} + false + true + + + + + compile + + + + + + org.ngengine.chicory + chicory-compiler-maven-plugin + + + precompile-smoke-module + + compile + + + ${project.basedir}/src/main/resources/runtime-smoke.wasm + com.dylibso.chicory.teavm.PrecompiledSmoke + + + + + + + + + wasm-gc + + + teavm.backend + wasm-gc + + + + WEBASSEMBLY_GC + runtime-smoke.wasm + + + + + org.codehaus.mojo + exec-maven-plugin + + + execute-javascript + + + --experimental-wasm-exnref + run-smoke.mjs + wasm-gc + + + + + + + org.ngengine + teavm-maven-plugin + + + copy-wasm-runtime + + copy-webassembly-gc-runtime + + + wasm-runtime.mjs + true + + + + + + + + + diff --git a/teavm-tests/run-smoke.mjs b/teavm-tests/run-smoke.mjs new file mode 100644 index 000000000..a6f158cb5 --- /dev/null +++ b/teavm-tests/run-smoke.mjs @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; + +const backend = process.argv[2] || 'javascript'; +assert(['javascript', 'wasm-gc'].includes(backend), `Unknown TeaVM backend: ${backend}`); +// Read after compilation: these bytes are passed to Chicory's runtime parser. +const binary = await readFile(new URL('./src/main/resources/runtime-smoke.wasm', import.meta.url)); +const hex = binary.toString('hex'); +const output = []; +const log = console.log; +const info = console.info; +console.log = (...args) => { + output.push(args.join(' ')); + log(...args); +}; +console.info = console.log; + +try { + let run; + if (backend === 'javascript') { + const { main } = await import('./target/javascript/runtime-smoke.mjs'); + run = args => new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('TeaVM main timed out')), 60000); + main(args, result => { + clearTimeout(timeout); + result instanceof Error ? reject(result) : resolve(); + }); + }); + } else { + const { load } = await import('./target/wasm-gc/wasm-runtime.mjs'); + const wasm = await readFile(new URL('./target/wasm-gc/runtime-smoke.wasm', import.meta.url)); + const application = await load(wasm); + run = args => application.exports.main(args); + } + for (const mode of ['interpreter', 'precompiled']) { + await run([mode, hex]); + assert(output.includes(`CHICORY_TEAVM_OK ${mode}`), `Missing completion: ${backend}/${mode}`); + log(`CHICORY_TEAVM_PASSED ${backend}/${mode}`); + } +} finally { + console.log = log; + console.info = info; +} diff --git a/teavm-tests/src/main/java/com/dylibso/chicory/teavm/RuntimeSmoke.java b/teavm-tests/src/main/java/com/dylibso/chicory/teavm/RuntimeSmoke.java new file mode 100644 index 000000000..8d3b893ea --- /dev/null +++ b/teavm-tests/src/main/java/com/dylibso/chicory/teavm/RuntimeSmoke.java @@ -0,0 +1,94 @@ +package com.dylibso.chicory.teavm; + +import com.dylibso.chicory.runtime.ByteArrayMemory; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.runtime.WasmRuntimeException; +import com.dylibso.chicory.wasm.Parser; + +/** + * Exercises runtime-loaded and precompiled modules in both NGE TeaVM backends. + */ +public final class RuntimeSmoke { + private RuntimeSmoke() {} + + public static void main(String[] args) { + require(args.length == 2, "mode and runtime Wasm bytes are required"); + boolean precompiled = args[0].equals("precompiled"); + require(precompiled || args[0].equals("interpreter"), "unknown execution mode"); + var binary = decodeHex(args[1]); + run(binary, precompiled, false); + run(binary, precompiled, true); + System.out.println("CHICORY_TEAVM_OK " + args[0]); + } + + private static void run(byte[] binary, boolean precompiled, boolean useByteArrayMemory) { + var builder = + Instance.builder(precompiled ? PrecompiledSmoke.load() : Parser.parse(binary)); + if (precompiled) { + builder.withMachineFactory(PrecompiledSmoke::create); + } + if (useByteArrayMemory) { + builder.withMemoryFactory(ByteArrayMemory::new); + } + var instance = builder.build(); + require(instance.export("run").apply(10000)[0] == 10000, "conditional loop"); + require(instance.export("table").apply(10000)[0] == 10000, "table loop"); + // Compiled RETURN_CALL currently uses the host call stack. + require(instance.export("tail").apply(precompiled ? 128 : 10000)[0] == 37, "tail call"); + require(instance.export("self").apply()[0] == 0, "active element ref.func"); + var memory = instance.memory(); + require(memory.readInt(0) == 10000, "memory round trip"); + memory.writeLong(8, 0x0123456789ABCDEFL); + require(memory.readLong(8) == 0x0123456789ABCDEFL, "64-bit memory round trip"); + require(memory.grow(1) == 1 && memory.pages() == 2, "memory growth"); + require(memory.readInt(0) == 10000, "memory preserved after growth"); + memory.writeLong(65533, 0x0123456789ABCDEFL); + require(memory.readLong(65533) == 0x0123456789ABCDEFL, "cross-page memory"); + requireTrap(() -> memory.read(-1)); + requireTrap(() -> memory.readShort(-1)); + requireTrap(() -> memory.readInt(-1)); + requireTrap(() -> memory.readLong(Integer.MIN_VALUE)); + requireTrap(() -> memory.readFloat(memory.pages() * 65536)); + requireTrap(() -> memory.readDouble(Integer.MAX_VALUE)); + requireTrap(() -> memory.writeByte(-1, (byte) 0)); + requireTrap(() -> memory.writeShort(-1, (short) 0)); + requireTrap(() -> memory.writeI32(-1, 0)); + requireTrap(() -> memory.writeLong(Integer.MIN_VALUE, 0)); + requireTrap(() -> memory.writeF32(memory.pages() * 65536, 0)); + requireTrap(() -> memory.writeF64(Integer.MAX_VALUE, 0)); + boolean trapped = false; + try { + instance.export("trap").apply(); + } catch (WasmRuntimeException expected) { + trapped = true; + } + require(trapped, "negative-address bounds trap"); + } + + private static void requireTrap(Runnable operation) { + try { + operation.run(); + } catch (WasmRuntimeException expected) { + return; + } + throw new AssertionError("missing memory bounds trap"); + } + + private static byte[] decodeHex(String hex) { + require((hex.length() & 1) == 0, "invalid Wasm hex length"); + var binary = new byte[hex.length() / 2]; + for (int i = 0; i < binary.length; i++) { + int high = Character.digit(hex.charAt(i * 2), 16); + int low = Character.digit(hex.charAt(i * 2 + 1), 16); + require(high >= 0 && low >= 0, "invalid Wasm hex digit"); + binary[i] = (byte) ((high << 4) | low); + } + return binary; + } + + private static void require(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } +} diff --git a/teavm-tests/src/main/java/com/dylibso/chicory/teavm/SmokeResources.java b/teavm-tests/src/main/java/com/dylibso/chicory/teavm/SmokeResources.java new file mode 100644 index 000000000..360e4b370 --- /dev/null +++ b/teavm-tests/src/main/java/com/dylibso/chicory/teavm/SmokeResources.java @@ -0,0 +1,14 @@ +package com.dylibso.chicory.teavm; + +import java.util.Collection; +import org.teavm.extension.spi.resources.ResourcesPolicy; + +/** + * Bundles the precompiled module metadata for both TeaVM backends. + */ +public final class SmokeResources implements ResourcesPolicy { + @Override + public String[] supplyResources(Collection availableClassNames) { + return new String[] {"com/dylibso/chicory/teavm/PrecompiledSmoke.meta"}; + } +} diff --git a/teavm-tests/src/main/resources/META-INF/services/org.teavm.extension.spi.resources.ResourcesPolicy b/teavm-tests/src/main/resources/META-INF/services/org.teavm.extension.spi.resources.ResourcesPolicy new file mode 100644 index 000000000..9399ca36e --- /dev/null +++ b/teavm-tests/src/main/resources/META-INF/services/org.teavm.extension.spi.resources.ResourcesPolicy @@ -0,0 +1 @@ +com.dylibso.chicory.teavm.SmokeResources diff --git a/teavm-tests/src/main/resources/runtime-smoke.wasm b/teavm-tests/src/main/resources/runtime-smoke.wasm new file mode 100644 index 000000000..a12ad24ef Binary files /dev/null and b/teavm-tests/src/main/resources/runtime-smoke.wasm differ diff --git a/teavm-tests/src/main/resources/runtime-smoke.wat b/teavm-tests/src/main/resources/runtime-smoke.wat new file mode 100644 index 000000000..d0706ce61 --- /dev/null +++ b/teavm-tests/src/main/resources/runtime-smoke.wat @@ -0,0 +1,25 @@ +(module + (memory (export "memory") 1) + (table 1 funcref) + (elem (i32.const 0) $self) + (func $self (export "self") (result funcref) ref.func $self) + (func (export "run") (param i32) (result i32) + (local i32) + (loop $loop + (block local.get 1 i32.const 1 i32.add local.set 1) + local.get 1 local.get 0 i32.lt_u br_if $loop) + i32.const 0 local.get 1 i32.store + i32.const 0 i32.load) + (func $tail (export "tail") (param i32) (result i32) + local.get 0 i32.eqz + if (result i32) i32.const 37 + else local.get 0 i32.const 1 i32.sub return_call $tail end) + (func (export "trap") (result i32) i32.const -1 i32.load) + (func (export "table") (param i32) (result i32) + (local i32) + (block $done + (loop $again + local.get 1 i32.const 1 i32.add local.set 1 + local.get 1 local.get 0 i32.lt_u i32.eqz + br_table $again $done)) + local.get 1)) diff --git a/wasi-test-gen-plugin/src/main/java/com/dylibso/chicory/maven/WasiTestGenMojo.java b/wasi-test-gen-plugin/src/main/java/com/dylibso/chicory/maven/WasiTestGenMojo.java index 37b1aadfd..ae0a9cbd8 100644 --- a/wasi-test-gen-plugin/src/main/java/com/dylibso/chicory/maven/WasiTestGenMojo.java +++ b/wasi-test-gen-plugin/src/main/java/com/dylibso/chicory/maven/WasiTestGenMojo.java @@ -54,9 +54,9 @@ public class WasiTestGenMojo extends AbstractMojo { private String testSuiteRepo; /** - * Repository of the test suite. + * Revision of the test suite, pinned to the supported specification format. */ - @Parameter(required = true, defaultValue = "prod/testsuite-base") + @Parameter(required = true, defaultValue = "6345da2237c562d9a94e281332c653b5528fdd52") private String testSuiteRepoRef; /** diff --git a/wasi-test-gen-plugin/src/main/java/com/dylibso/chicory/maven/WasiTestSuiteDownloader.java b/wasi-test-gen-plugin/src/main/java/com/dylibso/chicory/maven/WasiTestSuiteDownloader.java index 78dddb27f..4a786c4b5 100644 --- a/wasi-test-gen-plugin/src/main/java/com/dylibso/chicory/maven/WasiTestSuiteDownloader.java +++ b/wasi-test-gen-plugin/src/main/java/com/dylibso/chicory/maven/WasiTestSuiteDownloader.java @@ -27,7 +27,7 @@ public void downloadTestsuite( log.warn("Cloning the testsuite at ref: " + testSuiteRepoRef); String zipName = testSuiteRepoRef + ".zip"; - URL url = new URL(testSuiteRepo + "/archive/refs/heads/" + zipName); + URL url = new URL(testSuiteRepo + "/archive/" + zipName); HttpURLConnection con = (HttpURLConnection) url.openConnection(); con.setRequestMethod("GET"); diff --git a/wasm-corpus/src/main/resources/compiled/compiler-exception-regressions.wat.wasm b/wasm-corpus/src/main/resources/compiled/compiler-exception-regressions.wat.wasm new file mode 100644 index 000000000..fc8f11ad8 Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/compiler-exception-regressions.wat.wasm differ diff --git a/wasm-corpus/src/main/resources/compiled/compiler-fuzz-regressions.wat.wasm b/wasm-corpus/src/main/resources/compiled/compiler-fuzz-regressions.wat.wasm new file mode 100644 index 000000000..fb3c3deda Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/compiler-fuzz-regressions.wat.wasm differ diff --git a/wasm-corpus/src/main/resources/compiled/cross-instance-tail-call.wat.wasm b/wasm-corpus/src/main/resources/compiled/cross-instance-tail-call.wat.wasm new file mode 100644 index 000000000..8783a90e1 Binary files /dev/null and b/wasm-corpus/src/main/resources/compiled/cross-instance-tail-call.wat.wasm differ diff --git a/wasm-corpus/src/main/resources/wat/compiler-exception-regressions.wat b/wasm-corpus/src/main/resources/wat/compiler-exception-regressions.wat new file mode 100644 index 000000000..0c1fdf8e7 --- /dev/null +++ b/wasm-corpus/src/main/resources/wat/compiler-exception-regressions.wat @@ -0,0 +1,20 @@ +(module + (tag $e) + + (func (export "nested-handlers") (result i32) + (block $outer + (try_table (catch $e $outer) + (block $inner + (try_table (catch $e $inner) + (throw $e)) + (return (i32.const 1))) + (return (i32.const 42)))) + i32.const 99) + + (func (export "unreachable-handler") (result i32) + (return (i32.const 7)) + (block $unused + (try_table (catch_all $unused) + unreachable)) + i32.const 9) +) diff --git a/wasm-corpus/src/main/resources/wat/compiler-fuzz-regressions.wat b/wasm-corpus/src/main/resources/wat/compiler-fuzz-regressions.wat new file mode 100644 index 000000000..fcef9a19d --- /dev/null +++ b/wasm-corpus/src/main/resources/wat/compiler-fuzz-regressions.wat @@ -0,0 +1,9 @@ +(module + (func (export "function-reference") (result funcref i32) + ref.null func + i32.const 17) + (func (export "external-reference") (result externref i32) + ref.null extern + i32.const 23) + +) diff --git a/wasm-corpus/src/main/resources/wat/cross-instance-tail-call.wat b/wasm-corpus/src/main/resources/wat/cross-instance-tail-call.wat new file mode 100644 index 000000000..ac896a239 --- /dev/null +++ b/wasm-corpus/src/main/resources/wat/cross-instance-tail-call.wat @@ -0,0 +1,11 @@ +(module + (type $t (func (param i32) (result i32))) + (table 1 funcref) + (func (type $t) (param i32) (result i32) + local.get 0 + i32.const 1 + i32.add) + (func (export "run") (param i32) (result i32) + local.get 0 + i32.const 0 + return_call_indirect (type $t))) diff --git a/wasm-tools/src/test/java/com/dylibso/chicory/tools/wasm/WasmToolsTest.java b/wasm-tools/src/test/java/com/dylibso/chicory/tools/wasm/WasmToolsTest.java index 1b28c12ee..4ef65fc97 100644 --- a/wasm-tools/src/test/java/com/dylibso/chicory/tools/wasm/WasmToolsTest.java +++ b/wasm-tools/src/test/java/com/dylibso/chicory/tools/wasm/WasmToolsTest.java @@ -59,6 +59,25 @@ public void shouldRunWat2WasmOnString() { assertEquals(42L, results[0]); } + @Test + public void shouldParseLineCommentsWithEveryNewline() { + StringBuilder combined = new StringBuilder(); + int index = 0; + for (String newline : new String[] {"\n", "\r", "\r\n"}) { + String wat = + "(func (export \"run" + + index++ + + "\") (result i32) (i32.const 1) ;; comment" + + newline + + " (return (i32.const 2))\n)"; + combined.append(wat).append(' '); + } + var instance = Instance.builder(Parser.parse(Wat2Wasm.parse(combined.toString()))).build(); + for (int i = 0; i < index; i++) { + assertEquals(2L, instance.export("run" + i).apply()[0]); + } + } + @Test public void shouldThrowMalformedException() throws Exception { var exitException = diff --git a/wasm/src/main/java/com/dylibso/chicory/wasm/Validator.java b/wasm/src/main/java/com/dylibso/chicory/wasm/Validator.java index 2589aecbe..bd668641e 100644 --- a/wasm/src/main/java/com/dylibso/chicory/wasm/Validator.java +++ b/wasm/src/main/java/com/dylibso/chicory/wasm/Validator.java @@ -11,7 +11,6 @@ import com.dylibso.chicory.wasm.types.ArrayType; import com.dylibso.chicory.wasm.types.CatchOpCode; import com.dylibso.chicory.wasm.types.CompType; -import com.dylibso.chicory.wasm.types.DeclarativeElement; import com.dylibso.chicory.wasm.types.Element; import com.dylibso.chicory.wasm.types.ExternalType; import com.dylibso.chicory.wasm.types.FieldType; @@ -138,7 +137,6 @@ private static class CtrlFrame { this.declaredFunctions = module.elementSection().stream() - .filter(DeclarativeElement.class::isInstance) .flatMap(element -> element.initializers().stream()) .flatMap(this::declaredFunctions) .collect(toSet()); diff --git a/wasm/src/test/java/com/dylibso/chicory/wasm/ElementFunctionDeclarationTest.java b/wasm/src/test/java/com/dylibso/chicory/wasm/ElementFunctionDeclarationTest.java new file mode 100644 index 000000000..7df80b2a9 --- /dev/null +++ b/wasm/src/test/java/com/dylibso/chicory/wasm/ElementFunctionDeclarationTest.java @@ -0,0 +1,62 @@ +package com.dylibso.chicory.wasm; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayOutputStream; +import org.junit.jupiter.api.Test; + +/** + * All element modes declare their function references; absent/invalid ones remain invalid. + */ +public class ElementFunctionDeclarationTest { + @Test + public void activeElementDeclaresSelfReference() { + assertDoesNotThrow(() -> Parser.parse(module(0, 0))); + } + + @Test + public void passiveElementDeclaresSelfReference() { + assertDoesNotThrow(() -> Parser.parse(module(1, 0))); + } + + @Test + public void declarativeElementStillDeclaresSelfReference() { + assertDoesNotThrow(() -> Parser.parse(module(3, 0))); + } + + @Test + public void undeclaredSelfReferenceRemainsInvalid() { + assertThrows(InvalidException.class, () -> Parser.parse(module(-1, 0))); + } + + @Test + public void outOfRangeReferenceRemainsInvalid() { + assertThrows(InvalidException.class, () -> Parser.parse(module(0, 1))); + } + + private static byte[] module(int elementMode, int reference) { + // One function: (func (result funcref) (ref.func )). Its own index + // is 0, so the pre-fix self-reference validation cannot accidentally be bypassed. + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.writeBytes(new byte[] {0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}); + section(bytes, 1, 1, 0x60, 0, 1, 0x70); + section(bytes, 3, 1, 0); + if (elementMode == 0) { + section(bytes, 4, 1, 0x70, 0, 1); + section(bytes, 9, 1, 0, 0x41, 0, 0x0b, 1, 0); + } else if (elementMode >= 0) { + section(bytes, 9, 1, elementMode, 0, 1, 0); + } + section(bytes, 10, 1, 4, 0, 0xd2, reference, 0x0b); + return bytes.toByteArray(); + } + + private static void section(ByteArrayOutputStream bytes, int id, int... body) { + bytes.write(id); + bytes.write(body.length); // These fixed test sections fit one canonical ULEB byte. + for (int value : body) { + bytes.write(value); + } + } +}