From 96a30383caa953268ac44144b02c0ab7a5de1cd4 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 12:07:50 +0100 Subject: [PATCH 01/14] fix function reference declarations in element segments Port the Validator fix from upstream 43d9100b and soxdev 7679b1d1. Active and passive elements declare function references just as declarative elements do. Regression tests reproduced both rejected valid modules before the fix. --- .../com/dylibso/chicory/wasm/Validator.java | 2 - .../wasm/ElementFunctionDeclarationTest.java | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 wasm/src/test/java/com/dylibso/chicory/wasm/ElementFunctionDeclarationTest.java 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); + } + } +} From cf74715bdde37df5fb69bf6c990faabab9f441fb Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 12:07:53 +0100 Subject: [PATCH 02/14] fix interruption checks on conditional branch backedges Port the interpreter fix from upstream 43d9100b and soxdev 7679b1d1. Real BR_IF-only and BR_TABLE-only loops now stop on thread interruption. Both regressions failed on the original implementation with bounded worker cleanup. --- .../chicory/runtime/InterpreterMachine.java | 2 + .../ConditionalBranchInterruptionTest.java | 104 ++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 runtime/src/test/java/com/dylibso/chicory/runtime/ConditionalBranchInterruptionTest.java 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..e1809348c 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/InterpreterMachine.java @@ -3030,6 +3030,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 +3045,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) { 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..f83e7d9a7 --- /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.writeBytes(new byte[] {0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}); + 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); + } + } +} From 1ded9f70f2aa23f42db37ca7471a1c1e046aa6f5 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 12:07:54 +0100 Subject: [PATCH 03/14] avoid operand adapters in built-in interpreter handlers Port soxdev f1154c78. Read operands directly from the existing instruction while retaining the protected CALL, evalDefault and readMemPtr Operands hooks. Cover listener order, retained extension operands, memory failures and numeric/GC handlers. --- .../chicory/runtime/InterpreterMachine.java | 488 +++++++++--------- .../runtime/InstructionOperandsTest.java | 338 ++++++++++++ 2 files changed, 592 insertions(+), 234 deletions(-) create mode 100644 runtime/src/test/java/com/dylibso/chicory/runtime/InstructionOperandsTest.java 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 e1809348c..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); @@ -3174,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) @@ -3186,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 @@ -3203,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"); @@ -3224,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) { @@ -3241,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]; @@ -3251,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]; @@ -3264,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(); @@ -3275,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(); @@ -3295,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); @@ -3314,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) { @@ -3337,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(); @@ -3365,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(); @@ -3414,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(); @@ -3440,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(); @@ -3470,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( @@ -3482,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)) { @@ -3498,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); @@ -3518,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/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..81ad8e8aa --- /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.writeBytes(new byte[] {0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}); + 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.writeBytes(values); + } + + 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); + } +} From 28a5153ce7ca08efc6d1944a8c3de5da76aacdf4 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 12:07:55 +0100 Subject: [PATCH 04/14] reuse bounded immutable control frames within each activation Port and qualify soxdev a1f0240e. Reuse at most 64 inactive control frames only when every immutable field matches. Preserve detached frames, deep stack behavior, tail calls and exception unwinding. The 10000-iteration interpreter workload reduces median allocated bytes per call from 640344 to 320384 on JDK 25 with default escape analysis. --- .../dylibso/chicory/runtime/StackFrame.java | 42 ++- .../runtime/ImmutableControlFramesTest.java | 312 ++++++++++++++++++ 2 files changed, 346 insertions(+), 8 deletions(-) create mode 100644 runtime/src/test/java/com/dylibso/chicory/runtime/ImmutableControlFramesTest.java 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/ImmutableControlFramesTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/ImmutableControlFramesTest.java new file mode 100644 index 000000000..9b85ff799 --- /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.writeBytes(new byte[] {0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}); + ByteArrayOutputStream data = new ByteArrayOutputStream(); + unsigned(data, types.length); + for (byte[] value : types) { + data.writeBytes(value); + } + 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.writeBytes(new byte[] {1, 0, 0}); + section(out, 13, data); + } + data = new ByteArrayOutputStream(); + data.writeBytes(new byte[] {1, 3, 'r', 'u', 'n', 0, 0}); + section(out, 7, data); + data = new ByteArrayOutputStream(); + unsigned(data, bodies.length); + for (byte[] value : bodies) { + unsigned(data, value.length); + data.writeBytes(value); + } + 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.writeBytes(data.toByteArray()); + } + + 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); + } +} From 3c6670b054af77105a674ceab6c7a6df7cf5495d Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 12:08:28 +0100 Subject: [PATCH 05/14] fix compiler bridges and exception handler analysis Select the production compiler fixes from upstream 43d9100b. Deduplicate equivalent JVM multi-value bridges, register only reachable exception ranges with inner handlers first, and handle already-exited branch scopes. The original compiler reproduced duplicate-method ClassFormatError and ASM frame-analysis NullPointerException on the added binary fixtures. Compare execution with the interpreter and preserve cross-instance indirect tail calls to both compiled and interpreted targets. --- .../chicory/compiler/internal/Compiler.java | 10 +++- .../chicory/compiler/internal/TypeStack.java | 2 +- .../compiler/internal/WasmAnalyzer.java | 51 +++++++++++++++--- .../chicory/compiler/internal/CallTest.java | 20 +++++++ .../compiler/internal/FuzzRegressionTest.java | 44 +++++++++++++++ .../compiler-exception-regressions.wat.wasm | Bin 0 -> 126 bytes .../compiler-fuzz-regressions.wat.wasm | Bin 0 -> 88 bytes .../cross-instance-tail-call.wat.wasm | Bin 0 -> 57 bytes .../wat/compiler-exception-regressions.wat | 20 +++++++ .../wat/compiler-fuzz-regressions.wat | 9 ++++ .../wat/cross-instance-tail-call.wat | 11 ++++ 11 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 compiler/src/test/java/com/dylibso/chicory/compiler/internal/FuzzRegressionTest.java create mode 100644 wasm-corpus/src/main/resources/compiled/compiler-exception-regressions.wat.wasm create mode 100644 wasm-corpus/src/main/resources/compiled/compiler-fuzz-regressions.wat.wasm create mode 100644 wasm-corpus/src/main/resources/compiled/cross-instance-tail-call.wat.wasm create mode 100644 wasm-corpus/src/main/resources/wat/compiler-exception-regressions.wat create mode 100644 wasm-corpus/src/main/resources/wat/compiler-fuzz-regressions.wat create mode 100644 wasm-corpus/src/main/resources/wat/cross-instance-tail-call.wat 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..b432af314 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 @@ -547,15 +547,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 -> { 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/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/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 0000000000000000000000000000000000000000..fc8f11ad852ad21a5e3b07528f138d297d453b17 GIT binary patch literal 126 zcmZQbEY4+QU|?Y6U`k+MNMK;BXJ%$%WaMQA3bJc5@#m!$m!ziXW+djN)9{7h^N7YZB&xq(!^qc}GJmHiew literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8783a90e1f81b2011c98acf24a6bfc3a4c42118c GIT binary patch literal 57 zcmZQbEY4+QU|?WmV@zPIXRK#tW@2DqVPPy_U}R@!WG*VrV_@VGW@2YhU~pv2;^qX= J48jZy+yKbW1{weW literal 0 HcmV?d00001 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))) From 80ade1907c0f90e428bedd30a871a25d1b1ffabe Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 12:18:01 +0100 Subject: [PATCH 06/14] size compiler dispatch chunks for JVM JIT limits Port upstream ea8351c4 while bounding chunk sizes for zero, negative and extreme chicory.hugeMethodLimit values. Keep indirect-call chunking unchanged. Check generated method sizes and dispatch across chunk boundaries, and include the upstream JMH workload. On JDK 25, the 2000-function microbenchmark measured 6.25-6.40 ns/op with the default budget versus 43.60-46.16 ns/op with the previous chunk dimensions. --- .../chicory/compiler/internal/Compiler.java | 31 +++-- .../compiler/internal/DispatchChunkTest.java | 129 ++++++++++++++++++ docs/docs/usage/runtime-compiler.md | 15 ++ .../bench/BenchmarkDispatchChunkSize.java | 67 +++++++++ 4 files changed, 234 insertions(+), 8 deletions(-) create mode 100644 compiler/src/test/java/com/dylibso/chicory/compiler/internal/DispatchChunkTest.java create mode 100644 jmh/src/main/java/com/dylibso/chicory/bench/BenchmarkDispatchChunkSize.java 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 b432af314..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. @@ -781,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(), @@ -1042,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); @@ -1069,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, @@ -1093,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/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/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)); + } +} From 26a7c292fda499359467b309a0b76d80ab626944 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 12:52:25 +0100 Subject: [PATCH 07/14] pin WASI tests to the supported specification format --- .github/workflows/ci.yaml | 6 ++++-- .../java/com/dylibso/chicory/maven/WasiTestGenMojo.java | 4 ++-- .../com/dylibso/chicory/maven/WasiTestSuiteDownloader.java | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b5b1afe4e..9ced2f895 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,7 +44,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: @@ -104,7 +105,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/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"); From e22d4a59eef4738e3d31010416e4d51d2adf14a8 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 12:55:53 +0100 Subject: [PATCH 08/14] verify the runtime with the NGE TeaVM fork in CI --- .github/workflows/teavm.yaml | 36 ++++++ pom.xml | 12 ++ teavm-tests/README.md | 23 ++++ teavm-tests/pom.xml | 119 ++++++++++++++++++ teavm-tests/run-smoke.mjs | 14 +++ .../dylibso/chicory/teavm/RuntimeSmoke.java | 68 ++++++++++ .../src/main/resources/runtime-smoke.wat | 25 ++++ 7 files changed, 297 insertions(+) create mode 100644 .github/workflows/teavm.yaml create mode 100644 teavm-tests/README.md create mode 100644 teavm-tests/pom.xml create mode 100644 teavm-tests/run-smoke.mjs create mode 100644 teavm-tests/src/main/java/com/dylibso/chicory/teavm/RuntimeSmoke.java create mode 100644 teavm-tests/src/main/resources/runtime-smoke.wat diff --git a/.github/workflows/teavm.yaml b/.github/workflows/teavm.yaml new file mode 100644 index 000000000..65641a4f1 --- /dev/null +++ b/.github/workflows/teavm.yaml @@ -0,0 +1,36 @@ +name: NGE TeaVM Runtime + +on: + push: + branches: [ main ] + pull_request: + +permissions: + contents: read + +jobs: + runtime: + name: Compile and run runtime JavaScript + runs-on: ubuntu-latest + timeout-minutes: 20 + 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 -pl teavm-tests -am verify + - name: Upload generated JavaScript + if: always() + uses: actions/upload-artifact@v7 + with: + name: teavm-runtime-javascript + path: teavm-tests/target/javascript/ 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/teavm-tests/README.md b/teavm-tests/README.md new file mode 100644 index 000000000..c388274bc --- /dev/null +++ b/teavm-tests/README.md @@ -0,0 +1,23 @@ +# NGE TeaVM runtime tests + +With JDK 25 and Node.js 22 installed, run from the repository root: + +```sh +mvn -B -ntp -U -Dteavm-tests -pl teavm-tests -am verify +``` + +This compiles Chicory's parser and interpreter to JavaScript with +[`NostrGameEngine/teavm`](https://github.com/NostrGameEngine/teavm), using the +`org.ngengine:teavm-maven-plugin` and `org.ngengine:teavm-classlib` +`0.16.0-SNAPSHOT` artifacts from Maven Central snapshots. Override the version +with `-Dteavm.version=...` when testing another published fork version. + +The generated JavaScript runs in Node.js and checks conditional/table loops, +10,000 tail calls, active element function references, 32/64-bit memory access, +memory growth and bounds traps with both the default memory and `ByteArrayMemory`. +`verify` fails on compilation errors, failed assertions or a missing completion +callback. The generated module is in `target/javascript/runtime-smoke.mjs`. + +The WebAssembly fixture in `src/main/resources/runtime-smoke.wat` is embedded in +`RuntimeSmoke.java`; regenerate its bytes with `wat2wasm --enable-tail-call` when +changing the fixture. diff --git a/teavm-tests/pom.xml b/teavm-tests/pom.xml new file mode 100644 index 000000000..a13394675 --- /dev/null +++ b/teavm-tests/pom.xml @@ -0,0 +1,119 @@ + + + 4.0.0 + + + org.ngengine.chicory + chicory + 999-SNAPSHOT + ../pom.xml + + teavm-tests + Chicory - NGE TeaVM Runtime Tests + Compile and execute the interpreter with the NGE TeaVM fork + + + + 9.10.1 + 0.16.0-SNAPSHOT + true + true + + + + + org.ngengine.chicory + runtime + + + org.ngengine.chicory + wasm + + + org.ngengine + teavm-classlib + ${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 + + 90000 + + + + + + org.ngengine + teavm-maven-plugin + ${teavm.version} + + com.dylibso.chicory.teavm.RuntimeSmoke + JAVASCRIPT + ES2015 + runtime-smoke.mjs + false + true + + + + + compile + + + + + + + diff --git a/teavm-tests/run-smoke.mjs b/teavm-tests/run-smoke.mjs new file mode 100644 index 000000000..c2b5b5f16 --- /dev/null +++ b/teavm-tests/run-smoke.mjs @@ -0,0 +1,14 @@ +import { main } from './target/javascript/runtime-smoke.mjs'; + +await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error('TeaVM runtime smoke test timed out')), 60000); + main([], result => { + clearTimeout(timeout); + if (result instanceof Error) { + reject(result); + } else { + resolve(); + } + }); +}); +console.log('CHICORY_TEAVM_RUNTIME_PASSED'); 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..76ee1edbe --- /dev/null +++ b/teavm-tests/src/main/java/com/dylibso/chicory/teavm/RuntimeSmoke.java @@ -0,0 +1,68 @@ +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 the parser and interpreter after compilation to JavaScript by NGE TeaVM. + */ +public final class RuntimeSmoke { + // Compiled from src/main/resources/runtime-smoke.wat with wat2wasm --enable-tail-call. + private static final byte[] MODULE = { + 0, 97, 115, 109, 1, 0, 0, 0, 1, 14, 3, 96, 0, 1, 112, 96, + 1, 127, 1, 127, 96, 0, 1, 127, 3, 6, 5, 0, 1, 1, 2, 1, + 4, 4, 1, 112, 0, 1, 5, 3, 1, 0, 1, 7, 45, 6, 6, 109, + 101, 109, 111, 114, 121, 2, 0, 4, 115, 101, 108, 102, 0, 0, 3, 114, + 117, 110, 0, 1, 4, 116, 97, 105, 108, 0, 2, 4, 116, 114, 97, 112, + 0, 3, 5, 116, 97, 98, 108, 101, 0, 4, 9, 7, 1, 0, 65, 0, + 11, 1, 0, 10, 100, 5, 4, 0, -46, 0, 11, 36, 1, 1, 127, 3, + 64, 2, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 1, 32, 0, 73, + 13, 0, 11, 65, 0, 32, 1, 54, 2, 0, 65, 0, 40, 2, 0, 11, + 18, 0, 32, 0, 69, 4, 127, 65, 37, 5, 32, 0, 65, 1, 107, 18, + 2, 11, 11, 7, 0, 65, 127, 40, 2, 0, 11, 29, 1, 1, 127, 2, + 64, 3, 64, 32, 1, 65, 1, 106, 33, 1, 32, 1, 32, 0, 73, 69, + 14, 1, 0, 1, 11, 11, 32, 1, 11, + }; + + private RuntimeSmoke() {} + + public static void main(String[] args) { + run(false); + run(true); + System.out.println( + "CHICORY_TEAVM_JS_OK loops=10000 table=10000 tail=10000 ref.func memory bounds"); + } + + private static void run(boolean useByteArrayMemory) { + var builder = Instance.builder(Parser.parse(MODULE)); + 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"); + require(instance.export("tail").apply(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"); + boolean trapped = false; + try { + instance.export("trap").apply(); + } catch (WasmRuntimeException expected) { + trapped = true; + } + require(trapped, "negative-address bounds trap"); + } + + private static void require(boolean condition, String message) { + if (!condition) { + throw new AssertionError(message); + } + } +} 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)) From 22ef7855cae947c3559a15acd8d007bef70decad Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 13:03:56 +0100 Subject: [PATCH 09/14] keep runtime regression fixtures compatible with Android 28 --- .../runtime/ConditionalBranchInterruptionTest.java | 2 +- .../chicory/runtime/ImmutableControlFramesTest.java | 12 ++++++------ .../chicory/runtime/InstructionOperandsTest.java | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/ConditionalBranchInterruptionTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/ConditionalBranchInterruptionTest.java index f83e7d9a7..c0050d44c 100644 --- a/runtime/src/test/java/com/dylibso/chicory/runtime/ConditionalBranchInterruptionTest.java +++ b/runtime/src/test/java/com/dylibso/chicory/runtime/ConditionalBranchInterruptionTest.java @@ -82,7 +82,7 @@ private static void assertInterrupted(OpCode backedge, int[] body) throws Interr private static byte[] module(int[] body) { ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - bytes.writeBytes(new byte[] {0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}); + 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}); diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/ImmutableControlFramesTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/ImmutableControlFramesTest.java index 9b85ff799..61cf5a41f 100644 --- a/runtime/src/test/java/com/dylibso/chicory/runtime/ImmutableControlFramesTest.java +++ b/runtime/src/test/java/com/dylibso/chicory/runtime/ImmutableControlFramesTest.java @@ -265,11 +265,11 @@ private static byte[] body(int locals, int... instructions) { private static byte[] module(byte[][] types, int[] functions, boolean tag, byte[]... bodies) { ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.writeBytes(new byte[] {0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}); + 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.writeBytes(value); + data.write(value, 0, value.length); } section(out, 1, data); data = new ByteArrayOutputStream(); @@ -280,17 +280,17 @@ private static byte[] module(byte[][] types, int[] functions, boolean tag, byte[ section(out, 3, data); if (tag) { data = new ByteArrayOutputStream(); - data.writeBytes(new byte[] {1, 0, 0}); + data.write(new byte[] {1, 0, 0}, 0, 3); section(out, 13, data); } data = new ByteArrayOutputStream(); - data.writeBytes(new byte[] {1, 3, 'r', 'u', 'n', 0, 0}); + 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.writeBytes(value); + data.write(value, 0, value.length); } section(out, 10, data); return out.toByteArray(); @@ -299,7 +299,7 @@ private static byte[] module(byte[][] types, int[] functions, boolean tag, byte[ private static void section(ByteArrayOutputStream out, int id, ByteArrayOutputStream data) { out.write(id); unsigned(out, data.size()); - out.writeBytes(data.toByteArray()); + out.write(data.toByteArray(), 0, data.size()); } private static void unsigned(ByteArrayOutputStream out, int value) { diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/InstructionOperandsTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/InstructionOperandsTest.java index 81ad8e8aa..124cd05a1 100644 --- a/runtime/src/test/java/com/dylibso/chicory/runtime/InstructionOperandsTest.java +++ b/runtime/src/test/java/com/dylibso/chicory/runtime/InstructionOperandsTest.java @@ -292,7 +292,7 @@ private static int[] join(int[]... parts) { private static byte[] module(int result, boolean memory, int[]... bodies) { ByteArrayOutputStream out = new ByteArrayOutputStream(); - out.writeBytes(new byte[] {0, 0x61, 0x73, 0x6d, 1, 0, 0, 0}); + 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; @@ -325,7 +325,7 @@ private static void section(ByteArrayOutputStream out, int id, int[] values) { private static void section(ByteArrayOutputStream out, int id, byte[] values) { out.write(id); unsigned(out, values.length); - out.writeBytes(values); + out.write(values, 0, values.length); } private static void unsigned(ByteArrayOutputStream out, int value) { From f44187a0a3137e8742cf1238ff5e9499c90b7f50 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 13:13:42 +0100 Subject: [PATCH 10/14] preserve scalar memory traps in TeaVM targets --- .../chicory/runtime/ByteBufferMemory.java | 33 ++++++++++++------- .../dylibso/chicory/runtime/MemoryTest.java | 30 +++++++++++++++++ 2 files changed, 51 insertions(+), 12 deletions(-) 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..8b9d3e3a9 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,19 @@ 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) { + checkBounds(addr, accessSize, sizeInBytes(), WasmRuntimeException::new); + } + 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 +388,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 +410,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 +431,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 +457,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 +482,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 +502,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 +523,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 +534,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 +553,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 +567,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 +581,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/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( From 9bd9d8f68e3835dadbdeae28dfd7e6c3ab04e6d6 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 13:13:43 +0100 Subject: [PATCH 11/14] exercise runtime and precompiled modules in both TeaVM backends --- .github/workflows/teavm.yaml | 14 +-- teavm-tests/README.md | 72 +++++++++++---- teavm-tests/pom.xml | 84 +++++++++++++++++- teavm-tests/run-smoke.mjs | 55 +++++++++--- .../dylibso/chicory/teavm/RuntimeSmoke.java | 76 ++++++++++------ .../dylibso/chicory/teavm/SmokeResources.java | 14 +++ ...vm.extension.spi.resources.ResourcesPolicy | 1 + .../src/main/resources/runtime-smoke.wasm | Bin 0 -> 201 bytes 8 files changed, 255 insertions(+), 61 deletions(-) create mode 100644 teavm-tests/src/main/java/com/dylibso/chicory/teavm/SmokeResources.java create mode 100644 teavm-tests/src/main/resources/META-INF/services/org.teavm.extension.spi.resources.ResourcesPolicy create mode 100644 teavm-tests/src/main/resources/runtime-smoke.wasm diff --git a/.github/workflows/teavm.yaml b/.github/workflows/teavm.yaml index 65641a4f1..b0888c450 100644 --- a/.github/workflows/teavm.yaml +++ b/.github/workflows/teavm.yaml @@ -10,9 +10,13 @@ permissions: jobs: runtime: - name: Compile and run runtime JavaScript + 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 @@ -27,10 +31,10 @@ jobs: with: node-version: '22' - name: Compile and execute with the NGE TeaVM fork - run: mvn -B -ntp -U -Dteavm-tests -pl teavm-tests -am verify - - name: Upload generated JavaScript + 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-javascript - path: teavm-tests/target/javascript/ + name: teavm-runtime-${{ matrix.backend }} + path: teavm-tests/target/${{ matrix.backend }}/ diff --git a/teavm-tests/README.md b/teavm-tests/README.md index c388274bc..36fec98df 100644 --- a/teavm-tests/README.md +++ b/teavm-tests/README.md @@ -3,21 +3,63 @@ 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 ``` -This compiles Chicory's parser and interpreter to JavaScript with -[`NostrGameEngine/teavm`](https://github.com/NostrGameEngine/teavm), using the -`org.ngengine:teavm-maven-plugin` and `org.ngengine:teavm-classlib` -`0.16.0-SNAPSHOT` artifacts from Maven Central snapshots. Override the version -with `-Dteavm.version=...` when testing another published fork version. - -The generated JavaScript runs in Node.js and checks conditional/table loops, -10,000 tail calls, active element function references, 32/64-bit memory access, -memory growth and bounds traps with both the default memory and `ByteArrayMemory`. -`verify` fails on compilation errors, failed assertions or a missing completion -callback. The generated module is in `target/javascript/runtime-smoke.mjs`. - -The WebAssembly fixture in `src/main/resources/runtime-smoke.wat` is embedded in -`RuntimeSmoke.java`; regenerate its bytes with `wat2wasm --enable-tail-call` when -changing the fixture. +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 index a13394675..956386e27 100644 --- a/teavm-tests/pom.xml +++ b/teavm-tests/pom.xml @@ -10,12 +10,15 @@ teavm-tests Chicory - NGE TeaVM Runtime Tests - Compile and execute the interpreter with the NGE TeaVM fork + 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 @@ -35,6 +38,12 @@ ${teavm.version} provided + + org.ngengine + teavm-extension-spi + ${teavm.version} + provided + @@ -88,6 +97,7 @@ ${project.basedir} run-smoke.mjs + ${teavm.backend} 90000 @@ -100,9 +110,10 @@ ${teavm.version} com.dylibso.chicory.teavm.RuntimeSmoke - JAVASCRIPT + ${teavm.targetType} ES2015 - runtime-smoke.mjs + ${project.build.directory}/${teavm.backend} + ${teavm.targetFileName} false true @@ -114,6 +125,73 @@ + + 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 index c2b5b5f16..a6f158cb5 100644 --- a/teavm-tests/run-smoke.mjs +++ b/teavm-tests/run-smoke.mjs @@ -1,14 +1,43 @@ -import { main } from './target/javascript/runtime-smoke.mjs'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; -await new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('TeaVM runtime smoke test timed out')), 60000); - main([], result => { - clearTimeout(timeout); - if (result instanceof Error) { - reject(result); - } else { - resolve(); - } - }); -}); -console.log('CHICORY_TEAVM_RUNTIME_PASSED'); +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 index 76ee1edbe..8d3b893ea 100644 --- a/teavm-tests/src/main/java/com/dylibso/chicory/teavm/RuntimeSmoke.java +++ b/teavm-tests/src/main/java/com/dylibso/chicory/teavm/RuntimeSmoke.java @@ -6,44 +6,35 @@ import com.dylibso.chicory.wasm.Parser; /** - * Exercises the parser and interpreter after compilation to JavaScript by NGE TeaVM. + * Exercises runtime-loaded and precompiled modules in both NGE TeaVM backends. */ public final class RuntimeSmoke { - // Compiled from src/main/resources/runtime-smoke.wat with wat2wasm --enable-tail-call. - private static final byte[] MODULE = { - 0, 97, 115, 109, 1, 0, 0, 0, 1, 14, 3, 96, 0, 1, 112, 96, - 1, 127, 1, 127, 96, 0, 1, 127, 3, 6, 5, 0, 1, 1, 2, 1, - 4, 4, 1, 112, 0, 1, 5, 3, 1, 0, 1, 7, 45, 6, 6, 109, - 101, 109, 111, 114, 121, 2, 0, 4, 115, 101, 108, 102, 0, 0, 3, 114, - 117, 110, 0, 1, 4, 116, 97, 105, 108, 0, 2, 4, 116, 114, 97, 112, - 0, 3, 5, 116, 97, 98, 108, 101, 0, 4, 9, 7, 1, 0, 65, 0, - 11, 1, 0, 10, 100, 5, 4, 0, -46, 0, 11, 36, 1, 1, 127, 3, - 64, 2, 64, 32, 1, 65, 1, 106, 33, 1, 11, 32, 1, 32, 0, 73, - 13, 0, 11, 65, 0, 32, 1, 54, 2, 0, 65, 0, 40, 2, 0, 11, - 18, 0, 32, 0, 69, 4, 127, 65, 37, 5, 32, 0, 65, 1, 107, 18, - 2, 11, 11, 7, 0, 65, 127, 40, 2, 0, 11, 29, 1, 1, 127, 2, - 64, 3, 64, 32, 1, 65, 1, 106, 33, 1, 32, 1, 32, 0, 73, 69, - 14, 1, 0, 1, 11, 11, 32, 1, 11, - }; - private RuntimeSmoke() {} public static void main(String[] args) { - run(false); - run(true); - System.out.println( - "CHICORY_TEAVM_JS_OK loops=10000 table=10000 tail=10000 ref.func memory bounds"); + 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(boolean useByteArrayMemory) { - var builder = Instance.builder(Parser.parse(MODULE)); + 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"); - require(instance.export("tail").apply(10000)[0] == 37, "tail call"); + // 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"); @@ -51,6 +42,20 @@ private static void run(boolean useByteArrayMemory) { 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(); @@ -60,6 +65,27 @@ private static void run(boolean useByteArrayMemory) { 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 0000000000000000000000000000000000000000..a12ad24efced05b67831a9f7c6da1c827eb3230b GIT binary patch literal 201 zcmXwzJqp4=7({3GCqX|wi3O=>413UOg6S?vx~6*!F9kC;?HSO+^A4J+zO+zg8tNX}PW7>EU}h=d zA;kP-4c?G7qqb0mqvv%+adbE=ARdl;6}MXj$>C6%;5XL6^Oh@-6h3^bW#m<%|NIXx Jvv^FRN Date: Tue, 27 Oct 2026 14:15:46 +0100 Subject: [PATCH 12/14] make CI failures blocking and preserve WAT parser diagnostics --- .github/workflows/ci.yaml | 10 ++++++++-- .../java/com/dylibso/chicory/testing/TestModule.java | 2 +- .../java/com/dylibso/chicory/testing/TestModule.java | 2 +- .../dylibso/chicory/tools/wasm/WasmToolsTest.java | 12 ++++++++++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9ced2f895..fa9962f3a 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: @@ -56,6 +55,14 @@ jobs: run: mvn -B clean install env: MAVEN_OPTS: "-ea" + - 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/ docs-ci: runs-on: ubuntu-latest @@ -85,7 +92,6 @@ jobs: test-results: name: Test Results runs-on: ubuntu-latest - continue-on-error: true strategy: fail-fast: false matrix: 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/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/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..5770e6ddb 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,18 @@ public void shouldRunWat2WasmOnString() { assertEquals(42L, results[0]); } + @Test + public void shouldParseLineCommentsWithEveryNewline() { + for (String newline : new String[] {"\n", "\r", "\r\n"}) { + String wat = + "(func (export \"run\") (result i32) (i32.const 1) ;; comment" + + newline + + " (return (i32.const 2))\n)"; + var instance = Instance.builder(Parser.parse(Wat2Wasm.parse(wat))).build(); + assertEquals(2L, instance.export("run").apply()[0]); + } + } + @Test public void shouldThrowMalformedException() throws Exception { var exitException = From 2de36701904d6d73d2786eecc40e67b849f45b71 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 14:42:45 +0100 Subject: [PATCH 13/14] avoid nested unsigned comparisons on older JVMs --- .github/workflows/ci.yaml | 9 +++++++++ .../com/dylibso/chicory/runtime/OpcodeImpl.java | 3 ++- .../chicory/runtime/UnsignedComparisonTest.java | 17 +++++++++++++++++ .../chicory/tools/wasm/WasmToolsTest.java | 13 ++++++++++--- 4 files changed, 38 insertions(+), 4 deletions(-) create mode 100644 runtime/src/test/java/com/dylibso/chicory/runtime/UnsignedComparisonTest.java diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fa9962f3a..06a550cdd 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -55,6 +55,11 @@ 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 @@ -63,6 +68,10 @@ jobs: 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 diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/OpcodeImpl.java b/runtime/src/main/java/com/dylibso/chicory/runtime/OpcodeImpl.java index cadf4ffad..fb71775b1 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/OpcodeImpl.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/OpcodeImpl.java @@ -85,7 +85,8 @@ public static int I32_GE_S(int a, int b) { @OpCodeIdentifier(OpCode.I32_GE_U) public static int I32_GE_U(int a, int b) { - return Integer.compareUnsigned(a, b) >= 0 ? TRUE : FALSE; + // Use one comparison to avoid the comparison folding affected by JDK-8376400. + return (a ^ Integer.MIN_VALUE) >= (b ^ Integer.MIN_VALUE) ? TRUE : FALSE; } @OpCodeIdentifier(OpCode.I32_GT_S) diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/UnsignedComparisonTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/UnsignedComparisonTest.java new file mode 100644 index 000000000..a6d09c41a --- /dev/null +++ b/runtime/src/test/java/com/dylibso/chicory/runtime/UnsignedComparisonTest.java @@ -0,0 +1,17 @@ +package com.dylibso.chicory.runtime; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +class UnsignedComparisonTest { + @Test + void greaterOrEqualAcrossSignedBoundary() { + int[] ordered = {0, 1, 8, 16, Integer.MAX_VALUE, Integer.MIN_VALUE, -16, -8, -1}; + for (int a = 0; a < ordered.length; a++) { + for (int b = 0; b < ordered.length; b++) { + assertEquals(a >= b ? 1 : 0, OpcodeImpl.I32_GE_U(ordered[a], ordered[b])); + } + } + } +} 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 5770e6ddb..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 @@ -61,13 +61,20 @@ public void shouldRunWat2WasmOnString() { @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\") (result i32) (i32.const 1) ;; comment" + "(func (export \"run" + + index++ + + "\") (result i32) (i32.const 1) ;; comment" + newline + " (return (i32.const 2))\n)"; - var instance = Instance.builder(Parser.parse(Wat2Wasm.parse(wat))).build(); - assertEquals(2L, instance.export("run").apply()[0]); + 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]); } } From 00102c7a26dc7a0c9d808f4b4c16f0e3556c7399 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Tue, 27 Oct 2026 15:11:03 +0100 Subject: [PATCH 14/14] throw allocated-page traps directly on older ARM JVMs --- .../chicory/runtime/ByteBufferMemory.java | 9 ++++++++- .../com/dylibso/chicory/runtime/OpcodeImpl.java | 3 +-- .../chicory/runtime/UnsignedComparisonTest.java | 17 ----------------- 3 files changed, 9 insertions(+), 20 deletions(-) delete mode 100644 runtime/src/test/java/com/dylibso/chicory/runtime/UnsignedComparisonTest.java 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 8b9d3e3a9..ea0917696 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/ByteBufferMemory.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/ByteBufferMemory.java @@ -349,7 +349,14 @@ 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) { - checkBounds(addr, accessSize, sizeInBytes(), WasmRuntimeException::new); + // 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]; } diff --git a/runtime/src/main/java/com/dylibso/chicory/runtime/OpcodeImpl.java b/runtime/src/main/java/com/dylibso/chicory/runtime/OpcodeImpl.java index fb71775b1..cadf4ffad 100644 --- a/runtime/src/main/java/com/dylibso/chicory/runtime/OpcodeImpl.java +++ b/runtime/src/main/java/com/dylibso/chicory/runtime/OpcodeImpl.java @@ -85,8 +85,7 @@ public static int I32_GE_S(int a, int b) { @OpCodeIdentifier(OpCode.I32_GE_U) public static int I32_GE_U(int a, int b) { - // Use one comparison to avoid the comparison folding affected by JDK-8376400. - return (a ^ Integer.MIN_VALUE) >= (b ^ Integer.MIN_VALUE) ? TRUE : FALSE; + return Integer.compareUnsigned(a, b) >= 0 ? TRUE : FALSE; } @OpCodeIdentifier(OpCode.I32_GT_S) diff --git a/runtime/src/test/java/com/dylibso/chicory/runtime/UnsignedComparisonTest.java b/runtime/src/test/java/com/dylibso/chicory/runtime/UnsignedComparisonTest.java deleted file mode 100644 index a6d09c41a..000000000 --- a/runtime/src/test/java/com/dylibso/chicory/runtime/UnsignedComparisonTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.dylibso.chicory.runtime; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -class UnsignedComparisonTest { - @Test - void greaterOrEqualAcrossSignedBoundary() { - int[] ordered = {0, 1, 8, 16, Integer.MAX_VALUE, Integer.MIN_VALUE, -16, -8, -1}; - for (int a = 0; a < ordered.length; a++) { - for (int b = 0; b < ordered.length; b++) { - assertEquals(a >= b ? 1 : 0, OpcodeImpl.I32_GE_U(ordered[a], ordered[b])); - } - } - } -}