+ * [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