diff --git a/.github/workflows/redline.yaml b/.github/workflows/redline.yaml new file mode 100644 index 00000000..96e8a422 --- /dev/null +++ b/.github/workflows/redline.yaml @@ -0,0 +1,41 @@ +name: Redline CI + +on: + push: + branches: [ main ] + pull_request: + +jobs: + redline: + name: Redline (Java 25, ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + steps: + - name: Checkout sources + uses: actions/checkout@v7 + - name: Checkout testsuite + uses: actions/checkout@v7 + with: + repository: WebAssembly/testsuite + path: testsuite + ref: 88e97b0f742f4c3ee01fea683da130f344dd7b02 + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '25' + cache: maven + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + - name: Build cranelift_bridge.wasm + working-directory: redline/wasm-build + run: make all + - name: Build and test redline + run: mvn -B clean install -Predline -Ddev + env: + MAVEN_OPTS: "-ea" diff --git a/.gitignore b/.gitignore index ce916145..92a7a094 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ scripts/jmh-tmp /main/ /*.wasm +redline/cranelift_bridge.wasm +redline/wasm-build/target/ .approval_tests_temp/ # mac-os files diff --git a/pom.xml b/pom.xml index b4b8edcd..028d6b87 100644 --- a/pom.xml +++ b/pom.xml @@ -244,6 +244,26 @@ log ${project.version} + + run.endive + redline-api-experimental + ${project.version} + + + run.endive + redline-bridge-experimental + ${project.version} + + + run.endive + redline-compiler-experimental + ${project.version} + + + run.endive + redline-runner-experimental + ${project.version} + run.endive runtime @@ -1002,6 +1022,13 @@ + + redline + + redline + + + default-all-modules diff --git a/redline/api/pom.xml b/redline/api/pom.xml new file mode 100644 index 00000000..4ebe1c1e --- /dev/null +++ b/redline/api/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + + run.endive + redline-parent-experimental + 999-SNAPSHOT + ../pom.xml + + redline-api-experimental + jar + Endive - Redline API + Redline native compiler API + + + + run.endive + wasm + + + diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/Interruptible.java b/redline/api/src/main/java/run/endive/redline/experimental/api/Interruptible.java new file mode 100644 index 00000000..84c3d0af --- /dev/null +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/Interruptible.java @@ -0,0 +1,7 @@ +package run.endive.redline.experimental.api; + +public interface Interruptible { + void requestInterrupt(); + + void clearInterrupt(); +} diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java new file mode 100644 index 00000000..f4cf3cb8 --- /dev/null +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/NativeCodeSerializer.java @@ -0,0 +1,75 @@ +package run.endive.redline.experimental.api; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Serializes/deserializes pre-compiled native code (byte[][]). + * + *

Format: + *

+ *   [4 bytes: magic "CL4J"]
+ *   [4 bytes: version (1)]
+ *   [4 bytes: function count]
+ *   For each function:
+ *     [4 bytes: code length, 0 for null/uncompiled]
+ *     [N bytes: native code]
+ * 
+ */ +public final class NativeCodeSerializer { + + private static final int MAGIC = 0x434C344A; // "CL4J" + private static final int VERSION = 1; + + private NativeCodeSerializer() {} + + public static void serialize(byte[][] code, OutputStream out) throws IOException { + DataOutputStream dos = new DataOutputStream(out); + dos.writeInt(MAGIC); + dos.writeInt(VERSION); + dos.writeInt(code.length); + for (byte[] func : code) { + if (func != null) { + dos.writeInt(func.length); + dos.write(func); + } else { + dos.writeInt(0); + } + } + dos.flush(); + } + + public static byte[][] deserialize(InputStream in) throws IOException { + DataInputStream dis = new DataInputStream(in); + int magic = dis.readInt(); + if (magic != MAGIC) { + throw new IOException( + "Invalid native code file: bad magic 0x" + Integer.toHexString(magic)); + } + int version = dis.readInt(); + if (version != VERSION) { + throw new IOException("Unsupported native code version: " + version); + } + int count = dis.readInt(); + byte[][] code = new byte[count][]; + for (int i = 0; i < count; i++) { + int len = dis.readInt(); + if (len > 0) { + code[i] = dis.readNBytes(len); + if (code[i].length != len) { + throw new IOException( + "Truncated native code for function " + + i + + ": expected " + + len + + " bytes, got " + + code[i].length); + } + } + } + return code; + } +} diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/RedlineTarget.java b/redline/api/src/main/java/run/endive/redline/experimental/api/RedlineTarget.java new file mode 100644 index 00000000..19ce999b --- /dev/null +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/RedlineTarget.java @@ -0,0 +1,49 @@ +package run.endive.redline.experimental.api; + +import java.util.Locale; +import java.util.Optional; + +public enum RedlineTarget { + LINUX_X86_64("x86_64-unknown-linux-gnu", "x86_64-linux"), + LINUX_AARCH64("aarch64-unknown-linux-gnu", "aarch64-linux"), + MACOS_X86_64("x86_64-apple-darwin", "x86_64-darwin"), + MACOS_AARCH64("aarch64-apple-darwin", "aarch64-darwin"), + WINDOWS_X86_64("x86_64-pc-windows-msvc", "x86_64-windows"), + WINDOWS_AARCH64("aarch64-pc-windows-msvc", "aarch64-windows"); + + private final String triple; + private final String resourceSuffix; + + RedlineTarget(String triple, String resourceSuffix) { + this.triple = triple; + this.resourceSuffix = resourceSuffix; + } + + public String triple() { + return triple; + } + + public String resourceSuffix() { + return resourceSuffix; + } + + public static Optional detectHost() { + String osName = + System.getProperty("endive.redline.os.name", System.getProperty("os.name", "")) + .toLowerCase(Locale.ROOT); + String arch = + System.getProperty("endive.redline.os.arch", System.getProperty("os.arch", "")) + .toLowerCase(Locale.ROOT); + + boolean isAarch64 = arch.equals("aarch64") || arch.equals("arm64"); + + if (osName.contains("linux")) { + return Optional.of(isAarch64 ? LINUX_AARCH64 : LINUX_X86_64); + } else if (osName.contains("mac") || osName.contains("darwin")) { + return Optional.of(isAarch64 ? MACOS_AARCH64 : MACOS_X86_64); + } else if (osName.contains("windows")) { + return Optional.of(isAarch64 ? WINDOWS_AARCH64 : WINDOWS_X86_64); + } + return Optional.empty(); + } +} diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/CtxBuffer.java b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/CtxBuffer.java new file mode 100644 index 00000000..7d37d076 --- /dev/null +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/CtxBuffer.java @@ -0,0 +1,87 @@ +package run.endive.redline.experimental.api.internal; + +/** + * Layout of the shared context buffer passed between Java and native compiled code. + * + *
+ * Offset  Type   Field             Description
+ * ------  -----  ----------------  ------------------------------------------
+ *   0     i64    funcTablePtr      Pointer to function pointer table
+ *   8     i64    trampolinePtr     Upcall stub for CALL_INDIRECT fallback
+ *  16     i32    trapCode          Trap code written by native pre-checks
+ *  20     i32    typeId            CALL_INDIRECT: expected type index
+ *  24     i32    tableIdx          CALL_INDIRECT: table index
+ *  28     i32    elemIdx           CALL_INDIRECT: table element index
+ *  32     i32    argCount          Number of call arguments
+ *  36     i32    memGrowDelta      Page count delta for memory.grow
+ *  40     i64    argsPtr           Pointer to separate args buffer
+ *  48     i64    stackLimit        Stack pointer limit for call depth guard
+ *  56     i64    memmovePtr        Pointer to libc memmove
+ *  64     i64    memsetPtr         Pointer to libc memset
+ *  72     i64    interruptFlag     Non-zero = interrupt requested
+ * 200     i64    globalsPtr        Pointer to globals buffer
+ * 208     i64    memGrowPtr        Upcall stub for memory.grow
+ * 216     i32    memoryPages       Current memory page count
+ * 224     i64    memBaseAddr       Current memory base address
+ * 232     i64    tablePtrs         Pointer to array of table buffer pointers
+ * 240     i64    funcTypesPtr      Pointer to funcTypes array (i32 per func)
+ * ------  -----  ----------------  ------------------------------------------
+ * Total: 248 bytes used, 256 allocated (CTX_SIZE)
+ * 
+ */ +public final class CtxBuffer { + + private CtxBuffer() {} + + public static final int CTX_SIZE = 256; + public static final int ARGS_BUFFER_CAPACITY = 1024; + + public static final int FUNC_TABLE_PTR = 0; + public static final int TRAMPOLINE_PTR = 8; + + public static final int TRAP_CODE = 16; + public static final int TYPE_ID = 20; + public static final int TABLE_IDX = 24; + public static final int ELEM_IDX = 28; + public static final int ARG_COUNT = 32; + public static final int MEM_GROW_DELTA = 36; + public static final int ARGS_PTR = 40; + public static final int STACK_LIMIT = 48; + public static final int MEMMOVE_PTR = 56; + public static final int MEMSET_PTR = 64; + public static final int INTERRUPT_FLAG = 72; + + public static final int GLOBALS_PTR = 200; + public static final int MEM_GROW_PTR = 208; + public static final int MEMORY_PAGES = 216; + public static final int MEM_BASE_ADDR = 224; + public static final int TABLE_PTRS = 232; + public static final int FUNC_TYPES_PTR = 240; + + public static final int TRAP_NONE = 0; + public static final int TRAP_DIV_BY_ZERO = 1; + public static final int TRAP_INT_OVERFLOW = 2; + public static final int TRAP_UNREACHABLE = 3; + public static final int TRAP_TRUNC_OVERFLOW = 4; + public static final int TRAP_OOB = 5; + public static final int TRAP_CALL_STACK_EXHAUSTED = 6; + public static final int TRAP_TABLE_OOB = 7; + public static final int TRAP_UNDEFINED_ELEMENT = 8; + public static final int TRAP_INDIRECT_CALL_TYPE_MISMATCH = 9; + public static final int TRAP_UNINITIALIZED_ELEMENT = 10; + public static final int TRAP_TRUNC_NAN = 11; + public static final int TRAP_UNALIGNED_ATOMIC = 12; + public static final int TRAP_INTERRUPTED = 13; + + public static final int TABLE_SIZE_OFFSET = 0; + public static final int TABLE_MAX_OFFSET = 4; + public static final int TABLE_ENTRIES_OFFSET = 8; + public static final int TABLE_ENTRY_SIZE = 16; + public static final int ENTRY_TYPE_IDX_OFFSET = 0; + public static final int ENTRY_FUNC_ID_OFFSET = 4; + public static final int ENTRY_FUNC_PTR_OFFSET = 8; + + public static int argOffset(int i) { + return 8 * i; + } +} diff --git a/redline/api/src/main/java/run/endive/redline/experimental/api/internal/TypeMapUtils.java b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/TypeMapUtils.java new file mode 100644 index 00000000..0f625372 --- /dev/null +++ b/redline/api/src/main/java/run/endive/redline/experimental/api/internal/TypeMapUtils.java @@ -0,0 +1,37 @@ +package run.endive.redline.experimental.api.internal; + +import java.util.HashMap; +import run.endive.wasm.WasmModule; +import run.endive.wasm.types.FunctionType; + +/** + * Builds canonical type maps for call_indirect type checking. + * Structurally equal FunctionTypes get the same canonical index. + */ +public final class TypeMapUtils { + + private TypeMapUtils() {} + + public static int[] buildCanonicalTypeMap(WasmModule module) { + var ts = module.typeSection(); + int count = ts.subTypeCount(); + int[] map = new int[count]; + var seen = new HashMap(); + for (int i = 0; i < count; i++) { + var type = ts.getType(i); + if (type instanceof FunctionType) { + FunctionType ft = (FunctionType) type; + Integer canonical = seen.get(ft); + if (canonical != null) { + map[i] = canonical; + } else { + seen.put(ft, i); + map[i] = i; + } + } else { + map[i] = i; + } + } + return map; + } +} diff --git a/redline/bridge/pom.xml b/redline/bridge/pom.xml new file mode 100644 index 00000000..08b04fb7 --- /dev/null +++ b/redline/bridge/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + run.endive + redline-parent-experimental + 999-SNAPSHOT + ../pom.xml + + redline-bridge-experimental + jar + Endive - Redline Bridge + Java wrapper for the Cranelift code generator (compiled to Wasm) + + + + run.endive + log + + + run.endive + runtime + + + run.endive + wasi + + + run.endive + wasm + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + check-cranelift-wasm + + enforce + + validate + + + + + ${project.basedir}/../cranelift_bridge.wasm + + cranelift_bridge.wasm not found. Build it with: + cd redline/wasm-build && make all +(requires Rust with wasm32-wasip1 target) + + + + + + + + run.endive + endive-compiler-maven-plugin + + + redline-bridge + + compile + + + run.endive.redline.experimental.bridge.Cranelift + ${project.basedir}/../cranelift_bridge.wasm + WARN + run.endive.redline.experimental.bridge.CraneliftBridge + + + + + + + diff --git a/redline/bridge/src/main/java/run/endive/redline/experimental/bridge/CraneliftBridge.java b/redline/bridge/src/main/java/run/endive/redline/experimental/bridge/CraneliftBridge.java new file mode 100644 index 00000000..eaad88c3 --- /dev/null +++ b/redline/bridge/src/main/java/run/endive/redline/experimental/bridge/CraneliftBridge.java @@ -0,0 +1,235 @@ +package run.endive.redline.experimental.bridge; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import run.endive.runtime.ImportValues; +import run.endive.runtime.Instance; +import run.endive.wasi.WasiOptions; +import run.endive.wasi.WasiPreview1; +import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.ValType; + +public final class CraneliftBridge implements AutoCloseable { + + private final Instance instance; + private final WasiPreview1 wasi; + private final CraneliftBridge_ModuleExports exports; + + public CraneliftBridge() { + var wasiOpts = WasiOptions.builder().build(); + wasi = WasiPreview1.builder().withOptions(wasiOpts).build(); + var imports = ImportValues.builder().addFunction(wasi.toHostFunctions()).build(); + + instance = + Instance.builder(Cranelift.load()) + .withImportValues(imports) + .withMachineFactory(Cranelift::create) + .build(); + exports = new CraneliftBridge_ModuleExports(instance); + } + + @Override + public void close() { + instance.close(); + wasi.close(); + } + + public static final int TYPE_I32 = 0; + public static final int TYPE_I64 = 1; + public static final int TYPE_F32 = 2; + public static final int TYPE_F64 = 3; + + public static int valTypeToBridgeType(ValType type) { + if (type.equals(ValType.I32)) { + return TYPE_I32; + } + if (type.equals(ValType.I64)) { + return TYPE_I64; + } + if (type.equals(ValType.F32)) { + return TYPE_F32; + } + if (type.equals(ValType.F64)) { + return TYPE_F64; + } + int op = type.opcode(); + if (op == ValType.ID.RefNull || op == ValType.ID.Ref) { + return TYPE_I64; + } + throw new UnsupportedOperationException("Unsupported ValType for native: " + type); + } + + public void init(String target) { + byte[] bytes = target.getBytes(StandardCharsets.UTF_8); + int ptr = exports.wasmMalloc(bytes.length); + for (int i = 0; i < bytes.length; i++) { + exports.memory().writeByte(ptr + i, bytes[i]); + } + exports.init(ptr, bytes.length); + exports.wasmFree(ptr, bytes.length); + } + + public CraneliftBridge_ModuleExports exports() { + return exports; + } + + public byte[] compile() { + exports.compile(); + int codePtr = exports.getCodePtr(); + int codeLen = exports.getCodeLen(); + return exports.memory().readBytes(codePtr, codeLen); + } + + private void buildTrampolineSig(FunctionType funcType) { + exports.beginTrampolineSig(); + exports.trampolineSigAddParam(TYPE_I64); // memBase + exports.trampolineSigAddParam(TYPE_I64); // ctxPtr + for (ValType param : funcType.params()) { + exports.trampolineSigAddParam(valTypeToBridgeType(param)); + } + if (funcType.returns().size() > 1) { + exports.trampolineSigAddReturn(TYPE_I64); + } else { + for (ValType ret : funcType.returns()) { + exports.trampolineSigAddReturn(valTypeToBridgeType(ret)); + } + } + } + + private byte[] readCompiledCode() { + int ptr = exports.getCodePtr(); + int len = exports.getCodeLen(); + return exports.memory().readBytes(ptr, len); + } + + private byte[] compileImportTrampolineRaw(long stubAddr) { + exports.compileImportTrampoline((int) (stubAddr & 0xFFFFFFFFL), (int) (stubAddr >>> 32)); + return readCompiledCode(); + } + + public byte[] compileEntryTrampoline(FunctionType funcType) { + buildTrampolineSig(funcType); + exports.compileEntryTrampoline(); + return readCompiledCode(); + } + + public byte[] compileImportTrampoline(FunctionType funcType, long stubAddr) { + buildTrampolineSig(funcType); + return compileImportTrampolineRaw(stubAddr); + } + + public byte[] compileStubTrampoline(long stubAddr, int[] paramTypes, int[] returnTypes) { + exports.beginTrampolineSig(); + for (int p : paramTypes) { + exports.trampolineSigAddParam(p); + } + for (int r : returnTypes) { + exports.trampolineSigAddReturn(r); + } + return compileImportTrampolineRaw(stubAddr); + } + + public static final class CompiledTrampolines { + private final Map entryTrampolines; + private final byte[][] importTrampolines; + private final byte[] trampolineStubTramp; + private final byte[] memGrowStubTramp; + private final byte[] memmoveTramp; + private final byte[] memsetTramp; + + CompiledTrampolines( + Map entryTrampolines, + byte[][] importTrampolines, + byte[] trampolineStubTramp, + byte[] memGrowStubTramp, + byte[] memmoveTramp, + byte[] memsetTramp) { + this.entryTrampolines = entryTrampolines; + this.importTrampolines = importTrampolines; + this.trampolineStubTramp = trampolineStubTramp; + this.memGrowStubTramp = memGrowStubTramp; + this.memmoveTramp = memmoveTramp; + this.memsetTramp = memsetTramp; + } + + public Map entryTrampolines() { + return entryTrampolines; + } + + public byte[][] importTrampolines() { + return importTrampolines; + } + + public byte[] trampolineStubTramp() { + return trampolineStubTramp; + } + + public byte[] memGrowStubTramp() { + return memGrowStubTramp; + } + + public byte[] memmoveTramp() { + return memmoveTramp; + } + + public byte[] memsetTramp() { + return memsetTramp; + } + + public long totalSize() { + long size = 0; + for (byte[] code : entryTrampolines.values()) { + size += align(code.length, 16); + } + for (byte[] code : importTrampolines) { + size += align(code.length, 16); + } + size += align(trampolineStubTramp.length, 16); + size += align(memGrowStubTramp.length, 16); + size += align(memmoveTramp.length, 16); + size += align(memsetTramp.length, 16); + return size; + } + } + + public CompiledTrampolines compileTrampolines( + byte[][] compiledCode, + FunctionType[] funcTypesByBody, + FunctionType[] importTypes, + long[] importStubAddrs, + long trampolineStubAddr, + long memGrowStubAddr, + long memmoveAddr, + long memsetAddr) { + + Map entryTrampolineCode = new HashMap<>(); + for (int i = 0; i < compiledCode.length; i++) { + if (compiledCode[i] != null && !entryTrampolineCode.containsKey(funcTypesByBody[i])) { + entryTrampolineCode.put( + funcTypesByBody[i], compileEntryTrampoline(funcTypesByBody[i])); + } + } + + byte[][] importTrampolineCode = new byte[importTypes.length][]; + for (int i = 0; i < importTypes.length; i++) { + importTrampolineCode[i] = compileImportTrampoline(importTypes[i], importStubAddrs[i]); + } + + int[] i64Param = {TYPE_I64}; + int[] i64Return = {TYPE_I64}; + int[] i64x3Param = {TYPE_I64, TYPE_I64, TYPE_I64}; + + return new CompiledTrampolines( + entryTrampolineCode, + importTrampolineCode, + compileStubTrampoline(trampolineStubAddr, i64Param, i64Return), + compileStubTrampoline(memGrowStubAddr, i64Param, i64Return), + compileStubTrampoline(memmoveAddr, i64x3Param, i64Return), + compileStubTrampoline(memsetAddr, i64x3Param, i64Return)); + } + + public static long align(long value, long alignment) { + return (value + alignment - 1) & ~(alignment - 1); + } +} diff --git a/redline/compiler/pom.xml b/redline/compiler/pom.xml new file mode 100644 index 00000000..b6ddf697 --- /dev/null +++ b/redline/compiler/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + + run.endive + redline-parent-experimental + 999-SNAPSHOT + ../pom.xml + + redline-compiler-experimental + jar + Endive - Redline Compiler + Wasm to native code compilation via Cranelift + + + + run.endive + redline-api-experimental + + + run.endive + redline-bridge-experimental + + + run.endive + runtime + + + run.endive + wasm + + + diff --git a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/EmitContext.java b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/EmitContext.java new file mode 100644 index 00000000..52311582 --- /dev/null +++ b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/EmitContext.java @@ -0,0 +1,258 @@ +package run.endive.redline.experimental.compiler.internal; + +import java.util.Map; +import run.endive.redline.experimental.api.internal.CtxBuffer; +import run.endive.redline.experimental.bridge.CraneliftBridge; +import run.endive.wasm.WasmModule; +import run.endive.wasm.types.ExternalType; +import run.endive.wasm.types.FunctionImport; +import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.ValType; + +/** + * Shared state passed to {@link NativeEmitters} methods during code emission. + */ +final class EmitContext { + + final CraneliftBridge bridge; + final NativeValueStack valueStack; + final WasmModule module; + final int numImports; + final FunctionType funcType; + final int[] localVars; + final int memBaseVar; + final int ctxPtrVar; + final Map sigRefCache; + final boolean multiReturn; + final int[] canonicalTypeMap; + + EmitContext( + CraneliftBridge bridge, + NativeValueStack valueStack, + WasmModule module, + int numImports, + FunctionType funcType, + int[] localVars, + int memBaseVar, + int ctxPtrVar, + Map sigRefCache, + boolean multiReturn, + int[] canonicalTypeMap) { + this.bridge = bridge; + this.valueStack = valueStack; + this.module = module; + this.numImports = numImports; + this.funcType = funcType; + this.localVars = localVars; + this.memBaseVar = memBaseVar; + this.ctxPtrVar = ctxPtrVar; + this.sigRefCache = sigRefCache; + this.multiReturn = multiReturn; + this.canonicalTypeMap = canonicalTypeMap; + } + + // --- Helpers used by emitters --- + + int emitZero(ValType type) { + if (type.equals(ValType.I32)) { + return bridge.exports().emitIconst32(0); + } + if (type.equals(ValType.I64)) { + return bridge.exports().emitIconst64(0, 0); + } + if (type.equals(ValType.F32)) { + return bridge.exports().emitF32const(0); + } + if (type.equals(ValType.F64)) { + return bridge.exports().emitF64const(0, 0); + } + int op = type.opcode(); + if (op == ValType.ID.RefNull || op == ValType.ID.Ref) { + return bridge.exports().emitIconst64(0, 0); + } + throw new UnsupportedOperationException("Unsupported type: " + type); + } + + static int valTypeToBridgeType(ValType type) { + return CraneliftBridge.valTypeToBridgeType(type); + } + + void emitReturnForFuncType() { + if (funcType.returns().isEmpty()) { + bridge.exports().emitReturnVoid(); + } else if (!multiReturn) { + bridge.exports().emitReturn(emitZero(funcType.returns().get(0))); + } else { + // Multi-return: write zeros to argsBuffer, return dummy i64 + emitWriteReturnsToArgsBuffer(funcType.returns(), null); + bridge.exports().emitReturn(bridge.exports().emitIconst64(0, 0)); + } + } + + /** + * Write return values to argsBuffer (widened to i64). + * If vals is null, writes zeros. + */ + void emitWriteReturnsToArgsBuffer(java.util.List types, int[] vals) { + int zero = bridge.exports().emitIconst32(0); + int argsPtr = + bridge.exports() + .emitLoadI64(bridge.exports().useVar(ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + for (int i = 0; i < types.size(); i++) { + int val = (vals != null) ? vals[i] : emitZero(types.get(i)); + int widened = widenToI64ForType(val, types.get(i)); + bridge.exports().emitStoreI64(argsPtr, zero, widened, CtxBuffer.argOffset(i)); + } + } + + FunctionType resolveCallTargetType(int funcId) { + if (funcId < numImports) { + int idx = 0; + for (var imp : + module.importSection().stream().collect(java.util.stream.Collectors.toList())) { + if (imp.importType() == ExternalType.FUNCTION) { + if (idx == funcId) { + int typeIdx = ((FunctionImport) imp).typeIndex(); + return (FunctionType) module.typeSection().getType(typeIdx); + } + idx++; + } + } + throw new IllegalArgumentException("Import function not found: " + funcId); + } + int bodyIdx = funcId - numImports; + int typeIdx = module.functionSection().getFunctionType(bodyIdx); + return (FunctionType) module.typeSection().getType(typeIdx); + } + + ValType resolveGlobalType(int globalIdx) { + int importGlobalIdx = 0; + for (var imp : + module.importSection().stream().collect(java.util.stream.Collectors.toList())) { + if (imp.importType() == ExternalType.GLOBAL) { + if (importGlobalIdx == globalIdx) { + return ((run.endive.wasm.types.GlobalImport) imp).type(); + } + importGlobalIdx++; + } + } + int moduleGlobalIdx = globalIdx - importGlobalIdx; + return module.globalSection().getGlobal(moduleGlobalIdx).valueType(); + } + + int widenToI64(int valId, ValType type) { + if (type.equals(ValType.I32)) { + return bridge.exports().emitUextendI64(valId); + } + return valId; + } + + int narrowFromI64(int valId, ValType type) { + if (type.equals(ValType.I32)) { + return bridge.exports().emitIreduceI32(valId); + } + return valId; + } + + int widenToI64ForType(int valId, ValType type) { + if (type.equals(ValType.I32)) { + return bridge.exports().emitUextendI64(valId); + } + if (type.equals(ValType.F32)) { + int bits = bridge.exports().emitBitcastF32ToI32(valId); + return bridge.exports().emitUextendI64(bits); + } + if (type.equals(ValType.F64)) { + return bridge.exports().emitBitcastF64ToI64(valId); + } + return valId; // I64 + } + + int narrowFromI64ForType(int valId, ValType type) { + if (type.equals(ValType.I32)) { + return bridge.exports().emitIreduceI32(valId); + } + if (type.equals(ValType.F32)) { + int narrow = bridge.exports().emitIreduceI32(valId); + return bridge.exports().emitBitcastI32ToF32(narrow); + } + if (type.equals(ValType.F64)) { + return bridge.exports().emitBitcastI64ToF64(valId); + } + return valId; // I64 + } + + int getOrCreateSigRef(FunctionType ft) { + String key = ft.toString(); + Integer cached = sigRefCache.get(key); + if (cached != null) { + return cached; + } + + bridge.exports().beginSig(); + bridge.exports().sigAddParam(CraneliftBridge.TYPE_I64); // memBase + bridge.exports().sigAddParam(CraneliftBridge.TYPE_I64); // ctxPtr + for (ValType param : ft.params()) { + bridge.exports().sigAddParam(valTypeToBridgeType(param)); + } + for (ValType ret : ft.returns()) { + bridge.exports().sigAddReturn(valTypeToBridgeType(ret)); + } + int sigRef = bridge.exports().endSig(); + sigRefCache.put(key, sigRef); + return sigRef; + } + + int getOrCreateMultiReturnSigRef(FunctionType ft) { + String key = "__mr__" + ft.toString(); + Integer cached = sigRefCache.get(key); + if (cached != null) { + return cached; + } + + bridge.exports().beginSig(); + bridge.exports().sigAddParam(CraneliftBridge.TYPE_I64); // memBase + bridge.exports().sigAddParam(CraneliftBridge.TYPE_I64); // ctxPtr + for (ValType param : ft.params()) { + bridge.exports().sigAddParam(valTypeToBridgeType(param)); + } + // Multi-return: single i64 dummy return (actual values in argsBuffer) + bridge.exports().sigAddReturn(CraneliftBridge.TYPE_I64); + int sigRef = bridge.exports().endSig(); + sigRefCache.put(key, sigRef); + return sigRef; + } + + int getOrCreateTrampolineSigRef() { + String key = "__trampoline__"; + Integer cached = sigRefCache.get(key); + if (cached != null) { + return cached; + } + + bridge.exports().beginSig(); + bridge.exports().sigAddParam(CraneliftBridge.TYPE_I64); + bridge.exports().sigAddReturn(CraneliftBridge.TYPE_I64); + int sigRef = bridge.exports().endSig(); + sigRefCache.put(key, sigRef); + return sigRef; + } + + /** Signature for memmove/memset: (i64, i64, i64) -> i64. */ + int getOrCreateMemopSigRef() { + String key = "__memop__"; + Integer cached = sigRefCache.get(key); + if (cached != null) { + return cached; + } + + bridge.exports().beginSig(); + bridge.exports().sigAddParam(CraneliftBridge.TYPE_I64); // dst ptr + bridge.exports().sigAddParam(CraneliftBridge.TYPE_I64); // src ptr / value + bridge.exports().sigAddParam(CraneliftBridge.TYPE_I64); // size + bridge.exports().sigAddReturn(CraneliftBridge.TYPE_I64); + int sigRef = bridge.exports().endSig(); + sigRefCache.put(key, sigRef); + return sigRef; + } +} diff --git a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeCompiler.java b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeCompiler.java new file mode 100644 index 00000000..fb3189ab --- /dev/null +++ b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeCompiler.java @@ -0,0 +1,1894 @@ +package run.endive.redline.experimental.compiler.internal; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import run.endive.redline.experimental.api.internal.CtxBuffer; +import run.endive.redline.experimental.api.internal.TypeMapUtils; +import run.endive.redline.experimental.bridge.CraneliftBridge; +import run.endive.wasm.WasmEngineException; +import run.endive.wasm.WasmModule; +import run.endive.wasm.types.AnnotatedInstruction; +import run.endive.wasm.types.ExternalType; +import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.OpCode; +import run.endive.wasm.types.ValType; + +/** + * Thin orchestrator that compiles Wasm functions to native code via Cranelift. + * + *

Walks instructions, delegates opcodes to {@link NativeEmitters}, + * handles control flow (BLOCK/LOOP/IF/ELSE/END/BR/BR_IF/BR_TABLE/RETURN) inline. + * Dead code after unconditional transfers is tracked via the control stack's + * {@code unreachable} flag. + * + *

The {@link NativeValueStack} tracks Cranelift value IDs as the + * operand stack during emission. + */ +public final class NativeCompiler { + + private final CraneliftBridge bridge; + private final String triple; + private final WasmModule module; + private final int numImports; + private final int[] canonicalTypeMap; + + public NativeCompiler(String triple, WasmModule module) { + this.bridge = null; + this.triple = triple; + this.module = module; + this.numImports = + (int) + module.importSection().stream() + .filter(i -> i.importType() == ExternalType.FUNCTION) + .count(); + this.canonicalTypeMap = TypeMapUtils.buildCanonicalTypeMap(module); + } + + private NativeCompiler(CraneliftBridge bridge, String triple, WasmModule module) { + this.bridge = bridge; + this.triple = triple; + this.module = module; + this.numImports = + (int) + module.importSection().stream() + .filter(i -> i.importType() == ExternalType.FUNCTION) + .count(); + this.canonicalTypeMap = TypeMapUtils.buildCanonicalTypeMap(module); + } + + // --- Control frame --- + + private static final class ControlFrame { + enum Kind { + BLOCK, + LOOP, + IF, + FUNCTION + } + + final Kind kind; + final int mergeBlock; + final int loopBlock; + int elseBlock; + final int[] mergeParamIds; + final FunctionType blockType; + final int stackHeight; + int[] elseParamIds; // IF only: block param IDs for else block + boolean unreachable; + boolean hasElse; + + ControlFrame( + Kind kind, + int mergeBlock, + int loopBlock, + int elseBlock, + int[] mergeParamIds, + FunctionType blockType, + int stackHeight) { + this.kind = kind; + this.mergeBlock = mergeBlock; + this.loopBlock = loopBlock; + this.elseBlock = elseBlock; + this.mergeParamIds = mergeParamIds; + this.blockType = blockType; + this.stackHeight = stackHeight; + } + + int branchTarget() { + return kind == Kind.LOOP ? loopBlock : mergeBlock; + } + + int branchArgCount() { + return kind == Kind.LOOP ? blockType.params().size() : blockType.returns().size(); + } + } + + // --- Compilation --- + + private static final int THREAD_COUNT = + Math.max(1, Runtime.getRuntime().availableProcessors() / 2); + + private static final ExecutorService POOL = Executors.newFixedThreadPool(THREAD_COUNT); + + public byte[][] compileAll() { + return compileAll(null); + } + + /** + * Compiles functions to native code. If {@code filter} is non-null, only + * functions where {@code filter[numImports + bodyIndex]} is {@code true} + * are compiled; others get {@code null} entries. + */ + public byte[][] compileAll(boolean[] filter) { + int count = module.codeSection().functionBodyCount(); + byte[][] results = new byte[count][]; + int threads = Math.min(THREAD_COUNT, count); + if (threads < 1) { + threads = 1; + } + + int chunkSize = (count + threads - 1) / threads; + List> futures = new ArrayList<>(); + + for (int t = 0; t < threads; t++) { + int start = t * chunkSize; + int end = Math.min(start + chunkSize, count); + if (start >= count) { + break; + } + + futures.add( + POOL.submit( + () -> { + try (var threadBridge = new CraneliftBridge()) { + threadBridge.init(triple); + var threadCompiler = + new NativeCompiler(threadBridge, triple, module); + for (int i = start; i < end; i++) { + if (filter != null && !filter[numImports + i]) { + continue; + } + try { + results[i] = threadCompiler.compileFunction(i); + } catch (RuntimeException e) { + throw new WasmEngineException( + "Failed to compile function " + + i + + ": " + + e.getMessage(), + e); + } + } + } + return null; + })); + } + + for (Future f : futures) { + try { + f.get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } + throw new WasmEngineException("Parallel compilation failed", e.getCause()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new WasmEngineException("Compilation interrupted", e); + } + } + return results; + } + + private byte[] compileFunction(int bodyIndex) { + var body = module.codeSection().getFunctionBody(bodyIndex); + int typeIdx = module.functionSection().getFunctionType(bodyIndex); + var funcType = (FunctionType) module.typeSection().getType(typeIdx); + + // Multi-return: functions with >1 return write results to argsBuffer + // and return a single i64 (dummy). Single-return uses registers (fast path). + boolean multiReturn = funcType.returns().size() > 1; + + // --- Setup function --- + bridge.exports().createFunction(); + + bridge.exports().addParamType(CraneliftBridge.TYPE_I64); // memBase + bridge.exports().addParamType(CraneliftBridge.TYPE_I64); // ctxPtr + for (ValType param : funcType.params()) { + bridge.exports().addParamType(EmitContext.valTypeToBridgeType(param)); + } + if (multiReturn) { + // Multi-return: return single i64 dummy (actual values in argsBuffer) + bridge.exports().addReturnType(CraneliftBridge.TYPE_I64); + } else { + for (ValType ret : funcType.returns()) { + bridge.exports().addReturnType(EmitContext.valTypeToBridgeType(ret)); + } + } + + bridge.exports().buildFunction(); + + // Create entry block + int entry = bridge.exports().createBlock(); + bridge.exports().appendBlockParamsForFuncParams(entry); + bridge.exports().switchToBlock(entry); + + // Get params + int memBaseParam = bridge.exports().funcParam(entry, 0); + int ctxPtrParam = bridge.exports().funcParam(entry, 1); + int[] paramVals = new int[funcType.params().size()]; + for (int i = 0; i < paramVals.length; i++) { + paramVals[i] = bridge.exports().funcParam(entry, i + 2); + } + + // memBase and ctxPtr as variables + int memBaseVar = bridge.exports().declareVar(CraneliftBridge.TYPE_I64); + bridge.exports().defVar(memBaseVar, memBaseParam); + int ctxPtrVar = bridge.exports().declareVar(CraneliftBridge.TYPE_I64); + bridge.exports().defVar(ctxPtrVar, ctxPtrParam); + + // Locals + int numParams = funcType.params().size(); + int numBodyLocals = body.localTypes().size(); + int totalLocals = numParams + numBodyLocals; + int[] localVars = new int[totalLocals]; + for (int i = 0; i < numParams; i++) { + localVars[i] = + bridge.exports() + .declareVar(EmitContext.valTypeToBridgeType(funcType.params().get(i))); + bridge.exports().defVar(localVars[i], paramVals[i]); + } + for (int i = 0; i < numBodyLocals; i++) { + ValType localType = body.localTypes().get(i); + localVars[numParams + i] = + bridge.exports().declareVar(EmitContext.valTypeToBridgeType(localType)); + int zero = emitZero(localType); + bridge.exports().defVar(localVars[numParams + i], zero); + } + + // --- Stack depth guard (wasmtime-style) --- + // At function entry: read RSP via get_stack_pointer. + // If STACK_LIMIT == 0 (first call): store RSP - 512KB as limit. + // Otherwise: if RSP < STACK_LIMIT → trap "call stack exhausted". + // Cost: 1 load + 1 compare + 1 branch (predicted not-taken). + { + int sp = bridge.exports().emitGetStackPointer(); + int zero = bridge.exports().emitIconst32(0); + int stackLimit = + bridge.exports() + .emitLoadI64( + bridge.exports().useVar(ctxPtrVar), + zero, + CtxBuffer.STACK_LIMIT); + + // Check if limit needs initialization (== 0) + int zeroI64 = bridge.exports().emitIconst64(0, 0); + int needsInit = bridge.exports().emitIcmp(0, stackLimit, zeroI64); // EQ + int initBlock = bridge.exports().createBlock(); + int checkBlock = bridge.exports().createBlock(); + bridge.exports().emitBrif(needsInit, initBlock, checkBlock); + + // Init block: store SP - 512KB as limit, then continue + bridge.exports().switchToBlock(initBlock); + int reserve = bridge.exports().emitIconst64(524288, 0); // 512KB + int newLimit = bridge.exports().emitIsub(sp, reserve); + bridge.exports() + .emitStoreI64( + bridge.exports().useVar(ctxPtrVar), + bridge.exports().emitIconst32(0), + newLimit, + CtxBuffer.STACK_LIMIT); + bridge.exports().emitJump(checkBlock); + + // Check block: compare SP against limit + bridge.exports().switchToBlock(checkBlock); + // Re-load limit (may have been just written) + int limit2 = + bridge.exports() + .emitLoadI64( + bridge.exports().useVar(ctxPtrVar), + bridge.exports().emitIconst32(0), + CtxBuffer.STACK_LIMIT); + int exhausted = bridge.exports().emitIcmp(3, sp, limit2); // LT unsigned + int trapBlock = bridge.exports().createBlock(); + int okBlock = bridge.exports().createBlock(); + bridge.exports().emitBrif(exhausted, trapBlock, okBlock); + + // Trap block: write trap code and return + bridge.exports().switchToBlock(trapBlock); + int ctxVal = bridge.exports().useVar(ctxPtrVar); + int zeroT = bridge.exports().emitIconst32(0); + int code = bridge.exports().emitIconst32(CtxBuffer.TRAP_CALL_STACK_EXHAUSTED); + bridge.exports().emitStoreI32(ctxVal, zeroT, code, CtxBuffer.TRAP_CODE); + if (multiReturn || funcType.returns().isEmpty()) { + if (funcType.returns().isEmpty()) { + bridge.exports().emitReturnVoid(); + } else { + bridge.exports().emitReturn(bridge.exports().emitIconst64(0, 0)); + } + } else { + bridge.exports().emitReturn(emitZero(funcType.returns().get(0))); + } + + bridge.exports().switchToBlock(okBlock); + } + + // --- Interrupt check --- + // If INTERRUPT_FLAG != 0 → trap immediately. + // Cost: 1 load + 1 compare + 1 branch (predicted not-taken). + { + int flag = + bridge.exports() + .emitLoadI64( + bridge.exports().useVar(ctxPtrVar), + bridge.exports().emitIconst32(0), + CtxBuffer.INTERRUPT_FLAG); + int zeroI64 = bridge.exports().emitIconst64(0, 0); + int interrupted = bridge.exports().emitIcmp(1, flag, zeroI64); // NE + int trapBlock = bridge.exports().createBlock(); + int okBlock = bridge.exports().createBlock(); + bridge.exports().emitBrif(interrupted, trapBlock, okBlock); + + bridge.exports().switchToBlock(trapBlock); + int ctxVal = bridge.exports().useVar(ctxPtrVar); + int zeroT = bridge.exports().emitIconst32(0); + int code = bridge.exports().emitIconst32(CtxBuffer.TRAP_INTERRUPTED); + bridge.exports().emitStoreI32(ctxVal, zeroT, code, CtxBuffer.TRAP_CODE); + if (multiReturn || funcType.returns().isEmpty()) { + if (funcType.returns().isEmpty()) { + bridge.exports().emitReturnVoid(); + } else { + bridge.exports().emitReturn(bridge.exports().emitIconst64(0, 0)); + } + } else { + bridge.exports().emitReturn(emitZero(funcType.returns().get(0))); + } + + bridge.exports().switchToBlock(okBlock); + } + + // --- Create emit context --- + var valueStack = new NativeValueStack(); + var ctx = + new EmitContext( + bridge, + valueStack, + module, + numImports, + funcType, + localVars, + memBaseVar, + ctxPtrVar, + new HashMap<>(), + multiReturn, + canonicalTypeMap); + + // --- Emission loop --- + Deque controlStack = new ArrayDeque<>(); + + // Implicit function-level frame + controlStack.push( + new ControlFrame(ControlFrame.Kind.FUNCTION, -1, -1, -1, new int[0], funcType, 0)); + List instructions = body.instructions(); + for (int idx = 0; idx < instructions.size(); idx++) { + AnnotatedInstruction ins = instructions.get(idx); + + if (!controlStack.isEmpty() && controlStack.peek().unreachable) { + switch (ins.opcode()) { + case END: + case ELSE: + // Process normally — these exit the unreachable state + break; + case BLOCK: + case LOOP: + case IF: + // Push dummy frame to keep control stack balanced + controlStack.push( + new ControlFrame( + ins.opcode() == OpCode.IF + ? ControlFrame.Kind.IF + : ins.opcode() == OpCode.LOOP + ? ControlFrame.Kind.LOOP + : ControlFrame.Kind.BLOCK, + -1, + -1, + -1, + new int[0], + FunctionType.empty(), + valueStack.size())); + controlStack.peek().unreachable = true; + continue; + default: + continue; // skip all other instructions + } + } + + if (ins.opcode() == OpCode.END) { + emitEnd(ctx, controlStack); + continue; + } + + emitInstruction(ctx, ins, controlStack); + } + + bridge.exports().sealAllBlocks(); + return bridge.compile(); + } + + // --- Block type decoding --- + + private FunctionType decodeBlockType(AnnotatedInstruction ins) { + long typeId = ins.operands()[0]; + if (typeId == 0x40) { + return FunctionType.empty(); + } + if (ValType.isValid(typeId)) { + return FunctionType.returning(ValType.builder().fromId(typeId).build()); + } + return (FunctionType) module.typeSection().getType((int) typeId); + } + + // --- Control stack helpers --- + + private static ControlFrame getControlFrame(Deque controlStack, int depth) { + Iterator it = controlStack.iterator(); + for (int i = 0; i < depth; i++) { + it.next(); + } + return it.next(); + } + + private int[] appendBlockParams(int blockId, List types) { + int[] paramIds = new int[types.size()]; + for (int i = 0; i < types.size(); i++) { + paramIds[i] = + bridge.exports() + .appendBlockParam( + blockId, EmitContext.valTypeToBridgeType(types.get(i))); + } + return paramIds; + } + + private void emitJumpToBlock(int blockId, int argCount, NativeValueStack valueStack) { + if (argCount == 0) { + bridge.exports().emitJump(blockId); + } else if (argCount == 1) { + bridge.exports().emitJumpWithArg(blockId, valueStack.pop()); + } else { + int[] args = new int[argCount]; + for (int i = argCount - 1; i >= 0; i--) { + args[i] = valueStack.pop(); + } + for (int a : args) { + bridge.exports().pushCallArg(a); + } + bridge.exports().emitJumpWithArgs(blockId); + } + } + + private void emitDeadPredecessor(int targetBlock, List types) { + int deadBlock = bridge.exports().createBlock(); + bridge.exports().switchToBlock(deadBlock); + if (types.size() == 1) { + bridge.exports().emitJumpWithArg(targetBlock, emitZero(types.get(0))); + } else { + for (ValType t : types) { + bridge.exports().pushCallArg(emitZero(t)); + } + bridge.exports().emitJumpWithArgs(targetBlock); + } + } + + private int emitZero(ValType type) { + if (type.equals(ValType.I32)) { + return bridge.exports().emitIconst32(0); + } + if (type.equals(ValType.I64)) { + return bridge.exports().emitIconst64(0, 0); + } + if (type.equals(ValType.F32)) { + return bridge.exports().emitF32const(0); + } + if (type.equals(ValType.F64)) { + return bridge.exports().emitF64const(0, 0); + } + // Reference types use i64 representation + int op = type.opcode(); + if (op == ValType.ID.RefNull || op == ValType.ID.Ref) { + return bridge.exports().emitIconst64(0, 0); + } + throw new UnsupportedOperationException("Unsupported type: " + type); + } + + // --- Instruction emission --- + + private void emitInstruction( + EmitContext ctx, AnnotatedInstruction ins, Deque controlStack) { + + var valueStack = ctx.valueStack; + + switch (ins.opcode()) { + // --- Constants --- + case I32_CONST: + NativeEmitters.emitI32Const(ctx, ins); + break; + case I64_CONST: + NativeEmitters.emitI64Const(ctx, ins); + break; + case F32_CONST: + NativeEmitters.emitF32Const(ctx, ins); + break; + case F64_CONST: + NativeEmitters.emitF64Const(ctx, ins); + break; + + // --- i32 Arithmetic --- + case I32_ADD: + NativeEmitters.emitI32BinaryOp(ctx, 0); + break; + case I32_SUB: + NativeEmitters.emitI32BinaryOp(ctx, 1); + break; + case I32_MUL: + NativeEmitters.emitI32BinaryOp(ctx, 2); + break; + case I32_DIV_S: + NativeEmitters.emitSafeDiv(ctx, true, false, false); + break; + case I32_DIV_U: + NativeEmitters.emitSafeDiv(ctx, false, false, false); + break; + case I32_REM_S: + NativeEmitters.emitSafeDiv(ctx, true, true, false); + break; + case I32_REM_U: + NativeEmitters.emitSafeDiv(ctx, false, true, false); + break; + case I32_AND: + NativeEmitters.emitI32BinaryOp(ctx, 3); + break; + case I32_OR: + NativeEmitters.emitI32BinaryOp(ctx, 4); + break; + case I32_XOR: + NativeEmitters.emitI32BinaryOp(ctx, 5); + break; + case I32_SHL: + NativeEmitters.emitI32BinaryOp(ctx, 6); + break; + case I32_SHR_S: + NativeEmitters.emitI32BinaryOp(ctx, 7); + break; + case I32_SHR_U: + NativeEmitters.emitI32BinaryOp(ctx, 8); + break; + case I32_ROTL: + NativeEmitters.emitI32BinaryOp(ctx, 9); + break; + case I32_ROTR: + NativeEmitters.emitI32BinaryOp(ctx, 10); + break; + case I32_CLZ: + NativeEmitters.emitI32UnaryOp(ctx, 0); + break; + case I32_CTZ: + NativeEmitters.emitI32UnaryOp(ctx, 1); + break; + case I32_POPCNT: + NativeEmitters.emitI32UnaryOp(ctx, 2); + break; + + // --- i32 Comparisons --- + case I32_EQZ: + NativeEmitters.emitI32UnaryOp(ctx, 3); + break; + case I32_EQ: + NativeEmitters.emitIcmp(ctx, 0); + break; + case I32_NE: + NativeEmitters.emitIcmp(ctx, 1); + break; + case I32_LT_S: + NativeEmitters.emitIcmp(ctx, 2); + break; + case I32_LT_U: + NativeEmitters.emitIcmp(ctx, 3); + break; + case I32_GT_S: + NativeEmitters.emitIcmp(ctx, 4); + break; + case I32_GT_U: + NativeEmitters.emitIcmp(ctx, 5); + break; + case I32_LE_S: + NativeEmitters.emitIcmp(ctx, 6); + break; + case I32_LE_U: + NativeEmitters.emitIcmp(ctx, 7); + break; + case I32_GE_S: + NativeEmitters.emitIcmp(ctx, 8); + break; + case I32_GE_U: + NativeEmitters.emitIcmp(ctx, 9); + break; + + // --- i32 Extensions --- + case I32_EXTEND_8_S: + NativeEmitters.emitI32Extend8S(ctx); + break; + case I32_EXTEND_16_S: + NativeEmitters.emitI32Extend16S(ctx); + break; + + // --- i64 Arithmetic --- + case I64_ADD: + NativeEmitters.emitI32BinaryOp(ctx, 0); + break; + case I64_SUB: + NativeEmitters.emitI32BinaryOp(ctx, 1); + break; + case I64_MUL: + NativeEmitters.emitI32BinaryOp(ctx, 2); + break; + case I64_DIV_S: + NativeEmitters.emitSafeDiv(ctx, true, false, true); + break; + case I64_DIV_U: + NativeEmitters.emitSafeDiv(ctx, false, false, true); + break; + case I64_REM_S: + NativeEmitters.emitSafeDiv(ctx, true, true, true); + break; + case I64_REM_U: + NativeEmitters.emitSafeDiv(ctx, false, true, true); + break; + case I64_AND: + NativeEmitters.emitI32BinaryOp(ctx, 3); + break; + case I64_OR: + NativeEmitters.emitI32BinaryOp(ctx, 4); + break; + case I64_XOR: + NativeEmitters.emitI32BinaryOp(ctx, 5); + break; + case I64_SHL: + NativeEmitters.emitI32BinaryOp(ctx, 6); + break; + case I64_SHR_S: + NativeEmitters.emitI32BinaryOp(ctx, 7); + break; + case I64_SHR_U: + NativeEmitters.emitI32BinaryOp(ctx, 8); + break; + case I64_ROTL: + NativeEmitters.emitI32BinaryOp(ctx, 9); + break; + case I64_ROTR: + NativeEmitters.emitI32BinaryOp(ctx, 10); + break; + case I64_CLZ: + NativeEmitters.emitI64UnaryOp(ctx, 0); + break; + case I64_CTZ: + NativeEmitters.emitI64UnaryOp(ctx, 1); + break; + case I64_POPCNT: + NativeEmitters.emitI64UnaryOp(ctx, 2); + break; + + // --- i64 Comparisons --- + case I64_EQZ: + NativeEmitters.emitI64UnaryOp(ctx, 3); + break; + case I64_EQ: + NativeEmitters.emitIcmp(ctx, 0); + break; + case I64_NE: + NativeEmitters.emitIcmp(ctx, 1); + break; + case I64_LT_S: + NativeEmitters.emitIcmp(ctx, 2); + break; + case I64_LT_U: + NativeEmitters.emitIcmp(ctx, 3); + break; + case I64_GT_S: + NativeEmitters.emitIcmp(ctx, 4); + break; + case I64_GT_U: + NativeEmitters.emitIcmp(ctx, 5); + break; + case I64_LE_S: + NativeEmitters.emitIcmp(ctx, 6); + break; + case I64_LE_U: + NativeEmitters.emitIcmp(ctx, 7); + break; + case I64_GE_S: + NativeEmitters.emitIcmp(ctx, 8); + break; + case I64_GE_U: + NativeEmitters.emitIcmp(ctx, 9); + break; + + // --- i64 Extensions --- + case I64_EXTEND_I32_S: + NativeEmitters.emitI64ExtendI32S(ctx); + break; + case I64_EXTEND_I32_U: + NativeEmitters.emitI64ExtendI32U(ctx); + break; + case I64_EXTEND_8_S: + NativeEmitters.emitI64Extend8S(ctx); + break; + case I64_EXTEND_16_S: + NativeEmitters.emitI64Extend16S(ctx); + break; + case I64_EXTEND_32_S: + NativeEmitters.emitI64Extend32S(ctx); + break; + case I32_WRAP_I64: + NativeEmitters.emitI32WrapI64(ctx); + break; + + // --- Memory loads --- + case I32_LOAD: + NativeEmitters.emitLoad(ctx, ins, 0); + break; + case I64_LOAD: + NativeEmitters.emitLoad(ctx, ins, 1); + break; + case F32_LOAD: + NativeEmitters.emitLoad(ctx, ins, 2); + break; + case F64_LOAD: + NativeEmitters.emitLoad(ctx, ins, 3); + break; + case I32_LOAD8_U: + NativeEmitters.emitLoad(ctx, ins, 4); + break; + case I32_LOAD8_S: + NativeEmitters.emitLoad(ctx, ins, 5); + break; + case I32_LOAD16_U: + NativeEmitters.emitLoad(ctx, ins, 6); + break; + case I32_LOAD16_S: + NativeEmitters.emitLoad(ctx, ins, 7); + break; + case I64_LOAD8_U: + NativeEmitters.emitLoad(ctx, ins, 8); + break; + case I64_LOAD8_S: + NativeEmitters.emitLoad(ctx, ins, 9); + break; + case I64_LOAD16_U: + NativeEmitters.emitLoad(ctx, ins, 10); + break; + case I64_LOAD16_S: + NativeEmitters.emitLoad(ctx, ins, 11); + break; + case I64_LOAD32_U: + NativeEmitters.emitLoad(ctx, ins, 12); + break; + case I64_LOAD32_S: + NativeEmitters.emitLoad(ctx, ins, 13); + break; + + // --- Memory stores --- + case I32_STORE: + NativeEmitters.emitStore(ctx, ins, 0); + break; + case I64_STORE: + NativeEmitters.emitStore(ctx, ins, 1); + break; + case F32_STORE: + NativeEmitters.emitStore(ctx, ins, 2); + break; + case F64_STORE: + NativeEmitters.emitStore(ctx, ins, 3); + break; + case I32_STORE8: + NativeEmitters.emitStore(ctx, ins, 4); + break; + case I32_STORE16: + NativeEmitters.emitStore(ctx, ins, 5); + break; + case I64_STORE8: + NativeEmitters.emitStore(ctx, ins, 6); + break; + case I64_STORE16: + NativeEmitters.emitStore(ctx, ins, 7); + break; + case I64_STORE32: + NativeEmitters.emitStore(ctx, ins, 8); + break; + + // --- Locals --- + case LOCAL_GET: + NativeEmitters.emitLocalGet(ctx, ins); + break; + case LOCAL_SET: + NativeEmitters.emitLocalSet(ctx, ins); + break; + case LOCAL_TEE: + NativeEmitters.emitLocalTee(ctx, ins); + break; + + // --- Select --- + case SELECT: + case SELECT_T: + NativeEmitters.emitSelect(ctx); + break; + + // --- Globals --- + case GLOBAL_GET: + NativeEmitters.emitGlobalGet(ctx, ins); + break; + case GLOBAL_SET: + NativeEmitters.emitGlobalSet(ctx, ins); + break; + + // --- Memory operations --- + case MEMORY_SIZE: + NativeEmitters.emitMemorySize(ctx); + break; + case MEMORY_GROW: + NativeEmitters.emitMemoryGrow(ctx); + break; + case MEMORY_COPY: + NativeEmitters.emitMemoryCopy(ctx); + break; + case MEMORY_FILL: + NativeEmitters.emitMemoryFill(ctx); + break; + case MEMORY_INIT: + NativeEmitters.emitMemoryInit(ctx, ins); + break; + case DATA_DROP: + NativeEmitters.emitDataDrop(ctx, ins); + break; + + // --- Misc --- + case NOP: + break; + case DROP: + valueStack.pop(); + break; + case UNREACHABLE: + NativeEmitters.emitUnreachable(ctx); + controlStack.peek().unreachable = true; + break; + + // --- f32 Arithmetic --- + case F32_ADD: + NativeEmitters.emitFloatBinaryOp(ctx, 0); + break; + case F32_SUB: + NativeEmitters.emitFloatBinaryOp(ctx, 1); + break; + case F32_MUL: + NativeEmitters.emitFloatBinaryOp(ctx, 2); + break; + case F32_DIV: + NativeEmitters.emitFloatBinaryOp(ctx, 3); + break; + case F32_MIN: + NativeEmitters.emitFloatBinaryOp(ctx, 4); + break; + case F32_MAX: + NativeEmitters.emitFloatBinaryOp(ctx, 5); + break; + case F32_COPYSIGN: + NativeEmitters.emitFloatBinaryOp(ctx, 6); + break; + case F32_ABS: + NativeEmitters.emitFloatUnaryOp(ctx, 0); + break; + case F32_NEG: + NativeEmitters.emitFloatUnaryOp(ctx, 1); + break; + case F32_CEIL: + NativeEmitters.emitFloatUnaryOp(ctx, 2); + break; + case F32_FLOOR: + NativeEmitters.emitFloatUnaryOp(ctx, 3); + break; + case F32_TRUNC: + NativeEmitters.emitFloatUnaryOp(ctx, 4); + break; + case F32_NEAREST: + NativeEmitters.emitFloatUnaryOp(ctx, 5); + break; + case F32_SQRT: + NativeEmitters.emitFloatUnaryOp(ctx, 6); + break; + + // --- f32 Comparisons --- + case F32_EQ: + NativeEmitters.emitFcmp(ctx, 0); + break; + case F32_NE: + NativeEmitters.emitFcmp(ctx, 1); + break; + case F32_LT: + NativeEmitters.emitFcmp(ctx, 2); + break; + case F32_GT: + NativeEmitters.emitFcmp(ctx, 3); + break; + case F32_LE: + NativeEmitters.emitFcmp(ctx, 4); + break; + case F32_GE: + NativeEmitters.emitFcmp(ctx, 5); + break; + + // --- f64 Arithmetic --- + case F64_ADD: + NativeEmitters.emitFloatBinaryOp(ctx, 0); + break; + case F64_SUB: + NativeEmitters.emitFloatBinaryOp(ctx, 1); + break; + case F64_MUL: + NativeEmitters.emitFloatBinaryOp(ctx, 2); + break; + case F64_DIV: + NativeEmitters.emitFloatBinaryOp(ctx, 3); + break; + case F64_MIN: + NativeEmitters.emitFloatBinaryOp(ctx, 4); + break; + case F64_MAX: + NativeEmitters.emitFloatBinaryOp(ctx, 5); + break; + case F64_COPYSIGN: + NativeEmitters.emitFloatBinaryOp(ctx, 6); + break; + case F64_ABS: + NativeEmitters.emitFloatUnaryOp(ctx, 0); + break; + case F64_NEG: + NativeEmitters.emitFloatUnaryOp(ctx, 1); + break; + case F64_CEIL: + NativeEmitters.emitFloatUnaryOp(ctx, 2); + break; + case F64_FLOOR: + NativeEmitters.emitFloatUnaryOp(ctx, 3); + break; + case F64_TRUNC: + NativeEmitters.emitFloatUnaryOp(ctx, 4); + break; + case F64_NEAREST: + NativeEmitters.emitFloatUnaryOp(ctx, 5); + break; + case F64_SQRT: + NativeEmitters.emitFloatUnaryOp(ctx, 6); + break; + + // --- f64 Comparisons --- + case F64_EQ: + NativeEmitters.emitFcmp(ctx, 0); + break; + case F64_NE: + NativeEmitters.emitFcmp(ctx, 1); + break; + case F64_LT: + NativeEmitters.emitFcmp(ctx, 2); + break; + case F64_GT: + NativeEmitters.emitFcmp(ctx, 3); + break; + case F64_LE: + NativeEmitters.emitFcmp(ctx, 4); + break; + case F64_GE: + NativeEmitters.emitFcmp(ctx, 5); + break; + + // --- Conversions --- + case I32_TRUNC_F32_S: + NativeEmitters.emitSafeTrunc( + ctx, CraneliftBridge.TYPE_I32, CraneliftBridge.TYPE_F32, true); + break; + case I32_TRUNC_F32_U: + NativeEmitters.emitSafeTrunc( + ctx, CraneliftBridge.TYPE_I32, CraneliftBridge.TYPE_F32, false); + break; + case I32_TRUNC_F64_S: + NativeEmitters.emitSafeTrunc( + ctx, CraneliftBridge.TYPE_I32, CraneliftBridge.TYPE_F64, true); + break; + case I32_TRUNC_F64_U: + NativeEmitters.emitSafeTrunc( + ctx, CraneliftBridge.TYPE_I32, CraneliftBridge.TYPE_F64, false); + break; + case I64_TRUNC_F32_S: + NativeEmitters.emitSafeTrunc( + ctx, CraneliftBridge.TYPE_I64, CraneliftBridge.TYPE_F32, true); + break; + case I64_TRUNC_F32_U: + NativeEmitters.emitSafeTrunc( + ctx, CraneliftBridge.TYPE_I64, CraneliftBridge.TYPE_F32, false); + break; + case I64_TRUNC_F64_S: + NativeEmitters.emitSafeTrunc( + ctx, CraneliftBridge.TYPE_I64, CraneliftBridge.TYPE_F64, true); + break; + case I64_TRUNC_F64_U: + NativeEmitters.emitSafeTrunc( + ctx, CraneliftBridge.TYPE_I64, CraneliftBridge.TYPE_F64, false); + break; + + case I32_TRUNC_SAT_F32_S: + NativeEmitters.emitTruncSat(ctx, CraneliftBridge.TYPE_I32, true); + break; + case I32_TRUNC_SAT_F32_U: + NativeEmitters.emitTruncSat(ctx, CraneliftBridge.TYPE_I32, false); + break; + case I32_TRUNC_SAT_F64_S: + NativeEmitters.emitTruncSat(ctx, CraneliftBridge.TYPE_I32, true); + break; + case I32_TRUNC_SAT_F64_U: + NativeEmitters.emitTruncSat(ctx, CraneliftBridge.TYPE_I32, false); + break; + case I64_TRUNC_SAT_F32_S: + NativeEmitters.emitTruncSat(ctx, CraneliftBridge.TYPE_I64, true); + break; + case I64_TRUNC_SAT_F32_U: + NativeEmitters.emitTruncSat(ctx, CraneliftBridge.TYPE_I64, false); + break; + case I64_TRUNC_SAT_F64_S: + NativeEmitters.emitTruncSat(ctx, CraneliftBridge.TYPE_I64, true); + break; + case I64_TRUNC_SAT_F64_U: + NativeEmitters.emitTruncSat(ctx, CraneliftBridge.TYPE_I64, false); + break; + + case F32_CONVERT_I32_S: + NativeEmitters.emitConvertFloat(ctx, CraneliftBridge.TYPE_F32, true); + break; + case F32_CONVERT_I32_U: + NativeEmitters.emitConvertFloat(ctx, CraneliftBridge.TYPE_F32, false); + break; + case F32_CONVERT_I64_S: + NativeEmitters.emitConvertFloat(ctx, CraneliftBridge.TYPE_F32, true); + break; + case F32_CONVERT_I64_U: + NativeEmitters.emitConvertFloat(ctx, CraneliftBridge.TYPE_F32, false); + break; + case F64_CONVERT_I32_S: + NativeEmitters.emitConvertFloat(ctx, CraneliftBridge.TYPE_F64, true); + break; + case F64_CONVERT_I32_U: + NativeEmitters.emitConvertFloat(ctx, CraneliftBridge.TYPE_F64, false); + break; + case F64_CONVERT_I64_S: + NativeEmitters.emitConvertFloat(ctx, CraneliftBridge.TYPE_F64, true); + break; + case F64_CONVERT_I64_U: + NativeEmitters.emitConvertFloat(ctx, CraneliftBridge.TYPE_F64, false); + break; + + case F64_PROMOTE_F32: + NativeEmitters.emitFpromote(ctx); + break; + case F32_DEMOTE_F64: + NativeEmitters.emitFdemote(ctx); + break; + + case F32_REINTERPRET_I32: + NativeEmitters.emitBitcastI32ToF32(ctx); + break; + case I32_REINTERPRET_F32: + NativeEmitters.emitBitcastF32ToI32(ctx); + break; + case F64_REINTERPRET_I64: + NativeEmitters.emitBitcastI64ToF64(ctx); + break; + case I64_REINTERPRET_F64: + NativeEmitters.emitBitcastF64ToI64(ctx); + break; + + // --- Calls --- + case CALL: + NativeEmitters.emitCall(ctx, ins); + break; + case CALL_INDIRECT: + NativeEmitters.emitCallIndirect(ctx, ins); + break; + case RETURN_CALL: + NativeEmitters.emitReturnCall(ctx, ins); + controlStack.peek().unreachable = true; + break; + case RETURN_CALL_INDIRECT: + NativeEmitters.emitReturnCallIndirect(ctx, ins); + controlStack.peek().unreachable = true; + break; + + // --- Control flow (stays in orchestrator) --- + case BLOCK: + emitBlock(ctx, ins, controlStack); + break; + case LOOP: + emitLoop(ctx, ins, controlStack); + break; + case IF: + emitIf(ctx, ins, controlStack); + break; + case ELSE: + emitElse(ctx, controlStack); + break; + // END is handled in the main loop, not here + case BR: + emitBr(ctx, ins, controlStack); + break; + case BR_IF: + emitBrIf(ctx, ins, controlStack); + break; + case BR_TABLE: + emitBrTable(ctx, ins, controlStack); + break; + case RETURN: + emitReturn(ctx, controlStack); + break; + + // --- Table operations --- + case TABLE_GET: + NativeEmitters.emitTableGet(ctx, ins); + break; + case TABLE_SET: + NativeEmitters.emitTableSet(ctx, ins); + break; + case TABLE_SIZE: + NativeEmitters.emitTableSize(ctx, ins); + break; + case TABLE_GROW: + NativeEmitters.emitTableGrow(ctx, ins); + break; + case TABLE_FILL: + NativeEmitters.emitTableFill(ctx, ins); + break; + case TABLE_COPY: + NativeEmitters.emitTableCopy(ctx, ins); + break; + case TABLE_INIT: + NativeEmitters.emitTableInit(ctx, ins); + break; + case ELEM_DROP: + NativeEmitters.emitElemDrop(ctx, ins); + break; + + // --- Atomic loads --- + case I32_ATOMIC_LOAD: + NativeEmitters.emitAtomicLoad(ctx, ins, 0); + break; + case I64_ATOMIC_LOAD: + NativeEmitters.emitAtomicLoad(ctx, ins, 1); + break; + case I32_ATOMIC_LOAD8_U: + NativeEmitters.emitAtomicLoad(ctx, ins, 2); + break; + case I32_ATOMIC_LOAD16_U: + NativeEmitters.emitAtomicLoad(ctx, ins, 3); + break; + case I64_ATOMIC_LOAD8_U: + NativeEmitters.emitAtomicLoad(ctx, ins, 4); + break; + case I64_ATOMIC_LOAD16_U: + NativeEmitters.emitAtomicLoad(ctx, ins, 5); + break; + case I64_ATOMIC_LOAD32_U: + NativeEmitters.emitAtomicLoad(ctx, ins, 6); + break; + + // --- Atomic stores --- + case I32_ATOMIC_STORE: + NativeEmitters.emitAtomicStore(ctx, ins, 0); + break; + case I64_ATOMIC_STORE: + NativeEmitters.emitAtomicStore(ctx, ins, 1); + break; + case I32_ATOMIC_STORE8: + NativeEmitters.emitAtomicStore(ctx, ins, 2); + break; + case I32_ATOMIC_STORE16: + NativeEmitters.emitAtomicStore(ctx, ins, 3); + break; + case I64_ATOMIC_STORE8: + NativeEmitters.emitAtomicStore(ctx, ins, 4); + break; + case I64_ATOMIC_STORE16: + NativeEmitters.emitAtomicStore(ctx, ins, 5); + break; + case I64_ATOMIC_STORE32: + NativeEmitters.emitAtomicStore(ctx, ins, 6); + break; + + // --- Atomic RMW add --- + case I32_ATOMIC_RMW_ADD: + NativeEmitters.emitAtomicRmw(ctx, ins, 0, 0); + break; + case I64_ATOMIC_RMW_ADD: + NativeEmitters.emitAtomicRmw(ctx, ins, 1, 0); + break; + case I32_ATOMIC_RMW8_ADD_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 2, 0); + break; + case I32_ATOMIC_RMW16_ADD_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 3, 0); + break; + case I64_ATOMIC_RMW8_ADD_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 4, 0); + break; + case I64_ATOMIC_RMW16_ADD_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 5, 0); + break; + case I64_ATOMIC_RMW32_ADD_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 6, 0); + break; + + // --- Atomic RMW sub --- + case I32_ATOMIC_RMW_SUB: + NativeEmitters.emitAtomicRmw(ctx, ins, 0, 1); + break; + case I64_ATOMIC_RMW_SUB: + NativeEmitters.emitAtomicRmw(ctx, ins, 1, 1); + break; + case I32_ATOMIC_RMW8_SUB_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 2, 1); + break; + case I32_ATOMIC_RMW16_SUB_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 3, 1); + break; + case I64_ATOMIC_RMW8_SUB_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 4, 1); + break; + case I64_ATOMIC_RMW16_SUB_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 5, 1); + break; + case I64_ATOMIC_RMW32_SUB_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 6, 1); + break; + + // --- Atomic RMW and --- + case I32_ATOMIC_RMW_AND: + NativeEmitters.emitAtomicRmw(ctx, ins, 0, 2); + break; + case I64_ATOMIC_RMW_AND: + NativeEmitters.emitAtomicRmw(ctx, ins, 1, 2); + break; + case I32_ATOMIC_RMW8_AND_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 2, 2); + break; + case I32_ATOMIC_RMW16_AND_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 3, 2); + break; + case I64_ATOMIC_RMW8_AND_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 4, 2); + break; + case I64_ATOMIC_RMW16_AND_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 5, 2); + break; + case I64_ATOMIC_RMW32_AND_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 6, 2); + break; + + // --- Atomic RMW or --- + case I32_ATOMIC_RMW_OR: + NativeEmitters.emitAtomicRmw(ctx, ins, 0, 3); + break; + case I64_ATOMIC_RMW_OR: + NativeEmitters.emitAtomicRmw(ctx, ins, 1, 3); + break; + case I32_ATOMIC_RMW8_OR_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 2, 3); + break; + case I32_ATOMIC_RMW16_OR_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 3, 3); + break; + case I64_ATOMIC_RMW8_OR_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 4, 3); + break; + case I64_ATOMIC_RMW16_OR_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 5, 3); + break; + case I64_ATOMIC_RMW32_OR_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 6, 3); + break; + + // --- Atomic RMW xor --- + case I32_ATOMIC_RMW_XOR: + NativeEmitters.emitAtomicRmw(ctx, ins, 0, 4); + break; + case I64_ATOMIC_RMW_XOR: + NativeEmitters.emitAtomicRmw(ctx, ins, 1, 4); + break; + case I32_ATOMIC_RMW8_XOR_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 2, 4); + break; + case I32_ATOMIC_RMW16_XOR_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 3, 4); + break; + case I64_ATOMIC_RMW8_XOR_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 4, 4); + break; + case I64_ATOMIC_RMW16_XOR_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 5, 4); + break; + case I64_ATOMIC_RMW32_XOR_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 6, 4); + break; + + // --- Atomic RMW xchg --- + case I32_ATOMIC_RMW_XCHG: + NativeEmitters.emitAtomicRmw(ctx, ins, 0, 5); + break; + case I64_ATOMIC_RMW_XCHG: + NativeEmitters.emitAtomicRmw(ctx, ins, 1, 5); + break; + case I32_ATOMIC_RMW8_XCHG_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 2, 5); + break; + case I32_ATOMIC_RMW16_XCHG_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 3, 5); + break; + case I64_ATOMIC_RMW8_XCHG_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 4, 5); + break; + case I64_ATOMIC_RMW16_XCHG_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 5, 5); + break; + case I64_ATOMIC_RMW32_XCHG_U: + NativeEmitters.emitAtomicRmw(ctx, ins, 6, 5); + break; + + // --- Atomic cmpxchg --- + case I32_ATOMIC_RMW_CMPXCHG: + NativeEmitters.emitAtomicCmpxchg(ctx, ins, 0); + break; + case I64_ATOMIC_RMW_CMPXCHG: + NativeEmitters.emitAtomicCmpxchg(ctx, ins, 1); + break; + case I32_ATOMIC_RMW8_CMPXCHG_U: + NativeEmitters.emitAtomicCmpxchg(ctx, ins, 2); + break; + case I32_ATOMIC_RMW16_CMPXCHG_U: + NativeEmitters.emitAtomicCmpxchg(ctx, ins, 3); + break; + case I64_ATOMIC_RMW8_CMPXCHG_U: + NativeEmitters.emitAtomicCmpxchg(ctx, ins, 4); + break; + case I64_ATOMIC_RMW16_CMPXCHG_U: + NativeEmitters.emitAtomicCmpxchg(ctx, ins, 5); + break; + case I64_ATOMIC_RMW32_CMPXCHG_U: + NativeEmitters.emitAtomicCmpxchg(ctx, ins, 6); + break; + + // --- Atomic wait/notify/fence --- + case MEM_ATOMIC_WAIT32: + NativeEmitters.emitAtomicWait32(ctx, ins); + break; + case MEM_ATOMIC_WAIT64: + NativeEmitters.emitAtomicWait64(ctx, ins); + break; + case MEM_ATOMIC_NOTIFY: + NativeEmitters.emitAtomicNotify(ctx, ins); + break; + case ATOMIC_FENCE: + NativeEmitters.emitAtomicFence(ctx); + break; + + // --- Reference types --- + case REF_NULL: + NativeEmitters.emitRefNull(ctx); + break; + case REF_IS_NULL: + NativeEmitters.emitRefIsNull(ctx); + break; + case REF_FUNC: + NativeEmitters.emitRefFunc(ctx, ins); + break; + + default: + throw new UnsupportedOperationException( + "Opcode not yet supported: " + ins.opcode()); + } + } + + // --- Control flow handlers --- + + private void emitBlock( + EmitContext ctx, AnnotatedInstruction ins, Deque controlStack) { + FunctionType bt = decodeBlockType(ins); + int mergeBlock = bridge.exports().createBlock(); + int[] mergeParamIds = appendBlockParams(mergeBlock, bt.returns()); + int savedHeight = ctx.valueStack.size() - bt.params().size(); + controlStack.push( + new ControlFrame( + ControlFrame.Kind.BLOCK, + mergeBlock, + -1, + -1, + mergeParamIds, + bt, + savedHeight)); + } + + private void emitLoop( + EmitContext ctx, AnnotatedInstruction ins, Deque controlStack) { + FunctionType bt = decodeBlockType(ins); + int loopHeader = bridge.exports().createBlock(); + int mergeBlock = bridge.exports().createBlock(); + int[] mergeParamIds = appendBlockParams(mergeBlock, bt.returns()); + int[] loopParamIds = appendBlockParams(loopHeader, bt.params()); + int savedHeight = ctx.valueStack.size() - bt.params().size(); + emitJumpToBlock(loopHeader, bt.params().size(), ctx.valueStack); + bridge.exports().switchToBlock(loopHeader); + for (int pid : loopParamIds) { + ctx.valueStack.push(pid); + } + + // --- Interrupt check at loop header --- + { + int flag = + bridge.exports() + .emitLoadI64( + bridge.exports().useVar(ctx.ctxPtrVar), + bridge.exports().emitIconst32(0), + CtxBuffer.INTERRUPT_FLAG); + int zeroI64 = bridge.exports().emitIconst64(0, 0); + int interrupted = bridge.exports().emitIcmp(1, flag, zeroI64); // NE + int trapBlock = bridge.exports().createBlock(); + int okBlock = bridge.exports().createBlock(); + bridge.exports().emitBrif(interrupted, trapBlock, okBlock); + + NativeEmitters.fillTrapBlock(ctx, trapBlock, CtxBuffer.TRAP_INTERRUPTED); + + bridge.exports().switchToBlock(okBlock); + } + + controlStack.push( + new ControlFrame( + ControlFrame.Kind.LOOP, + mergeBlock, + loopHeader, + -1, + mergeParamIds, + bt, + savedHeight)); + } + + private void emitIf( + EmitContext ctx, AnnotatedInstruction ins, Deque controlStack) { + FunctionType bt = decodeBlockType(ins); + int condition = ctx.valueStack.pop(); + int thenBlock = bridge.exports().createBlock(); + int elseBlock = bridge.exports().createBlock(); + int mergeBlock = bridge.exports().createBlock(); + int[] mergeParamIds = appendBlockParams(mergeBlock, bt.returns()); + int savedHeight = ctx.valueStack.size() - bt.params().size(); + + int[] elseParamIds = null; + if (bt.params().isEmpty()) { + bridge.exports().emitBrif(condition, thenBlock, elseBlock); + bridge.exports().switchToBlock(thenBlock); + bridge.exports().sealBlock(thenBlock); + } else { + // Block has params — both branches need them. + // brif only passes args to the true target, so we use a + // fallthrough block for the false path. + int[] thenParamIds = appendBlockParams(thenBlock, bt.params()); + elseParamIds = appendBlockParams(elseBlock, bt.params()); + + int paramCount = bt.params().size(); + int[] paramVals = new int[paramCount]; + for (int i = paramCount - 1; i >= 0; i--) { + paramVals[i] = ctx.valueStack.pop(); + } + + // brif → thenBlock (with args) / fallthroughBlock (no args) + int fallthroughBlock = bridge.exports().createBlock(); + for (int pv : paramVals) { + bridge.exports().pushCallArg(pv); + } + bridge.exports().emitBrifWithJumpArgs(condition, thenBlock, fallthroughBlock); + + // Fallthrough → elseBlock (with args) + bridge.exports().switchToBlock(fallthroughBlock); + if (paramCount == 1) { + bridge.exports().emitJumpWithArg(elseBlock, paramVals[0]); + } else { + for (int pv : paramVals) { + bridge.exports().pushCallArg(pv); + } + bridge.exports().emitJumpWithArgs(elseBlock); + } + + // Switch to then block, push its block params onto value stack + bridge.exports().switchToBlock(thenBlock); + bridge.exports().sealBlock(thenBlock); + for (int pid : thenParamIds) { + ctx.valueStack.push(pid); + } + } + + var frame = + new ControlFrame( + ControlFrame.Kind.IF, + mergeBlock, + -1, + elseBlock, + mergeParamIds, + bt, + savedHeight); + frame.elseParamIds = elseParamIds; + controlStack.push(frame); + } + + private void emitElse(EmitContext ctx, Deque controlStack) { + ControlFrame frame = controlStack.peek(); + if (frame.mergeBlock < 0) { + frame.hasElse = true; + return; + } + if (!frame.unreachable) { + emitJumpToBlock(frame.mergeBlock, frame.blockType.returns().size(), ctx.valueStack); + } + ctx.valueStack.trimTo(frame.stackHeight); + bridge.exports().switchToBlock(frame.elseBlock); + bridge.exports().sealBlock(frame.elseBlock); + + // Push else block's param IDs for the else branch + if (frame.elseParamIds != null) { + for (int pid : frame.elseParamIds) { + ctx.valueStack.push(pid); + } + } + + frame.hasElse = true; + frame.unreachable = false; + } + + private void emitEnd(EmitContext ctx, Deque controlStack) { + ControlFrame frame = controlStack.pop(); + var valueStack = ctx.valueStack; + boolean isDummy = frame.mergeBlock < 0 && frame.kind != ControlFrame.Kind.FUNCTION; + boolean dead = frame.unreachable; + + switch (frame.kind) { + case FUNCTION: + if (!dead) { + emitFuncReturn(ctx, frame.blockType); + } + // If dead, the block is already terminated (by BR/RETURN at + // function level). No return needed — inner block ENDs switch + // to merge blocks and reset unreachable, so they go through + // the !dead path above. + + break; + + case BLOCK: + case LOOP: + if (isDummy) { + break; + } + if (!dead) { + emitJumpToBlock(frame.mergeBlock, frame.blockType.returns().size(), valueStack); + } else if (frame.mergeParamIds.length > 0) { + emitDeadPredecessor(frame.mergeBlock, frame.blockType.returns()); + } + bridge.exports().switchToBlock(frame.mergeBlock); + bridge.exports().sealBlock(frame.mergeBlock); + // Always use trimTo + push: merge block params are the canonical + // values regardless of whether the block was reachable. + valueStack.trimTo(frame.stackHeight); + for (int pid : frame.mergeParamIds) { + valueStack.push(pid); + } + + break; + + case IF: + if (isDummy) { + break; + } + if (!frame.hasElse) { + if (!dead) { + emitJumpToBlock( + frame.mergeBlock, frame.blockType.returns().size(), valueStack); + } + bridge.exports().switchToBlock(frame.elseBlock); + bridge.exports().sealBlock(frame.elseBlock); + // Implicit else: pass block params through to merge. + // For IF without ELSE, params == returns per Wasm spec. + if (frame.elseParamIds != null && frame.elseParamIds.length > 0) { + // Use else block's own param values + if (frame.elseParamIds.length == 1) { + bridge.exports() + .emitJumpWithArg(frame.mergeBlock, frame.elseParamIds[0]); + } else { + for (int pid : frame.elseParamIds) { + bridge.exports().pushCallArg(pid); + } + bridge.exports().emitJumpWithArgs(frame.mergeBlock); + } + } else if (frame.blockType.returns().isEmpty()) { + bridge.exports().emitJump(frame.mergeBlock); + } else { + // No else params (paramless IF) — use dummy zeros + for (ValType t : frame.blockType.returns()) { + bridge.exports().pushCallArg(emitZero(t)); + } + bridge.exports().emitJumpWithArgs(frame.mergeBlock); + } + } else { + if (!dead) { + emitJumpToBlock( + frame.mergeBlock, frame.blockType.returns().size(), valueStack); + } else if (frame.mergeParamIds.length > 0) { + emitDeadPredecessor(frame.mergeBlock, frame.blockType.returns()); + } + } + bridge.exports().switchToBlock(frame.mergeBlock); + bridge.exports().sealBlock(frame.mergeBlock); + valueStack.trimTo(frame.stackHeight); + for (int pid : frame.mergeParamIds) { + valueStack.push(pid); + } + + break; + } + if (!controlStack.isEmpty() && !isDummy) { + controlStack.peek().unreachable = false; + } + } + + private void emitFuncReturn(EmitContext ctx, FunctionType funcType) { + int retCount = funcType.returns().size(); + var valueStack = ctx.valueStack; + if (retCount == 0) { + bridge.exports().emitReturnVoid(); + } else if (!ctx.multiReturn && retCount == 1 && !valueStack.isEmpty()) { + bridge.exports().emitReturn(valueStack.pop()); + } else if (retCount >= 1 && valueStack.size() >= retCount) { + int[] retVals = new int[retCount]; + for (int ri = retCount - 1; ri >= 0; ri--) { + retVals[ri] = valueStack.pop(); + } + if (ctx.multiReturn) { + ctx.emitWriteReturnsToArgsBuffer(funcType.returns(), retVals); + bridge.exports().emitReturn(bridge.exports().emitIconst64(0, 0)); + } else { + bridge.exports().emitReturn(retVals[0]); + } + } else { + ctx.emitReturnForFuncType(); + } + } + + private void emitReturnWithArgs(EmitContext ctx, int[] args, int argCount) { + if (argCount == 0) { + bridge.exports().emitReturnVoid(); + } else if (!ctx.multiReturn && argCount == 1) { + bridge.exports().emitReturn(args[0]); + } else if (ctx.multiReturn) { + ctx.emitWriteReturnsToArgsBuffer(ctx.funcType.returns(), args); + bridge.exports().emitReturn(bridge.exports().emitIconst64(0, 0)); + } else { + bridge.exports().emitReturn(args[0]); + } + } + + private void emitBrToFunction(EmitContext ctx, ControlFrame funcFrame) { + int retCount = funcFrame.blockType.returns().size(); + if (retCount == 0) { + bridge.exports().emitReturnVoid(); + } else if (!ctx.multiReturn && retCount == 1) { + bridge.exports().emitReturn(ctx.valueStack.pop()); + } else { + int[] retVals = new int[retCount]; + for (int i = retCount - 1; i >= 0; i--) { + retVals[i] = ctx.valueStack.pop(); + } + if (ctx.multiReturn) { + ctx.emitWriteReturnsToArgsBuffer(funcFrame.blockType.returns(), retVals); + bridge.exports().emitReturn(bridge.exports().emitIconst64(0, 0)); + } else { + bridge.exports().emitReturn(retVals[0]); + } + } + } + + private void emitBr( + EmitContext ctx, AnnotatedInstruction ins, Deque controlStack) { + int depth = (int) ins.operands()[0]; + ControlFrame target = getControlFrame(controlStack, depth); + if (target.kind == ControlFrame.Kind.FUNCTION) { + emitBrToFunction(ctx, target); + } else { + int brTarget = target.branchTarget(); + int argCount = target.branchArgCount(); + emitJumpToBlock(brTarget, argCount, ctx.valueStack); + } + controlStack.peek().unreachable = true; + } + + private void emitBrIf( + EmitContext ctx, AnnotatedInstruction ins, Deque controlStack) { + int depth = (int) ins.operands()[0]; + int condition = ctx.valueStack.pop(); + ControlFrame target = getControlFrame(controlStack, depth); + + if (target.kind == ControlFrame.Kind.FUNCTION) { + // BR_IF targeting function = conditional return + int returnBlock = bridge.exports().createBlock(); + int fallthroughBlock = bridge.exports().createBlock(); + int argCount = target.branchArgCount(); + + if (argCount > 0) { + int[] args = new int[argCount]; + for (int i = argCount - 1; i >= 0; i--) { + args[i] = ctx.valueStack.pop(); + } + // Add block params to returnBlock so brif can pass values + int[] returnParams = new int[argCount]; + var returns = target.blockType.returns(); + for (int i = 0; i < argCount; i++) { + returnParams[i] = + bridge.exports() + .appendBlockParam( + returnBlock, + EmitContext.valTypeToBridgeType(returns.get(i))); + } + for (int a : args) { + bridge.exports().pushCallArg(a); + } + bridge.exports().emitBrifWithJumpArgs(condition, returnBlock, fallthroughBlock); + // Push args back for fallthrough + for (int a : args) { + ctx.valueStack.push(a); + } + // Return block: emit return with block params (not original args) + bridge.exports().switchToBlock(returnBlock); + emitReturnWithArgs(ctx, returnParams, argCount); + } else { + bridge.exports().emitBrif(condition, returnBlock, fallthroughBlock); + bridge.exports().switchToBlock(returnBlock); + bridge.exports().emitReturnVoid(); + } + bridge.exports().switchToBlock(fallthroughBlock); + return; + } + + int brTarget = target.branchTarget(); + int fallthroughBlock = bridge.exports().createBlock(); + int argCount = target.branchArgCount(); + + if (argCount > 0) { + int[] args = new int[argCount]; + for (int i = argCount - 1; i >= 0; i--) { + args[i] = ctx.valueStack.pop(); + } + for (int a : args) { + bridge.exports().pushCallArg(a); + } + bridge.exports().emitBrifWithJumpArgs(condition, brTarget, fallthroughBlock); + for (int a : args) { + ctx.valueStack.push(a); + } + } else { + bridge.exports().emitBrif(condition, brTarget, fallthroughBlock); + } + bridge.exports().switchToBlock(fallthroughBlock); + } + + private void emitBrTable( + EmitContext ctx, AnnotatedInstruction ins, Deque controlStack) { + int index = ctx.valueStack.pop(); + int defaultIdx = ins.operandCount() - 1; + int defaultDepth = (int) ins.operand(defaultIdx); + + ControlFrame defaultTarget = getControlFrame(controlStack, defaultDepth); + int argCount = defaultTarget.branchArgCount(); + int[] brArgs = new int[argCount]; + for (int i = argCount - 1; i >= 0; i--) { + brArgs[i] = ctx.valueStack.pop(); + } + + // Build jump table targets with edge splitting. + // Cranelift's br_table doesn't support block arguments, and FUNCTION + // targets need a return instead of a jump — so we create intermediate + // blocks for any target that needs special handling. + int[] intermediates = new int[defaultIdx]; + boolean[] needsIntermediate = new boolean[defaultIdx]; + for (int i = 0; i < defaultIdx; i++) { + int depth = (int) ins.operand(i); + ControlFrame target = getControlFrame(controlStack, depth); + if (argCount > 0 || target.kind == ControlFrame.Kind.FUNCTION) { + intermediates[i] = bridge.exports().createBlock(); + needsIntermediate[i] = true; + bridge.exports().pushBrTableTarget(intermediates[i]); + } else { + bridge.exports().pushBrTableTarget(target.branchTarget()); + } + } + boolean defaultNeedsIntermediate = + argCount > 0 || defaultTarget.kind == ControlFrame.Kind.FUNCTION; + int defaultJtTarget; + if (defaultNeedsIntermediate) { + defaultJtTarget = bridge.exports().createBlock(); + } else { + defaultJtTarget = defaultTarget.branchTarget(); + } + bridge.exports().emitBrTable(index, defaultJtTarget); + + // Emit intermediate blocks + for (int i = 0; i < defaultIdx; i++) { + if (!needsIntermediate[i]) { + continue; + } + bridge.exports().switchToBlock(intermediates[i]); + bridge.exports().sealBlock(intermediates[i]); + int depth = (int) ins.operand(i); + ControlFrame target = getControlFrame(controlStack, depth); + if (target.kind == ControlFrame.Kind.FUNCTION) { + emitReturnWithArgs(ctx, brArgs, argCount); + } else if (argCount == 0) { + bridge.exports().emitJump(target.branchTarget()); + } else { + emitJumpWithArgs(target.branchTarget(), brArgs, argCount); + } + } + + if (defaultNeedsIntermediate) { + bridge.exports().switchToBlock(defaultJtTarget); + bridge.exports().sealBlock(defaultJtTarget); + if (defaultTarget.kind == ControlFrame.Kind.FUNCTION) { + emitReturnWithArgs(ctx, brArgs, argCount); + } else if (argCount == 0) { + bridge.exports().emitJump(defaultTarget.branchTarget()); + } else { + emitJumpWithArgs(defaultTarget.branchTarget(), brArgs, argCount); + } + } + + controlStack.peek().unreachable = true; + } + + private void emitJumpWithArgs(int blockId, int[] args, int argCount) { + if (argCount == 1) { + bridge.exports().emitJumpWithArg(blockId, args[0]); + } else { + for (int a : args) { + bridge.exports().pushCallArg(a); + } + bridge.exports().emitJumpWithArgs(blockId); + } + } + + private void emitReturn(EmitContext ctx, Deque controlStack) { + ControlFrame funcFrame = null; + for (ControlFrame f : controlStack) { + funcFrame = f; + } + if (funcFrame != null) { + emitBrToFunction(ctx, funcFrame); + } else { + bridge.exports().emitReturnVoid(); + } + controlStack.peek().unreachable = true; + } +} diff --git a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeEmitters.java b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeEmitters.java new file mode 100644 index 00000000..8b37329c --- /dev/null +++ b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeEmitters.java @@ -0,0 +1,1845 @@ +package run.endive.redline.experimental.compiler.internal; + +import run.endive.redline.experimental.api.internal.CtxBuffer; +import run.endive.wasm.types.AnnotatedInstruction; +import run.endive.wasm.types.ExternalType; +import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.TableImport; +import run.endive.wasm.types.ValType; + +/** + * Static methods that emit Cranelift IR for each opcode category. + * No control flow logic — these are pure opcode handlers that pop + * operands from the value stack, call bridge exports, and push results. + */ +final class NativeEmitters { + + private NativeEmitters() {} + + // --- Constants --- + + static void emitI32Const(EmitContext ctx, AnnotatedInstruction ins) { + ctx.valueStack.push(ctx.bridge.exports().emitIconst32((int) ins.operands()[0])); + } + + static void emitI64Const(EmitContext ctx, AnnotatedInstruction ins) { + long val = ins.operands()[0]; + ctx.valueStack.push(ctx.bridge.exports().emitIconst64((int) val, (int) (val >>> 32))); + } + + static void emitF32Const(EmitContext ctx, AnnotatedInstruction ins) { + ctx.valueStack.push(ctx.bridge.exports().emitF32const((int) ins.operands()[0])); + } + + static void emitF64Const(EmitContext ctx, AnnotatedInstruction ins) { + long bits = ins.operands()[0]; + ctx.valueStack.push(ctx.bridge.exports().emitF64const((int) bits, (int) (bits >>> 32))); + } + + // --- i32 Arithmetic --- + + static void emitI32BinaryOp(EmitContext ctx, int op) { + int b = ctx.valueStack.pop(); + int a = ctx.valueStack.pop(); + int result; + switch (op) { + case 0: + result = ctx.bridge.exports().emitIadd(a, b); + break; + case 1: + result = ctx.bridge.exports().emitIsub(a, b); + break; + case 2: + result = ctx.bridge.exports().emitImul(a, b); + break; + case 3: + result = ctx.bridge.exports().emitBand(a, b); + break; + case 4: + result = ctx.bridge.exports().emitBor(a, b); + break; + case 5: + result = ctx.bridge.exports().emitBxor(a, b); + break; + case 6: + result = ctx.bridge.exports().emitIshl(a, b); + break; + case 7: + result = ctx.bridge.exports().emitSshr(a, b); + break; + case 8: + result = ctx.bridge.exports().emitUshr(a, b); + break; + case 9: + result = ctx.bridge.exports().emitRotl(a, b); + break; + case 10: + result = ctx.bridge.exports().emitRotr(a, b); + break; + default: + throw new IllegalArgumentException("Unknown i32 binary op: " + op); + } + ctx.valueStack.push(result); + } + + static void emitI32UnaryOp(EmitContext ctx, int op) { + int val = ctx.valueStack.pop(); + int result; + switch (op) { + case 0: + result = ctx.bridge.exports().emitClz(val); + break; + case 1: + result = ctx.bridge.exports().emitCtz(val); + break; + case 2: + result = ctx.bridge.exports().emitPopcnt(val); + break; + case 3: + result = ctx.bridge.exports().emitEqz(val); + break; + default: + throw new IllegalArgumentException("Unknown i32 unary op: " + op); + } + ctx.valueStack.push(result); + } + + // --- i64 Arithmetic --- + + static void emitI64UnaryOp(EmitContext ctx, int op) { + int val = ctx.valueStack.pop(); + int result; + switch (op) { + case 0: + result = ctx.bridge.exports().emitClz(val); + break; + case 1: + result = ctx.bridge.exports().emitCtz(val); + break; + case 2: + result = ctx.bridge.exports().emitPopcnt(val); + break; + case 3: + result = ctx.bridge.exports().emitEqzI64(val); + break; + default: + throw new IllegalArgumentException("Unknown i64 unary op: " + op); + } + ctx.valueStack.push(result); + } + + // --- Comparisons (i32 and i64 share emitIcmp) --- + + static void emitIcmp(EmitContext ctx, int cmpCode) { + int b = ctx.valueStack.pop(); + int a = ctx.valueStack.pop(); + ctx.valueStack.push(ctx.bridge.exports().emitIcmp(cmpCode, a, b)); + } + + // --- Float Arithmetic --- + + static void emitFloatBinaryOp(EmitContext ctx, int op) { + int b = ctx.valueStack.pop(); + int a = ctx.valueStack.pop(); + int result; + switch (op) { + case 0: + result = ctx.bridge.exports().emitFadd(a, b); + break; + case 1: + result = ctx.bridge.exports().emitFsub(a, b); + break; + case 2: + result = ctx.bridge.exports().emitFmul(a, b); + break; + case 3: + result = ctx.bridge.exports().emitFdiv(a, b); + break; + case 4: + result = ctx.bridge.exports().emitFmin(a, b); + break; + case 5: + result = ctx.bridge.exports().emitFmax(a, b); + break; + case 6: + result = ctx.bridge.exports().emitFcopysign(a, b); + break; + default: + throw new IllegalArgumentException("Unknown float binary op: " + op); + } + ctx.valueStack.push(result); + } + + static void emitFloatUnaryOp(EmitContext ctx, int op) { + int val = ctx.valueStack.pop(); + int result; + switch (op) { + case 0: + result = ctx.bridge.exports().emitFabs(val); + break; + case 1: + result = ctx.bridge.exports().emitFneg(val); + break; + case 2: + result = ctx.bridge.exports().emitCeil(val); + break; + case 3: + result = ctx.bridge.exports().emitFloor(val); + break; + case 4: + result = ctx.bridge.exports().emitTruncFloat(val); + break; + case 5: + result = ctx.bridge.exports().emitNearest(val); + break; + case 6: + result = ctx.bridge.exports().emitSqrt(val); + break; + default: + throw new IllegalArgumentException("Unknown float unary op: " + op); + } + ctx.valueStack.push(result); + } + + // --- Float Comparisons --- + + static void emitFcmp(EmitContext ctx, int cmpCode) { + int b = ctx.valueStack.pop(); + int a = ctx.valueStack.pop(); + ctx.valueStack.push(ctx.bridge.exports().emitFcmp(cmpCode, a, b)); + } + + // --- Safe Division (with trap pre-checks) --- + + static void emitSafeDiv(EmitContext ctx, boolean signed, boolean isRem, boolean is64) { + int divisor = ctx.valueStack.pop(); + int dividend = ctx.valueStack.pop(); + + int trapBlockZero = ctx.bridge.exports().createBlock(); + int safeBlock; + + int zero = + is64 + ? ctx.bridge.exports().emitIconst64(0, 0) + : ctx.bridge.exports().emitIconst32(0); + int isZero = ctx.bridge.exports().emitIcmp(0, divisor, zero); // EQ + + if (signed && !isRem) { + int checkOverflow = ctx.bridge.exports().createBlock(); + safeBlock = ctx.bridge.exports().createBlock(); + ctx.bridge.exports().emitBrif(isZero, trapBlockZero, checkOverflow); + + ctx.bridge.exports().switchToBlock(checkOverflow); + int intMin; + int negOne; + if (is64) { + intMin = ctx.bridge.exports().emitIconst64(0, 0x80000000); + negOne = ctx.bridge.exports().emitIconst64(-1, -1); + } else { + intMin = ctx.bridge.exports().emitIconst32(0x80000000); + negOne = ctx.bridge.exports().emitIconst32(-1); + } + int isMin = ctx.bridge.exports().emitIcmp(0, dividend, intMin); + int isNeg1 = ctx.bridge.exports().emitIcmp(0, divisor, negOne); + int both = ctx.bridge.exports().emitBand(isMin, isNeg1); + + int trapBlockOverflow = ctx.bridge.exports().createBlock(); + ctx.bridge.exports().emitBrif(both, trapBlockOverflow, safeBlock); + + fillTrapBlock(ctx, trapBlockOverflow, CtxBuffer.TRAP_INT_OVERFLOW); + } else { + safeBlock = ctx.bridge.exports().createBlock(); + ctx.bridge.exports().emitBrif(isZero, trapBlockZero, safeBlock); + } + + fillTrapBlock(ctx, trapBlockZero, CtxBuffer.TRAP_DIV_BY_ZERO); + + ctx.bridge.exports().switchToBlock(safeBlock); + int result; + if (signed) { + result = + isRem + ? ctx.bridge.exports().emitSrem(dividend, divisor) + : ctx.bridge.exports().emitSdiv(dividend, divisor); + } else { + result = + isRem + ? ctx.bridge.exports().emitUrem(dividend, divisor) + : ctx.bridge.exports().emitUdiv(dividend, divisor); + } + ctx.valueStack.push(result); + } + + // --- Safe Float Truncation --- + + static void emitSafeTrunc(EmitContext ctx, int targetType, int sourceType, boolean signed) { + int fval = ctx.valueStack.pop(); + + int satResult = + signed + ? ctx.bridge.exports().emitFcvtToSintSat(targetType, fval) + : ctx.bridge.exports().emitFcvtToUintSat(targetType, fval); + + // NaN check: fcmp NE x,x → true if NaN + int isNan = ctx.bridge.exports().emitFcmp(1, fval, fval); + int nanTrapBlock = ctx.bridge.exports().createBlock(); + int rangeCheckBlock = ctx.bridge.exports().createBlock(); + ctx.bridge.exports().emitBrif(isNan, nanTrapBlock, rangeCheckBlock); + fillTrapBlock(ctx, nanTrapBlock, CtxBuffer.TRAP_TRUNC_NAN); + + // Range check: trap if value is out of representable integer range + ctx.bridge.exports().switchToBlock(rangeCheckBlock); + int tooHigh = emitTruncUpperCheck(ctx, fval, targetType, sourceType, signed); + int tooLow = emitTruncLowerCheck(ctx, fval, targetType, sourceType, signed); + int outOfRange = ctx.bridge.exports().emitBor(tooHigh, tooLow); + int overflowTrapBlock = ctx.bridge.exports().createBlock(); + int okBlock = ctx.bridge.exports().createBlock(); + ctx.bridge.exports().emitBrif(outOfRange, overflowTrapBlock, okBlock); + + fillTrapBlock(ctx, overflowTrapBlock, CtxBuffer.TRAP_TRUNC_OVERFLOW); + + ctx.bridge.exports().switchToBlock(okBlock); + ctx.valueStack.push(satResult); + } + + // Emit: fval >= upperBound (GE=5) + private static int emitTruncUpperCheck( + EmitContext ctx, int fval, int targetType, int sourceType, boolean signed) { + if (sourceType == 2 /* F32 */) { + float bound; + if (targetType == 0 /* I32 */) { + bound = signed ? 2147483648.0f : 4294967296.0f; + } else { + bound = signed ? 9223372036854775808.0f : 18446744073709551616.0f; + } + int c = ctx.bridge.exports().emitF32const(Float.floatToRawIntBits(bound)); + return ctx.bridge.exports().emitFcmp(5, fval, c); // GE + } else { + double bound; + if (targetType == 0 /* I32 */) { + bound = signed ? 2147483648.0 : 4294967296.0; + } else { + bound = signed ? 9223372036854775808.0 : 18446744073709551616.0; + } + long bits = Double.doubleToRawLongBits(bound); + int c = ctx.bridge.exports().emitF64const((int) bits, (int) (bits >>> 32)); + return ctx.bridge.exports().emitFcmp(5, fval, c); // GE + } + } + + // Emit: fval <= lowerBound (LE=4) + private static int emitTruncLowerCheck( + EmitContext ctx, int fval, int targetType, int sourceType, boolean signed) { + if (sourceType == 2 /* F32 */) { + float bound; + if (signed) { + // For signed: trap if val < MIN (use LE with MIN - 1 equivalent) + // i32: -2147483648.0f is exactly representable, trap if val < it + // i64: -9223372036854775808.0f is exactly representable + bound = targetType == 0 /* I32 */ ? -2147483904.0f : -9223373136366403584.0f; + // Use LE: trap if fval <= bound (since bound is just below MIN) + } else { + bound = -1.0f; + } + int c = ctx.bridge.exports().emitF32const(Float.floatToRawIntBits(bound)); + return ctx.bridge.exports().emitFcmp(4, fval, c); // LE + } else { + double bound; + if (signed) { + bound = targetType == 0 /* I32 */ ? -2147483649.0 : -9223372036854777856.0; + } else { + bound = -1.0; + } + long bits = Double.doubleToRawLongBits(bound); + int c = ctx.bridge.exports().emitF64const((int) bits, (int) (bits >>> 32)); + return ctx.bridge.exports().emitFcmp(4, fval, c); // LE + } + } + + // --- Trap helper --- + + static void fillTrapBlock(EmitContext ctx, int trapBlock, int trapCode) { + ctx.bridge.exports().switchToBlock(trapBlock); + int ctxVal = ctx.bridge.exports().useVar(ctx.ctxPtrVar); + int zero = ctx.bridge.exports().emitIconst32(0); + int code = ctx.bridge.exports().emitIconst32(trapCode); + ctx.bridge.exports().emitStoreI32(ctxVal, zero, code, CtxBuffer.TRAP_CODE); + ctx.emitReturnForFuncType(); + } + + // --- Extensions --- + + static void emitI32Extend8S(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitSextend832(ctx.valueStack.pop())); + } + + static void emitI32Extend16S(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitSextend1632(ctx.valueStack.pop())); + } + + static void emitI64ExtendI32S(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitSextendI64(ctx.valueStack.pop())); + } + + static void emitI64ExtendI32U(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitUextendI64(ctx.valueStack.pop())); + } + + static void emitI64Extend8S(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitSextend864(ctx.valueStack.pop())); + } + + static void emitI64Extend16S(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitSextend1664(ctx.valueStack.pop())); + } + + static void emitI64Extend32S(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitSextend3264(ctx.valueStack.pop())); + } + + static void emitI32WrapI64(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitI32WrapI64(ctx.valueStack.pop())); + } + + // --- Reload memBase after calls (callee may have done memory.grow) --- + + private static void reloadMemBase(EmitContext ctx) { + int zero = ctx.bridge.exports().emitIconst32(0); + int newMemBase = + ctx.bridge + .exports() + .emitLoadI64( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + CtxBuffer.MEM_BASE_ADDR); + ctx.bridge.exports().defVar(ctx.memBaseVar, newMemBase); + } + + // --- Memory bounds check --- + + private static final int[] LOAD_ACCESS_SIZE = {4, 8, 4, 8, 1, 1, 2, 2, 1, 1, 2, 2, 4, 4}; + private static final int[] STORE_ACCESS_SIZE = {4, 8, 4, 8, 1, 2, 1, 2, 4}; + + /** + * Emit a bounds check: if addr + offset + accessSize > memPages * 65536, + * trap with OOB. Single compare + branch, predicted not-taken. + */ + private static void emitBoundsCheck(EmitContext ctx, int addr, int offset, int accessSize) { + // Compute effective end address as i64 to avoid i32 overflow + int addr64 = ctx.bridge.exports().emitUextendI64(addr); + long endOffset = Integer.toUnsignedLong(offset) + accessSize; + int end = + ctx.bridge + .exports() + .emitIadd( + addr64, + ctx.bridge + .exports() + .emitIconst64( + (int) (endOffset & 0xFFFFFFFFL), + (int) (endOffset >>> 32))); + + // Load memory size in bytes: memPages * 65536 + int zero = ctx.bridge.exports().emitIconst32(0); + int memPages = + ctx.bridge + .exports() + .emitLoadI32( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + CtxBuffer.MEMORY_PAGES); + int memPages64 = ctx.bridge.exports().emitUextendI64(memPages); + int memSize = + ctx.bridge.exports().emitIshl(memPages64, ctx.bridge.exports().emitIconst64(16, 0)); + + // if end > memSize → trap + int oob = ctx.bridge.exports().emitIcmp(5, end, memSize); // GT unsigned + int trapBlock = ctx.bridge.exports().createBlock(); + int okBlock = ctx.bridge.exports().createBlock(); + ctx.bridge.exports().emitBrif(oob, trapBlock, okBlock); + + fillTrapBlock(ctx, trapBlock, CtxBuffer.TRAP_OOB); + + ctx.bridge.exports().switchToBlock(okBlock); + } + + // --- Memory loads --- + + static void emitLoad(EmitContext ctx, AnnotatedInstruction ins, int loadType) { + int addr = ctx.valueStack.pop(); + int offset = (int) ins.operands()[1]; + emitBoundsCheck(ctx, addr, offset, LOAD_ACCESS_SIZE[loadType]); + int memBase = ctx.bridge.exports().useVar(ctx.memBaseVar); + int result; + switch (loadType) { + case 0: + result = ctx.bridge.exports().emitLoadI32(memBase, addr, offset); + break; + case 1: + result = ctx.bridge.exports().emitLoadI64(memBase, addr, offset); + break; + case 2: + result = ctx.bridge.exports().emitLoadF32(memBase, addr, offset); + break; + case 3: + result = ctx.bridge.exports().emitLoadF64(memBase, addr, offset); + break; + case 4: + result = ctx.bridge.exports().emitLoad8u(memBase, addr, offset); + break; + case 5: + result = ctx.bridge.exports().emitLoad8s(memBase, addr, offset); + break; + case 6: + result = ctx.bridge.exports().emitLoad16u(memBase, addr, offset); + break; + case 7: + result = ctx.bridge.exports().emitLoad16s(memBase, addr, offset); + break; + case 8: + result = ctx.bridge.exports().emitLoad8uI64(memBase, addr, offset); + break; + case 9: + result = ctx.bridge.exports().emitLoad8sI64(memBase, addr, offset); + break; + case 10: + result = ctx.bridge.exports().emitLoad16uI64(memBase, addr, offset); + break; + case 11: + result = ctx.bridge.exports().emitLoad16sI64(memBase, addr, offset); + break; + case 12: + result = ctx.bridge.exports().emitLoad32uI64(memBase, addr, offset); + break; + case 13: + result = ctx.bridge.exports().emitLoad32sI64(memBase, addr, offset); + break; + default: + throw new IllegalArgumentException("Unknown load type: " + loadType); + } + ctx.valueStack.push(result); + } + + // --- Memory stores --- + + static void emitStore(EmitContext ctx, AnnotatedInstruction ins, int storeType) { + int value = ctx.valueStack.pop(); + int addr = ctx.valueStack.pop(); + int offset = (int) ins.operands()[1]; + emitBoundsCheck(ctx, addr, offset, STORE_ACCESS_SIZE[storeType]); + int memBase = ctx.bridge.exports().useVar(ctx.memBaseVar); + switch (storeType) { + case 0: + ctx.bridge.exports().emitStoreI32(memBase, addr, value, offset); + break; + case 1: + ctx.bridge.exports().emitStoreI64(memBase, addr, value, offset); + break; + case 2: + ctx.bridge.exports().emitStoreF32(memBase, addr, value, offset); + break; + case 3: + ctx.bridge.exports().emitStoreF64(memBase, addr, value, offset); + break; + case 4: + ctx.bridge.exports().emitStore8(memBase, addr, value, offset); + break; + case 5: + ctx.bridge.exports().emitStore16(memBase, addr, value, offset); + break; + case 6: + ctx.bridge.exports().emitStore8I64(memBase, addr, value, offset); + break; + case 7: + ctx.bridge.exports().emitStore16I64(memBase, addr, value, offset); + break; + case 8: + ctx.bridge.exports().emitStore32I64(memBase, addr, value, offset); + break; + default: + throw new IllegalArgumentException("Unknown store type: " + storeType); + } + } + + // --- Locals --- + + static void emitLocalGet(EmitContext ctx, AnnotatedInstruction ins) { + ctx.valueStack.push(ctx.bridge.exports().useVar(ctx.localVars[(int) ins.operands()[0]])); + } + + static void emitLocalSet(EmitContext ctx, AnnotatedInstruction ins) { + int val = ctx.valueStack.pop(); + ctx.bridge.exports().defVar(ctx.localVars[(int) ins.operands()[0]], val); + } + + static void emitLocalTee(EmitContext ctx, AnnotatedInstruction ins) { + int val = ctx.valueStack.peek(); + ctx.bridge.exports().defVar(ctx.localVars[(int) ins.operands()[0]], val); + } + + // --- Select --- + + static void emitSelect(EmitContext ctx) { + int cond = ctx.valueStack.pop(); + int val2 = ctx.valueStack.pop(); + int val1 = ctx.valueStack.pop(); + ctx.valueStack.push(ctx.bridge.exports().emitSelect(cond, val1, val2)); + } + + // --- Globals --- + + static void emitGlobalGet(EmitContext ctx, AnnotatedInstruction ins) { + int globalIdx = (int) ins.operands()[0]; + int zero = ctx.bridge.exports().emitIconst32(0); + int globalsPtr = + ctx.bridge + .exports() + .emitLoadI64( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + CtxBuffer.GLOBALS_PTR); + int offsetVal = ctx.bridge.exports().emitIconst32(globalIdx * 8); + int rawVal = ctx.bridge.exports().emitLoadI64(globalsPtr, offsetVal, 0); + ValType globalType = ctx.resolveGlobalType(globalIdx); + ctx.valueStack.push(ctx.narrowFromI64ForType(rawVal, globalType)); + } + + static void emitGlobalSet(EmitContext ctx, AnnotatedInstruction ins) { + int globalIdx = (int) ins.operands()[0]; + int value = ctx.valueStack.pop(); + ValType globalType = ctx.resolveGlobalType(globalIdx); + int widened = ctx.widenToI64ForType(value, globalType); + int zero = ctx.bridge.exports().emitIconst32(0); + int globalsPtr = + ctx.bridge + .exports() + .emitLoadI64( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + CtxBuffer.GLOBALS_PTR); + int offsetVal = ctx.bridge.exports().emitIconst32(globalIdx * 8); + ctx.bridge.exports().emitStoreI64(globalsPtr, offsetVal, widened, 0); + } + + // --- Memory operations --- + + static void emitMemorySize(EmitContext ctx) { + int zero = ctx.bridge.exports().emitIconst32(0); + int pages = + ctx.bridge + .exports() + .emitLoadI32( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + CtxBuffer.MEMORY_PAGES); + ctx.valueStack.push(pages); + } + + static void emitMemoryGrow(EmitContext ctx) { + int delta = ctx.valueStack.pop(); + int zero = ctx.bridge.exports().emitIconst32(0); + ctx.bridge + .exports() + .emitStoreI32( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + delta, + CtxBuffer.MEM_GROW_DELTA); + int memGrowPtr = + ctx.bridge + .exports() + .emitLoadI64( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + CtxBuffer.MEM_GROW_PTR); + int growSig = ctx.getOrCreateTrampolineSigRef(); + ctx.bridge.exports().pushCallArg(ctx.bridge.exports().useVar(ctx.ctxPtrVar)); + int rawResult = ctx.bridge.exports().emitCallIndirect(growSig, memGrowPtr); + int result = ctx.bridge.exports().emitIreduceI32(rawResult); + ctx.valueStack.push(result); + int newMemBase = + ctx.bridge + .exports() + .emitLoadI64( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + CtxBuffer.MEM_BASE_ADDR); + ctx.bridge.exports().defVar(ctx.memBaseVar, newMemBase); + } + + // --- Unreachable --- + + static void emitUnreachable(EmitContext ctx) { + int ctxVal = ctx.bridge.exports().useVar(ctx.ctxPtrVar); + int zero = ctx.bridge.exports().emitIconst32(0); + int code = ctx.bridge.exports().emitIconst32(CtxBuffer.TRAP_UNREACHABLE); + ctx.bridge.exports().emitStoreI32(ctxVal, zero, code, CtxBuffer.TRAP_CODE); + ctx.emitReturnForFuncType(); + } + + // --- Conversions --- + + static void emitTruncSat(EmitContext ctx, int targetType, boolean signed) { + int val = ctx.valueStack.pop(); + ctx.valueStack.push( + signed + ? ctx.bridge.exports().emitFcvtToSintSat(targetType, val) + : ctx.bridge.exports().emitFcvtToUintSat(targetType, val)); + } + + static void emitConvertFloat(EmitContext ctx, int targetType, boolean signed) { + int val = ctx.valueStack.pop(); + ctx.valueStack.push( + signed + ? ctx.bridge.exports().emitFcvtFromSint(targetType, val) + : ctx.bridge.exports().emitFcvtFromUint(targetType, val)); + } + + static void emitFpromote(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitFpromote(ctx.valueStack.pop())); + } + + static void emitFdemote(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitFdemote(ctx.valueStack.pop())); + } + + static void emitBitcastI32ToF32(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitBitcastI32ToF32(ctx.valueStack.pop())); + } + + static void emitBitcastF32ToI32(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitBitcastF32ToI32(ctx.valueStack.pop())); + } + + static void emitBitcastI64ToF64(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitBitcastI64ToF64(ctx.valueStack.pop())); + } + + static void emitBitcastF64ToI64(EmitContext ctx) { + ctx.valueStack.push(ctx.bridge.exports().emitBitcastF64ToI64(ctx.valueStack.pop())); + } + + // --- Calls --- + + static void emitCall(EmitContext ctx, AnnotatedInstruction ins) { + int targetFuncId = (int) ins.operands()[0]; + FunctionType targetType = ctx.resolveCallTargetType(targetFuncId); + + boolean calleeMultiReturn = targetType.returns().size() > 1; + int sigRef = + calleeMultiReturn + ? ctx.getOrCreateMultiReturnSigRef(targetType) + : ctx.getOrCreateSigRef(targetType); + + int argCount = targetType.params().size(); + int[] argVals = new int[argCount]; + for (int i = argCount - 1; i >= 0; i--) { + argVals[i] = ctx.valueStack.pop(); + } + + int zero = ctx.bridge.exports().emitIconst32(0); + ctx.bridge + .exports() + .emitStoreI32( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + ctx.bridge.exports().emitIconst32(argCount), + CtxBuffer.ARG_COUNT); + int argsPtr = + ctx.bridge + .exports() + .emitLoadI64( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + CtxBuffer.ARGS_PTR); + for (int i = 0; i < argCount; i++) { + int widened = ctx.widenToI64(argVals[i], targetType.params().get(i)); + ctx.bridge.exports().emitStoreI64(argsPtr, zero, widened, CtxBuffer.argOffset(i)); + } + + int funcTablePtr = + ctx.bridge + .exports() + .emitLoadI64( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + CtxBuffer.FUNC_TABLE_PTR); + int funcIdOffset = ctx.bridge.exports().emitIconst32(targetFuncId * 8); + int funcPtr = ctx.bridge.exports().emitLoadI64(funcTablePtr, funcIdOffset, 0); + + ctx.bridge.exports().pushCallArg(ctx.bridge.exports().useVar(ctx.memBaseVar)); + ctx.bridge.exports().pushCallArg(ctx.bridge.exports().useVar(ctx.ctxPtrVar)); + for (int i = 0; i < argCount; i++) { + ctx.bridge.exports().pushCallArg(argVals[i]); + } + + int rawResult = ctx.bridge.exports().emitCallIndirect(sigRef, funcPtr); + + if (calleeMultiReturn) { + // Read return values from argsBuffer + int zero2 = ctx.bridge.exports().emitIconst32(0); + int argsPtr2 = + ctx.bridge + .exports() + .emitLoadI64( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero2, + CtxBuffer.ARGS_PTR); + for (int i = 0; i < targetType.returns().size(); i++) { + int raw = ctx.bridge.exports().emitLoadI64(argsPtr2, zero2, CtxBuffer.argOffset(i)); + ctx.valueStack.push(ctx.narrowFromI64ForType(raw, targetType.returns().get(i))); + } + } else if (!targetType.returns().isEmpty()) { + ctx.valueStack.push(rawResult); + } + + // Reload memBase — callee may have called memory.grow + reloadMemBase(ctx); + } + + static void emitCallIndirect(EmitContext ctx, AnnotatedInstruction ins) { + int typeId = (int) ins.operands()[0]; + int tableIdx = (int) ins.operands()[1]; + FunctionType targetType = (FunctionType) ctx.module.typeSection().getType(typeId); + + // Pop table element index (i32) + int elemIdx = ctx.valueStack.pop(); + + // Pop call arguments + int argCount = targetType.params().size(); + int[] argVals = new int[argCount]; + for (int i = argCount - 1; i >= 0; i--) { + argVals[i] = ctx.valueStack.pop(); + } + + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + int ctxPtr = b.useVar(ctx.ctxPtrVar); + + // 1. Load tablePtr from TABLE_PTRS[tableIdx] + int tablePtrsPtr = b.emitLoadI64(ctxPtr, zero, CtxBuffer.TABLE_PTRS); + int tableOffset = b.emitIconst32(tableIdx * 8); + int tablePtr = b.emitLoadI64(tablePtrsPtr, tableOffset, 0); + + // 2. Bounds check: elemIdx >= table.size → trap "undefined element" + int tableSize = b.emitLoadI32(tablePtr, zero, CtxBuffer.TABLE_SIZE_OFFSET); + int oobCheck = b.emitIcmp(9, b.emitUextendI64(elemIdx), b.emitUextendI64(tableSize)); + int trapOobBlock = b.createBlock(); + int afterOobBlock = b.createBlock(); + b.emitBrif(oobCheck, trapOobBlock, afterOobBlock); + fillTrapBlock(ctx, trapOobBlock, CtxBuffer.TRAP_UNDEFINED_ELEMENT); + b.switchToBlock(afterOobBlock); + + // 3. Calculate entry offset: elemIdx * 16 (TABLE_ENTRY_SIZE) + int entryOffset = b.emitImul(elemIdx, b.emitIconst32(CtxBuffer.TABLE_ENTRY_SIZE)); + + // 4. Load funcPtr from entry: tablePtr[ENTRIES_OFFSET + entryOffset + FUNC_PTR_OFFSET] + int funcPtr = + b.emitLoadI64( + tablePtr, + entryOffset, + CtxBuffer.TABLE_ENTRIES_OFFSET + CtxBuffer.ENTRY_FUNC_PTR_OFFSET); + + // 5. Null check: funcPtr == 0 → trap + int zero64 = b.emitIconst64(0, 0); + int isNull = b.emitIcmp(0, funcPtr, zero64); // EQ + int trapNullBlock = b.createBlock(); + int afterNullBlock = b.createBlock(); + b.emitBrif(isNull, trapNullBlock, afterNullBlock); + fillTrapBlock(ctx, trapNullBlock, CtxBuffer.TRAP_UNINITIALIZED_ELEMENT); + b.switchToBlock(afterNullBlock); + + // 6. Type check: entry.typeIdx == expectedCanonicalType + int actualType = + b.emitLoadI32( + tablePtr, + entryOffset, + CtxBuffer.TABLE_ENTRIES_OFFSET + CtxBuffer.ENTRY_TYPE_IDX_OFFSET); + int canonicalTypeId = ctx.canonicalTypeMap[typeId]; + int expectedType = b.emitIconst32(canonicalTypeId); + int typeMismatch = + b.emitIcmp(1, b.emitUextendI64(actualType), b.emitUextendI64(expectedType)); // NE + int trapTypeBlock = b.createBlock(); + int afterTypeBlock = b.createBlock(); + b.emitBrif(typeMismatch, trapTypeBlock, afterTypeBlock); + fillTrapBlock(ctx, trapTypeBlock, CtxBuffer.TRAP_INDIRECT_CALL_TYPE_MISMATCH); + b.switchToBlock(afterTypeBlock); + + // 7. Determine if callee might be multi-return (>1 return) + boolean calleeMultiReturn = targetType.returns().size() > 1; + + // Write args to argsBuffer (for import stubs that read from it) + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + int zero2 = b.emitIconst32(0); + b.emitStoreI32( + b.useVar(ctx.ctxPtrVar), zero2, b.emitIconst32(argCount), CtxBuffer.ARG_COUNT); + for (int i = 0; i < argCount; i++) { + int widened = ctx.widenToI64(argVals[i], targetType.params().get(i)); + b.emitStoreI64(argsPtr, zero2, widened, CtxBuffer.argOffset(i)); + } + + // 8. Build SigRef and call (funcPtr loaded directly from table entry) + int sigRef; + if (calleeMultiReturn) { + sigRef = ctx.getOrCreateMultiReturnSigRef(targetType); + } else { + sigRef = ctx.getOrCreateSigRef(targetType); + } + + b.pushCallArg(b.useVar(ctx.memBaseVar)); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + for (int i = 0; i < argCount; i++) { + b.pushCallArg(argVals[i]); + } + + int rawResult = b.emitCallIndirect(sigRef, funcPtr); + + // 9. Handle results + if (calleeMultiReturn) { + int zero3 = b.emitIconst32(0); + int argsPtr2 = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero3, CtxBuffer.ARGS_PTR); + for (int i = 0; i < targetType.returns().size(); i++) { + int raw = b.emitLoadI64(argsPtr2, zero3, CtxBuffer.argOffset(i)); + ctx.valueStack.push(ctx.narrowFromI64ForType(raw, targetType.returns().get(i))); + } + } else if (!targetType.returns().isEmpty()) { + ctx.valueStack.push(rawResult); + } + + // Reload memBase — callee may have called memory.grow + reloadMemBase(ctx); + } + + // --- Tail calls (return_call / return_call_indirect) --- + + static void emitReturnCall(EmitContext ctx, AnnotatedInstruction ins) { + int targetFuncId = (int) ins.operands()[0]; + FunctionType targetType = ctx.resolveCallTargetType(targetFuncId); + + boolean calleeMultiReturn = targetType.returns().size() > 1; + int sigRef = + calleeMultiReturn + ? ctx.getOrCreateMultiReturnSigRef(targetType) + : ctx.getOrCreateSigRef(targetType); + + int argCount = targetType.params().size(); + int[] argVals = new int[argCount]; + for (int i = argCount - 1; i >= 0; i--) { + argVals[i] = ctx.valueStack.pop(); + } + + int zero = ctx.bridge.exports().emitIconst32(0); + ctx.bridge + .exports() + .emitStoreI32( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + ctx.bridge.exports().emitIconst32(argCount), + CtxBuffer.ARG_COUNT); + int argsPtr = + ctx.bridge + .exports() + .emitLoadI64( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + CtxBuffer.ARGS_PTR); + for (int i = 0; i < argCount; i++) { + int widened = ctx.widenToI64(argVals[i], targetType.params().get(i)); + ctx.bridge.exports().emitStoreI64(argsPtr, zero, widened, CtxBuffer.argOffset(i)); + } + + int funcTablePtr = + ctx.bridge + .exports() + .emitLoadI64( + ctx.bridge.exports().useVar(ctx.ctxPtrVar), + zero, + CtxBuffer.FUNC_TABLE_PTR); + int funcIdOffset = ctx.bridge.exports().emitIconst32(targetFuncId * 8); + int funcPtr = ctx.bridge.exports().emitLoadI64(funcTablePtr, funcIdOffset, 0); + + ctx.bridge.exports().pushCallArg(ctx.bridge.exports().useVar(ctx.memBaseVar)); + ctx.bridge.exports().pushCallArg(ctx.bridge.exports().useVar(ctx.ctxPtrVar)); + for (int i = 0; i < argCount; i++) { + ctx.bridge.exports().pushCallArg(argVals[i]); + } + + ctx.bridge.exports().emitReturnCallIndirect(sigRef, funcPtr); + } + + static void emitReturnCallIndirect(EmitContext ctx, AnnotatedInstruction ins) { + int typeId = (int) ins.operands()[0]; + int tableIdx = (int) ins.operands()[1]; + FunctionType targetType = (FunctionType) ctx.module.typeSection().getType(typeId); + + int elemIdx = ctx.valueStack.pop(); + + int argCount = targetType.params().size(); + int[] argVals = new int[argCount]; + for (int i = argCount - 1; i >= 0; i--) { + argVals[i] = ctx.valueStack.pop(); + } + + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + int ctxPtr = b.useVar(ctx.ctxPtrVar); + + int tablePtrsPtr = b.emitLoadI64(ctxPtr, zero, CtxBuffer.TABLE_PTRS); + int tableOffset = b.emitIconst32(tableIdx * 8); + int tablePtr = b.emitLoadI64(tablePtrsPtr, tableOffset, 0); + + int tableSize = b.emitLoadI32(tablePtr, zero, CtxBuffer.TABLE_SIZE_OFFSET); + int oobCheck = b.emitIcmp(9, b.emitUextendI64(elemIdx), b.emitUextendI64(tableSize)); + int trapOobBlock = b.createBlock(); + int afterOobBlock = b.createBlock(); + b.emitBrif(oobCheck, trapOobBlock, afterOobBlock); + fillTrapBlock(ctx, trapOobBlock, CtxBuffer.TRAP_UNDEFINED_ELEMENT); + b.switchToBlock(afterOobBlock); + + int entryOffset = b.emitImul(elemIdx, b.emitIconst32(CtxBuffer.TABLE_ENTRY_SIZE)); + + int funcPtr = + b.emitLoadI64( + tablePtr, + entryOffset, + CtxBuffer.TABLE_ENTRIES_OFFSET + CtxBuffer.ENTRY_FUNC_PTR_OFFSET); + + int zero64 = b.emitIconst64(0, 0); + int isNull = b.emitIcmp(0, funcPtr, zero64); + int trapNullBlock = b.createBlock(); + int afterNullBlock = b.createBlock(); + b.emitBrif(isNull, trapNullBlock, afterNullBlock); + fillTrapBlock(ctx, trapNullBlock, CtxBuffer.TRAP_UNINITIALIZED_ELEMENT); + b.switchToBlock(afterNullBlock); + + int actualType = + b.emitLoadI32( + tablePtr, + entryOffset, + CtxBuffer.TABLE_ENTRIES_OFFSET + CtxBuffer.ENTRY_TYPE_IDX_OFFSET); + int canonicalTypeId = ctx.canonicalTypeMap[typeId]; + int expectedType = b.emitIconst32(canonicalTypeId); + int typeMismatch = + b.emitIcmp(1, b.emitUextendI64(actualType), b.emitUextendI64(expectedType)); + int trapTypeBlock = b.createBlock(); + int afterTypeBlock = b.createBlock(); + b.emitBrif(typeMismatch, trapTypeBlock, afterTypeBlock); + fillTrapBlock(ctx, trapTypeBlock, CtxBuffer.TRAP_INDIRECT_CALL_TYPE_MISMATCH); + b.switchToBlock(afterTypeBlock); + + boolean calleeMultiReturn = targetType.returns().size() > 1; + + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + int zero2 = b.emitIconst32(0); + b.emitStoreI32( + b.useVar(ctx.ctxPtrVar), zero2, b.emitIconst32(argCount), CtxBuffer.ARG_COUNT); + for (int i = 0; i < argCount; i++) { + int widened = ctx.widenToI64(argVals[i], targetType.params().get(i)); + b.emitStoreI64(argsPtr, zero2, widened, CtxBuffer.argOffset(i)); + } + + int sigRef; + if (calleeMultiReturn) { + sigRef = ctx.getOrCreateMultiReturnSigRef(targetType); + } else { + sigRef = ctx.getOrCreateSigRef(targetType); + } + + b.pushCallArg(b.useVar(ctx.memBaseVar)); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + for (int i = 0; i < argCount; i++) { + b.pushCallArg(argVals[i]); + } + + b.emitReturnCallIndirect(sigRef, funcPtr); + } + + // --- Table operations (fully native, no trampoline) --- + + private static int loadTablePtr(EmitContext ctx, int tableIdx) { + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + int tablePtrsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TABLE_PTRS); + int offset = b.emitIconst32(tableIdx * 8); + return b.emitLoadI64(tablePtrsPtr, offset, 0); + } + + private static boolean isExternRefTable(EmitContext ctx, int tableIdx) { + int importTableCount = 0; + for (var imp : + ctx.module.importSection().stream().collect(java.util.stream.Collectors.toList())) { + if (imp.importType() == ExternalType.TABLE) { + if (importTableCount == tableIdx) { + return ((TableImport) imp).entryType().equals(ValType.ExternRef); + } + importTableCount++; + } + } + int definedIdx = tableIdx - importTableCount; + return ctx.module + .tableSection() + .getTable(definedIdx) + .elementType() + .equals(ValType.ExternRef); + } + + static void emitTableGet(EmitContext ctx, AnnotatedInstruction ins) { + int tableIdx = (int) ins.operands()[0]; + int index = ctx.valueStack.pop(); + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + int tablePtr = loadTablePtr(ctx, tableIdx); + int tableSize = b.emitLoadI32(tablePtr, zero, CtxBuffer.TABLE_SIZE_OFFSET); + + // Bounds check: index >= size → trap + int oob = b.emitIcmp(9, b.emitUextendI64(index), b.emitUextendI64(tableSize)); + int trapBlock = b.createBlock(); + int okBlock = b.createBlock(); + b.emitBrif(oob, trapBlock, okBlock); + fillTrapBlock(ctx, trapBlock, CtxBuffer.TRAP_TABLE_OOB); + b.switchToBlock(okBlock); + + // Load funcId from 16-byte entry: entry.funcId at offset +4 + int entryOffset = b.emitImul(index, b.emitIconst32(CtxBuffer.TABLE_ENTRY_SIZE)); + int ref = + b.emitLoadI32( + tablePtr, + entryOffset, + CtxBuffer.TABLE_ENTRIES_OFFSET + CtxBuffer.ENTRY_FUNC_ID_OFFSET); + // Sign-extend so REF_NULL_VALUE (-1 as i32) stays -1 as i64. + ctx.valueStack.push(b.emitSextendI64(ref)); + } + + static void emitTableSet(EmitContext ctx, AnnotatedInstruction ins) { + int tableIdx = (int) ins.operands()[0]; + // Value is i64 on stack (ref type), narrow to i32 for table storage + int funcId = ctx.bridge.exports().emitIreduceI32(ctx.valueStack.pop()); + int index = ctx.valueStack.pop(); + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + int tablePtr = loadTablePtr(ctx, tableIdx); + int tableSize = b.emitLoadI32(tablePtr, zero, CtxBuffer.TABLE_SIZE_OFFSET); + + // Bounds check + int oob = b.emitIcmp(9, b.emitUextendI64(index), b.emitUextendI64(tableSize)); + int trapBlock = b.createBlock(); + int okBlock = b.createBlock(); + b.emitBrif(oob, trapBlock, okBlock); + fillTrapBlock(ctx, trapBlock, CtxBuffer.TRAP_TABLE_OOB); + b.switchToBlock(okBlock); + + // Calculate entry offset for 16-byte entries + int entryOffset = b.emitImul(index, b.emitIconst32(CtxBuffer.TABLE_ENTRY_SIZE)); + int entryTypeOff = CtxBuffer.TABLE_ENTRIES_OFFSET + CtxBuffer.ENTRY_TYPE_IDX_OFFSET; + int entryFuncIdOff = CtxBuffer.TABLE_ENTRIES_OFFSET + CtxBuffer.ENTRY_FUNC_ID_OFFSET; + int entryFuncPtrOff = CtxBuffer.TABLE_ENTRIES_OFFSET + CtxBuffer.ENTRY_FUNC_PTR_OFFSET; + + // Check if value is REF_NULL (-1) + int refNull = b.emitIconst32(-1); + int isNull = b.emitIcmp(0, b.emitUextendI64(funcId), b.emitUextendI64(refNull)); // EQ + + int nullBlock = b.createBlock(); + int nonNullBlock = b.createBlock(); + int mergeBlock = b.createBlock(); + b.emitBrif(isNull, nullBlock, nonNullBlock); + + // Null path: write null entry + b.switchToBlock(nullBlock); + b.emitStoreI32(tablePtr, entryOffset, b.emitIconst32(0), entryTypeOff); + b.emitStoreI32(tablePtr, entryOffset, refNull, entryFuncIdOff); + b.emitStoreI64(tablePtr, entryOffset, b.emitIconst64(0, 0), entryFuncPtrOff); + b.emitJump(mergeBlock); + + // Non-null path + b.switchToBlock(nonNullBlock); + if (isExternRefTable(ctx, tableIdx)) { + // ExternRef: store raw value directly, no function lookup + b.emitStoreI32(tablePtr, entryOffset, b.emitIconst32(0), entryTypeOff); + b.emitStoreI32(tablePtr, entryOffset, funcId, entryFuncIdOff); + b.emitStoreI64(tablePtr, entryOffset, b.emitIconst64(0, 0), entryFuncPtrOff); + } else { + // FuncRef: resolve funcId → funcPtr+typeIdx + int funcTablePtr = + b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.FUNC_TABLE_PTR); + int funcPtrOffset = b.emitImul(funcId, b.emitIconst32(8)); + int funcPtr = b.emitLoadI64(funcTablePtr, funcPtrOffset, 0); + int funcTypesPtr = + b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.FUNC_TYPES_PTR); + int typeIdxOffset = b.emitImul(funcId, b.emitIconst32(4)); + int typeIdx = b.emitLoadI32(funcTypesPtr, typeIdxOffset, 0); + b.emitStoreI32(tablePtr, entryOffset, typeIdx, entryTypeOff); + b.emitStoreI32(tablePtr, entryOffset, funcId, entryFuncIdOff); + b.emitStoreI64(tablePtr, entryOffset, funcPtr, entryFuncPtrOff); + } + b.emitJump(mergeBlock); + + b.switchToBlock(mergeBlock); + } + + static void emitTableSize(EmitContext ctx, AnnotatedInstruction ins) { + int tableIdx = (int) ins.operands()[0]; + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + int tablePtr = loadTablePtr(ctx, tableIdx); + int size = b.emitLoadI32(tablePtr, zero, CtxBuffer.TABLE_SIZE_OFFSET); + ctx.valueStack.push(size); + } + + static void emitTableGrow(EmitContext ctx, AnnotatedInstruction ins) { + int tableIdx = (int) ins.operands()[0]; + int delta = ctx.valueStack.pop(); + // Fill value is i64 (ref type on stack), narrow to i32 for table storage + int fillValue = ctx.bridge.exports().emitIreduceI32(ctx.valueStack.pop()); + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + int tablePtr = loadTablePtr(ctx, tableIdx); + int oldSize = b.emitLoadI32(tablePtr, zero, CtxBuffer.TABLE_SIZE_OFFSET); + int maxSize = b.emitLoadI32(tablePtr, zero, CtxBuffer.TABLE_MAX_OFFSET); + + // newSize = oldSize + delta + int newSize = b.emitIadd(b.emitUextendI64(oldSize), b.emitUextendI64(delta)); + + // if newSize > maxSize → return -1 + int overMax = b.emitIcmp(5, newSize, b.emitUextendI64(maxSize)); // UGT + // if delta < 0 (unsigned: very large) → also fail + // Combine: newSize > max OR newSize < oldSize (overflow) + int overflow = b.emitIcmp(3, newSize, b.emitUextendI64(oldSize)); // ULT = overflow + int fail = b.emitBor(overMax, overflow); + + int failBlock = b.createBlock(); + int okBlock = b.createBlock(); + int mergeBlock = b.createBlock(); + int mergeParam = b.appendBlockParam(mergeBlock, 0 /* TYPE_I32 */); + + b.emitBrif(fail, failBlock, okBlock); + + // Fail block: push -1 + b.switchToBlock(failBlock); + b.emitJumpWithArg(mergeBlock, b.emitIconst32(-1)); + + // OK block: update size, fill new slots via trampoline, return oldSize + b.switchToBlock(okBlock); + int newSizeI32 = b.emitIreduceI32(newSize); + b.emitStoreI32(tablePtr, zero, newSizeI32, CtxBuffer.TABLE_SIZE_OFFSET); + + // Fill new slots via trampoline (runtime loop) + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(oldSize), CtxBuffer.argOffset(0)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(newSizeI32), CtxBuffer.argOffset(1)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(fillValue), CtxBuffer.argOffset(2)); + b.emitStoreI64(argsPtr, zero, tablePtr, CtxBuffer.argOffset(3)); // already i64 + b.emitStoreI64( + argsPtr, zero, b.emitUextendI64(b.emitIconst32(tableIdx)), CtxBuffer.argOffset(4)); + + int tableOpsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAMPOLINE_PTR); + b.emitStoreI32( + b.useVar(ctx.ctxPtrVar), + zero, + b.emitIconst32(-1), // sentinel for table grow fill + CtxBuffer.ARG_COUNT); + int trampolineSig = ctx.getOrCreateTrampolineSigRef(); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + b.emitCallIndirect(trampolineSig, tableOpsPtr); + + b.emitJumpWithArg(mergeBlock, oldSize); + + b.switchToBlock(mergeBlock); + ctx.valueStack.push(mergeParam); + } + + static void emitTableFill(EmitContext ctx, AnnotatedInstruction ins) { + int tableIdx = (int) ins.operands()[0]; + int size = ctx.valueStack.pop(); + // Fill value is i64 (ref type on stack), narrow to i32 for table storage + int fillValue = ctx.bridge.exports().emitIreduceI32(ctx.valueStack.pop()); + int offset = ctx.valueStack.pop(); + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + int tablePtr = loadTablePtr(ctx, tableIdx); + int tableSize = b.emitLoadI32(tablePtr, zero, CtxBuffer.TABLE_SIZE_OFFSET); + + // Bounds check: offset + size > tableSize → trap + int end = b.emitIadd(b.emitUextendI64(offset), b.emitUextendI64(size)); + int oob = b.emitIcmp(5, end, b.emitUextendI64(tableSize)); // UGT + int trapBlock = b.createBlock(); + int okBlock = b.createBlock(); + b.emitBrif(oob, trapBlock, okBlock); + fillTrapBlock(ctx, trapBlock, CtxBuffer.TRAP_TABLE_OOB); + b.switchToBlock(okBlock); + + // Emit fill via trampoline (runtime loop) + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(offset), CtxBuffer.argOffset(0)); + int endI32 = b.emitIreduceI32(end); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(endI32), CtxBuffer.argOffset(1)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(fillValue), CtxBuffer.argOffset(2)); + b.emitStoreI64(argsPtr, zero, tablePtr, CtxBuffer.argOffset(3)); // already i64 + b.emitStoreI64( + argsPtr, zero, b.emitUextendI64(b.emitIconst32(tableIdx)), CtxBuffer.argOffset(4)); + + b.emitStoreI32( + b.useVar(ctx.ctxPtrVar), + zero, + b.emitIconst32(-2), // sentinel for table fill + CtxBuffer.ARG_COUNT); + int trampolinePtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAMPOLINE_PTR); + int trampolineSig = ctx.getOrCreateTrampolineSigRef(); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + b.emitCallIndirect(trampolineSig, trampolinePtr); + } + + static void emitTableCopy(EmitContext ctx, AnnotatedInstruction ins) { + int dstTableIdx = (int) ins.operands()[0]; + int srcTableIdx = (int) ins.operands()[1]; + int size = ctx.valueStack.pop(); + int srcOffset = ctx.valueStack.pop(); + int dstOffset = ctx.valueStack.pop(); + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + int srcTablePtr = loadTablePtr(ctx, srcTableIdx); + int dstTablePtr = loadTablePtr(ctx, dstTableIdx); + int srcTableSize = b.emitLoadI32(srcTablePtr, zero, CtxBuffer.TABLE_SIZE_OFFSET); + int dstTableSize = b.emitLoadI32(dstTablePtr, zero, CtxBuffer.TABLE_SIZE_OFFSET); + + // Bounds checks + int srcEnd = b.emitIadd(b.emitUextendI64(srcOffset), b.emitUextendI64(size)); + int dstEnd = b.emitIadd(b.emitUextendI64(dstOffset), b.emitUextendI64(size)); + int srcOob = b.emitIcmp(5, srcEnd, b.emitUextendI64(srcTableSize)); + int dstOob = b.emitIcmp(5, dstEnd, b.emitUextendI64(dstTableSize)); + int oob = b.emitBor(srcOob, dstOob); + + int trapBlock = b.createBlock(); + int okBlock = b.createBlock(); + b.emitBrif(oob, trapBlock, okBlock); + fillTrapBlock(ctx, trapBlock, CtxBuffer.TRAP_TABLE_OOB); + b.switchToBlock(okBlock); + + // Copy via trampoline (handles overlapping correctly) + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + b.emitStoreI64(argsPtr, zero, srcTablePtr, CtxBuffer.argOffset(0)); // already i64 + b.emitStoreI64(argsPtr, zero, dstTablePtr, CtxBuffer.argOffset(1)); // already i64 + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(srcOffset), CtxBuffer.argOffset(2)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(dstOffset), CtxBuffer.argOffset(3)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(size), CtxBuffer.argOffset(4)); + + b.emitStoreI32( + b.useVar(ctx.ctxPtrVar), + zero, + b.emitIconst32(-3), // sentinel for table copy + CtxBuffer.ARG_COUNT); + int trampolinePtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAMPOLINE_PTR); + int trampolineSig = ctx.getOrCreateTrampolineSigRef(); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + b.emitCallIndirect(trampolineSig, trampolinePtr); + } + + static void emitTableInit(EmitContext ctx, AnnotatedInstruction ins) { + int elemIdx = (int) ins.operands()[0]; + int tableIdx = (int) ins.operands()[1]; + int size = ctx.valueStack.pop(); + int srcOffset = ctx.valueStack.pop(); + int dstOffset = ctx.valueStack.pop(); + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + // TABLE.INIT always goes through trampoline (needs elem segment data) + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + b.emitStoreI64( + argsPtr, zero, b.emitUextendI64(b.emitIconst32(tableIdx)), CtxBuffer.argOffset(0)); + b.emitStoreI64( + argsPtr, zero, b.emitUextendI64(b.emitIconst32(elemIdx)), CtxBuffer.argOffset(1)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(dstOffset), CtxBuffer.argOffset(2)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(srcOffset), CtxBuffer.argOffset(3)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(size), CtxBuffer.argOffset(4)); + + b.emitStoreI32( + b.useVar(ctx.ctxPtrVar), + zero, + b.emitIconst32(-4), // sentinel for table init + CtxBuffer.ARG_COUNT); + int trampolinePtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAMPOLINE_PTR); + int trampolineSig = ctx.getOrCreateTrampolineSigRef(); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + b.emitCallIndirect(trampolineSig, trampolinePtr); + } + + static void emitElemDrop(EmitContext ctx, AnnotatedInstruction ins) { + int elemIdx = (int) ins.operands()[0]; + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + // ELEM.DROP always goes through trampoline + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + b.emitStoreI64( + argsPtr, zero, b.emitUextendI64(b.emitIconst32(elemIdx)), CtxBuffer.argOffset(0)); + + b.emitStoreI32( + b.useVar(ctx.ctxPtrVar), + zero, + b.emitIconst32(-5), // sentinel for elem drop + CtxBuffer.ARG_COUNT); + int trampolinePtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAMPOLINE_PTR); + int trampolineSig = ctx.getOrCreateTrampolineSigRef(); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + b.emitCallIndirect(trampolineSig, trampolinePtr); + } + + // --- Bulk memory operations (inline native memmove/memset) --- + + /** + * Emit OOB check for bulk memory ops: if addr + size > memPages * 65536, trap. + * Both addr and size are i32 runtime values. + */ + private static void emitBulkBoundsCheck(EmitContext ctx, int addr, int size) { + var b = ctx.bridge.exports(); + int addr64 = b.emitUextendI64(addr); + int size64 = b.emitUextendI64(size); + int end = b.emitIadd(addr64, size64); + + int zero = b.emitIconst32(0); + int memPages = b.emitLoadI32(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.MEMORY_PAGES); + int memPages64 = b.emitUextendI64(memPages); + int memSize = b.emitIshl(memPages64, b.emitIconst64(16, 0)); + + int oob = b.emitIcmp(5, end, memSize); // GT unsigned + int trapBlock = b.createBlock(); + int okBlock = b.createBlock(); + b.emitBrif(oob, trapBlock, okBlock); + + fillTrapBlock(ctx, trapBlock, CtxBuffer.TRAP_OOB); + b.switchToBlock(okBlock); + } + + static void emitMemoryCopy(EmitContext ctx) { + int size = ctx.valueStack.pop(); + int srcOffset = ctx.valueStack.pop(); + int dstOffset = ctx.valueStack.pop(); + var b = ctx.bridge.exports(); + + // OOB checks for both src and dst ranges + emitBulkBoundsCheck(ctx, srcOffset, size); + emitBulkBoundsCheck(ctx, dstOffset, size); + + // Compute native addresses: memBase + offset + int memBase = b.useVar(ctx.memBaseVar); + int effectiveDst = b.emitIadd(memBase, b.emitUextendI64(dstOffset)); + int effectiveSrc = b.emitIadd(memBase, b.emitUextendI64(srcOffset)); + int size64 = b.emitUextendI64(size); + + // Call memmove(dst, src, size) via function pointer in ctxBuffer + int zero = b.emitIconst32(0); + int memmovePtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.MEMMOVE_PTR); + int sigRef = ctx.getOrCreateMemopSigRef(); + b.pushCallArg(effectiveDst); + b.pushCallArg(effectiveSrc); + b.pushCallArg(size64); + b.emitCallIndirect(sigRef, memmovePtr); + } + + static void emitMemoryFill(EmitContext ctx) { + int size = ctx.valueStack.pop(); + int value = ctx.valueStack.pop(); + int dstOffset = ctx.valueStack.pop(); + var b = ctx.bridge.exports(); + + // OOB check for dst range + emitBulkBoundsCheck(ctx, dstOffset, size); + + // Compute native address: memBase + dstOffset + int memBase = b.useVar(ctx.memBaseVar); + int effectiveDst = b.emitIadd(memBase, b.emitUextendI64(dstOffset)); + int value64 = b.emitUextendI64(value); + int size64 = b.emitUextendI64(size); + + // Call memset(dst, value, size) via function pointer in ctxBuffer + int zero = b.emitIconst32(0); + int memsetPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.MEMSET_PTR); + int sigRef = ctx.getOrCreateMemopSigRef(); + b.pushCallArg(effectiveDst); + b.pushCallArg(value64); + b.pushCallArg(size64); + b.emitCallIndirect(sigRef, memsetPtr); + } + + static void emitMemoryInit(EmitContext ctx, AnnotatedInstruction ins) { + int segmentId = (int) ins.operands()[0]; + int size = ctx.valueStack.pop(); + int srcOffset = ctx.valueStack.pop(); + int dstOffset = ctx.valueStack.pop(); + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + b.emitStoreI64( + argsPtr, zero, b.emitUextendI64(b.emitIconst32(segmentId)), CtxBuffer.argOffset(0)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(dstOffset), CtxBuffer.argOffset(1)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(srcOffset), CtxBuffer.argOffset(2)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(size), CtxBuffer.argOffset(3)); + + b.emitStoreI32( + b.useVar(ctx.ctxPtrVar), + zero, + b.emitIconst32(-8), // sentinel for memory.init + CtxBuffer.ARG_COUNT); + int trampolinePtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAMPOLINE_PTR); + int trampolineSig = ctx.getOrCreateTrampolineSigRef(); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + b.emitCallIndirect(trampolineSig, trampolinePtr); + } + + static void emitDataDrop(EmitContext ctx, AnnotatedInstruction ins) { + int segmentId = (int) ins.operands()[0]; + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + b.emitStoreI64( + argsPtr, zero, b.emitUextendI64(b.emitIconst32(segmentId)), CtxBuffer.argOffset(0)); + + b.emitStoreI32( + b.useVar(ctx.ctxPtrVar), + zero, + b.emitIconst32(-9), // sentinel for data.drop + CtxBuffer.ARG_COUNT); + int trampolinePtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAMPOLINE_PTR); + int trampolineSig = ctx.getOrCreateTrampolineSigRef(); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + b.emitCallIndirect(trampolineSig, trampolinePtr); + } + + // --- Atomic operations --- + + private static final int[] ATOMIC_LOAD_ACCESS_SIZE = {4, 8, 1, 2, 1, 2, 4}; + private static final int[] ATOMIC_STORE_ACCESS_SIZE = {4, 8, 1, 2, 1, 2, 4}; + private static final int[] ATOMIC_RMW_ACCESS_SIZE = {4, 8, 1, 2, 1, 2, 4}; + + private static void emitAlignmentCheck(EmitContext ctx, int addr, int offset, int alignment) { + if (alignment <= 1) { + return; // no alignment needed for byte ops + } + var b = ctx.bridge.exports(); + // effective = (addr + offset) as i64 + int addr64 = b.emitUextendI64(addr); + int effective = + b.emitIadd( + addr64, + b.emitIconst64( + (int) (Integer.toUnsignedLong(offset) & 0xFFFFFFFFL), + (int) (Integer.toUnsignedLong(offset) >>> 32))); + int mask = b.emitIconst64(alignment - 1, 0); + int rem = b.emitBand(effective, mask); + int zero64 = b.emitIconst64(0, 0); + int misaligned = b.emitIcmp(1, rem, zero64); // NE + int trapBlock = b.createBlock(); + int okBlock = b.createBlock(); + b.emitBrif(misaligned, trapBlock, okBlock); + fillTrapBlock(ctx, trapBlock, CtxBuffer.TRAP_UNALIGNED_ATOMIC); + b.switchToBlock(okBlock); + } + + static void emitAtomicLoad(EmitContext ctx, AnnotatedInstruction ins, int loadType) { + int addr = ctx.valueStack.pop(); + int offset = (int) ins.operands()[1]; + int accessSize = ATOMIC_LOAD_ACCESS_SIZE[loadType]; + emitAlignmentCheck(ctx, addr, offset, accessSize); + emitBoundsCheck(ctx, addr, offset, accessSize); + int memBase = ctx.bridge.exports().useVar(ctx.memBaseVar); + int result; + switch (loadType) { + case 0: + result = ctx.bridge.exports().emitAtomicLoadI32(memBase, addr, offset); + break; + case 1: + result = ctx.bridge.exports().emitAtomicLoadI64(memBase, addr, offset); + break; + case 2: + result = ctx.bridge.exports().emitAtomicLoad8UI32(memBase, addr, offset); + break; + case 3: + result = ctx.bridge.exports().emitAtomicLoad16UI32(memBase, addr, offset); + break; + case 4: + result = ctx.bridge.exports().emitAtomicLoad8UI64(memBase, addr, offset); + break; + case 5: + result = ctx.bridge.exports().emitAtomicLoad16UI64(memBase, addr, offset); + break; + case 6: + result = ctx.bridge.exports().emitAtomicLoad32UI64(memBase, addr, offset); + break; + default: + throw new IllegalArgumentException("Unknown atomic load type: " + loadType); + } + ctx.valueStack.push(result); + } + + static void emitAtomicStore(EmitContext ctx, AnnotatedInstruction ins, int storeType) { + int value = ctx.valueStack.pop(); + int addr = ctx.valueStack.pop(); + int offset = (int) ins.operands()[1]; + int accessSize = ATOMIC_STORE_ACCESS_SIZE[storeType]; + emitAlignmentCheck(ctx, addr, offset, accessSize); + emitBoundsCheck(ctx, addr, offset, accessSize); + int memBase = ctx.bridge.exports().useVar(ctx.memBaseVar); + switch (storeType) { + case 0: + ctx.bridge.exports().emitAtomicStoreI32(memBase, addr, value, offset); + break; + case 1: + ctx.bridge.exports().emitAtomicStoreI64(memBase, addr, value, offset); + break; + case 2: + ctx.bridge.exports().emitAtomicStore8(memBase, addr, value, offset); + break; + case 3: + ctx.bridge.exports().emitAtomicStore16(memBase, addr, value, offset); + break; + case 4: + ctx.bridge.exports().emitAtomicStore8I64(memBase, addr, value, offset); + break; + case 5: + ctx.bridge.exports().emitAtomicStore16I64(memBase, addr, value, offset); + break; + case 6: + ctx.bridge.exports().emitAtomicStore32I64(memBase, addr, value, offset); + break; + default: + throw new IllegalArgumentException("Unknown atomic store type: " + storeType); + } + } + + // rmwOp: 0=add, 1=sub, 2=and, 3=or, 4=xor, 5=xchg + static void emitAtomicRmw(EmitContext ctx, AnnotatedInstruction ins, int widthType, int rmwOp) { + int value = ctx.valueStack.pop(); + int addr = ctx.valueStack.pop(); + int offset = (int) ins.operands()[1]; + int accessSize = ATOMIC_RMW_ACCESS_SIZE[widthType]; + emitAlignmentCheck(ctx, addr, offset, accessSize); + emitBoundsCheck(ctx, addr, offset, accessSize); + int memBase = ctx.bridge.exports().useVar(ctx.memBaseVar); + int result; + switch (widthType) { + case 0: + result = ctx.bridge.exports().emitAtomicRmwI32(rmwOp, memBase, addr, value, offset); + break; + case 1: + result = ctx.bridge.exports().emitAtomicRmwI64(rmwOp, memBase, addr, value, offset); + break; + case 2: + result = + ctx.bridge + .exports() + .emitAtomicRmw8UI32(rmwOp, memBase, addr, value, offset); + break; + case 3: + result = + ctx.bridge + .exports() + .emitAtomicRmw16UI32(rmwOp, memBase, addr, value, offset); + break; + case 4: + result = + ctx.bridge + .exports() + .emitAtomicRmw8UI64(rmwOp, memBase, addr, value, offset); + break; + case 5: + result = + ctx.bridge + .exports() + .emitAtomicRmw16UI64(rmwOp, memBase, addr, value, offset); + break; + case 6: + result = + ctx.bridge + .exports() + .emitAtomicRmw32UI64(rmwOp, memBase, addr, value, offset); + break; + default: + throw new IllegalArgumentException("Unknown atomic RMW width: " + widthType); + } + ctx.valueStack.push(result); + } + + static void emitAtomicCmpxchg(EmitContext ctx, AnnotatedInstruction ins, int widthType) { + int replacement = ctx.valueStack.pop(); + int expected = ctx.valueStack.pop(); + int addr = ctx.valueStack.pop(); + int offset = (int) ins.operands()[1]; + int accessSize = ATOMIC_RMW_ACCESS_SIZE[widthType]; + emitAlignmentCheck(ctx, addr, offset, accessSize); + emitBoundsCheck(ctx, addr, offset, accessSize); + int memBase = ctx.bridge.exports().useVar(ctx.memBaseVar); + int result; + switch (widthType) { + case 0: + result = + ctx.bridge + .exports() + .emitAtomicCasI32(memBase, addr, expected, replacement, offset); + break; + case 1: + result = + ctx.bridge + .exports() + .emitAtomicCasI64(memBase, addr, expected, replacement, offset); + break; + case 2: + result = + ctx.bridge + .exports() + .emitAtomicCas8UI32(memBase, addr, expected, replacement, offset); + break; + case 3: + result = + ctx.bridge + .exports() + .emitAtomicCas16UI32(memBase, addr, expected, replacement, offset); + break; + case 4: + result = + ctx.bridge + .exports() + .emitAtomicCas8UI64(memBase, addr, expected, replacement, offset); + break; + case 5: + result = + ctx.bridge + .exports() + .emitAtomicCas16UI64(memBase, addr, expected, replacement, offset); + break; + case 6: + result = + ctx.bridge + .exports() + .emitAtomicCas32UI64(memBase, addr, expected, replacement, offset); + break; + default: + throw new IllegalArgumentException("Unknown atomic CAS width: " + widthType); + } + ctx.valueStack.push(result); + } + + static void emitAtomicWait32(EmitContext ctx, AnnotatedInstruction ins) { + int timeout = ctx.valueStack.pop(); // i64 + int expected = ctx.valueStack.pop(); // i32 + int addr = ctx.valueStack.pop(); // i32 + int offset = (int) ins.operands()[1]; + emitAlignmentCheck(ctx, addr, offset, 4); + emitBoundsCheck(ctx, addr, offset, 4); + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(addr), CtxBuffer.argOffset(0)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(expected), CtxBuffer.argOffset(1)); + b.emitStoreI64( + argsPtr, + zero, + b.emitIconst64( + (int) (Integer.toUnsignedLong(offset) & 0xFFFFFFFFL), + (int) (Integer.toUnsignedLong(offset) >>> 32)), + CtxBuffer.argOffset(2)); + b.emitStoreI64(argsPtr, zero, timeout, CtxBuffer.argOffset(3)); + + b.emitStoreI32(b.useVar(ctx.ctxPtrVar), zero, b.emitIconst32(-10), CtxBuffer.ARG_COUNT); + int trampolinePtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAMPOLINE_PTR); + int trampolineSig = ctx.getOrCreateTrampolineSigRef(); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + int rawResult = b.emitCallIndirect(trampolineSig, trampolinePtr); + ctx.valueStack.push(b.emitIreduceI32(rawResult)); + } + + static void emitAtomicWait64(EmitContext ctx, AnnotatedInstruction ins) { + int timeout = ctx.valueStack.pop(); // i64 + int expected = ctx.valueStack.pop(); // i64 + int addr = ctx.valueStack.pop(); // i32 + int offset = (int) ins.operands()[1]; + emitAlignmentCheck(ctx, addr, offset, 8); + emitBoundsCheck(ctx, addr, offset, 8); + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(addr), CtxBuffer.argOffset(0)); + b.emitStoreI64(argsPtr, zero, expected, CtxBuffer.argOffset(1)); + b.emitStoreI64( + argsPtr, + zero, + b.emitIconst64( + (int) (Integer.toUnsignedLong(offset) & 0xFFFFFFFFL), + (int) (Integer.toUnsignedLong(offset) >>> 32)), + CtxBuffer.argOffset(2)); + b.emitStoreI64(argsPtr, zero, timeout, CtxBuffer.argOffset(3)); + + b.emitStoreI32(b.useVar(ctx.ctxPtrVar), zero, b.emitIconst32(-11), CtxBuffer.ARG_COUNT); + int trampolinePtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAMPOLINE_PTR); + int trampolineSig = ctx.getOrCreateTrampolineSigRef(); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + int rawResult = b.emitCallIndirect(trampolineSig, trampolinePtr); + ctx.valueStack.push(b.emitIreduceI32(rawResult)); + } + + static void emitAtomicNotify(EmitContext ctx, AnnotatedInstruction ins) { + int count = ctx.valueStack.pop(); // i32 + int addr = ctx.valueStack.pop(); // i32 + int offset = (int) ins.operands()[1]; + emitAlignmentCheck(ctx, addr, offset, 4); + emitBoundsCheck(ctx, addr, offset, 4); + var b = ctx.bridge.exports(); + int zero = b.emitIconst32(0); + + int argsPtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.ARGS_PTR); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(addr), CtxBuffer.argOffset(0)); + b.emitStoreI64(argsPtr, zero, b.emitUextendI64(count), CtxBuffer.argOffset(1)); + b.emitStoreI64( + argsPtr, + zero, + b.emitIconst64( + (int) (Integer.toUnsignedLong(offset) & 0xFFFFFFFFL), + (int) (Integer.toUnsignedLong(offset) >>> 32)), + CtxBuffer.argOffset(2)); + + b.emitStoreI32(b.useVar(ctx.ctxPtrVar), zero, b.emitIconst32(-12), CtxBuffer.ARG_COUNT); + int trampolinePtr = b.emitLoadI64(b.useVar(ctx.ctxPtrVar), zero, CtxBuffer.TRAMPOLINE_PTR); + int trampolineSig = ctx.getOrCreateTrampolineSigRef(); + b.pushCallArg(b.useVar(ctx.ctxPtrVar)); + int rawResult = b.emitCallIndirect(trampolineSig, trampolinePtr); + ctx.valueStack.push(b.emitIreduceI32(rawResult)); + } + + static void emitAtomicFence(EmitContext ctx) { + ctx.bridge.exports().emitFence(); + } + + // --- Reference type operations --- + + static void emitRefNull(EmitContext ctx) { + // Ref types are i64 on the value stack. REF_NULL_VALUE = -1. + // Sign-extend so -1 as i32 → -1 as i64 (matching Java's (long)(int)-1). + var b = ctx.bridge.exports(); + ctx.valueStack.push(b.emitSextendI64(b.emitIconst32(-1))); + } + + static void emitRefIsNull(EmitContext ctx) { + var b = ctx.bridge.exports(); + int val = ctx.valueStack.pop(); + // val is i64 (ref type), compare against REF_NULL_VALUE (-1 as i64) + int refNull = b.emitSextendI64(b.emitIconst32(-1)); + int isNull = b.emitIcmp(0, val, refNull); // EQ, both i64 + ctx.valueStack.push(isNull); + } + + static void emitRefFunc(EmitContext ctx, AnnotatedInstruction ins) { + int funcIdx = (int) ins.operands()[0]; + // Ref types are i64 on the value stack. FuncIds are non-negative, + // so sign-extend and uextend give the same result. + var b = ctx.bridge.exports(); + ctx.valueStack.push(b.emitSextendI64(b.emitIconst32(funcIdx))); + } +} diff --git a/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeValueStack.java b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeValueStack.java new file mode 100644 index 00000000..429b0303 --- /dev/null +++ b/redline/compiler/src/main/java/run/endive/redline/experimental/compiler/internal/NativeValueStack.java @@ -0,0 +1,35 @@ +package run.endive.redline.experimental.compiler.internal; + +import java.util.ArrayDeque; +import java.util.Deque; + +final class NativeValueStack { + + private final Deque stack = new ArrayDeque<>(); + + void push(int valueId) { + stack.push(valueId); + } + + int pop() { + return stack.pop(); + } + + int peek() { + return stack.peek(); + } + + int size() { + return stack.size(); + } + + boolean isEmpty() { + return stack.isEmpty(); + } + + void trimTo(int height) { + while (stack.size() > height) { + stack.pop(); + } + } +} diff --git a/redline/plan.md b/redline/plan.md new file mode 100644 index 00000000..f3ec55ac --- /dev/null +++ b/redline/plan.md @@ -0,0 +1,257 @@ +# Redline Integration Plan + +Redline is a native compiler for WebAssembly that uses Cranelift to compile Wasm functions to machine code (x86_64, aarch64). It currently lives as a standalone project at `chicory-redline`. This plan describes how to rebuild it inside endive as a first-class, experimental module family. + +## Architecture Overview + +### What redline does (compilation pipeline) + +``` +Wasm bytecode + | + v +NativeCompiler (Java) -- walks Wasm instructions, emits Cranelift IR + | via RedlineBridge (Cranelift compiled to Wasm, run by endive itself) + v +Native machine code (byte[][] -- one blob per Wasm function) + | + v +NativeMachine (Java) -- maps code into executable memory, builds trampolines, + executes via Panama FFM downcalls (Java 25+) +``` + +### Current chicory-redline modules + +| Module | Purpose | +|--------|---------| +| `api` | `RedlineInstance`, `RedlineMachineFactoryProvider` (SPI), `NativeCodeSerializer`, `RedlineTarget`, `CtxBuffer` | +| `bridge` | `RedlineBridge` -- wraps `cranelift_bridge.wasm` (the Cranelift compiler itself running as Wasm) | +| `compiler` | `NativeCompiler`, `NativeAnalyzer`, `NativeEmitters`, `NativeValueStack`, `EmitContext` | +| `build-time-compiler` | `Generator` -- produces Java sources + serialized native code at build time | +| `compiler-maven-plugin` | Maven mojo wrapping the Generator | +| `runner` | Panama FFM backend (Java 25+): `NativeMachineFactory`, `NativeInstance`, `NativeMachine`, `NativeMemory`, `NativeTable`, `NativeGlobalInstance` | +| `runner-jffi` | JFFI backend (Java 11+): same structure, alternative FFI | +| `redline` | `UniversalInstance` -- smart builder with native-first, bytecode-fallback | + +## Design Decisions + +### 1. No UniversalInstance + +The `UniversalInstance` in chicory-redline is a smart builder that tries native execution first, then falls back to endive's JVM bytecode compiler. This adds an indirection layer that isn't necessary. + +**Decision:** Drop `UniversalInstance` entirely. The generated `Module` class from the build-time compiler can directly expose a builder that tries native first and falls back. The fallback logic is a few lines in the generated code -- it doesn't need its own module. + +### 2. Instance becomes AutoCloseable + +Currently endive's `Instance` (`run.endive.runtime.Instance`) is NOT `AutoCloseable`. The chicory-redline codebase works around this with `RedlineInstance` (an AutoCloseable wrapper) and `RedlineInstanceTracker` (a JUnit extension that tracks instances and closes them in `@AfterAll`). + +**Decision:** Make `Instance` implement `AutoCloseable`. The `close()` method delegates to the `Machine` (if the machine is `AutoCloseable`). For the interpreter, `close()` is a no-op. For redline's `NativeMachine`, it releases off-heap resources (Arena, mmap'd code regions, native memory). + +This eliminates: +- `RedlineInstance` wrapper +- `RedlineInstanceTracker` JUnit extension +- `NativeInstance` convenience alias +- The entire concept of a "closeable wrapper around Instance" + +Tests become: +```java +try (var instance = Instance.builder(module) + .withMachineFactory(factory::compile) + .build()) { + instance.export("func").apply(); +} +``` + +### 3. Cleanup: AutoCloseable only, no Cleaner + +The current redline uses `java.lang.ref.Cleaner` as a safety net in `NativeMachine` -- registering a `CleanupAction` that runs if the instance is GC'd without being closed. The `NativeMachineFactory.close()` then calls `cleanable.clean()` explicitly. + +**Decision:** Remove the Cleaner safety net. Use only `AutoCloseable` (try-with-resources or explicit `close()`). Rationale: +- Cleaner-based cleanup is unpredictable (GC-dependent timing) +- It masks resource leaks instead of surfacing them +- It adds complexity (CleanupAction record, captured references, races between explicit and GC cleanup) +- The principled approach: if you allocate native resources, you close them. If you forget, you leak (and we can add leak detection in debug builds later) + +The cleanup chain becomes: +``` +Instance.close() + -> Machine.close() (if Machine implements AutoCloseable) + -> Arena.close() (frees all Panama allocations, upcall stubs) + -> munmap() (code region, trampoline region) + -> NativeMemory.close() (the 4GB virtual reservation) +``` + +### 4. Test codegen changes + +The current test generator (`test-gen-lib` / `test-gen-plugin`) generates tests that create `Instance` objects without closing them. For the interpreter this is fine (no native resources), but for redline we need cleanup. + +**Decision:** Modify the test codegen templates to wrap Instance creation in try-with-resources where feasible, or add an `@AfterEach` / `@AfterAll` hook that closes tracked instances. Since Instance is now AutoCloseable, this is natural. The linking tests (which create multiple instances across a test class) can use a list + cleanup in `@AfterAll`. + +### 5. No SPI -- direct factory + +The chicory-redline SPI pattern (`RedlineMachineFactoryProvider` via ServiceLoader) was designed for swapping Panama vs JFFI by classpath. Since we're starting with Panama only and deferring JFFI, the SPI adds unnecessary indirection. + +**Decision:** Drop the SPI layer entirely. The runner module exposes `NativeMachineFactory` directly. Add SPI back when JFFI is ported. This eliminates `RedlineMachineFactoryProvider`, `PanamaMachineFactoryProvider`, and the `META-INF/services` registration. + +### 6. Integrate into existing Maven plugin + +Instead of creating a new `redline-compiler-maven-plugin-experimental`, extend the existing `endive-compiler-maven-plugin` with a redline compilation goal or configuration flag. The build-time compiler library (`redline-build-time-compiler-experimental`) remains a separate module, but the user-facing Maven plugin is a single entry point. + +### 7. Module structure and naming + +All redline modules live under `redline/` in the endive repo. Maven artifact IDs use `-experimental` suffix. Group ID is `run.endive`. JFFI is deferred. + +``` +redline/ + api/ -> run.endive:redline-api-experimental + bridge/ -> run.endive:redline-bridge-experimental + compiler/ -> run.endive:redline-compiler-experimental + build-time-compiler/ -> run.endive:redline-build-time-compiler-experimental + runner/ -> run.endive:redline-runner-experimental (Java 25+) + runner-tests/ -> run.endive:redline-runner-tests-experimental + jmh/ -> run.endive:redline-jmh-experimental +``` + +The existing `compiler-maven-plugin/` module is extended (not duplicated). + +Package namespace: `run.endive.redline.*` (mirroring the current `io.roastedroot.redline.*` structure). + +### 8. Dependency direction + +Redline modules depend on endive core (`runtime`, `wasm`, `compiler`, `codegen`, `wasi`), never the reverse. The core modules have zero knowledge of redline. This is the same clean boundary that exists in chicory-redline today (which depends on chicory as an external dep). + +### 9. The Cranelift bridge Wasm module + +The `cranelift_bridge.wasm` file is a pre-compiled Cranelift compiler itself, running as a Wasm guest inside endive. The `RedlineBridge` class wraps it using the `@WasmModuleInterface` annotation processor. This pattern stays -- it's elegant and self-hosting. + +The bridge wasm file moves to `redline/bridge/src/main/resources/` (or referenced via the templated `WasmResource.java` as it is today). + +### 10. Safe mode in MachineFactory + +When a module has been AOT-compiled with Cranelift, the MachineFactory must offer a "safe" mode that **bypasses native code entirely and forces endive's JVM bytecode compiler** instead. No architecture detection, no native code loading -- just the proven bytecode path. + +This is a runtime switch on the factory/builder, so users can flip between native and safe without recompiling. Useful for debugging ("is this a redline bug or a wasm bug?"), gradual rollout, and fallback. + +## Principles + +- **No hacks.** Every change must be organic and principled. No `@SuppressWarnings`, no instanceof downcasts where a proper type hierarchy works, no workarounds. If the right fix is to change an interface, change the interface. +- **Incremental.** Each phase produces a working, testable state. +- **Ask if in doubt.** Don't assume -- clarify before committing to a design. + +## Implementation Phases + +### Phase 1: Make Instance AutoCloseable -- DONE (3a5e0813) + +- `Machine extends AutoCloseable` with `default void close() {}` +- `Instance implements AutoCloseable`, `close()` delegates to `machine.close()` +- `@FunctionalInterface` preserved, no hacks +- All 79 existing runtime tests pass + +### Phase 2: Create module skeleton -- DONE (3a5e0813) + +- `redline/pom.xml` aggregator, parents root POM +- `redline/api/pom.xml` child, parents aggregator +- `-Predline` profile in root POM +- `redline-api-experimental` in root `` + +### Phase 3: Port the API module -- DONE (3a5e0813) + +Package: `run.endive.redline.api` / `run.endive.redline.api.internal` +- `RedlineTarget` -- refactored from string constants to enum with `triple()`, `resourceSuffix()`, `detectHost()` returning `Optional` +- `NativeCodeSerializer` -- serialization of `byte[][]` native code, unchanged logic +- `Interruptible` -- fixed spelling from `Interruptable` +- `CtxBuffer` -- native call context struct layout constants, unchanged +- `TypeMapUtils` -- canonical type map for `call_indirect`, updated imports to `run.endive.*` +- **Dropped** `RedlineInstance` (replaced by AutoCloseable Instance) +- **Dropped** `RedlineMachineFactoryProvider` (no SPI) +- POM depends only on `wasm` (not `runtime`) + +### Phase 4: Port the bridge module -- DONE (1691e2ee) + +- Renamed `RedlineBridge` → `CraneliftBridge` +- Removed `WasmResource` template, `@WasmModuleInterface`, antrun, templating plugin, annotations dep +- Uses `moduleInterface` plugin property instead +- Enforcer check for missing wasm binary +- Rust sources in `redline/wasm-build/`, cranelift_bridge.wasm gitignored + +### Phase 5: Port the compiler module -- DONE (53088b95) + +- Removed `NativeAnalyzer` — dead code handling consolidated into control stack +- Removed `enterScope`/`exitScope`/`scopeRestore` from NativeValueStack +- `ChicoryException` → `WasmEngineException` + +### Phase 6: Port runner + spec tests -- DONE + +- Runner: removed Cleaner from NativeMachine and NativeMemory, direct AutoCloseable +- Runner: dropped RedlineInstance, NativeInstance, PanamaMachineFactoryProvider +- Runner: `build()` returns `Instance` directly +- Runner-tests: 28,713 spec tests passing (32 excluded — externref gap) +- No `RedlineInstanceTracker` — AutoCloseable Instance handles cleanup + +### Externref gap (future work) + +32 spec tests excluded — all externref-related (table_fill, table_grow, select, br_table with extern refs). Root cause: redline represents refs as i64 in native code, but endive now passes externref as Java Objects directly. + +Long-term fix: Cranelift stack maps (`enable_safepoints`). The compiler emits metadata recording which stack slots hold live externrefs at each safepoint. Java-side GC can then walk native frames to find roots. This is what Wasmtime uses. Significant effort — separate project. + +--- + +### ORIGINAL PLAN (below) — kept for reference + + + +- Copy `cranelift_bridge.wasm` resource +- Port `RedlineBridge` -- update to use endive's annotation processor (`@WasmModuleInterface`) +- Update the `WasmResource.java` template + +### Phase 5: Port the compiler module + +Port the core compiler classes: +- `NativeCompiler` -- the orchestrator (walks Wasm instructions, builds Cranelift IR via bridge) +- `NativeAnalyzer` -- pre-pass reachability analysis +- `NativeEmitters` -- instruction emission helpers +- `NativeValueStack` -- scope-aware value stack +- `EmitContext` -- emission state + +These are largely self-contained. Main changes: +- Update imports from `com.dylibso.chicory.*` to `run.endive.*` +- Update bridge references to the new package + +### Phase 6: Port the runner module (Panama) + spec tests + +Port `NativeMachineFactory`, `NativeMachine`, `NativeMemory`, `NativeTable`, `NativeGlobalInstance`, `PanamaExecutor`. + +Key changes: +- **Remove Cleaner from NativeMachine** -- just implement `AutoCloseable` directly +- `NativeMachineFactory` used directly (no SPI indirection) +- The factory provides `Machine`, `TableFactory`, `GlobalFactory`, `MemoryFactory` callbacks to `Instance.Builder` +- `NativeMachine` implements `AutoCloseable` and is what `Instance.close()` eventually delegates to + +**Tests (immediately after runner compiles):** +- Port `runner-tests` with endive's test-gen infrastructure +- Update `Spectest` / `TestModule` / `NativeInstanceBuilder` helpers to use AutoCloseable Instance directly +- No `RedlineInstanceTracker` -- use try-with-resources / `@AfterAll` cleanup +- Run the full spec test suite to validate the runner end-to-end +- Port hand-written tests: `NativeMemoryLeakTest`, `FactoryReuseTest`, `HostExceptionTest`, `InterruptionTest`, `ShootoutTest` + +### Phase 7: Extend the existing Maven plugin + port build-time compiler + +- Port `Generator` (as `redline-build-time-compiler-experimental`) -- update generated code to use endive APIs directly (no `RedlineInstance`, no `UniversalInstance`, no SPI) +- Extend `endive-compiler-maven-plugin` with a redline compilation goal/configuration +- Generated code produces `Instance.Builder`-compatible output using `NativeMachineFactory` directly + +### Phase 8: Benchmarks and integration tests + +- Port JMH benchmarks +- Port integration tests +- Validate performance parity with standalone chicory-redline + +## Verification + +- `mvn clean install -pl redline -am` builds all redline modules +- Spec tests pass for Panama runner +- No Cleaner/Cleanable references in redline code (grep to verify) +- No `RedlineInstance`, `UniversalInstance`, or `RedlineMachineFactoryProvider` references +- No separate `redline-compiler-maven-plugin` -- redline compilation is a feature of the existing plugin +- Instance.close() is a no-op for interpreter tests (no regression) +- JMH benchmarks show comparable performance to standalone chicory-redline diff --git a/redline/pom.xml b/redline/pom.xml new file mode 100644 index 00000000..eb56b061 --- /dev/null +++ b/redline/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + + run.endive + endive + 999-SNAPSHOT + ../pom.xml + + redline-parent-experimental + pom + Endive - Redline + Redline native compiler for WebAssembly + + + api + bridge + compiler + runner + runner-tests + + + diff --git a/redline/runner-tests/pom.xml b/redline/runner-tests/pom.xml new file mode 100644 index 00000000..776ef7b7 --- /dev/null +++ b/redline/runner-tests/pom.xml @@ -0,0 +1,280 @@ + + + 4.0.0 + + + run.endive + redline-parent-experimental + 999-SNAPSHOT + ../pom.xml + + redline-runner-tests-experimental + jar + Endive - Redline Runner Tests + Spec tests for the Redline native runner + + + 25 + true + + + + + run.endive + redline-api-experimental + + + run.endive + redline-runner-experimental + + + run.endive + runtime + + + run.endive + wasm + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + run.endive + redline-compiler-experimental + test + + + run.endive + wasm-corpus + test + + + run.endive + wasm-tools + test + + + + + + + run.endive + test-gen-plugin + ${project.version} + + ${project.basedir}/../../testsuite + + address.wast + align.wast + binary-leb128.wast + binary.wast + block.wast + br.wast + br_if.wast + br_table.wast + bulk.wast + call.wast + call_indirect.wast + comments.wast + const.wast + conversions.wast + custom.wast + data.wast + elem.wast + endianness.wast + exports.wast + f32.wast + f32_bitwise.wast + f32_cmp.wast + f64.wast + f64_bitwise.wast + f64_cmp.wast + fac.wast + float_exprs.wast + float_literals.wast + float_memory.wast + float_misc.wast + forward.wast + func.wast + func_ptrs.wast + global.wast + i32.wast + i64.wast + if.wast + imports.wast + inline-module.wast + int_exprs.wast + int_literals.wast + labels.wast + left-to-right.wast + linking.wast + load.wast + local_get.wast + local_set.wast + local_tee.wast + loop.wast + memory.wast + memory_copy.wast + memory_fill.wast + memory_grow.wast + memory_init.wast + memory_redundancy.wast + memory_size.wast + memory_trap.wast + names.wast + nop.wast + proposals/tail-call/return_call.wast + proposals/tail-call/return_call_indirect.wast + proposals/threads/atomic.wast + proposals/threads/exports.wast + proposals/threads/imports.wast + proposals/threads/memory.wast + ref_func.wast + ref_is_null.wast + ref_null.wast + return.wast + select.wast + skip-stack-guard-page.wast + stack.wast + start.wast + store.wast + switch.wast + table-sub.wast + table.wast + table_copy.wast + table_fill.wast + table_get.wast + table_grow.wast + table_init.wast + table_set.wast + table_size.wast + token.wast + traps.wast + type.wast + unreachable.wast + unreached-invalid.wast + unreached-valid.wast + unwind.wast + utf8-custom-section-id.wast + utf8-import-field.wast + utf8-import-module.wast + utf8-invalid-encoding.wast + + SpecV1GlobalTest.test76, SpecV1GlobalTest.test77, + SpecV1MemoryTest.test6, SpecV1MemoryTest.test7, + SpecV1ElemTest.test14, SpecV1ElemTest.test15, SpecV1ElemTest.test34, SpecV1ElemTest.test36, + SpecV1ElemTest.test97, + SpecV1BulkTest.test75, + SpecV1AlignTest.test157, SpecV1AlignTest.test158, SpecV1AlignTest.test159, + SpecV1AlignTest.test160, SpecV1AlignTest.test161, + SpecV1BinaryTest.test41, SpecV1BinaryTest.test42, SpecV1BinaryTest.test43, + SpecV1BinaryTest.test44, SpecV1BinaryTest.test45, SpecV1BinaryTest.test46, + SpecV1BinaryTest.test47, SpecV1BinaryTest.test48, SpecV1BinaryTest.test49, + SpecV1BinaryTest.test50, + SpecV1DataTest.test9, SpecV1DataTest.test10, + SpecV1ImportsTest.instantiate_testModule8Instance, + SpecV1ImportsTest.test118, SpecV1ImportsTest.test119, SpecV1ImportsTest.test120, + SpecV1ImportsTest.test123, SpecV1ImportsTest.test124, SpecV1ImportsTest.test125, + SpecV1ImportsTest.test127, SpecV1ImportsTest.test128, SpecV1ImportsTest.test129, + SpecV1LinkingTest.test129, SpecV1LinkingTest.test130, SpecV1LinkingTest.test131, + SpecV1StartTest.test18, + SpecV1FuncTest.test85, + SpecV1ThreadsImportsTest.test64, SpecV1ThreadsImportsTest.test65, + SpecV1ThreadsImportsTest.test66, + SpecV1ThreadsImportsTest.test98, SpecV1ThreadsImportsTest.test99, + SpecV1ThreadsImportsTest.test100, + SpecV1ThreadsMemoryTest.test9, SpecV1ThreadsMemoryTest.test10, + SpecV1BrTableTest.test147, SpecV1BrTableTest.test148, SpecV1BrTableTest.test149, + SpecV1ElemTest.test89, SpecV1ElemTest.test90, SpecV1ElemTest.test93, + SpecV1GlobalTest.test28, + SpecV1RefIsNullTest.test2, + SpecV1SelectTest.test30, SpecV1SelectTest.test35, SpecV1SelectTest.test36, + SpecV1TableFillTest.test8, SpecV1TableFillTest.test9, SpecV1TableFillTest.test10, + SpecV1TableFillTest.test13, SpecV1TableFillTest.test14, SpecV1TableFillTest.test15, + SpecV1TableFillTest.test18, SpecV1TableFillTest.test19, SpecV1TableFillTest.test20, + SpecV1TableFillTest.test23, SpecV1TableFillTest.test24, SpecV1TableFillTest.test26, + SpecV1TableFillTest.test32, + SpecV1TableGetTest.test3, + SpecV1TableGrowTest.test8, SpecV1TableGrowTest.test13, SpecV1TableGrowTest.test15, + SpecV1TableGrowTest.test16, SpecV1TableGrowTest.test17, SpecV1TableGrowTest.test19, + SpecV1TableSetTest.test3 + + obsolete-keywords.wast + simd_address.wast + simd_align.wast + simd_bit_shift.wast + simd_bitwise.wast + simd_boolean.wast + simd_const.wast + simd_conversions.wast + simd_f32x4.wast + simd_f32x4_arith.wast + simd_f32x4_cmp.wast + simd_f32x4_pmin_pmax.wast + simd_f32x4_rounding.wast + simd_f64x2.wast + simd_f64x2_arith.wast + simd_f64x2_cmp.wast + simd_f64x2_pmin_pmax.wast + simd_f64x2_rounding.wast + simd_i16x8_arith.wast + simd_i16x8_arith2.wast + simd_i16x8_cmp.wast + simd_i16x8_extadd_pairwise_i8x16.wast + simd_i16x8_extmul_i8x16.wast + simd_i16x8_q15mulr_sat_s.wast + simd_i16x8_sat_arith.wast + simd_i32x4_arith.wast + simd_i32x4_arith2.wast + simd_i32x4_cmp.wast + simd_i32x4_dot_i16x8.wast + simd_i32x4_extadd_pairwise_i16x8.wast + simd_i32x4_extmul_i16x8.wast + simd_i32x4_trunc_sat_f32x4.wast + simd_i32x4_trunc_sat_f64x2.wast + simd_i64x2_arith.wast + simd_i64x2_arith2.wast + simd_i64x2_cmp.wast + simd_i64x2_extmul_i32x4.wast + simd_i8x16_arith.wast + simd_i8x16_arith2.wast + simd_i8x16_cmp.wast + simd_i8x16_sat_arith.wast + simd_int_to_int_extend.wast + simd_lane.wast + simd_linking.wast + simd_load.wast + simd_load16_lane.wast + simd_load32_lane.wast + simd_load64_lane.wast + simd_load8_lane.wast + simd_load_extend.wast + simd_load_splat.wast + simd_load_zero.wast + simd_select.wast + simd_splat.wast + simd_store.wast + simd_store16_lane.wast + simd_store32_lane.wast + simd_store64_lane.wast + simd_store8_lane.wast + + + + + + wasm-test-gen + + + + + + + diff --git a/redline/runner-tests/src/test/java/run/endive/testing/ArgsAdapter.java b/redline/runner-tests/src/test/java/run/endive/testing/ArgsAdapter.java new file mode 100644 index 00000000..b4682012 --- /dev/null +++ b/redline/runner-tests/src/test/java/run/endive/testing/ArgsAdapter.java @@ -0,0 +1,58 @@ +package run.endive.testing; + +import java.util.ArrayList; +import java.util.List; +import run.endive.runtime.CallResult; +import run.endive.runtime.ExportFunction; + +public final class ArgsAdapter { + private final List longs = new ArrayList<>(); + private final List refs = new ArrayList<>(); + private boolean hasRefs; + + private ArgsAdapter() {} + + public static ArgsAdapter builder() { + return new ArgsAdapter(); + } + + public ArgsAdapter add(long arg) { + longs.add(arg); + refs.add(null); + return this; + } + + public ArgsAdapter add(long[] args) { + for (var arg : args) { + longs.add(arg); + refs.add(null); + } + return this; + } + + public ArgsAdapter addRef(Object ref) { + longs.add(0L); + refs.add(ref); + hasRefs = true; + return this; + } + + public long[] build() { + var result = new long[longs.size()]; + for (int i = 0; i < longs.size(); i++) { + result[i] = longs.get(i); + } + return result; + } + + public Object[] buildRefs() { + if (!hasRefs) { + return null; + } + return refs.toArray(); + } + + public CallResult applyWithRefs(ExportFunction func) { + return func.applyWithRefs(build(), buildRefs()); + } +} diff --git a/redline/runner-tests/src/test/java/run/endive/testing/NativeInstanceBuilder.java b/redline/runner-tests/src/test/java/run/endive/testing/NativeInstanceBuilder.java new file mode 100644 index 00000000..90a09d1c --- /dev/null +++ b/redline/runner-tests/src/test/java/run/endive/testing/NativeInstanceBuilder.java @@ -0,0 +1,40 @@ +package run.endive.testing; + +import run.endive.redline.experimental.api.RedlineTarget; +import run.endive.redline.experimental.compiler.internal.NativeCompiler; +import run.endive.redline.experimental.runner.NativeMachineFactory; +import run.endive.runtime.ImportValues; +import run.endive.runtime.Instance; +import run.endive.wasm.WasmModule; + +public final class NativeInstanceBuilder { + + private final NativeMachineFactory.Builder delegate; + + private NativeInstanceBuilder(NativeMachineFactory.Builder delegate) { + this.delegate = delegate; + } + + public static NativeInstanceBuilder builder(WasmModule module) { + var b = NativeMachineFactory.builder(module); + b.withCompilerFunction( + m -> + new NativeCompiler(RedlineTarget.detectHost().orElseThrow().triple(), m) + .compileAll()); + return new NativeInstanceBuilder(b); + } + + public NativeInstanceBuilder withImportValues(ImportValues importValues) { + delegate.withImportValues(importValues); + return this; + } + + public NativeInstanceBuilder withStart(boolean start) { + delegate.withStart(start); + return this; + } + + public Instance build() { + return delegate.build(); + } +} diff --git a/redline/runner-tests/src/test/java/run/endive/testing/Spectest.java b/redline/runner-tests/src/test/java/run/endive/testing/Spectest.java new file mode 100644 index 00000000..fdc36353 --- /dev/null +++ b/redline/runner-tests/src/test/java/run/endive/testing/Spectest.java @@ -0,0 +1,126 @@ +package run.endive.testing; + +import static run.endive.wasm.types.Value.REF_NULL_VALUE; + +import java.util.List; +import run.endive.redline.experimental.runner.NativeMachineFactory; +import run.endive.runtime.GlobalInstance; +import run.endive.runtime.HostFunction; +import run.endive.runtime.ImportGlobal; +import run.endive.runtime.ImportMemory; +import run.endive.runtime.ImportTable; +import run.endive.runtime.ImportValues; +import run.endive.runtime.Instance; +import run.endive.runtime.TableInstance; +import run.endive.runtime.WasmFunctionHandle; +import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.MemoryLimits; +import run.endive.wasm.types.Table; +import run.endive.wasm.types.TableLimits; +import run.endive.wasm.types.ValType; +import run.endive.wasm.types.Value; + +public final class Spectest { + private static final WasmFunctionHandle noop = (Instance instance, long... args) -> null; + + private Spectest() {} + + public static ImportValues toImportValues() { + return ImportValues.builder() + .addFunction(new HostFunction("spectest", "print", FunctionType.empty(), noop)) + .addFunction( + new HostFunction( + "spectest", + "print_i32", + FunctionType.of(List.of(ValType.I32), List.of()), + noop)) + .addFunction( + new HostFunction( + "spectest", + "print_i32_1", + FunctionType.of(List.of(ValType.I32), List.of()), + noop)) + .addFunction( + new HostFunction( + "spectest", + "print_i32_2", + FunctionType.of(List.of(ValType.I32), List.of()), + noop)) + .addFunction( + new HostFunction( + "spectest", + "print_f32", + FunctionType.of(List.of(ValType.F32), List.of()), + noop)) + .addFunction( + new HostFunction( + "spectest", + "print_i32_f32", + FunctionType.of(List.of(ValType.I32, ValType.F32), List.of()), + noop)) + .addFunction( + new HostFunction( + "spectest", + "print_i64", + FunctionType.of(List.of(ValType.I64), List.of()), + noop)) + .addFunction( + new HostFunction( + "spectest", + "print_i64_1", + FunctionType.of(List.of(ValType.I64), List.of()), + noop)) + .addFunction( + new HostFunction( + "spectest", + "print_i64_2", + FunctionType.of(List.of(ValType.I64), List.of()), + noop)) + .addFunction( + new HostFunction( + "spectest", + "print_f64", + FunctionType.of(List.of(ValType.F64), List.of()), + noop)) + .addFunction( + new HostFunction( + "spectest", + "print_f64_f64", + FunctionType.of(List.of(ValType.F64, ValType.F64), List.of()), + noop)) + .addGlobal( + new ImportGlobal( + "spectest", "global_i32", new GlobalInstance(Value.i32(666)))) + .addGlobal( + new ImportGlobal( + "spectest", "global_i64", new GlobalInstance(Value.i64(666)))) + .addGlobal( + new ImportGlobal( + "spectest", + "global_f32", + new GlobalInstance(Value.fromFloat(666.6f)))) + .addGlobal( + new ImportGlobal( + "spectest", + "global_f64", + new GlobalInstance(Value.fromDouble(666.6)))) + .addMemory( + new ImportMemory( + "spectest", + "memory", + NativeMachineFactory.createMemory(new MemoryLimits(1, 2)))) + .addMemory( + new ImportMemory( + "spectest", + "shared_memory", + NativeMachineFactory.createMemory(new MemoryLimits(1, 2, true)))) + .addTable( + new ImportTable( + "spectest", + "table", + new TableInstance( + new Table(ValType.FuncRef, new TableLimits(10, 20)), + REF_NULL_VALUE))) + .build(); + } +} diff --git a/redline/runner-tests/src/test/java/run/endive/testing/TestModule.java b/redline/runner-tests/src/test/java/run/endive/testing/TestModule.java new file mode 100644 index 00000000..5eb5fc2d --- /dev/null +++ b/redline/runner-tests/src/test/java/run/endive/testing/TestModule.java @@ -0,0 +1,73 @@ +package run.endive.testing; + +import java.io.IOException; +import run.endive.corpus.CorpusResources; +import run.endive.runtime.ImportValues; +import run.endive.runtime.Instance; +import run.endive.runtime.Store; +import run.endive.tools.wasm.Wat2Wasm; +import run.endive.wasm.MalformedException; +import run.endive.wasm.Parser; +import run.endive.wasm.WasmModule; + +public class TestModule { + + private final WasmModule module; + + private static final String HACK_MATCH_ALL_MALFORMED_EXCEPTION_TEXT = + "Matching keywords to get the WebAssembly testsuite to pass: " + + "malformed UTF-8 encoding " + + "import after function " + + "inline function type " + + "constant out of range" + + "unknown operator " + + "unexpected token " + + "unexpected mismatching " + + "mismatching label " + + "unknown type " + + "duplicate func " + + "duplicate local " + + "duplicate global " + + "duplicate memory " + + "duplicate table " + + "mismatching label " + + "import after global " + + "import after table " + + "import after memory " + + "i32 constant out of range " + + "unknown label " + + "alignment " + + "multiple start sections " + + "duplicate field"; + + public static TestModule of(String classpath) { + try (var is = CorpusResources.getResource(classpath.substring(1))) { + if (classpath.endsWith(".wat")) { + byte[] parsed; + try { + parsed = Wat2Wasm.parse(is); + } catch (RuntimeException e) { + throw new MalformedException( + e.getMessage() + HACK_MATCH_ALL_MALFORMED_EXCEPTION_TEXT); + } + return of(Parser.parse(parsed)); + } + return of(Parser.parse(is)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static TestModule of(WasmModule module) { + return new TestModule(module); + } + + public TestModule(WasmModule module) { + this.module = module; + } + + public Instance instantiate(Store s) { + ImportValues importValues = s.toImportValues(); + return NativeInstanceBuilder.builder(module).withImportValues(importValues).build(); + } +} diff --git a/redline/runner/pom.xml b/redline/runner/pom.xml new file mode 100644 index 00000000..42036892 --- /dev/null +++ b/redline/runner/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + run.endive + redline-parent-experimental + 999-SNAPSHOT + ../pom.xml + + redline-runner-experimental + jar + Endive - Redline Runner + Native code execution via Panama FFM (Java 25+) + + + 25 + + + + + run.endive + redline-api-experimental + + + run.endive + redline-bridge-experimental + + + run.endive + runtime + + + run.endive + wasm + + + diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/NativeMachineFactory.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/NativeMachineFactory.java new file mode 100644 index 00000000..61095689 --- /dev/null +++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/NativeMachineFactory.java @@ -0,0 +1,192 @@ +package run.endive.redline.experimental.runner; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import run.endive.redline.experimental.api.Interruptible; +import run.endive.redline.experimental.runner.internal.NativeGlobalInstance; +import run.endive.redline.experimental.runner.internal.NativeMachine; +import run.endive.redline.experimental.runner.internal.NativeMemory; +import run.endive.redline.experimental.runner.internal.NativeTable; +import run.endive.runtime.GlobalInstance; +import run.endive.runtime.ImportValues; +import run.endive.runtime.Instance; +import run.endive.runtime.Machine; +import run.endive.runtime.Memory; +import run.endive.runtime.TableInstance; +import run.endive.wasm.WasmModule; +import run.endive.wasm.types.MemoryLimits; +import run.endive.wasm.types.MutabilityType; +import run.endive.wasm.types.Table; +import run.endive.wasm.types.ValType; + +public final class NativeMachineFactory implements AutoCloseable, Interruptible { + + private final Arena arena = Arena.ofShared(); + private final WasmModule module; + private final byte[][] precompiledCode; + private final Function compilerFunction; + private final List nativeTables = new ArrayList<>(); + private MemorySegment globalsBuffer; + private int globalIndex; + private NativeMachine nativeMachine; + + private NativeMachineFactory( + WasmModule module, + byte[][] precompiledCode, + Function compilerFunction) { + this.module = module; + this.precompiledCode = precompiledCode; + this.compilerFunction = compilerFunction; + + int importGlobalCount = + (int) + module.importSection().stream() + .filter( + i -> + i.importType() + == run.endive.wasm.types.ExternalType + .GLOBAL) + .count(); + int definedGlobalCount = + module.globalSection() != null ? module.globalSection().globalCount() : 0; + int totalGlobals = importGlobalCount + definedGlobalCount; + this.globalsBuffer = + totalGlobals > 0 ? arena.allocate((long) totalGlobals * 8, 8) : MemorySegment.NULL; + this.globalIndex = importGlobalCount; + } + + public static Builder builder(WasmModule module) { + return new Builder(module); + } + + public TableInstance createTable(Table table, int initValue) { + var nativeTable = new NativeTable(table, arena); + nativeTables.add(nativeTable); + return nativeTable; + } + + public GlobalInstance createGlobal( + long value, long highValue, ValType type, MutabilityType mutability) { + return new NativeGlobalInstance(globalsBuffer, globalIndex++, value, type, mutability); + } + + public static Memory createMemory(MemoryLimits limits) { + return new NativeMemory(limits); + } + + public Machine compile(Instance instance) { + int importGlobalCount = + (int) + module.importSection().stream() + .filter( + i -> + i.importType() + == run.endive.wasm.types.ExternalType + .GLOBAL) + .count(); + this.globalIndex = importGlobalCount; + this.nativeTables.clear(); + this.nativeMachine = + new NativeMachine( + instance, + arena, + nativeTables, + globalsBuffer, + precompiledCode, + compilerFunction); + return nativeMachine; + } + + @Override + public void requestInterrupt() { + if (nativeMachine != null) { + nativeMachine.requestInterrupt(); + } + } + + @Override + public void clearInterrupt() { + if (nativeMachine != null) { + nativeMachine.clearInterrupt(); + } + } + + @Override + public void close() { + if (nativeMachine != null) { + nativeMachine.close(); + } + try { + arena.close(); + } catch (IllegalStateException e) { + // may already be closed by NativeMachine + } + } + + public static final class Builder { + + private final WasmModule module; + private byte[][] precompiledCode; + private Function compilerFunction; + private ImportValues importValues; + private MemoryLimits memoryLimits; + private boolean start = true; + private boolean initialize = true; + + Builder(WasmModule module) { + this.module = module; + } + + public Builder withPrecompiledCode(byte[][] precompiledCode) { + this.precompiledCode = precompiledCode; + return this; + } + + public Builder withCompilerFunction(Function compilerFunction) { + this.compilerFunction = compilerFunction; + return this; + } + + public Builder withImportValues(ImportValues importValues) { + this.importValues = importValues; + return this; + } + + public Builder withMemoryLimits(MemoryLimits limits) { + this.memoryLimits = limits; + return this; + } + + public Builder withStart(boolean start) { + this.start = start; + return this; + } + + public Builder withInitialize(boolean init) { + this.initialize = init; + return this; + } + + public Instance build() { + var factory = new NativeMachineFactory(module, precompiledCode, compilerFunction); + var instanceBuilder = + Instance.builder(module) + .withMachineFactory(factory::compile) + .withTableFactory(factory::createTable) + .withGlobalFactory(factory::createGlobal) + .withMemoryFactory(NativeMachineFactory::createMemory) + .withStart(start) + .withInitialize(initialize); + if (importValues != null) { + instanceBuilder.withImportValues(importValues); + } + if (memoryLimits != null) { + instanceBuilder.withMemoryLimits(memoryLimits); + } + return instanceBuilder.build(); + } + } +} diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeGlobalInstance.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeGlobalInstance.java new file mode 100644 index 00000000..09d61347 --- /dev/null +++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeGlobalInstance.java @@ -0,0 +1,56 @@ +package run.endive.redline.experimental.runner.internal; + +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import run.endive.runtime.GlobalInstance; +import run.endive.wasm.types.MutabilityType; +import run.endive.wasm.types.ValType; +import run.endive.wasm.types.Value; + +/** + * GlobalInstance backed by an off-heap MemorySegment buffer. + * Native code and Java code read/write the same memory — no sync needed. + */ +public final class NativeGlobalInstance extends GlobalInstance { + + private final MemorySegment buffer; + private final long offset; + + public NativeGlobalInstance( + MemorySegment buffer, + int index, + long initialValue, + ValType valType, + MutabilityType mutabilityType) { + super(initialValue, 0, valType, mutabilityType); + this.buffer = buffer; + this.offset = (long) index * 8; + // Write initial value to buffer + buffer.set(ValueLayout.JAVA_LONG, offset, initialValue); + } + + @Override + public long getValue() { + return buffer.get(ValueLayout.JAVA_LONG, offset); + } + + @Override + public long getValueLow() { + return buffer.get(ValueLayout.JAVA_LONG, offset); + } + + @Override + public void setValue(long value) { + buffer.set(ValueLayout.JAVA_LONG, offset, value); + } + + @Override + public void setValue(Value value) { + buffer.set(ValueLayout.JAVA_LONG, offset, value.raw()); + } + + @Override + public void setValueLow(long value) { + buffer.set(ValueLayout.JAVA_LONG, offset, value); + } +} diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java new file mode 100644 index 00000000..c99ce1f9 --- /dev/null +++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMachine.java @@ -0,0 +1,1137 @@ +package run.endive.redline.experimental.runner.internal; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.ref.Reference; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import run.endive.redline.experimental.api.RedlineTarget; +import run.endive.redline.experimental.api.internal.CtxBuffer; +import run.endive.redline.experimental.api.internal.TypeMapUtils; +import run.endive.redline.experimental.bridge.CraneliftBridge; +import run.endive.runtime.Instance; +import run.endive.runtime.Machine; +import run.endive.wasm.WasmEngineException; +import run.endive.wasm.types.FunctionType; +import run.endive.wasm.types.ValType; +import run.endive.wasm.types.Value; + +/** + * Machine implementation that compiles Wasm functions to native x86_64 + * via Cranelift and executes them through Panama FFM. + * + *

Calling convention for all compiled functions: + *

+ *   param 0: memBase  (i64/ADDRESS) — pointer to linear memory
+ *   param 1: ctxPtr   (i64/ADDRESS) — pointer to call context struct
+ *   param 2+: Wasm function parameters
+ *   return: Wasm return value
+ * 
+ * + *

See {@link CtxBuffer} for the full layout definition. + */ +public final class NativeMachine implements Machine { + + private static final int CTX_SIZE = CtxBuffer.CTX_SIZE; + + // Conversion handles for adapting downcalls to uniform (MS, MS, long[]) → long. + // These use Wasm bit-reinterpretation (not numeric casts). + private static final MethodHandle LONG_TO_FLOAT; + private static final MethodHandle LONG_TO_DOUBLE; + private static final MethodHandle FLOAT_TO_LONG; + private static final MethodHandle DOUBLE_TO_LONG; + + static { + try { + var lookup = MethodHandles.lookup(); + LONG_TO_FLOAT = + lookup.findStatic( + Value.class, + "longToFloat", + MethodType.methodType(float.class, long.class)); + LONG_TO_DOUBLE = + lookup.findStatic( + Value.class, + "longToDouble", + MethodType.methodType(double.class, long.class)); + FLOAT_TO_LONG = + lookup.findStatic( + Value.class, + "floatToLong", + MethodType.methodType(long.class, float.class)); + DOUBLE_TO_LONG = + lookup.findStatic( + Value.class, + "doubleToLong", + MethodType.methodType(long.class, double.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new ExceptionInInitializerError(e); + } + } + + private final Arena arena; + private final Instance instance; + private final MethodHandle[] downcalls; + private final MemorySegment codeRegion; + private final long codeRegionSize; + private MemorySegment trampolineRegion; + private long trampolineRegionSize; + private final MemorySegment ctxBuffer; + private final MemorySegment funcTable; + private final MemorySegment argsBuffer; + private final MemorySegment globalsBuffer; + private final MemorySegment funcTypesArray; + private MemorySegment tablePtrsArray; + private NativeTable[] nativeTables; + private boolean tablesInitialized; + private final int numImports; + private final int globalCount; + private boolean importGlobalsInitialized; + private MemorySegment cachedMemBase; + private boolean memBaseInitialized; + private NativeMemory nativeMemory; + private volatile Throwable pendingException; + + public NativeMachine( + Instance instance, + Arena arena, + java.util.List sharedTables, + MemorySegment sharedGlobalsBuffer, + byte[][] precompiledCode, + Function compilerFunction) { + this.instance = instance; + this.arena = arena; + var module = instance.module(); + this.numImports = + (int) + module.importSection().stream() + .filter( + i -> + i.importType() + == run.endive.wasm.types.ExternalType + .FUNCTION) + .count(); + int totalFuncs = numImports + module.codeSection().functionBodyCount(); + this.downcalls = new MethodHandle[totalFuncs]; + + // Allocate call context buffer + ctxBuffer = arena.allocate(CTX_SIZE, 8); + + // Allocate function pointer table (one i64 per function) + funcTable = arena.allocate((long) totalFuncs * 8, 8); + + // Globals buffer from factory (already has module globals written by Instance) + this.globalsBuffer = sharedGlobalsBuffer; + this.globalCount = + (int) + module.importSection().stream() + .filter( + i -> + i.importType() + == run.endive.wasm.types + .ExternalType.GLOBAL) + .count() + + (module.globalSection() != null + ? module.globalSection().globalCount() + : 0); + + // Allocate args buffer (separate from ctxBuffer, no fixed arg limit) + this.argsBuffer = arena.allocate((long) CtxBuffer.ARGS_BUFFER_CAPACITY * 8, 8); + + // Allocate funcTypes array with canonical type indices. + // Structurally equal FunctionTypes get the same canonical index, + // enabling correct call_indirect type checking with duplicate types. + int[] canonicalTypeMap = TypeMapUtils.buildCanonicalTypeMap(module); + this.funcTypesArray = arena.allocate((long) totalFuncs * 4, 4); + for (int i = 0; i < numImports; i++) { + int rawType = instance.functionType(i); + funcTypesArray.set(ValueLayout.JAVA_INT, (long) i * 4, canonicalTypeMap[rawType]); + } + for (int i = 0; i < module.functionSection().functionCount(); i++) { + int funcId = numImports + i; + int rawType = module.functionSection().getFunctionType(i); + funcTypesArray.set(ValueLayout.JAVA_INT, (long) funcId * 4, canonicalTypeMap[rawType]); + } + + // NativeTables: tables are created by Instance via tableFactory, but Instance + // hasn't finished constructing yet (tables created after Machine). We'll + // populate the tablePtrs lazily on first call() once tables exist. + this.nativeTables = null; + this.tablePtrsArray = MemorySegment.NULL; + this.tablesInitialized = false; + + // Create CALL_INDIRECT trampoline upcall stub (kept for TABLE.INIT/ELEM.DROP) + MemorySegment trampolineStub = createTrampolineStub(); + + // Create memory.grow upcall stub + MemorySegment memGrowStub = createMemGrowStub(); + + // Write pointers to ctxBuffer + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.FUNC_TABLE_PTR, funcTable.address()); + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.TRAMPOLINE_PTR, trampolineStub.address()); + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.ARGS_PTR, argsBuffer.address()); + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.GLOBALS_PTR, globalsBuffer.address()); + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.MEM_GROW_PTR, memGrowStub.address()); + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.TABLE_PTRS, tablePtrsArray.address()); + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.FUNC_TYPES_PTR, funcTypesArray.address()); + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.MEMMOVE_PTR, PanamaExecutor.MEMMOVE_ADDR); + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.MEMSET_PTR, PanamaExecutor.MEMSET_ADDR); + + // Use pre-compiled code, or compile at runtime if explicitly enabled + byte[][] compiledCode; + if (precompiledCode != null) { + compiledCode = precompiledCode; + } else if (compilerFunction != null) { + compiledCode = compilerFunction.apply(module); + } else { + throw new WasmEngineException( + "No precompiled code provided. Use the redline-compiler-maven-plugin" + + " to precompile, or use NativeMachineFactory.builder(module)" + + " for runtime compilation."); + } + + // mmap all compiled code into a single executable region + long totalSize = 0; + for (byte[] code : compiledCode) { + if (code != null) { + totalSize += CraneliftBridge.align(code.length, 16); + } + } + totalSize = Math.max(totalSize, 4096); + totalSize = CraneliftBridge.align(totalSize, 4096); + this.codeRegionSize = totalSize; + + try { + codeRegion = PanamaExecutor.mmapCode(totalSize); + long offset = 0; + + // Track per-function code pointers and types for downcall creation after trampolines + MemorySegment[] funcCodePtrs = new MemorySegment[compiledCode.length]; + FunctionType[] funcTypesByBody = new FunctionType[compiledCode.length]; + + for (int i = 0; i < compiledCode.length; i++) { + if (compiledCode[i] != null) { + int funcId = numImports + i; + MemorySegment.copy( + MemorySegment.ofArray(compiledCode[i]), + 0, + codeRegion, + offset, + compiledCode[i].length); + + var funcType = + (FunctionType) + module.typeSection() + .getType(module.functionSection().getFunctionType(i)); + + MemorySegment codePtr = codeRegion.asSlice(offset); + funcCodePtrs[i] = codePtr; + funcTypesByBody[i] = funcType; + + // Store native code address in function pointer table (Tail convention) + funcTable.set(ValueLayout.JAVA_LONG, (long) funcId * 8, codePtr.address()); + + offset += CraneliftBridge.align(compiledCode[i].length, 16); + } + } + PanamaExecutor.mprotectExec(codeRegion, totalSize); + + // Create import upcall stubs (platform ABI) + MemorySegment[] importStubs = new MemorySegment[numImports]; + FunctionType[] importTypes = new FunctionType[numImports]; + for (int funcId = 0; funcId < numImports; funcId++) { + var importFunc = instance.imports().function(funcId); + var funcType = importFunc.functionType(); + importStubs[funcId] = createImportStub(funcId, funcType); + importTypes[funcId] = funcType; + downcalls[funcId] = null; // imports dispatch through call() directly + } + + // Compile ABI trampolines via Cranelift bridge + long[] importStubAddrs = new long[numImports]; + for (int i = 0; i < numImports; i++) { + importStubAddrs[i] = importStubs[i].address(); + } + try (var bridge = new CraneliftBridge()) { + bridge.init( + RedlineTarget.detectHost() + .orElseThrow( + () -> + new WasmEngineException( + "Unsupported platform for native" + + " compilation")) + .triple()); + var trampolines = + bridge.compileTrampolines( + compiledCode, + funcTypesByBody, + importTypes, + importStubAddrs, + trampolineStub.address(), + memGrowStub.address(), + PanamaExecutor.MEMMOVE_ADDR, + PanamaExecutor.MEMSET_ADDR); + + long trampTotalSize = Math.max(trampolines.totalSize(), 4096); + trampTotalSize = CraneliftBridge.align(trampTotalSize, 4096); + this.trampolineRegionSize = trampTotalSize; + + // Mmap and copy trampoline code + trampolineRegion = PanamaExecutor.mmapCode(trampTotalSize); + long trampOffset = 0; + + // Copy entry trampolines and record their addresses + Map entryTrampolinePtrs = new HashMap<>(); + for (var entry : trampolines.entryTrampolines().entrySet()) { + entryTrampolinePtrs.put(entry.getKey(), trampolineRegion.asSlice(trampOffset)); + trampOffset = copyCode(entry.getValue(), trampolineRegion, trampOffset); + } + + // Copy import trampolines and store addresses in funcTable + for (int funcId = 0; funcId < numImports; funcId++) { + byte[] code = trampolines.importTrampolines()[funcId]; + long addr = trampolineRegion.asSlice(trampOffset).address(); + trampOffset = copyCode(code, trampolineRegion, trampOffset); + funcTable.set(ValueLayout.JAVA_LONG, (long) funcId * 8, addr); + } + + // Copy internal stub trampolines and update ctxBuffer + trampOffset = + copyCodeAndUpdateCtx( + trampolines.trampolineStubTramp(), + trampolineRegion, + trampOffset, + ctxBuffer, + CtxBuffer.TRAMPOLINE_PTR); + trampOffset = + copyCodeAndUpdateCtx( + trampolines.memGrowStubTramp(), + trampolineRegion, + trampOffset, + ctxBuffer, + CtxBuffer.MEM_GROW_PTR); + trampOffset = + copyCodeAndUpdateCtx( + trampolines.memmoveTramp(), + trampolineRegion, + trampOffset, + ctxBuffer, + CtxBuffer.MEMMOVE_PTR); + trampOffset = + copyCodeAndUpdateCtx( + trampolines.memsetTramp(), + trampolineRegion, + trampOffset, + ctxBuffer, + CtxBuffer.MEMSET_PTR); + + PanamaExecutor.mprotectExec(trampolineRegion, trampTotalSize); + + // Create downcall handles via entry trampolines + for (int i = 0; i < compiledCode.length; i++) { + if (compiledCode[i] != null) { + int funcId = numImports + i; + MemorySegment trampolinePtr = entryTrampolinePtrs.get(funcTypesByBody[i]); + downcalls[funcId] = + createDowncallViaTrampoline( + trampolinePtr, funcCodePtrs[i], funcTypesByBody[i]); + } + } + } + + } catch (Throwable e) { + throw new WasmEngineException("Failed to set up native code", e); + } + + this.nativeMemory = instance.memory() instanceof NativeMemory nm ? nm : null; + } + + @Override + public void close() { + if (nativeMemory != null) { + nativeMemory.close(); + } + try { + arena.close(); + } catch (IllegalStateException e) { + // may already be closed + } + try { + PanamaExecutor.munmap(codeRegion, codeRegionSize); + if (trampolineRegion != null && trampolineRegionSize > 0) { + PanamaExecutor.munmap(trampolineRegion, trampolineRegionSize); + } + } catch (Throwable e) { + throw new WasmEngineException("Failed to unmap native code", e); + } + } + + @SuppressWarnings("unchecked") + private static void sneakyThrow(Throwable e) throws E { + throw (E) e; + } + + // --- Downcall creation (Java → native via entry trampoline) --- + + private MethodHandle createDowncallViaTrampoline( + MemorySegment trampolinePtr, MemorySegment funcCodePtr, FunctionType funcType) { + // Entry trampoline has platform ABI signature: + // (ADDRESS funcPtr, ADDRESS memBase, ADDRESS ctxPtr, typed_params...) -> typed_return + var layouts = new ArrayList(); + layouts.add(ValueLayout.ADDRESS); // funcPtr + layouts.add(ValueLayout.ADDRESS); // memBase + layouts.add(ValueLayout.ADDRESS); // ctxPtr + + for (ValType param : funcType.params()) { + layouts.add(valTypeToLayout(param)); + } + + ValueLayout returnLayout = null; + if (!funcType.returns().isEmpty()) { + if (funcType.returns().size() > 1) { + returnLayout = ValueLayout.JAVA_LONG; + } else { + returnLayout = valTypeToLayout(funcType.returns().get(0)); + } + } + + FunctionDescriptor desc; + if (returnLayout != null) { + desc = FunctionDescriptor.of(returnLayout, layouts.toArray(new ValueLayout[0])); + } else { + desc = FunctionDescriptor.ofVoid(layouts.toArray(new ValueLayout[0])); + } + + MethodHandle handle = Linker.nativeLinker().downcallHandle(trampolinePtr, desc); + // Bind the wasm function pointer as the first argument so the call site + // signature remains (memBase, ctxPtr, args...) -> result + handle = MethodHandles.insertArguments(handle, 0, funcCodePtr); + return adaptHandle(handle, funcType); + } + + /** + * Adapt a native downcall handle to a uniform type: + * {@code (MemorySegment memBase, MemorySegment ctxPtr, long[] wasmArgs) -> long}. + * + *

This eliminates Object[] boxing and enables invokeExact at the call site. + * Float/double conversions use Wasm bit-reinterpretation (not numeric casts). + */ + private static MethodHandle adaptHandle(MethodHandle handle, FunctionType funcType) { + int paramCount = funcType.params().size(); + + // Step 1: Adapt return type to long + if (handle.type().returnType() == void.class) { + // Void → long: call void handle for side effects, return 0L + MethodHandle zero = + MethodHandles.dropArguments( + MethodHandles.constant(long.class, 0L), + 0, + handle.type().parameterList()); + handle = MethodHandles.foldArguments(zero, handle); + } else if (!funcType.returns().isEmpty() && funcType.returns().size() == 1) { + ValType retType = funcType.returns().get(0); + if (retType.equals(ValType.F32)) { + handle = MethodHandles.filterReturnValue(handle, FLOAT_TO_LONG); + } else if (retType.equals(ValType.F64)) { + handle = MethodHandles.filterReturnValue(handle, DOUBLE_TO_LONG); + } + // I32 → long widening and I64 identity handled by asType below + } + // Multi-return: native returns long dummy, no conversion needed + + // Step 2: Filter float/double params (bit-reinterpret long → float/double) + for (int i = 0; i < paramCount; i++) { + ValType paramType = funcType.params().get(i); + if (paramType.equals(ValType.F32)) { + handle = MethodHandles.filterArguments(handle, i + 2, LONG_TO_FLOAT); + } else if (paramType.equals(ValType.F64)) { + handle = MethodHandles.filterArguments(handle, i + 2, LONG_TO_DOUBLE); + } + } + + // Step 3: Normalize remaining params to long and return to long. + // explicitCastArguments handles long→int narrowing (asType only does widening). + var targetParams = new Class[2 + paramCount]; + targetParams[0] = MemorySegment.class; + targetParams[1] = MemorySegment.class; + for (int i = 0; i < paramCount; i++) { + targetParams[i + 2] = long.class; + } + handle = + MethodHandles.explicitCastArguments( + handle, MethodType.methodType(long.class, targetParams)); + + // Step 4: Spread wasm params from long[] + handle = handle.asSpreader(long[].class, paramCount); + // Final type: (MemorySegment, MemorySegment, long[]) → long + + return handle; + } + + // --- Import upcall stubs (native → Java for host imports) --- + + private MemorySegment createImportStub(int funcId, FunctionType funcType) { + try { + // Build the native descriptor matching compiled function convention: + // (ADDRESS memBase, ADDRESS ctxPtr, typed_params...) -> typed_return + var layouts = new ArrayList(); + layouts.add(ValueLayout.ADDRESS); // memBase + layouts.add(ValueLayout.ADDRESS); // ctxPtr + for (ValType param : funcType.params()) { + layouts.add(valTypeToLayout(param)); + } + + ValueLayout returnLayout = null; + if (!funcType.returns().isEmpty()) { + returnLayout = valTypeToLayout(funcType.returns().get(0)); + } + + FunctionDescriptor desc; + if (returnLayout != null) { + desc = FunctionDescriptor.of(returnLayout, layouts.toArray(new ValueLayout[0])); + } else { + desc = FunctionDescriptor.ofVoid(layouts.toArray(new ValueLayout[0])); + } + + // Build Java param types for the MethodHandle + var targetParamTypes = new ArrayList>(); + targetParamTypes.add(MemorySegment.class); // memBase + targetParamTypes.add(MemorySegment.class); // ctxPtr + for (ValType param : funcType.params()) { + targetParamTypes.add(valTypeToJavaClass(param)); + } + + // importDispatchDirect reads args from ctxBuffer (written by CALL handler) + MethodHandle directHandler = + MethodHandles.lookup() + .bind( + this, + "importDispatchDirect", + MethodType.methodType(long.class, int.class)); + directHandler = MethodHandles.insertArguments(directHandler, 0, funcId); + // Now: () -> long + + // Drop all native params (the stub ignores them, reads from ctxBuffer) + MethodHandle dropper = + MethodHandles.dropArguments( + directHandler, 0, targetParamTypes.toArray(new Class[0])); + + // Cast return type to match native descriptor + if (returnLayout == null) { + // Void function: discard the long return value + var voidType = + MethodType.methodType(void.class, targetParamTypes.toArray(new Class[0])); + dropper = dropper.asType(voidType); + } else if (!funcType.returns().isEmpty()) { + var retClass = valTypeToJavaClass(funcType.returns().get(0)); + if (!retClass.equals(long.class)) { + dropper = + MethodHandles.explicitCastArguments( + dropper, + MethodType.methodType( + retClass, targetParamTypes.toArray(new Class[0]))); + } + } + + return Linker.nativeLinker().upcallStub(dropper, desc, arena); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new WasmEngineException("Failed to create import stub for func " + funcId, e); + } + } + + /** + * Dispatches a function call from native code. Reads args from ctxBuffer. + * Called by import upcall stubs and bridge stubs for non-native functions. + */ + @SuppressWarnings("unused") + private long importDispatchDirect(int funcId) { + try { + if (Thread.interrupted()) { + requestInterrupt(); + Thread.currentThread().interrupt(); + } + int argCount = ctxBuffer.get(ValueLayout.JAVA_INT, CtxBuffer.ARG_COUNT); + long[] args = new long[argCount]; + for (int i = 0; i < argCount; i++) { + args[i] = argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(i)); + } + if (funcId < numImports) { + var importFunc = instance.imports().function(funcId); + long[] result = importFunc.handle().apply(instance, args); + return (result != null && result.length > 0) ? result[0] : 0L; + } + throw new WasmEngineException("Function " + funcId + " not compiled"); + } catch (Throwable t) { + pendingException = t; + return 0L; + } + } + + // --- CALL_INDIRECT trampoline --- + + private MemorySegment createTrampolineStub() { + try { + MethodHandle handler = + MethodHandles.lookup() + .bind( + this, + "callIndirectTrampoline", + MethodType.methodType(long.class, long.class)); + var desc = FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG); + return Linker.nativeLinker().upcallStub(handler, desc, arena); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new WasmEngineException("Failed to create trampoline stub", e); + } + } + + @SuppressWarnings("unused") + private long callIndirectTrampoline(long ctxAddr) { + try { + if (Thread.interrupted()) { + requestInterrupt(); + Thread.currentThread().interrupt(); + } + var ctx = MemorySegment.ofAddress(ctxAddr).reinterpret(CTX_SIZE); + int argCount = ctx.get(ValueLayout.JAVA_INT, CtxBuffer.ARG_COUNT); + + // Negative argCount = table operation sentinel + if (argCount < 0) { + return handleTableOperation(argCount); + } + + // Normal call_indirect path (fallback, rarely used now) + int typeId = ctx.get(ValueLayout.JAVA_INT, CtxBuffer.TYPE_ID); + int tableIdx = ctx.get(ValueLayout.JAVA_INT, CtxBuffer.TABLE_IDX); + int elemIdx = ctx.get(ValueLayout.JAVA_INT, CtxBuffer.ELEM_IDX); + + int funcId = nativeTables[tableIdx].requiredRef(elemIdx); + + // Type check + int actualTypeIdx = instance.functionType(funcId); + if (actualTypeIdx != typeId) { + throw new WasmEngineException("indirect call type mismatch"); + } + + long[] args = new long[argCount]; + for (int i = 0; i < argCount; i++) { + args[i] = argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(i)); + } + + long[] result = this.call(funcId, args); + return result.length > 0 ? result[0] : 0L; + } catch (Throwable t) { + pendingException = t; + return 0L; + } + } + + private long handleTableOperation(int opCode) { + switch (opCode) { + case -1 -> { // table grow fill + int oldSize = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(0)); + int newSize = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(1)); + int fillValue = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(2)); + long tableAddr = argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(3)); + int tblIdx = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(4)); + boolean externRef = nativeTables[tblIdx].isExternRef(); + var tableBuf = + MemorySegment.ofAddress(tableAddr) + .reinterpret( + CtxBuffer.TABLE_ENTRIES_OFFSET + + (long) newSize * CtxBuffer.TABLE_ENTRY_SIZE); + for (int i = oldSize; i < newSize; i++) { + writeTableEntry(tableBuf, i, fillValue, externRef); + } + // Update TableLimits so import validation sees the grown size + nativeTables[tblIdx].limits().grow(newSize - oldSize); + } + case -2 -> { // table fill + int offset = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(0)); + int end = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(1)); + int fillValue = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(2)); + long tableAddr = argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(3)); + int tblIdx = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(4)); + boolean externRef = nativeTables[tblIdx].isExternRef(); + var tableBuf = + MemorySegment.ofAddress(tableAddr) + .reinterpret( + CtxBuffer.TABLE_ENTRIES_OFFSET + + (long) end * CtxBuffer.TABLE_ENTRY_SIZE); + for (int i = offset; i < end; i++) { + writeTableEntry(tableBuf, i, fillValue, externRef); + } + } + case -3 -> { // table copy (16-byte entries) + long srcAddr = argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(0)); + long dstAddr = argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(1)); + int srcOff = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(2)); + int dstOff = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(3)); + int size = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(4)); + long entrySize = CtxBuffer.TABLE_ENTRY_SIZE; + var srcBuf = + MemorySegment.ofAddress(srcAddr) + .reinterpret( + CtxBuffer.TABLE_ENTRIES_OFFSET + + (long) (srcOff + size) * entrySize); + var dstBuf = + MemorySegment.ofAddress(dstAddr) + .reinterpret( + CtxBuffer.TABLE_ENTRIES_OFFSET + + (long) (dstOff + size) * entrySize); + // Copy 16-byte entries with correct overlap handling + if (dstOff <= srcOff) { + for (int i = 0; i < size; i++) { + copyTableEntry(srcBuf, srcOff + i, dstBuf, dstOff + i); + } + } else { + for (int i = size - 1; i >= 0; i--) { + copyTableEntry(srcBuf, srcOff + i, dstBuf, dstOff + i); + } + } + } + case -4 -> { // table init + int tableIdx = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(0)); + int elemIdx = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(1)); + int dstOffset = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(2)); + int srcOffset = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(3)); + int size = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(4)); + // Instance.table(tableIdx) returns NativeTable (set via tableFactory). + // NativeTable.setRef resolves funcId → funcPtr+typeIdx via funcResolver. + run.endive.runtime.OpcodeImpl.TABLE_INIT( + instance, tableIdx, elemIdx, size, srcOffset, dstOffset); + } + case -5 -> { // elem drop + int elemIdx = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(0)); + instance.setElement(elemIdx, null); + } + case -8 -> { // memory.init + int segmentId = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(0)); + int dst = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(1)); + int src = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(2)); + int size = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(3)); + instance.memory().initPassiveSegment(segmentId, dst, src, size); + } + case -9 -> { // data.drop + int segmentId = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(0)); + instance.memory().drop(segmentId); + } + case -10 -> { // memory.atomic.wait32 + int addr = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(0)); + int expected = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(1)); + int offset = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(2)); + long timeout = argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(3)); + return instance.memory().atomicWait(addr + offset, expected, timeout); + } + case -11 -> { // memory.atomic.wait64 + int addr = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(0)); + long expected = argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(1)); + int offset = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(2)); + long timeout = argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(3)); + return instance.memory().atomicWait(addr + offset, expected, timeout); + } + case -12 -> { // memory.atomic.notify + int addr = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(0)); + int count = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(1)); + int offset = (int) argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(2)); + return instance.memory().atomicNotify(addr + offset, count); + } + default -> throw new WasmEngineException("Unknown trampoline operation: " + opCode); + } + return 0L; + } + + private void writeTableEntry( + MemorySegment tableBuf, int index, int funcId, boolean isExternRef) { + long base = CtxBuffer.TABLE_ENTRIES_OFFSET + (long) index * CtxBuffer.TABLE_ENTRY_SIZE; + if (funcId == Value.REF_NULL_VALUE) { + tableBuf.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0); + tableBuf.set( + ValueLayout.JAVA_INT, + base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, + Value.REF_NULL_VALUE); + tableBuf.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L); + } else if (isExternRef) { + tableBuf.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0); + tableBuf.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, funcId); + tableBuf.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L); + } else { + long funcPtr = funcTable.get(ValueLayout.JAVA_LONG, (long) funcId * 8); + int typeIdx = funcTypesArray.get(ValueLayout.JAVA_INT, (long) funcId * 4); + tableBuf.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, typeIdx); + tableBuf.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, funcId); + tableBuf.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, funcPtr); + } + } + + /** Copy a single 16-byte table entry from src[srcIdx] to dst[dstIdx]. */ + private static void copyTableEntry( + MemorySegment src, int srcIdx, MemorySegment dst, int dstIdx) { + long srcBase = CtxBuffer.TABLE_ENTRIES_OFFSET + (long) srcIdx * CtxBuffer.TABLE_ENTRY_SIZE; + long dstBase = CtxBuffer.TABLE_ENTRIES_OFFSET + (long) dstIdx * CtxBuffer.TABLE_ENTRY_SIZE; + int typeIdx = src.get(ValueLayout.JAVA_INT, srcBase + CtxBuffer.ENTRY_TYPE_IDX_OFFSET); + int funcId = src.get(ValueLayout.JAVA_INT, srcBase + CtxBuffer.ENTRY_FUNC_ID_OFFSET); + long funcPtr = src.get(ValueLayout.JAVA_LONG, srcBase + CtxBuffer.ENTRY_FUNC_PTR_OFFSET); + dst.set(ValueLayout.JAVA_INT, dstBase + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, typeIdx); + dst.set(ValueLayout.JAVA_INT, dstBase + CtxBuffer.ENTRY_FUNC_ID_OFFSET, funcId); + dst.set(ValueLayout.JAVA_LONG, dstBase + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, funcPtr); + } + + // --- Memory grow upcall stub --- + + private MemorySegment createMemGrowStub() { + try { + MethodHandle handler = + MethodHandles.lookup() + .bind( + this, + "memoryGrowHandler", + MethodType.methodType(long.class, long.class)); + var desc = FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.JAVA_LONG); + return Linker.nativeLinker().upcallStub(handler, desc, arena); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new WasmEngineException("Failed to create memory grow stub", e); + } + } + + @SuppressWarnings("unused") + private long memoryGrowHandler(long ctxAddr) { + try { + if (Thread.interrupted()) { + requestInterrupt(); + Thread.currentThread().interrupt(); + } + var ctx = MemorySegment.ofAddress(ctxAddr).reinterpret(CTX_SIZE); + int delta = ctx.get(ValueLayout.JAVA_INT, CtxBuffer.MEM_GROW_DELTA); + var mem = instance.memory(); + int oldPages = mem.grow(delta); + // Update memory base and page count in ctxBuffer + if (oldPages != -1 && mem instanceof NativeMemory nativeMemory) { + ctx.set( + ValueLayout.JAVA_LONG, + CtxBuffer.MEM_BASE_ADDR, + nativeMemory.nativeAddress().address()); + ctx.set(ValueLayout.JAVA_INT, CtxBuffer.MEMORY_PAGES, mem.pages()); + } + return oldPages; + } catch (Throwable t) { + pendingException = t; + return -1L; + } + } + + // --- Globals initialization --- + + /** + * Lazily replace module-defined GlobalInstance objects with NativeGlobalInstance + * backed by the off-heap globalsBuffer. Called once on first native call, + * after Instance.initialize() has created the original GlobalInstance objects. + * + * For imported globals, we copy their current value into the buffer (read-only + * from native code's perspective — imported mutable globals are rare). + */ + private void initializeImportGlobals() { + if (importGlobalsInitialized || globalCount == 0) { + return; + } + importGlobalsInitialized = true; + + // Module-defined globals are already NativeGlobalInstance (created by globalFactory). + // Only need to copy imported global values into the shared buffer. + int importGlobalCount = + (int) + instance.module().importSection().stream() + .filter( + i -> + i.importType() + == run.endive.wasm.types.ExternalType + .GLOBAL) + .count(); + + for (int i = 0; i < importGlobalCount; i++) { + globalsBuffer.set(ValueLayout.JAVA_LONG, (long) i * 8, instance.global(i).getValue()); + } + } + + private void initializeNativeTables() { + if (tablesInitialized) { + return; + } + tablesInitialized = true; + + var module = instance.module(); + int importedTableCount = instance.imports().tableCount(); + int definedTableCount = module.tableSection().tableCount(); + int tableCount = importedTableCount + definedTableCount; + + if (tableCount == 0) { + this.nativeTables = new NativeTable[0]; + return; + } + + // Instance.table(i) returns NativeTable (created by tableFactory). + // Just collect references and build the pointer array. + this.nativeTables = new NativeTable[tableCount]; + this.tablePtrsArray = arena.allocate((long) tableCount * 8, 8); + + for (int i = 0; i < tableCount; i++) { + var table = instance.table(i); + if (table instanceof NativeTable nt) { + nativeTables[i] = nt; + } else { + // Imported table not created by our factory — wrap it + var tableDef = new run.endive.wasm.types.Table(table.elementType(), table.limits()); + var nt = new NativeTable(tableDef, arena); + for (int j = 0; j < table.size(); j++) { + nt.setRef(j, table.ref(j), instance); + } + nativeTables[i] = nt; + } + + tablePtrsArray.set( + ValueLayout.JAVA_LONG, (long) i * 8, nativeTables[i].nativeBuffer().address()); + } + + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.TABLE_PTRS, tablePtrsArray.address()); + } + + /** Package-private: used by NativeTable to resolve funcId → funcPtr across modules. */ + MemorySegment getFuncTable() { + return funcTable; + } + + /** Package-private: used by NativeTable to resolve funcId → canonicalTypeIdx across modules. */ + MemorySegment getFuncTypesArray() { + return funcTypesArray; + } + + private static WasmEngineException trapException(int trapCode) { + return switch (trapCode) { + case CtxBuffer.TRAP_DIV_BY_ZERO -> new WasmEngineException("integer divide by zero"); + case CtxBuffer.TRAP_INT_OVERFLOW -> new WasmEngineException("integer overflow"); + case CtxBuffer.TRAP_UNREACHABLE -> new WasmEngineException("unreachable"); + case CtxBuffer.TRAP_TRUNC_OVERFLOW -> new WasmEngineException("integer overflow"); + case CtxBuffer.TRAP_TRUNC_NAN -> + new WasmEngineException("invalid conversion to integer"); + case CtxBuffer.TRAP_OOB -> new WasmEngineException("out of bounds memory access"); + case CtxBuffer.TRAP_CALL_STACK_EXHAUSTED -> + new WasmEngineException("call stack exhausted"); + case CtxBuffer.TRAP_TABLE_OOB -> new WasmEngineException("out of bounds table access"); + case CtxBuffer.TRAP_UNDEFINED_ELEMENT -> new WasmEngineException("undefined element"); + case CtxBuffer.TRAP_UNINITIALIZED_ELEMENT -> + new WasmEngineException("uninitialized element"); + case CtxBuffer.TRAP_INDIRECT_CALL_TYPE_MISMATCH -> + new WasmEngineException("indirect call type mismatch"); + case CtxBuffer.TRAP_UNALIGNED_ATOMIC -> new WasmEngineException("unaligned atomic"); + case CtxBuffer.TRAP_INTERRUPTED -> new WasmEngineException("interrupted"); + default -> new WasmEngineException("trap: unknown code " + trapCode); + }; + } + + // --- Trampoline copy helpers --- + + private static long copyCode(byte[] code, MemorySegment region, long offset) { + MemorySegment.copy(MemorySegment.ofArray(code), 0, region, offset, code.length); + return offset + CraneliftBridge.align(code.length, 16); + } + + private static long copyCodeAndUpdateCtx( + byte[] code, MemorySegment region, long offset, MemorySegment ctxBuf, long ctxOffset) { + ctxBuf.set(ValueLayout.JAVA_LONG, ctxOffset, region.asSlice(offset).address()); + return copyCode(code, region, offset); + } + + // --- Main dispatch --- + + private ValueLayout valTypeToLayout(ValType type) { + if (type.equals(ValType.I32)) { + return ValueLayout.JAVA_INT; + } + if (type.equals(ValType.I64)) { + return ValueLayout.JAVA_LONG; + } + if (type.equals(ValType.F32)) { + return ValueLayout.JAVA_FLOAT; + } + if (type.equals(ValType.F64)) { + return ValueLayout.JAVA_DOUBLE; + } + // Reference types (funcref, externref) are opaque i64 + int op = type.opcode(); + if (op == ValType.ID.RefNull || op == ValType.ID.Ref) { + return ValueLayout.JAVA_LONG; + } + throw new WasmEngineException("Unsupported type for native: " + type); + } + + private Class valTypeToJavaClass(ValType type) { + if (type.equals(ValType.I32)) { + return int.class; + } + if (type.equals(ValType.I64)) { + return long.class; + } + if (type.equals(ValType.F32)) { + return float.class; + } + if (type.equals(ValType.F64)) { + return double.class; + } + int op = type.opcode(); + if (op == ValType.ID.RefNull || op == ValType.ID.Ref) { + return long.class; + } + throw new WasmEngineException("Unsupported type: " + type); + } + + @Override + public long[] call(int funcId, long[] args) throws WasmEngineException { + if (funcId < numImports) { + // Host import — delegate directly + var imprt = instance.imports().function(funcId); + return imprt.handle().apply(instance, args); + } + + var handle = downcalls[funcId]; + + try { + var funcType = (FunctionType) instance.type(instance.functionType(funcId)); + + initializeImportGlobals(); + initializeNativeTables(); + + // Reset stack limit so native code re-initializes from calling thread's RSP + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.STACK_LIMIT, 0L); + + if (!memBaseInitialized) { + var mem = instance.memory(); + if (mem instanceof NativeMemory nativeMemory) { + cachedMemBase = nativeMemory.nativeAddress(); + ctxBuffer.set( + ValueLayout.JAVA_LONG, + CtxBuffer.MEM_BASE_ADDR, + cachedMemBase.address()); + } else if (mem != null) { + throw new WasmEngineException( + "NativeMachine requires NativeMemory but got " + + mem.getClass().getName() + + ". Use NativeMachineFactory.createMemory() for all" + + " memories, including imports."); + } else { + cachedMemBase = MemorySegment.NULL; + } + memBaseInitialized = true; + } + // MEMORY_PAGES can change via grow() — must refresh every call + var mem = instance.memory(); + if (mem != null) { + ctxBuffer.set(ValueLayout.JAVA_INT, CtxBuffer.MEMORY_PAGES, mem.pages()); + } + + if (Thread.interrupted()) { + throw new WasmEngineException("interrupted"); + } + + Thread caller = Thread.currentThread(); + Thread watchdog = + new Thread( + () -> { + while (!Thread.currentThread().isInterrupted()) { + if (caller.isInterrupted()) { + requestInterrupt(); + return; + } + try { + Thread.sleep(1); + } catch (InterruptedException e) { + return; + } + } + }); + watchdog.setDaemon(true); + watchdog.start(); + long result; + try { + result = (long) handle.invokeExact(cachedMemBase, ctxBuffer, args); + } finally { + watchdog.interrupt(); + } + + // Check for exceptions from upcall stubs first — a host function + // may throw (e.g. WasiExitException from proc_exit) and then the + // native code hits unreachable, setting both pendingException and + // a trap code. The pending exception is the real cause. + if (pendingException != null) { + var ex = pendingException; + pendingException = null; + ctxBuffer.set(ValueLayout.JAVA_INT, CtxBuffer.TRAP_CODE, 0); + sneakyThrow(ex); + } + + // Check for traps (pre-checks write trap code to ctxBuffer) + int trapCode = ctxBuffer.get(ValueLayout.JAVA_INT, CtxBuffer.TRAP_CODE); + if (trapCode != 0) { + ctxBuffer.set(ValueLayout.JAVA_INT, CtxBuffer.TRAP_CODE, 0); + if (trapCode == CtxBuffer.TRAP_INTERRUPTED) { + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.INTERRUPT_FLAG, 0L); + Thread.currentThread().interrupt(); + } + throw trapException(trapCode); + } + + if (funcType.returns().isEmpty()) { + return new long[0]; + } + + if (funcType.returns().size() > 1) { + // Multi-return: read values from argsBuffer + long[] results = new long[funcType.returns().size()]; + for (int i = 0; i < results.length; i++) { + long raw = argsBuffer.get(ValueLayout.JAVA_LONG, CtxBuffer.argOffset(i)); + results[i] = narrowReturnValue(raw, funcType.returns().get(i)); + } + return results; + } + + return new long[] {result}; + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + sneakyThrow(e); + throw new AssertionError("unreachable"); + } finally { + // Prevent the JIT from considering this machine unreachable during + // the native call, which would let GC collect and close() free + // native memory while code is executing. + Reference.reachabilityFence(this); + } + } + + public void requestInterrupt() { + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.INTERRUPT_FLAG, 1L); + } + + public void clearInterrupt() { + ctxBuffer.set(ValueLayout.JAVA_LONG, CtxBuffer.INTERRUPT_FLAG, 0L); + } + + private static long narrowReturnValue(long raw, ValType type) { + if (type.equals(ValType.I32)) { + return (int) raw; + } + if (type.equals(ValType.F32)) { + return Value.floatToLong(Float.intBitsToFloat((int) raw)); + } + if (type.equals(ValType.F64)) { + return Value.doubleToLong(Double.longBitsToDouble(raw)); + } + return raw; // I64 + } +} diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMemory.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMemory.java new file mode 100644 index 00000000..0601d78e --- /dev/null +++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeMemory.java @@ -0,0 +1,369 @@ +package run.endive.redline.experimental.runner.internal; + +import static run.endive.runtime.ConstantEvaluators.computeConstantValue; + +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.nio.ByteOrder; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import run.endive.runtime.Instance; +import run.endive.runtime.Memory; +import run.endive.wasm.UninstantiableException; +import run.endive.wasm.WasmEngineException; +import run.endive.wasm.types.ActiveDataSegment; +import run.endive.wasm.types.DataSegment; +import run.endive.wasm.types.MemoryLimits; +import run.endive.wasm.types.PassiveDataSegment; + +/** + * Off-heap contiguous Memory backed by mmap/mprotect. + * + *

Reserves the full maximum address range upfront with PROT_NONE (no physical + * memory committed), then mprotects pages as needed. Grow is zero-copy — just + * mprotect the next range. The base address never changes. + */ +public final class NativeMemory implements Memory, AutoCloseable { + + private static final class WaitState { + int waiterCount; + int pendingWakeups; + } + + private final Map waitStates = new ConcurrentHashMap<>(); + private final MemoryLimits limits; + private final MemorySegment reserved; + private MemorySegment segment; + private int nPages; + private final long reservedSize; + private DataSegment[] dataSegments; + + public NativeMemory(MemoryLimits limits) { + this.limits = limits; + this.nPages = limits.initialPages(); + int maxPages = Math.min(limits.maximumPages(), RUNTIME_MAX_PAGES); + this.reservedSize = PAGE_SIZE * (long) maxPages; + + try { + this.reserved = PanamaExecutor.mmapNoAccess(reservedSize); + + if (nPages > 0) { + PanamaExecutor.mprotectReadWrite(reserved, PAGE_SIZE * (long) nPages); + } + } catch (Throwable t) { + throw new WasmEngineException("Failed to mmap native memory", t); + } + + this.segment = reserved.reinterpret(PAGE_SIZE * (long) nPages); + } + + @Override + public void close() { + if (reservedSize > 0) { + try { + PanamaExecutor.munmap(reserved, reservedSize); + } catch (Throwable e) { + throw new WasmEngineException("Failed to unmap native memory", e); + } + } + } + + /** Get the native address of the memory buffer, for passing to native code. */ + public MemorySegment nativeAddress() { + return segment; + } + + @Override + public int pages() { + return nPages; + } + + @Override + public int grow(int size) { + var prevPages = nPages; + var numPages = prevPages + size; + if (numPages > maximumPages() || numPages < prevPages) { + return -1; + } + + try { + long newSize = PAGE_SIZE * (long) numPages; + PanamaExecutor.mprotectReadWrite(reserved, newSize); + this.segment = reserved.reinterpret(newSize); + this.nPages = numPages; + } catch (Throwable t) { + return -1; + } + + return prevPages; + } + + @Override + public int initialPages() { + return limits.initialPages(); + } + + @Override + public int maximumPages() { + return Math.min(limits.maximumPages(), RUNTIME_MAX_PAGES); + } + + @Override + public boolean shared() { + return limits.shared(); + } + + @SuppressWarnings("removal") + @Override + public Object lock(int address) { + if (!shared()) { + return new Object(); + } + return waitStates.computeIfAbsent(address, k -> new WaitState()); + } + + private int waitOn(int address, java.util.function.BooleanSupplier condition, long timeout) { + if (!shared()) { + throw new WasmEngineException("Attempt to wait on a non-shared memory, not supported."); + } + + long deadline = (timeout < 0) ? Long.MAX_VALUE : System.nanoTime() + timeout; + WaitState state = waitStates.computeIfAbsent(address, k -> new WaitState()); + + synchronized (state) { + if (!condition.getAsBoolean()) { + return 1; // not-equal + } + + state.waiterCount++; + try { + while (state.pendingWakeups == 0) { + long remaining = deadline - System.nanoTime(); + if (remaining <= 0) { + return 2; // timeout + } + long millis = Math.max(remaining / 1_000_000L, 0); + int nanos = Math.max((int) (remaining % 1_000_000L), 0); + try { + state.wait(millis, nanos); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new WasmEngineException("Thread interrupted"); + } + } + return 0; // woken + } finally { + if (state.pendingWakeups > 0) { + state.pendingWakeups--; + } + state.waiterCount--; + } + } + } + + @SuppressWarnings("removal") + @Override + public int waitOn(int address, int expected, long timeout) { + return waitOn(address, () -> readInt(address) == expected, timeout); + } + + @SuppressWarnings("removal") + @Override + public int waitOn(int address, long expected, long timeout) { + return waitOn(address, () -> readLong(address) == expected, timeout); + } + + @SuppressWarnings("removal") + @Override + public int notify(int address, int maxThreads) { + if (!shared()) { + return 0; + } + + WaitState state = waitStates.get(address); + if (state == null) { + return 0; + } + + synchronized (state) { + int actualWaiters = state.waiterCount - state.pendingWakeups; + if (actualWaiters == 0) { + return 0; + } + int toWake; + if (maxThreads < 0) { + toWake = actualWaiters; + } else { + toWake = Math.min(actualWaiters, maxThreads); + } + state.pendingWakeups += toWake; + state.notifyAll(); + return toWake; + } + } + + @Override + public void initialize(Instance instance, DataSegment[] dataSegments) { + this.dataSegments = dataSegments; + if (dataSegments == null) { + return; + } + for (var s : dataSegments) { + if (s instanceof ActiveDataSegment) { + var seg = (ActiveDataSegment) s; + var data = seg.data(); + var offset = (int) computeConstantValue(instance, seg.offsetInstructions())[0]; + if (offset < 0 || offset + data.length > sizeInBytes()) { + throw new UninstantiableException( + "out of bounds memory access: offset=" + + offset + + " size=" + + data.length); + } + MemorySegment.copy(MemorySegment.ofArray(data), 0, segment, offset, data.length); + } + } + } + + @Override + public void initPassiveSegment(int segmentId, int dest, int offset, int size) { + var seg = dataSegments[segmentId]; + if (seg == null || seg == run.endive.wasm.types.PassiveDataSegment.EMPTY) { + if (size > 0) { + throw new run.endive.runtime.WasmRuntimeException("out of bounds memory access"); + } + return; + } + write(dest, seg.data(), offset, size); + } + + private int sizeInBytes() { + return PAGE_SIZE * nPages; + } + + @Override + public void write(int addr, byte[] data, int offset, int size) { + long limit = segment.byteSize(); + if (Integer.toUnsignedLong(offset) + Integer.toUnsignedLong(size) > data.length + || Integer.toUnsignedLong(addr) + Integer.toUnsignedLong(size) > limit) { + throw new run.endive.runtime.WasmRuntimeException("out of bounds memory access"); + } + MemorySegment.copy(MemorySegment.ofArray(data), offset, segment, addr, size); + } + + @Override + public byte read(int addr) { + return segment.get(ValueLayout.JAVA_BYTE, addr); + } + + @Override + public byte[] readBytes(int addr, int len) { + return segment.asSlice(addr, len).toArray(ValueLayout.JAVA_BYTE); + } + + @Override + public void writeI32(int addr, int data) { + segment.set(ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr, data); + } + + @Override + public int readInt(int addr) { + return segment.get(ValueLayout.JAVA_INT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr); + } + + @Override + public void writeLong(int addr, long data) { + segment.set(ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr, data); + } + + @Override + public long readLong(int addr) { + return segment.get( + ValueLayout.JAVA_LONG_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr); + } + + @Override + public void writeShort(int addr, short data) { + segment.set( + ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr, data); + } + + @Override + public short readShort(int addr) { + return segment.get( + ValueLayout.JAVA_SHORT_UNALIGNED.withOrder(ByteOrder.LITTLE_ENDIAN), addr); + } + + @Override + public long readU16(int addr) { + return readShort(addr) & 0xFFFFL; + } + + @Override + public void writeByte(int addr, byte data) { + segment.set(ValueLayout.JAVA_BYTE, addr, data); + } + + @Override + public void writeF32(int addr, float data) { + writeI32(addr, Float.floatToRawIntBits(data)); + } + + @Override + public long readF32(int addr) { + return readInt(addr); + } + + @Override + public float readFloat(int addr) { + return Float.intBitsToFloat(readInt(addr)); + } + + @Override + public void writeF64(int addr, double data) { + writeLong(addr, Double.doubleToRawLongBits(data)); + } + + @Override + public double readDouble(int addr) { + return Double.longBitsToDouble(readLong(addr)); + } + + @Override + public long readF64(int addr) { + return readLong(addr); + } + + @Override + public void zero() { + segment.fill((byte) 0); + } + + @Override + public void copy(int dest, int src, int size) { + long limit = segment.byteSize(); + if (Integer.toUnsignedLong(src) + Integer.toUnsignedLong(size) > limit + || Integer.toUnsignedLong(dest) + Integer.toUnsignedLong(size) > limit) { + throw new run.endive.runtime.WasmRuntimeException("out of bounds memory access"); + } + MemorySegment.copy(segment, src, segment, dest, size); + } + + @Override + public void fill(byte value, int fromIndex, int toIndex) { + long limit = segment.byteSize(); + if (Integer.toUnsignedLong(fromIndex) > limit + || Integer.toUnsignedLong(toIndex) > limit + || fromIndex > toIndex) { + throw new run.endive.runtime.WasmRuntimeException("out of bounds memory access"); + } + segment.asSlice(fromIndex, toIndex - fromIndex).fill(value); + } + + @Override + public void drop(int segment) { + if (dataSegments != null) { + dataSegments[segment] = PassiveDataSegment.EMPTY; + } + } +} diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeTable.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeTable.java new file mode 100644 index 00000000..5f4a28be --- /dev/null +++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/NativeTable.java @@ -0,0 +1,210 @@ +package run.endive.redline.experimental.runner.internal; + +import static run.endive.wasm.types.Value.REF_NULL_VALUE; + +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import run.endive.redline.experimental.api.internal.CtxBuffer; +import run.endive.runtime.Instance; +import run.endive.runtime.TableInstance; +import run.endive.wasm.WasmEngineException; +import run.endive.wasm.types.Table; +import run.endive.wasm.types.TableLimits; +import run.endive.wasm.types.ValType; + +/** + * Off-heap table implementation for native compilation. + * + *

Layout: [size:i32 @ 0][max:i32 @ 4][entries... @ 8] + * + *

Each entry is 16 bytes: + *

+ *   [0..4)   i32  canonicalTypeIdx
+ *   [4..8)   i32  funcId
+ *   [8..16)  i64  funcPtr (native address)
+ * 
+ * + *

NULL entry: funcId == REF_NULL_VALUE (-1), funcPtr == 0, typeIdx == 0. + * + *

The entries array is pre-allocated to the table's max capacity so that + * {@code TABLE.GROW} only bumps the size field without reallocation. + * Native code reads/writes this buffer directly — no Java trampoline + * needed for GET/SET/SIZE/GROW/FILL/COPY. + * + *

Resolution of funcId → funcPtr+typeIdx uses the {@code instance} parameter + * passed to {@link #setRef}, obtaining the calling module's NativeMachine. + * This ensures cross-module shared tables resolve to the correct native addresses. + */ +public final class NativeTable extends TableInstance { + + private static final int MAX_PREALLOC = 1_000_000; + + private final MemorySegment buffer; + private final int capacity; + private final boolean isExternRef; + + public NativeTable(Table table, Arena arena) { + super(table, REF_NULL_VALUE); + this.isExternRef = table.elementType().equals(ValType.ExternRef); + int initial = (int) table.limits().min(); + int max = (int) table.limits().max(); + // Pre-allocate to max, capped at MAX_PREALLOC + this.capacity = (max > 0 && max <= MAX_PREALLOC) ? max : Math.max(initial, MAX_PREALLOC); + long bufferSize = + CtxBuffer.TABLE_ENTRIES_OFFSET + (long) capacity * CtxBuffer.TABLE_ENTRY_SIZE; + this.buffer = arena.allocate(bufferSize, 8); + + // Write header + buffer.set(ValueLayout.JAVA_INT, CtxBuffer.TABLE_SIZE_OFFSET, initial); + buffer.set(ValueLayout.JAVA_INT, CtxBuffer.TABLE_MAX_OFFSET, max > 0 ? max : capacity); + + // Fill all entries with null (funcId=-1, funcPtr=0, typeIdx=0) + for (int i = 0; i < capacity; i++) { + writeNullEntry(i); + } + } + + private long entryBase(int index) { + return CtxBuffer.TABLE_ENTRIES_OFFSET + (long) index * CtxBuffer.TABLE_ENTRY_SIZE; + } + + private void writeNullEntry(int index) { + long base = entryBase(index); + buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0); + buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, REF_NULL_VALUE); + buffer.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L); + } + + private void writeOpaqueEntry(int index, int value) { + long base = entryBase(index); + buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0); + buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, value); + buffer.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L); + } + + private void writeResolvedEntry(int index, int funcId, MemorySegment ft, MemorySegment fta) { + long base = entryBase(index); + long funcPtr = ft.get(ValueLayout.JAVA_LONG, (long) funcId * 8); + int typeIdx = fta.get(ValueLayout.JAVA_INT, (long) funcId * 4); + buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, typeIdx); + buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, funcId); + buffer.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, funcPtr); + } + + /** + * Try to resolve funcId → funcPtr+typeIdx using the instance's NativeMachine. + * For externref tables, stores the value as-is without function resolution. + * Returns true if resolved, false if not (instance is null or not NativeMachine). + */ + private boolean resolveFromInstance(int index, int funcId, Instance instance) { + if (isExternRef) { + writeOpaqueEntry(index, funcId); + return true; + } + if (instance != null && instance.getMachine() instanceof NativeMachine nm) { + writeResolvedEntry(index, funcId, nm.getFuncTable(), nm.getFuncTypesArray()); + return true; + } + return false; + } + + /** Get the native address of the table buffer, for passing to native code. */ + MemorySegment nativeBuffer() { + return buffer; + } + + boolean isExternRef() { + return isExternRef; + } + + @Override + public int size() { + return buffer.get(ValueLayout.JAVA_INT, CtxBuffer.TABLE_SIZE_OFFSET); + } + + @Override + public ValType elementType() { + return super.elementType(); + } + + @Override + public TableLimits limits() { + return super.limits(); + } + + @Override + public int ref(int index) { + if (index < 0 || index >= size()) { + throw new WasmEngineException("undefined element"); + } + long base = entryBase(index); + return buffer.get(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET); + } + + @Override + public int requiredRef(int index) { + int r = ref(index); + if (r == REF_NULL_VALUE) { + throw new WasmEngineException("uninitialized element " + index); + } + return r; + } + + @Override + public void setRef(int index, int value, Instance instance) { + if (index < 0 || index >= size()) { + throw new WasmEngineException("out of bounds table access"); + } + if (value == REF_NULL_VALUE) { + writeNullEntry(index); + } else if (resolveFromInstance(index, value, instance)) { + // Resolved using the calling module's NativeMachine + } else { + // No NativeMachine available — store funcId only (externref or non-native) + long base = entryBase(index); + buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0); + buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, value); + buffer.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L); + } + } + + @Override + public int grow(int delta, int value, Instance instance) { + int oldSize = size(); + int newSize = oldSize + delta; + int max = buffer.get(ValueLayout.JAVA_INT, CtxBuffer.TABLE_MAX_OFFSET); + if (delta < 0 || newSize > max || newSize > capacity) { + return -1; + } + // Fill new slots + for (int i = oldSize; i < newSize; i++) { + if (value == REF_NULL_VALUE) { + writeNullEntry(i); + } else if (!resolveFromInstance(i, value, instance)) { + long base = entryBase(i); + buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_TYPE_IDX_OFFSET, 0); + buffer.set(ValueLayout.JAVA_INT, base + CtxBuffer.ENTRY_FUNC_ID_OFFSET, value); + buffer.set(ValueLayout.JAVA_LONG, base + CtxBuffer.ENTRY_FUNC_PTR_OFFSET, 0L); + } + } + // Update size + buffer.set(ValueLayout.JAVA_INT, CtxBuffer.TABLE_SIZE_OFFSET, newSize); + limits().grow(delta); + return oldSize; + } + + @Override + public Instance instance(int index) { + // Single-module assumption — all entries belong to the same instance + return null; + } + + @Override + public void reset() { + int sz = size(); + for (int i = 0; i < sz; i++) { + writeNullEntry(i); + } + } +} diff --git a/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/PanamaExecutor.java b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/PanamaExecutor.java new file mode 100644 index 00000000..810ab9e0 --- /dev/null +++ b/redline/runner/src/main/java/run/endive/redline/experimental/runner/internal/PanamaExecutor.java @@ -0,0 +1,240 @@ +package run.endive.redline.experimental.runner.internal; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.foreign.ValueLayout; + +/** + * Helpers for native memory management via Panama FFM. + * Uses mmap/mprotect/munmap on POSIX, VirtualAlloc/VirtualProtect/VirtualFree on Windows. + */ +final class PanamaExecutor { + + private PanamaExecutor() {} + + private static final Linker LINKER = Linker.nativeLinker(); + private static final boolean IS_WINDOWS; + + /** Raw address of libc memmove, for use as a native function pointer. */ + static final long MEMMOVE_ADDR; + + /** Raw address of libc memset, for use as a native function pointer. */ + static final long MEMSET_ADDR; + + // --- POSIX handles (null on Windows) --- + private static final java.lang.invoke.MethodHandle MMAP; + private static final java.lang.invoke.MethodHandle MPROTECT; + private static final java.lang.invoke.MethodHandle MUNMAP; + + // POSIX constants + private static final int PROT_READ = 0x1; + private static final int PROT_WRITE = 0x2; + private static final int PROT_EXEC = 0x4; + private static final int MAP_PRIVATE = 0x02; + private static final int MAP_ANONYMOUS; + + // --- Windows handles (null on POSIX) --- + private static final java.lang.invoke.MethodHandle VIRTUAL_ALLOC; + private static final java.lang.invoke.MethodHandle VIRTUAL_PROTECT; + private static final java.lang.invoke.MethodHandle VIRTUAL_FREE; + + // Windows constants + private static final int MEM_COMMIT = 0x00001000; + private static final int MEM_RESERVE = 0x00002000; + private static final int MEM_RELEASE = 0x00008000; + private static final int PAGE_NOACCESS = 0x01; + private static final int PAGE_READWRITE = 0x04; + private static final int PAGE_EXECUTE_READ = 0x20; + + static { + String os = System.getProperty("os.name", "").toLowerCase(java.util.Locale.ROOT); + IS_WINDOWS = os.contains("windows"); + MAP_ANONYMOUS = os.contains("mac") || os.contains("darwin") ? 0x1000 : 0x20; + + var lookup = LINKER.defaultLookup(); + MEMMOVE_ADDR = lookup.find("memmove").orElseThrow().address(); + MEMSET_ADDR = lookup.find("memset").orElseThrow().address(); + + try { + if (IS_WINDOWS) { + var kernel32 = SymbolLookup.libraryLookup("kernel32", Arena.global()); + VIRTUAL_ALLOC = + LINKER.downcallHandle( + kernel32.find("VirtualAlloc").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT)); + VIRTUAL_PROTECT = + LINKER.downcallHandle( + kernel32.find("VirtualProtect").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS)); + VIRTUAL_FREE = + LINKER.downcallHandle( + kernel32.find("VirtualFree").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_INT)); + MMAP = null; + MPROTECT = null; + MUNMAP = null; + } else { + MMAP = + LINKER.downcallHandle( + lookup.find("mmap").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.ADDRESS, + ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_INT, + ValueLayout.JAVA_LONG)); + MPROTECT = + LINKER.downcallHandle( + lookup.find("mprotect").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG, + ValueLayout.JAVA_INT)); + MUNMAP = + LINKER.downcallHandle( + lookup.find("munmap").orElseThrow(), + FunctionDescriptor.of( + ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, + ValueLayout.JAVA_LONG)); + VIRTUAL_ALLOC = null; + VIRTUAL_PROTECT = null; + VIRTUAL_FREE = null; + } + } catch (Throwable e) { + throw new ExceptionInInitializerError(e); + } + } + + /** Allocate a writable memory region. Must call mprotectExec before executing. */ + static MemorySegment mmapCode(long size) throws Throwable { + if (IS_WINDOWS) { + MemorySegment addr = + (MemorySegment) + VIRTUAL_ALLOC.invokeExact( + MemorySegment.NULL, + size, + MEM_RESERVE | MEM_COMMIT, + PAGE_READWRITE); + if (addr.address() == 0) { + throw new RuntimeException("VirtualAlloc failed"); + } + return addr.reinterpret(size); + } + MemorySegment addr = + (MemorySegment) + MMAP.invokeExact( + MemorySegment.NULL, + size, + PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, + -1, + 0L); + return addr.reinterpret(size); + } + + /** Make a previously mmapped region executable (and remove write). */ + static void mprotectExec(MemorySegment addr, long size) throws Throwable { + if (IS_WINDOWS) { + try (var arena = Arena.ofConfined()) { + var oldProtect = arena.allocate(ValueLayout.JAVA_INT); + int result = + (int) + VIRTUAL_PROTECT.invokeExact( + addr, size, PAGE_EXECUTE_READ, oldProtect); + if (result == 0) { + throw new RuntimeException("VirtualProtect failed"); + } + } + return; + } + int result = (int) MPROTECT.invokeExact(addr, size, PROT_READ | PROT_EXEC); + if (result != 0) { + throw new RuntimeException("mprotect failed: " + result); + } + } + + /** Reserve address space with no access (PROT_NONE). */ + static MemorySegment mmapNoAccess(long size) throws Throwable { + if (size == 0) { + return MemorySegment.NULL; + } + if (IS_WINDOWS) { + MemorySegment addr = + (MemorySegment) + VIRTUAL_ALLOC.invokeExact( + MemorySegment.NULL, size, MEM_RESERVE, PAGE_NOACCESS); + if (addr.address() == 0) { + throw new RuntimeException("VirtualAlloc (reserve) failed"); + } + return addr.reinterpret(size); + } + MemorySegment addr = + (MemorySegment) + MMAP.invokeExact( + MemorySegment.NULL, + size, + 0, // PROT_NONE + MAP_PRIVATE | MAP_ANONYMOUS, + -1, + 0L); + return addr.reinterpret(size); + } + + /** Make a range readable and writable (commits reserved pages on Windows). */ + static void mprotectReadWrite(MemorySegment addr, long size) throws Throwable { + if (size == 0) { + return; + } + if (IS_WINDOWS) { + // Must use VirtualAlloc with MEM_COMMIT to commit reserved pages + MemorySegment result = + (MemorySegment) + VIRTUAL_ALLOC.invokeExact(addr, size, MEM_COMMIT, PAGE_READWRITE); + if (result.address() == 0) { + throw new RuntimeException("VirtualAlloc (commit) failed"); + } + return; + } + int result = (int) MPROTECT.invokeExact(addr, size, PROT_READ | PROT_WRITE); + if (result != 0) { + throw new RuntimeException("mprotect failed: " + result); + } + } + + /** Unmap a memory region. */ + static void munmap(MemorySegment addr, long size) throws Throwable { + if (IS_WINDOWS) { + // VirtualFree with MEM_RELEASE requires size=0 (frees entire allocation) + int result = (int) VIRTUAL_FREE.invokeExact(addr, 0L, MEM_RELEASE); + if (result == 0) { + throw new RuntimeException("VirtualFree failed"); + } + return; + } + int result = (int) MUNMAP.invokeExact(addr, size); + if (result != 0) { + throw new RuntimeException("munmap failed: " + result); + } + } +} diff --git a/redline/wasm-build/Cargo.lock b/redline/wasm-build/Cargo.lock new file mode 100644 index 00000000..f81c4908 --- /dev/null +++ b/redline/wasm-build/Cargo.lock @@ -0,0 +1,344 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046d4b584c3bb9b5eb500c8f29549bec36be11000f1ba2a927cef3d1a9875691" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b194a7870becb1490366fc0ae392ccd188065ff35f8391e77ac659db6fb977" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6a4ab44c6b371e661846b97dab687387a60ac4e2f864e2d4257284aad9e889" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b7a44150c2f471a94023482bda1902710746e4bed9f9973d60c5a94319b06d" +dependencies = [ + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bridge" +version = "0.1.0" +dependencies = [ + "cranelift-codegen", + "cranelift-control", + "cranelift-frontend", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b06598133b1dd76758b8b95f8d6747c124124aade50cea96a3d88b962da9fa" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.16.1", + "libm", + "log", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6190e2e7bcf0a678da2f715363d34ed530fedf7a2f0ab75edaefef72a70465ff" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f583cf203d1aa8b79560e3b01f929bdacf9070b015eec4ea9c46e22a3f83e4a0" + +[[package]] +name = "cranelift-control" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803159df35cc398ae54473c150b16d6c77e92ab2948be638488de126a3328fbc" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e417257082d88087f5bcce677525bdaa8322b88dd7f175ed1a1fd41d546c" +dependencies = [ + "cranelift-bitset", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14db6b0e0e4994c581092df78d837be2072578f7cb2528f96a6cf895e56dee63" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec66ea5025c7317383699778282ac98741d68444f956e3b1d7b62f12b7216e67" + +[[package]] +name = "cranelift-srcgen" +version = "0.130.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53619d3cd5c78fd998c6d9420547af26b72e6456f94c2a8a2334cb76b42baa" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap", + "stable_deref_trait", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regalloc2" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.0", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasmtime-internal-core" +version = "43.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22632b187e1b0716f1b9ac57ad29013bed33175fcb19e10bb6896126f82fac67" +dependencies = [ + "hashbrown 0.16.1", + "libm", +] diff --git a/redline/wasm-build/Cargo.toml b/redline/wasm-build/Cargo.toml new file mode 100644 index 00000000..faf59897 --- /dev/null +++ b/redline/wasm-build/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cranelift-bridge" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +cranelift-codegen = { version = "0.130", default-features = false, features = ["std", "x86", "arm64"] } +cranelift-frontend = "0.130" +cranelift-control = "0.130" +target-lexicon = "0.13" + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +panic = "abort" +strip = true diff --git a/redline/wasm-build/Makefile b/redline/wasm-build/Makefile new file mode 100644 index 00000000..90c01da6 --- /dev/null +++ b/redline/wasm-build/Makefile @@ -0,0 +1,22 @@ +PWD := $(CURDIR) + +.PHONY: clean +clean: + rm -rf target + +.PHONY: fmt +fmt: + cargo fmt + +.PHONY: build +build: + cd ${PWD} + cargo build --target wasm32-wasip1 --release + +.PHONY: release +release: + rm -f ${PWD}/../cranelift_bridge.wasm + cp ${PWD}/target/wasm32-wasip1/release/cranelift_bridge.wasm ${PWD}/../cranelift_bridge.wasm + +.PHONY: all +all: build release diff --git a/redline/wasm-build/src/lib.rs b/redline/wasm-build/src/lib.rs new file mode 100644 index 00000000..d764d212 --- /dev/null +++ b/redline/wasm-build/src/lib.rs @@ -0,0 +1,1869 @@ +//! Thin FFI bridge exposing Cranelift's FunctionBuilder API as flat Wasm exports. +//! +//! All handles (block_id, value_id, var_id) are opaque u32. +//! The Java side tracks them explicitly and passes them to subsequent calls. +//! Each export immediately calls the corresponding Cranelift API — no accumulation. + +use cranelift_codegen::ir::condcodes::IntCC; +use cranelift_codegen::ir::types; +use cranelift_codegen::ir::{AbiParam, AtomicRmwOp, BlockArg, BlockCall, Function, InstBuilder, MemFlags, Signature, UserFuncName}; +use cranelift_codegen::isa::{self, CallConv, TargetIsa}; +use cranelift_codegen::settings::{self, Configurable}; +use cranelift_codegen::Context; +use cranelift_control::ControlPlane; +use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext, Variable}; +use std::sync::Arc; +use target_lexicon::Triple; + +// --- Global state (single-threaded Wasm, safe) --- + +static mut ISA: Option> = None; +static mut COMPILED_CODE: Vec = Vec::new(); +static mut TRAMPOLINE_SIG: Option = None; + +/// Session holds the FunctionBuilder and its backing data. +/// We use raw pointers to keep FunctionBuilder alive across FFI calls, +/// working around the borrow checker. Safe because Wasm is single-threaded. +struct Session { + // These are heap-allocated and pinned so their addresses are stable. + func: Box, + builder_ctx: Box, + // The FunctionBuilder borrows func and builder_ctx. + // We store it as a raw pointer to avoid lifetime issues. + builder: *mut FunctionBuilder<'static>, + // Lookup tables: synthetic u32 IDs -> real Cranelift handles + blocks: Vec, + variables: Vec, + values: Vec, + sig_refs: Vec, + call_args: Vec, + // Temp signature builder + sig_builder: Option, + jump_tables: Vec, + br_table_targets: Vec, +} + +static mut SESSION: Option = None; + +fn wasm_type_to_clif(ty: u32) -> cranelift_codegen::ir::Type { + match ty { + 0 => types::I32, + 1 => types::I64, + 2 => types::F32, + 3 => types::F64, + _ => panic!("Unknown wasm type: {}", ty), + } +} + +/// Get the active FunctionBuilder. +fn b() -> &'static mut FunctionBuilder<'static> { + let session = unsafe { SESSION.as_ref().expect("No active session") }; + assert!(!session.builder.is_null(), "Builder used after compile()"); + unsafe { &mut *session.builder } +} + +/// Get the session. +fn s() -> &'static mut Session { + unsafe { SESSION.as_mut().unwrap() } +} + +// --- malloc/free using Rust's global allocator --- + +#[no_mangle] +pub extern "C" fn wasm_malloc(size: u32) -> *mut u8 { + if size == 0 { + return 8 as *mut u8; // Return aligned dangling pointer for zero-sized alloc + } + let layout = std::alloc::Layout::from_size_align(size as usize, 8).unwrap(); + unsafe { std::alloc::alloc(layout) } +} + +#[no_mangle] +pub extern "C" fn wasm_free(ptr: *mut u8, size: u32) { + if size == 0 { + return; // No-op for zero-sized alloc (matches wasm_malloc guard) + } + let layout = std::alloc::Layout::from_size_align(size as usize, 8).unwrap(); + unsafe { std::alloc::dealloc(ptr, layout) } +} + +// --- Exported functions --- + +#[no_mangle] +pub extern "C" fn init(target_ptr: *const u8, target_len: u32) { + let target_str = unsafe { + std::str::from_utf8(std::slice::from_raw_parts(target_ptr, target_len as usize)).unwrap() + }; + let mut flag_builder = settings::builder(); + flag_builder.set("opt_level", "speed").unwrap(); + flag_builder.set("is_pic", "false").unwrap(); + flag_builder.set("preserve_frame_pointers", "true").unwrap(); + let flags = settings::Flags::new(flag_builder); + let triple: Triple = target_str.parse().expect("Failed to parse target triple"); + let arch = triple.architecture; + let mut isa_builder = isa::lookup(triple).expect("Unsupported target"); + // Enable SSE4.1+ to avoid libcalls for ceil/floor/trunc/nearest + if arch == target_lexicon::Architecture::X86_64 { + isa_builder.enable("has_sse3").unwrap(); + isa_builder.enable("has_ssse3").unwrap(); + isa_builder.enable("has_sse41").unwrap(); + isa_builder.enable("has_sse42").unwrap(); + } + let isa = isa_builder + .finish(flags) + .expect("Failed to create ISA"); + unsafe { + ISA = Some(isa); + } +} + +/// Create a new function and its FunctionBuilder. Call add_param_type/add_return_type +/// BEFORE build_function. +#[no_mangle] +pub extern "C" fn create_function() { + let func = Box::new(Function::with_name_signature( + UserFuncName::user(0, 0), + Signature::new(CallConv::Tail), + )); + let builder_ctx = Box::new(FunctionBuilderContext::new()); + + unsafe { + SESSION = Some(Session { + func, + builder_ctx, + builder: std::ptr::null_mut(), + blocks: Vec::new(), + variables: Vec::new(), + values: Vec::new(), + sig_refs: Vec::new(), + call_args: Vec::new(), + sig_builder: None, + jump_tables: Vec::new(), + br_table_targets: Vec::new(), + }); + } +} + +#[no_mangle] +pub extern "C" fn add_param_type(wasm_type: u32) { + s().func.signature.params.push(AbiParam::new(wasm_type_to_clif(wasm_type))); +} + +#[no_mangle] +pub extern "C" fn add_return_type(wasm_type: u32) { + s().func.signature.returns.push(AbiParam::new(wasm_type_to_clif(wasm_type))); +} + +/// Finalize the signature and create the FunctionBuilder. +/// Must be called after all add_param_type/add_return_type and before any emit calls. +#[no_mangle] +pub extern "C" fn build_function() { + let session = s(); + // Create builder from raw pointers to avoid lifetime issues. + // Safe: single-threaded, session outlives builder, and we only drop in compile(). + let func_ptr: *mut Function = &mut *session.func; + let ctx_ptr: *mut FunctionBuilderContext = &mut *session.builder_ctx; + let builder = unsafe { + FunctionBuilder::new(&mut *func_ptr, &mut *ctx_ptr) + }; + let builder_box = Box::new(builder); + session.builder = Box::into_raw(builder_box) as *mut FunctionBuilder<'static>; +} + +// --- Blocks --- + +#[no_mangle] +pub extern "C" fn create_block() -> u32 { + let block = b().create_block(); + let session = s(); + let id = session.blocks.len() as u32; + session.blocks.push(block); + id +} + +#[no_mangle] +pub extern "C" fn switch_to_block(block_id: u32) { + let block = s().blocks[block_id as usize]; + b().switch_to_block(block); +} + +#[no_mangle] +pub extern "C" fn seal_block(block_id: u32) { + let block = s().blocks[block_id as usize]; + b().seal_block(block); +} + +#[no_mangle] +pub extern "C" fn seal_all_blocks() { + b().seal_all_blocks(); +} + +#[no_mangle] +pub extern "C" fn append_block_params_for_func_params(block_id: u32) { + let block = s().blocks[block_id as usize]; + b().append_block_params_for_function_params(block); +} + +// --- Variables --- + +#[no_mangle] +pub extern "C" fn declare_var(wasm_type: u32) -> u32 { + let var = b().declare_var(wasm_type_to_clif(wasm_type)); + let session = s(); + let id = session.variables.len() as u32; + session.variables.push(var); + id +} + +#[no_mangle] +pub extern "C" fn def_var(var_id: u32, val_id: u32) { + let var = s().variables[var_id as usize]; + let val = s().values[val_id as usize]; + b().def_var(var, val); +} + +#[no_mangle] +pub extern "C" fn use_var(var_id: u32) -> u32 { + let var = s().variables[var_id as usize]; + let val = b().use_var(var); + let session = s(); + let id = session.values.len() as u32; + session.values.push(val); + id +} + +// --- Params --- + +#[no_mangle] +pub extern "C" fn func_param(block_id: u32, index: u32) -> u32 { + let block = s().blocks[block_id as usize]; + let params = b().block_params(block); + let val = params[index as usize]; + let session = s(); + let id = session.values.len() as u32; + session.values.push(val); + id +} + +// --- Constants --- + +#[no_mangle] +pub extern "C" fn emit_iconst_32(val: i32) -> u32 { + let v = b().ins().iconst(types::I32, val as i64); + let session = s(); + let id = session.values.len() as u32; + session.values.push(v); + id +} + +#[no_mangle] +pub extern "C" fn emit_get_stack_pointer() -> u32 { + let v = b().ins().get_stack_pointer(types::I64); + let session = s(); + let id = session.values.len() as u32; + session.values.push(v); + id +} + +#[no_mangle] +pub extern "C" fn emit_iconst_64(val_lo: u32, val_hi: u32) -> u32 { + let val = (val_lo as i64) | ((val_hi as i64) << 32); + let v = b().ins().iconst(types::I64, val); + let session = s(); + let id = session.values.len() as u32; + session.values.push(v); + id +} + +#[no_mangle] +pub extern "C" fn emit_f32const(bits: u32) -> u32 { + let v = b().ins().f32const(f32::from_bits(bits)); + let session = s(); + let id = session.values.len() as u32; + session.values.push(v); + id +} + +#[no_mangle] +pub extern "C" fn emit_f64const(bits_lo: u32, bits_hi: u32) -> u32 { + let bits = (bits_lo as u64) | ((bits_hi as u64) << 32); + let v = b().ins().f64const(f64::from_bits(bits)); + let session = s(); + let id = session.values.len() as u32; + session.values.push(v); + id +} + +// --- Arithmetic --- + +#[no_mangle] +pub extern "C" fn emit_iadd(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().iadd(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_isub(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().isub(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_imul(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().imul(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_sdiv(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().sdiv(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_udiv(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().udiv(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_srem(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().srem(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_urem(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().urem(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_band(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().band(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_bor(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().bor(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_bxor(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().bxor(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_ishl(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().ishl(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_ushr(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().ushr(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_sshr(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().sshr(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_rotl(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().rotl(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_rotr(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().rotr(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_clz(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().clz(va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_ctz(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().ctz(va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_popcnt(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().popcnt(va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_eqz(a: u32) -> u32 { + let va = s().values[a as usize]; + let ty = b().func.dfg.value_type(va); + let zero = b().ins().iconst(ty, 0); + let cmp = b().ins().icmp(IntCC::Equal, va, zero); + // icmp returns I8, extend to I32 for Wasm compatibility + let r = b().ins().uextend(types::I32, cmp); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// cc: 0=eq, 1=ne, 2=lt_s, 3=lt_u, 4=gt_s, 5=gt_u, 6=le_s, 7=le_u, 8=ge_s, 9=ge_u +#[no_mangle] +pub extern "C" fn emit_icmp(cc: u32, a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let cond = match cc { + 0 => IntCC::Equal, + 1 => IntCC::NotEqual, + 2 => IntCC::SignedLessThan, + 3 => IntCC::UnsignedLessThan, + 4 => IntCC::SignedGreaterThan, + 5 => IntCC::UnsignedGreaterThan, + 6 => IntCC::SignedLessThanOrEqual, + 7 => IntCC::UnsignedLessThanOrEqual, + 8 => IntCC::SignedGreaterThanOrEqual, + 9 => IntCC::UnsignedGreaterThanOrEqual, + _ => panic!("Unknown icmp condition code: {}", cc), + }; + let cmp = b().ins().icmp(cond, va, vb); + // icmp returns I8, extend to I32 for Wasm compatibility + let r = b().ins().uextend(types::I32, cmp); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Sign-extend i8 to i32 +#[no_mangle] +pub extern "C" fn emit_sextend_8_32(a: u32) -> u32 { + let va = s().values[a as usize]; + let truncated = b().ins().ireduce(types::I8, va); + let r = b().ins().sextend(types::I32, truncated); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Sign-extend i16 to i32 +#[no_mangle] +pub extern "C" fn emit_sextend_16_32(a: u32) -> u32 { + let va = s().values[a as usize]; + let truncated = b().ins().ireduce(types::I16, va); + let r = b().ins().sextend(types::I32, truncated); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +// --- Memory --- + +#[no_mangle] +pub extern "C" fn emit_load_i32(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I32, MemFlags::new(), effective, offset); + let session = s(); + let id = session.values.len() as u32; + session.values.push(val); + id +} + +#[no_mangle] +pub extern "C" fn emit_store_i32(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let vvalue = s().values[value as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + b().ins().store(MemFlags::new(), vvalue, effective, offset); +} + +// --- Memory: f32 --- + +#[no_mangle] +pub extern "C" fn emit_load_f32(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::F32, MemFlags::new(), effective, offset); + let session = s(); + let id = session.values.len() as u32; + session.values.push(val); + id +} + +#[no_mangle] +pub extern "C" fn emit_store_f32(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let vvalue = s().values[value as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + b().ins().store(MemFlags::new(), vvalue, effective, offset); +} + +// --- Memory: f64 --- + +#[no_mangle] +pub extern "C" fn emit_load_f64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::F64, MemFlags::new(), effective, offset); + let session = s(); + let id = session.values.len() as u32; + session.values.push(val); + id +} + +#[no_mangle] +pub extern "C" fn emit_store_f64(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let vvalue = s().values[value as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + b().ins().store(MemFlags::new(), vvalue, effective, offset); +} + +// --- Memory: sub-word i32 loads --- + +/// Load u8 and zero-extend to i32 +#[no_mangle] +pub extern "C" fn emit_load_8u(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I8, MemFlags::new(), effective, offset); + let r = b().ins().uextend(types::I32, val); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Load i8 and sign-extend to i32 +#[no_mangle] +pub extern "C" fn emit_load_8s(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I8, MemFlags::new(), effective, offset); + let r = b().ins().sextend(types::I32, val); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Load u16 and zero-extend to i32 +#[no_mangle] +pub extern "C" fn emit_load_16u(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I16, MemFlags::new(), effective, offset); + let r = b().ins().uextend(types::I32, val); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Load i16 and sign-extend to i32 +#[no_mangle] +pub extern "C" fn emit_load_16s(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I16, MemFlags::new(), effective, offset); + let r = b().ins().sextend(types::I32, val); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Store low 8 bits of i32 +#[no_mangle] +pub extern "C" fn emit_store_8(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let vvalue = s().values[value as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let truncated = b().ins().ireduce(types::I8, vvalue); + b().ins().store(MemFlags::new(), truncated, effective, offset); +} + +/// Store low 16 bits of i32 +#[no_mangle] +pub extern "C" fn emit_store_16(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let vvalue = s().values[value as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let truncated = b().ins().ireduce(types::I16, vvalue); + b().ins().store(MemFlags::new(), truncated, effective, offset); +} + +// --- Memory: i64 sub-word loads --- + +/// Load u8 and zero-extend to i64 +#[no_mangle] +pub extern "C" fn emit_load_8u_i64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I8, MemFlags::new(), effective, offset); + let r = b().ins().uextend(types::I64, val); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Load i8 and sign-extend to i64 +#[no_mangle] +pub extern "C" fn emit_load_8s_i64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I8, MemFlags::new(), effective, offset); + let r = b().ins().sextend(types::I64, val); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Load u16 and zero-extend to i64 +#[no_mangle] +pub extern "C" fn emit_load_16u_i64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I16, MemFlags::new(), effective, offset); + let r = b().ins().uextend(types::I64, val); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Load i16 and sign-extend to i64 +#[no_mangle] +pub extern "C" fn emit_load_16s_i64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I16, MemFlags::new(), effective, offset); + let r = b().ins().sextend(types::I64, val); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Load u32 and zero-extend to i64 +#[no_mangle] +pub extern "C" fn emit_load_32u_i64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I32, MemFlags::new(), effective, offset); + let r = b().ins().uextend(types::I64, val); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Load i32 and sign-extend to i64 +#[no_mangle] +pub extern "C" fn emit_load_32s_i64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I32, MemFlags::new(), effective, offset); + let r = b().ins().sextend(types::I64, val); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Store low 8 bits of i64 +#[no_mangle] +pub extern "C" fn emit_store_8_i64(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let vvalue = s().values[value as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let truncated = b().ins().ireduce(types::I8, vvalue); + b().ins().store(MemFlags::new(), truncated, effective, offset); +} + +/// Store low 16 bits of i64 +#[no_mangle] +pub extern "C" fn emit_store_16_i64(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let vvalue = s().values[value as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let truncated = b().ins().ireduce(types::I16, vvalue); + b().ins().store(MemFlags::new(), truncated, effective, offset); +} + +/// Store low 32 bits of i64 +#[no_mangle] +pub extern "C" fn emit_store_32_i64(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let vvalue = s().values[value as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let truncated = b().ins().ireduce(types::I32, vvalue); + b().ins().store(MemFlags::new(), truncated, effective, offset); +} + +// --- Atomic helpers --- + +fn effective_addr(base: u32, wasm_addr: u32, offset: i32) -> cranelift_codegen::ir::Value { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let addr = b().ins().iadd(vbase, extended); + if offset != 0 { + b().ins().iadd_imm(addr, offset as i64) + } else { + addr + } +} + +fn wasm_rmw_op(op: u32) -> AtomicRmwOp { + match op { + 0 => AtomicRmwOp::Add, + 1 => AtomicRmwOp::Sub, + 2 => AtomicRmwOp::And, + 3 => AtomicRmwOp::Or, + 4 => AtomicRmwOp::Xor, + 5 => AtomicRmwOp::Xchg, + _ => panic!("Unknown atomic RMW op: {}", op), + } +} + +fn push_val(val: cranelift_codegen::ir::Value) -> u32 { + let session = s(); + let id = session.values.len() as u32; + session.values.push(val); + id +} + +// --- Atomic loads --- + +#[no_mangle] +pub extern "C" fn emit_atomic_load_i32(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let val = b().ins().atomic_load(types::I32, MemFlags::new(), addr); + push_val(val) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_load_i64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let val = b().ins().atomic_load(types::I64, MemFlags::new(), addr); + push_val(val) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_load8_u_i32(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let val = b().ins().atomic_load(types::I8, MemFlags::new(), addr); + let r = b().ins().uextend(types::I32, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_load16_u_i32(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let val = b().ins().atomic_load(types::I16, MemFlags::new(), addr); + let r = b().ins().uextend(types::I32, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_load8_u_i64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let val = b().ins().atomic_load(types::I8, MemFlags::new(), addr); + let r = b().ins().uextend(types::I64, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_load16_u_i64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let val = b().ins().atomic_load(types::I16, MemFlags::new(), addr); + let r = b().ins().uextend(types::I64, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_load32_u_i64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let val = b().ins().atomic_load(types::I32, MemFlags::new(), addr); + let r = b().ins().uextend(types::I64, val); + push_val(r) +} + +// --- Atomic stores --- + +#[no_mangle] +pub extern "C" fn emit_atomic_store_i32(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + b().ins().atomic_store(MemFlags::new(), vvalue, addr); +} + +#[no_mangle] +pub extern "C" fn emit_atomic_store_i64(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + b().ins().atomic_store(MemFlags::new(), vvalue, addr); +} + +#[no_mangle] +pub extern "C" fn emit_atomic_store8(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let truncated = b().ins().ireduce(types::I8, vvalue); + b().ins().atomic_store(MemFlags::new(), truncated, addr); +} + +#[no_mangle] +pub extern "C" fn emit_atomic_store16(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let truncated = b().ins().ireduce(types::I16, vvalue); + b().ins().atomic_store(MemFlags::new(), truncated, addr); +} + +#[no_mangle] +pub extern "C" fn emit_atomic_store8_i64(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let truncated = b().ins().ireduce(types::I8, vvalue); + b().ins().atomic_store(MemFlags::new(), truncated, addr); +} + +#[no_mangle] +pub extern "C" fn emit_atomic_store16_i64(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let truncated = b().ins().ireduce(types::I16, vvalue); + b().ins().atomic_store(MemFlags::new(), truncated, addr); +} + +#[no_mangle] +pub extern "C" fn emit_atomic_store32_i64(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let truncated = b().ins().ireduce(types::I32, vvalue); + b().ins().atomic_store(MemFlags::new(), truncated, addr); +} + +// --- Atomic RMW --- + +#[no_mangle] +pub extern "C" fn emit_atomic_rmw_i32(op: u32, base: u32, wasm_addr: u32, value: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let val = b().ins().atomic_rmw(types::I32, MemFlags::new(), wasm_rmw_op(op), addr, vvalue); + push_val(val) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_rmw_i64(op: u32, base: u32, wasm_addr: u32, value: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let val = b().ins().atomic_rmw(types::I64, MemFlags::new(), wasm_rmw_op(op), addr, vvalue); + push_val(val) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_rmw8_u_i32(op: u32, base: u32, wasm_addr: u32, value: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let truncated = b().ins().ireduce(types::I8, vvalue); + let val = b().ins().atomic_rmw(types::I8, MemFlags::new(), wasm_rmw_op(op), addr, truncated); + let r = b().ins().uextend(types::I32, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_rmw16_u_i32(op: u32, base: u32, wasm_addr: u32, value: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let truncated = b().ins().ireduce(types::I16, vvalue); + let val = b().ins().atomic_rmw(types::I16, MemFlags::new(), wasm_rmw_op(op), addr, truncated); + let r = b().ins().uextend(types::I32, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_rmw8_u_i64(op: u32, base: u32, wasm_addr: u32, value: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let truncated = b().ins().ireduce(types::I8, vvalue); + let val = b().ins().atomic_rmw(types::I8, MemFlags::new(), wasm_rmw_op(op), addr, truncated); + let r = b().ins().uextend(types::I64, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_rmw16_u_i64(op: u32, base: u32, wasm_addr: u32, value: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let truncated = b().ins().ireduce(types::I16, vvalue); + let val = b().ins().atomic_rmw(types::I16, MemFlags::new(), wasm_rmw_op(op), addr, truncated); + let r = b().ins().uextend(types::I64, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_rmw32_u_i64(op: u32, base: u32, wasm_addr: u32, value: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vvalue = s().values[value as usize]; + let truncated = b().ins().ireduce(types::I32, vvalue); + let val = b().ins().atomic_rmw(types::I32, MemFlags::new(), wasm_rmw_op(op), addr, truncated); + let r = b().ins().uextend(types::I64, val); + push_val(r) +} + +// --- Atomic CAS (compare-and-swap) --- + +#[no_mangle] +pub extern "C" fn emit_atomic_cas_i32(base: u32, wasm_addr: u32, expected: u32, replacement: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vexpected = s().values[expected as usize]; + let vreplacement = s().values[replacement as usize]; + let val = b().ins().atomic_cas(MemFlags::new(), addr, vexpected, vreplacement); + push_val(val) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_cas_i64(base: u32, wasm_addr: u32, expected: u32, replacement: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vexpected = s().values[expected as usize]; + let vreplacement = s().values[replacement as usize]; + let val = b().ins().atomic_cas(MemFlags::new(), addr, vexpected, vreplacement); + push_val(val) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_cas8_u_i32(base: u32, wasm_addr: u32, expected: u32, replacement: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vexpected = s().values[expected as usize]; + let vreplacement = s().values[replacement as usize]; + let te = b().ins().ireduce(types::I8, vexpected); + let tr = b().ins().ireduce(types::I8, vreplacement); + let val = b().ins().atomic_cas(MemFlags::new(), addr, te, tr); + let r = b().ins().uextend(types::I32, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_cas16_u_i32(base: u32, wasm_addr: u32, expected: u32, replacement: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vexpected = s().values[expected as usize]; + let vreplacement = s().values[replacement as usize]; + let te = b().ins().ireduce(types::I16, vexpected); + let tr = b().ins().ireduce(types::I16, vreplacement); + let val = b().ins().atomic_cas(MemFlags::new(), addr, te, tr); + let r = b().ins().uextend(types::I32, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_cas8_u_i64(base: u32, wasm_addr: u32, expected: u32, replacement: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vexpected = s().values[expected as usize]; + let vreplacement = s().values[replacement as usize]; + let te = b().ins().ireduce(types::I8, vexpected); + let tr = b().ins().ireduce(types::I8, vreplacement); + let val = b().ins().atomic_cas(MemFlags::new(), addr, te, tr); + let r = b().ins().uextend(types::I64, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_cas16_u_i64(base: u32, wasm_addr: u32, expected: u32, replacement: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vexpected = s().values[expected as usize]; + let vreplacement = s().values[replacement as usize]; + let te = b().ins().ireduce(types::I16, vexpected); + let tr = b().ins().ireduce(types::I16, vreplacement); + let val = b().ins().atomic_cas(MemFlags::new(), addr, te, tr); + let r = b().ins().uextend(types::I64, val); + push_val(r) +} + +#[no_mangle] +pub extern "C" fn emit_atomic_cas32_u_i64(base: u32, wasm_addr: u32, expected: u32, replacement: u32, offset: i32) -> u32 { + let addr = effective_addr(base, wasm_addr, offset); + let vexpected = s().values[expected as usize]; + let vreplacement = s().values[replacement as usize]; + let te = b().ins().ireduce(types::I32, vexpected); + let tr = b().ins().ireduce(types::I32, vreplacement); + let val = b().ins().atomic_cas(MemFlags::new(), addr, te, tr); + let r = b().ins().uextend(types::I64, val); + push_val(r) +} + +// --- Fence --- + +#[no_mangle] +pub extern "C" fn emit_fence() { + b().ins().fence(); +} + +// --- i64 extensions --- + +/// Sign-extend i32 to i64 +#[no_mangle] +pub extern "C" fn emit_sextend_i64(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().sextend(types::I64, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Sign-extend i8 to i64 +#[no_mangle] +pub extern "C" fn emit_sextend_8_64(a: u32) -> u32 { + let va = s().values[a as usize]; + let truncated = b().ins().ireduce(types::I8, va); + let r = b().ins().sextend(types::I64, truncated); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Sign-extend i16 to i64 +#[no_mangle] +pub extern "C" fn emit_sextend_16_64(a: u32) -> u32 { + let va = s().values[a as usize]; + let truncated = b().ins().ireduce(types::I16, va); + let r = b().ins().sextend(types::I64, truncated); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Sign-extend i32 to i64 (for extend32_s on i64 values) +#[no_mangle] +pub extern "C" fn emit_sextend_32_64(a: u32) -> u32 { + let va = s().values[a as usize]; + let truncated = b().ins().ireduce(types::I32, va); + let r = b().ins().sextend(types::I64, truncated); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +// --- i64 comparisons --- + +/// i64 eqz: compare with zero, return i32 +#[no_mangle] +pub extern "C" fn emit_eqz_i64(a: u32) -> u32 { + let va = s().values[a as usize]; + let zero = b().ins().iconst(types::I64, 0); + let cmp = b().ins().icmp(IntCC::Equal, va, zero); + let r = b().ins().uextend(types::I32, cmp); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +// --- i32 wrap i64 --- + +#[no_mangle] +pub extern "C" fn emit_i32_wrap_i64(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().ireduce(types::I32, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +// --- Bitcast (for f32/f64 <-> i32/i64) --- + +#[no_mangle] +pub extern "C" fn emit_bitcast_i32_to_f32(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().bitcast(types::F32, MemFlags::new(), va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_bitcast_f32_to_i32(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().bitcast(types::I32, MemFlags::new(), va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_bitcast_i64_to_f64(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().bitcast(types::F64, MemFlags::new(), va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +#[no_mangle] +pub extern "C" fn emit_bitcast_f64_to_i64(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().bitcast(types::I64, MemFlags::new(), va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +// --- Jump table (for br_table) --- + +/// Push a block target for br_table. +#[no_mangle] +pub extern "C" fn push_br_table_target(block_id: u32) { + let block = s().blocks[block_id as usize]; + s().br_table_targets.push(block); +} + +/// Emit br_table using Cranelift's native jump table (O(1) dispatch). +/// Pops accumulated targets, uses default_block for fallback. +#[no_mangle] +pub extern "C" fn emit_br_table(index: u32, default_block: u32) { + let vindex = s().values[index as usize]; + let default = s().blocks[default_block as usize]; + let targets: Vec = s().br_table_targets.drain(..).collect(); + + // Use dfg.block_call() to create BlockCalls (same approach as wasmtime) + let builder = b(); + let target_calls: Vec = targets + .iter() + .map(|&blk| builder.func.dfg.block_call(blk, &[])) + .collect(); + let default_call = builder.func.dfg.block_call(default, &[]); + + let jt_data = cranelift_codegen::ir::JumpTableData::new(default_call, &target_calls); + let jt = builder.create_jump_table(jt_data); + builder.ins().br_table(vindex, jt); +} + +// --- Trap --- + +#[no_mangle] +pub extern "C" fn emit_trap() { + b().ins().trap(cranelift_codegen::ir::TrapCode::user(1).unwrap()); +} + +// --- Block parameters --- + +/// Append a typed parameter to a block. Returns the block parameter Value ID. +/// Must be called before any instructions are added to the block. +#[no_mangle] +pub extern "C" fn append_block_param(block_id: u32, wasm_type: u32) -> u32 { + let block = s().blocks[block_id as usize]; + let ty = wasm_type_to_clif(wasm_type); + let val = b().append_block_param(block, ty); + let session = s(); + let id = session.values.len() as u32; + session.values.push(val); + id +} + +// --- Control flow --- + +#[no_mangle] +pub extern "C" fn emit_jump(block_id: u32) { + let block = s().blocks[block_id as usize]; + let no_args: &[BlockArg] = &[]; + b().ins().jump(block, no_args); +} + +/// Jump to block_id, passing one value argument. +#[no_mangle] +pub extern "C" fn emit_jump_with_arg(block_id: u32, val_id: u32) { + let block = s().blocks[block_id as usize]; + let val = s().values[val_id as usize]; + b().ins().jump(block, &[BlockArg::Value(val)]); +} + +/// Jump to block_id, passing accumulated call_args as block arguments. +/// Clears call_args after use. +#[no_mangle] +pub extern "C" fn emit_jump_with_args(block_id: u32) { + let block = s().blocks[block_id as usize]; + let args: Vec = s().call_args.drain(..).map(|v| BlockArg::Value(v)).collect(); + b().ins().jump(block, &args); +} + +/// Conditional branch where then_block gets accumulated call_args, else_block gets none. +/// Clears call_args after use. +#[no_mangle] +pub extern "C" fn emit_brif_with_jump_args(cond: u32, then_block: u32, else_block: u32) { + let vcond = s().values[cond as usize]; + let bt = s().blocks[then_block as usize]; + let be = s().blocks[else_block as usize]; + let args: Vec = s().call_args.drain(..).map(|v| BlockArg::Value(v)).collect(); + let no_args: &[BlockArg] = &[]; + b().ins().brif(vcond, bt, &args, be, no_args); +} + +#[no_mangle] +pub extern "C" fn emit_brif(cond: u32, then_block: u32, else_block: u32) { + let vcond = s().values[cond as usize]; + let bt = s().blocks[then_block as usize]; + let be = s().blocks[else_block as usize]; + let no_args: &[BlockArg] = &[]; + b().ins().brif(vcond, bt, no_args, be, no_args); +} + +/// Conditional branch with optional args per arm. +/// Use 0xFFFFFFFF (-1 as i32) for "no argument". +#[no_mangle] +pub extern "C" fn emit_brif_with_args( + cond: u32, + then_block: u32, then_arg: i32, + else_block: u32, else_arg: i32, +) { + let vcond = s().values[cond as usize]; + let bt = s().blocks[then_block as usize]; + let be = s().blocks[else_block as usize]; + + let then_args: Vec = if then_arg >= 0 { + vec![BlockArg::Value(s().values[then_arg as usize])] + } else { + vec![] + }; + let else_args: Vec = if else_arg >= 0 { + vec![BlockArg::Value(s().values[else_arg as usize])] + } else { + vec![] + }; + + b().ins().brif(vcond, bt, &then_args, be, &else_args); +} + +// --- i64 memory operations --- + +#[no_mangle] +pub extern "C" fn emit_load_i64(base: u32, wasm_addr: u32, offset: i32) -> u32 { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + let val = b().ins().load(types::I64, MemFlags::new(), effective, offset); + let session = s(); + let id = session.values.len() as u32; + session.values.push(val); + id +} + +#[no_mangle] +pub extern "C" fn emit_store_i64(base: u32, wasm_addr: u32, value: u32, offset: i32) { + let vbase = s().values[base as usize]; + let vaddr = s().values[wasm_addr as usize]; + let vvalue = s().values[value as usize]; + let extended = b().ins().uextend(types::I64, vaddr); + let effective = b().ins().iadd(vbase, extended); + b().ins().store(MemFlags::new(), vvalue, effective, offset); +} + +// --- Float arithmetic (polymorphic: works for both f32 and f64) --- + +macro_rules! emit_float_binop { + ($name:ident, $op:ident) => { + #[no_mangle] + pub extern "C" fn $name(a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let r = b().ins().$op(va, vb); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id + } + }; +} + +emit_float_binop!(emit_fadd, fadd); +emit_float_binop!(emit_fsub, fsub); +emit_float_binop!(emit_fmul, fmul); +emit_float_binop!(emit_fdiv, fdiv); +emit_float_binop!(emit_fmin, fmin); +emit_float_binop!(emit_fmax, fmax); +emit_float_binop!(emit_fcopysign, fcopysign); + +macro_rules! emit_float_unop { + ($name:ident, $op:ident) => { + #[no_mangle] + pub extern "C" fn $name(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().$op(va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id + } + }; +} + +emit_float_unop!(emit_fabs, fabs); +emit_float_unop!(emit_fneg, fneg); +emit_float_unop!(emit_ceil, ceil); +emit_float_unop!(emit_floor, floor); +emit_float_unop!(emit_trunc_float, trunc); +emit_float_unop!(emit_nearest, nearest); +emit_float_unop!(emit_sqrt, sqrt); + +// --- Float comparison --- + +use cranelift_codegen::ir::condcodes::FloatCC; + +/// cc: 0=eq, 1=ne, 2=lt, 3=gt, 4=le, 5=ge +#[no_mangle] +pub extern "C" fn emit_fcmp(cc: u32, a: u32, b_id: u32) -> u32 { + let va = s().values[a as usize]; + let vb = s().values[b_id as usize]; + let cond = match cc { + 0 => FloatCC::Equal, + 1 => FloatCC::NotEqual, + 2 => FloatCC::LessThan, + 3 => FloatCC::GreaterThan, + 4 => FloatCC::LessThanOrEqual, + 5 => FloatCC::GreaterThanOrEqual, + _ => panic!("Unknown fcmp condition code: {}", cc), + }; + let cmp = b().ins().fcmp(cond, va, vb); + let r = b().ins().uextend(types::I32, cmp); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +// --- Float conversions --- + +/// Convert float to signed int. target_type: 0=I32, 1=I64 +#[no_mangle] +pub extern "C" fn emit_fcvt_to_sint(target_type: u32, a: u32) -> u32 { + let va = s().values[a as usize]; + let ty = wasm_type_to_clif(target_type); + let r = b().ins().fcvt_to_sint(ty, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Convert float to unsigned int. target_type: 0=I32, 1=I64 +#[no_mangle] +pub extern "C" fn emit_fcvt_to_uint(target_type: u32, a: u32) -> u32 { + let va = s().values[a as usize]; + let ty = wasm_type_to_clif(target_type); + let r = b().ins().fcvt_to_uint(ty, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Convert float to signed int (saturating). target_type: 0=I32, 1=I64 +#[no_mangle] +pub extern "C" fn emit_fcvt_to_sint_sat(target_type: u32, a: u32) -> u32 { + let va = s().values[a as usize]; + let ty = wasm_type_to_clif(target_type); + let r = b().ins().fcvt_to_sint_sat(ty, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Convert float to unsigned int (saturating). target_type: 0=I32, 1=I64 +#[no_mangle] +pub extern "C" fn emit_fcvt_to_uint_sat(target_type: u32, a: u32) -> u32 { + let va = s().values[a as usize]; + let ty = wasm_type_to_clif(target_type); + let r = b().ins().fcvt_to_uint_sat(ty, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Convert signed int to float. target_type: 2=F32, 3=F64 +#[no_mangle] +pub extern "C" fn emit_fcvt_from_sint(target_type: u32, a: u32) -> u32 { + let va = s().values[a as usize]; + let ty = wasm_type_to_clif(target_type); + let r = b().ins().fcvt_from_sint(ty, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Convert unsigned int to float. target_type: 2=F32, 3=F64 +#[no_mangle] +pub extern "C" fn emit_fcvt_from_uint(target_type: u32, a: u32) -> u32 { + let va = s().values[a as usize]; + let ty = wasm_type_to_clif(target_type); + let r = b().ins().fcvt_from_uint(ty, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// f32 -> f64 +#[no_mangle] +pub extern "C" fn emit_fpromote(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().fpromote(types::F64, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// f64 -> f32 +#[no_mangle] +pub extern "C" fn emit_fdemote(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().fdemote(types::F32, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +// --- Select --- + +/// Wasm select: if cond != 0, return val_true, else val_false. +/// cond is an i32 (Wasm), convert to boolean for Cranelift's select. +#[no_mangle] +pub extern "C" fn emit_select(cond: u32, val_true: u32, val_false: u32) -> u32 { + let vcond = s().values[cond as usize]; + let vt = s().values[val_true as usize]; + let vf = s().values[val_false as usize]; + // Convert i32 condition to boolean (i8): cond != 0 + let bool_cond = b().ins().icmp_imm(IntCC::NotEqual, vcond, 0); + let r = b().ins().select(bool_cond, vt, vf); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +// --- Type widening/narrowing --- + +/// Zero-extend i32 to i64 +#[no_mangle] +pub extern "C" fn emit_uextend_i64(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().uextend(types::I64, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +/// Truncate i64 to i32 +#[no_mangle] +pub extern "C" fn emit_ireduce_i32(a: u32) -> u32 { + let va = s().values[a as usize]; + let r = b().ins().ireduce(types::I32, va); + let session = s(); + let id = session.values.len() as u32; + session.values.push(r); + id +} + +// --- SigRef builder (for call_indirect) --- + +/// Start building a new signature. Call sig_add_param/sig_add_return, then end_sig. +#[no_mangle] +pub extern "C" fn begin_sig() { + s().sig_builder = Some(Signature::new(CallConv::Tail)); +} + +/// Add a parameter type to the current signature being built. +#[no_mangle] +pub extern "C" fn sig_add_param(wasm_type: u32) { + s().sig_builder.as_mut().unwrap() + .params.push(AbiParam::new(wasm_type_to_clif(wasm_type))); +} + +/// Add a return type to the current signature being built. +#[no_mangle] +pub extern "C" fn sig_add_return(wasm_type: u32) { + s().sig_builder.as_mut().unwrap() + .returns.push(AbiParam::new(wasm_type_to_clif(wasm_type))); +} + +/// Finalize the signature and import it. Returns a sig_ref_id. +#[no_mangle] +pub extern "C" fn end_sig() -> u32 { + let sig = s().sig_builder.take().unwrap(); + let sig_ref = s().func.import_signature(sig); + let session = s(); + let id = session.sig_refs.len() as u32; + session.sig_refs.push(sig_ref); + id +} + +// --- Indirect call (accumulator pattern) --- + +/// Push a value as an argument for the next call_indirect. +#[no_mangle] +pub extern "C" fn push_call_arg(val_id: u32) { + let val = s().values[val_id as usize]; + s().call_args.push(val); +} + +/// Emit call_indirect with accumulated args. Returns the first result value ID. +/// Use -1 (0xFFFFFFFF) as return if the call has no return value. +#[no_mangle] +pub extern "C" fn emit_call_indirect(sig_ref_id: u32, callee: u32) -> u32 { + let sig_ref = s().sig_refs[sig_ref_id as usize]; + let vcallee = s().values[callee as usize]; + let args: Vec = s().call_args.drain(..).collect(); + let inst = b().ins().call_indirect(sig_ref, vcallee, &args); + let results = b().inst_results(inst); + if results.is_empty() { + return 0xFFFFFFFF; + } + let result = results[0]; + let session = s(); + let id = session.values.len() as u32; + session.values.push(result); + id +} + +/// Emit return_call_indirect (tail call) with accumulated args. +/// This is a terminator — the callee returns directly to our caller. +#[no_mangle] +pub extern "C" fn emit_return_call_indirect(sig_ref_id: u32, callee: u32) { + let sig_ref = s().sig_refs[sig_ref_id as usize]; + let vcallee = s().values[callee as usize]; + let args: Vec = s().call_args.drain(..).collect(); + b().ins().return_call_indirect(sig_ref, vcallee, &args); +} + +// --- Return --- + +#[no_mangle] +pub extern "C" fn emit_return(val_id: u32) { + let val = s().values[val_id as usize]; + b().ins().return_(&[val]); +} + +#[no_mangle] +pub extern "C" fn emit_return_void() { + b().ins().return_(&[]); +} + +/// Return multiple values from accumulated call_args. +/// Clears call_args after use. +#[no_mangle] +pub extern "C" fn emit_return_multi() { + let args: Vec = s().call_args.drain(..).collect(); + b().ins().return_(&args); +} + +// --- Compile --- + +/// Finalize the builder, compile to native code, return code length. +/// Code bytes are stored internally; read with get_code_ptr/get_code_len. +#[no_mangle] +pub extern "C" fn compile() -> u32 { + let isa = unsafe { ISA.as_ref().expect("ISA not initialized") }; + let session = s(); + + // Take ownership of the builder and finalize it + let builder = unsafe { Box::from_raw(session.builder) }; + builder.finalize(); + session.builder = std::ptr::null_mut(); + + // Take ownership of the function (avoids cloning the entire IR) + let func = std::mem::replace(&mut *session.func, Function::new()); + let mut ctx = Context::for_function(func); + let compiled = ctx + .compile(isa.as_ref(), &mut ControlPlane::default()) + .expect("Compilation failed"); + + let code = compiled.code_buffer(); + + // Clear session vecs to free memory between compilations + session.blocks.clear(); + session.variables.clear(); + session.values.clear(); + session.sig_refs.clear(); + session.call_args.clear(); + session.jump_tables.clear(); + session.br_table_targets.clear(); + + unsafe { + COMPILED_CODE = code.to_vec(); + COMPILED_CODE.len() as u32 + } +} + +#[no_mangle] +pub extern "C" fn get_code_ptr() -> *const u8 { + unsafe { COMPILED_CODE.as_ptr() } +} + +#[no_mangle] +pub extern "C" fn get_code_len() -> u32 { + unsafe { COMPILED_CODE.len() as u32 } +} + +// --- Trampoline compilation --- + +#[no_mangle] +pub extern "C" fn begin_trampoline_sig() { + unsafe { TRAMPOLINE_SIG = Some(Signature::new(CallConv::Tail)); } +} + +#[no_mangle] +pub extern "C" fn trampoline_sig_add_param(wasm_type: u32) { + unsafe { + TRAMPOLINE_SIG.as_mut().unwrap() + .params.push(AbiParam::new(wasm_type_to_clif(wasm_type))); + } +} + +#[no_mangle] +pub extern "C" fn trampoline_sig_add_return(wasm_type: u32) { + unsafe { + TRAMPOLINE_SIG.as_mut().unwrap() + .returns.push(AbiParam::new(wasm_type_to_clif(wasm_type))); + } +} + +fn compile_trampoline( + outer_sig: Signature, + inner_sig: Signature, + build_call: impl FnOnce( + &mut FunctionBuilder, + cranelift_codegen::ir::SigRef, + &[cranelift_codegen::ir::Value], + ) -> cranelift_codegen::ir::Inst, +) -> u32 { + let isa = unsafe { ISA.as_ref().expect("ISA not initialized") }; + let mut func = Function::with_name_signature(UserFuncName::user(0, 0), outer_sig); + let sig_ref = func.import_signature(inner_sig); + + let mut builder_ctx = FunctionBuilderContext::new(); + let mut builder = FunctionBuilder::new(&mut func, &mut builder_ctx); + + let entry = builder.create_block(); + builder.append_block_params_for_function_params(entry); + builder.switch_to_block(entry); + builder.seal_block(entry); + + let params = builder.block_params(entry).to_vec(); + let inst = build_call(&mut builder, sig_ref, ¶ms); + let results = builder.inst_results(inst).to_vec(); + builder.ins().return_(&results); + builder.finalize(); + + let mut ctx = Context::for_function(func); + let compiled = ctx + .compile(isa.as_ref(), &mut ControlPlane::default()) + .expect("Trampoline compilation failed"); + + unsafe { + COMPILED_CODE = compiled.code_buffer().to_vec(); + COMPILED_CODE.len() as u32 + } +} + +fn copy_sig(src: &Signature, conv: CallConv) -> Signature { + let mut sig = Signature::new(conv); + for p in &src.params { sig.params.push(p.clone()); } + for r in &src.returns { sig.returns.push(r.clone()); } + sig +} + +/// Compile an entry trampoline: platform ABI → Tail convention. +/// Takes (funcPtr, memBase, ctxPtr, args...) with platform ABI, +/// calls through funcPtr with Tail convention. +#[no_mangle] +pub extern "C" fn compile_entry_trampoline() -> u32 { + let isa = unsafe { ISA.as_ref().expect("ISA not initialized") }; + let tail_sig = unsafe { TRAMPOLINE_SIG.take().expect("No trampoline sig") }; + + let mut outer_sig = Signature::new(isa.default_call_conv()); + outer_sig.params.push(AbiParam::new(types::I64)); // funcPtr + for p in &tail_sig.params { outer_sig.params.push(p.clone()); } + for r in &tail_sig.returns { outer_sig.returns.push(r.clone()); } + + compile_trampoline(outer_sig, tail_sig, |builder, sig_ref, params| { + builder.ins().call_indirect(sig_ref, params[0], ¶ms[1..]) + }) +} + +/// Compile an import trampoline: Tail convention → platform ABI. +/// Takes (memBase, ctxPtr, args...) with Tail convention, +/// calls platform-ABI stub at baked-in address. +#[no_mangle] +pub extern "C" fn compile_import_trampoline(stub_addr_lo: u32, stub_addr_hi: u32) -> u32 { + let isa = unsafe { ISA.as_ref().expect("ISA not initialized") }; + let tail_sig = unsafe { TRAMPOLINE_SIG.take().expect("No trampoline sig") }; + let stub_addr = ((stub_addr_hi as u64) << 32) | (stub_addr_lo as u64); + + let outer_sig = copy_sig(&tail_sig, CallConv::Tail); + let platform_sig = copy_sig(&tail_sig, isa.default_call_conv()); + + compile_trampoline(outer_sig, platform_sig, |builder, sig_ref, params| { + let stub_ptr = builder.ins().iconst(types::I64, stub_addr as i64); + builder.ins().call_indirect(sig_ref, stub_ptr, params) + }) +} diff --git a/runtime/src/main/java/run/endive/runtime/Instance.java b/runtime/src/main/java/run/endive/runtime/Instance.java index d929a281..ea6bdc4a 100644 --- a/runtime/src/main/java/run/endive/runtime/Instance.java +++ b/runtime/src/main/java/run/endive/runtime/Instance.java @@ -51,7 +51,7 @@ import run.endive.wasm.types.ValType; import run.endive.wasm.types.Value; -public class Instance { +public class Instance implements AutoCloseable { public static final String START_FUNCTION_NAME = "_start"; private final WasmModule module; @@ -548,6 +548,11 @@ public Machine getMachine() { return machine; } + @Override + public void close() { + machine.close(); + } + public boolean isTailCallPending() { return tailCallPending != null; } diff --git a/runtime/src/main/java/run/endive/runtime/Machine.java b/runtime/src/main/java/run/endive/runtime/Machine.java index 01eedac8..6a5d7911 100644 --- a/runtime/src/main/java/run/endive/runtime/Machine.java +++ b/runtime/src/main/java/run/endive/runtime/Machine.java @@ -3,7 +3,7 @@ import run.endive.wasm.WasmEngineException; @FunctionalInterface -public interface Machine { +public interface Machine extends AutoCloseable { long[] call(int funcId, long[] args) throws WasmEngineException; @@ -16,4 +16,7 @@ default CallResult callWithRefs(int funcId, long[] args, Object[] refArgs) throws WasmEngineException { return CallResult.of(call(funcId, args, refArgs), null); } + + @Override + default void close() {} }