export function generateWasmBody()

in src/mono/browser/runtime/jiterpreter-trace-generator.ts [239:1746]


export function generateWasmBody (
    frame: NativePointer, traceName: string, ip: MintOpcodePtr,
    startOfBody: MintOpcodePtr, endOfBody: MintOpcodePtr,
    builder: WasmBuilder, instrumentedTraceId: number,
    backwardBranchTable: Uint16Array | null
): number {
    const abort = <MintOpcodePtr><any>0;
    let isFirstInstruction = true, isConditionallyExecuted = false,
        containsSimd = false,
        pruneOpcodes = false, hasEmittedUnreachable = false;
    let result = 0,
        prologueOpcodeCounter = 0,
        conditionalOpcodeCounter = 0;

    eraseInferredState();

    // If a trace is instrumented, also activate back branch tracing
    builder.backBranchTraceLevel = instrumentedTraceId
        ? 2
        : defaultTraceBackBranches;

    // Record the address of our prepare_jiterpreter opcode as the entry point, not the opcode after it.
    // Some back-branches will target prepare_jiterpreter directly, and we need them to work.
    let rip = builder.cfg.entry(ip);

    while (ip) {
        // This means some code went 'ip = abort; continue'
        if (!ip)
            break;

        builder.cfg.ip = ip;

        if (ip >= endOfBody) {
            record_abort(builder.traceIndex, ip, traceName, "end-of-body");
            if (instrumentedTraceId)
                mono_log_info(`instrumented trace ${traceName} exited at end of body @${(<any>ip).toString(16)}`);
            break;
        }

        // HACK: Browsers set a limit of 4KB, we lower it slightly since a single opcode
        //  might generate a ton of code and we generate a bit of an epilogue after
        //  we finish
        const maxBytesGenerated = 3840,
            spaceLeft = maxBytesGenerated - builder.bytesGeneratedSoFar - builder.cfg.overheadBytes;
        if (builder.size >= spaceLeft) {
            // mono_log_info(`trace too big, estimated size is ${builder.size + builder.bytesGeneratedSoFar}`);
            record_abort(builder.traceIndex, ip, traceName, "trace-too-big");
            if (instrumentedTraceId)
                mono_log_info(`instrumented trace ${traceName} exited because of size limit at @${(<any>ip).toString(16)} (spaceLeft=${spaceLeft}b)`);
            break;
        }

        if (instrumentedTraceId && traceEip) {
            builder.i32_const(instrumentedTraceId);
            builder.ip_const(ip);
            builder.callImport("trace_eip");
        }

        let opcode = getU16(ip);
        const numSregs = cwraps.mono_jiterp_get_opcode_info(opcode, OpcodeInfoType.Sregs),
            numDregs = cwraps.mono_jiterp_get_opcode_info(opcode, OpcodeInfoType.Dregs),
            opLengthU16 = cwraps.mono_jiterp_get_opcode_info(opcode, OpcodeInfoType.Length);

        const isSimdIntrins = (opcode >= MintOpcode.MINT_SIMD_INTRINS_P_P) &&
            (opcode <= MintOpcode.MINT_SIMD_INTRINS_P_PPP);
        const simdIntrinsArgCount = isSimdIntrins
            ? opcode - MintOpcode.MINT_SIMD_INTRINS_P_P + 2
            : 0;
        const simdIntrinsIndex = isSimdIntrins
            ? getArgU16(ip, 1 + simdIntrinsArgCount)
            : 0;

        mono_assert((opcode >= 0) && (opcode < MintOpcode.MINT_LASTOP), () => `invalid opcode ${opcode}`);

        const opname = isSimdIntrins
            ? SimdInfo[simdIntrinsArgCount][simdIntrinsIndex]
            : getOpcodeName(opcode);
        const _ip = ip;
        const isBackBranchTarget = builder.options.noExitBackwardBranches &&
            is_backward_branch_target(ip, startOfBody, backwardBranchTable),
            isForwardBranchTarget = builder.branchTargets.has(ip),
            startBranchBlock = isBackBranchTarget || isForwardBranchTarget ||
                // If a method contains backward branches, we also need to check eip at the first insn
                //  because a backward branch might target a point in the middle of the trace
                (isFirstInstruction && backwardBranchTable),
            // We want to approximate the number of unconditionally executed instructions along with
            //  the ones that were probably conditionally executed by the time we reached the exit point
            // We don't know the exact path that would have taken us to a given point, but it's a reasonable
            //  guess that methods dense with branches are more likely to take a complex path to reach
            //  a given exit
            exitOpcodeCounter = conditionalOpcodeCounter + prologueOpcodeCounter +
                builder.branchTargets.size;
        let skipDregInvalidation = false,
            opcodeValue = getOpcodeTableValue(opcode);

        // We record the offset of each backward branch we encounter, so that later branch
        //  opcodes know that it's available by branching to the top of the dispatch loop
        if (isBackBranchTarget) {
            if (builder.backBranchTraceLevel > 1)
                mono_log_info(`${traceName} recording back branch target 0x${(<any>ip).toString(16)}`);
            builder.backBranchOffsets.push(ip);
        }

        if (startBranchBlock) {
            // We've reached a branch target so we need to stop pruning opcodes, since
            //  we are no longer in a dead zone that execution can't reach
            pruneOpcodes = false;
            hasEmittedUnreachable = false;
            // If execution runs past the end of the current branch block, ensure
            //  that the instruction pointer is updated appropriately. This will
            //  also guarantee that the branch target block's comparison will
            //  succeed so that execution continues.
            // We make sure above that this isn't done for the start of the trace,
            //  otherwise loops will run forever and never terminate since after
            //  branching to the top of the loop we would blow away eip
            append_branch_target_block(builder, ip, isBackBranchTarget);
            isConditionallyExecuted = true;
            eraseInferredState();
            // Monitoring wants an opcode count that is a measurement of how many opcodes
            //  we definitely executed, so we want to ignore any opcodes that might
            //  have been skipped due to forward branching. This gives us an approximation
            //  of that by only counting how far we are from the most recent branch target
            conditionalOpcodeCounter = 0;
        }

        // Handle the _OUTSIDE_BRANCH_BLOCK table entries
        if ((opcodeValue < -1) && isConditionallyExecuted)
            opcodeValue = (opcodeValue === -2) ? 2 : 0;

        isFirstInstruction = false;

        if (opcode === MintOpcode.MINT_SWITCH) {
            // HACK: This opcode breaks all our table-based parsing and will cause the trace compiler to hang
            //  if it encounters a switch inside of a pruning region, so we need to let the normal code path
            //  run even if pruning is on
        } else if (disabledOpcodes.indexOf(opcode) >= 0) {
            append_bailout(builder, ip, BailoutReason.Debugging);
            opcode = MintOpcode.MINT_NOP;
            // Intentionally leave the correct info in place so we skip the right number of bytes
        } else if (pruneOpcodes) {
            opcode = MintOpcode.MINT_NOP;
        }

        switch (opcode) {
            case MintOpcode.MINT_NOP: {
                // This typically means the current opcode was disabled or pruned
                if (pruneOpcodes) {
                    // We emit an unreachable opcode so that if execution somehow reaches a pruned opcode, we will abort
                    // This should be impossible anyway but it's also useful to have pruning visible in the wasm
                    // FIXME: Ideally we would stop generating opcodes after the first unreachable, but that causes v8 to hang
                    if (!hasEmittedUnreachable)
                        builder.appendU8(WasmOpcode.unreachable);
                    // Each unreachable opcode could generate a bunch of native code in a bad wasm jit so generate nops after it
                    hasEmittedUnreachable = true;
                }
                break;
            }
            case MintOpcode.MINT_INITLOCAL:
            case MintOpcode.MINT_INITLOCALS: {
                // FIXME: We should move the first entry point after initlocals if it exists
                const startOffsetInBytes = getArgU16(ip, 1),
                    sizeInBytes = getArgU16(ip, 2);
                append_memset_local(builder, startOffsetInBytes, 0, sizeInBytes);
                break;
            }
            case MintOpcode.MINT_LOCALLOC: {
                // dest
                append_ldloca(builder, getArgU16(ip, 1));
                // len
                append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                // frame
                builder.local("frame");
                builder.callImport("localloc");
                break;
            }
            case MintOpcode.MINT_ZEROBLK: {
                // dest
                append_ldloc(builder, getArgU16(ip, 1), WasmOpcode.i32_load);
                // value
                builder.i32_const(0);
                // count
                append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                // memset
                builder.appendU8(WasmOpcode.PREFIX_sat);
                builder.appendU8(11);
                builder.appendU8(0);
                break;
            }
            case MintOpcode.MINT_ZEROBLK_IMM: {
                append_ldloc(builder, getArgU16(ip, 1), WasmOpcode.i32_load);
                append_memset_dest(builder, 0, getArgU16(ip, 2));
                break;
            }
            case MintOpcode.MINT_CPBLK: {
                const sizeOffset = getArgU16(ip, 3),
                    srcOffset = getArgU16(ip, 2),
                    destOffset = getArgU16(ip, 1),
                    constantSize = get_known_constant_value(builder, sizeOffset);

                if (constantSize !== 0) {
                    if (typeof (constantSize) !== "number") {
                        // size (FIXME: uint32 not int32)
                        append_ldloc(builder, sizeOffset, WasmOpcode.i32_load);
                        builder.local("count", WasmOpcode.tee_local);
                        // if size is 0 then don't do anything
                        builder.block(WasmValtype.void, WasmOpcode.if_); // if size
                    } else {
                        // Store the count into the local in case the unroll fails
                        builder.i32_const(constantSize);
                        builder.local("count", WasmOpcode.set_local);
                    }

                    // stash dest then check for null
                    append_ldloc(builder, destOffset, WasmOpcode.i32_load);
                    builder.local("dest_ptr", WasmOpcode.tee_local);
                    builder.appendU8(WasmOpcode.i32_eqz);
                    // stash src then check for null
                    append_ldloc(builder, srcOffset, WasmOpcode.i32_load);
                    builder.local("src_ptr", WasmOpcode.tee_local);
                    builder.appendU8(WasmOpcode.i32_eqz);

                    // now we memmove if both dest and src are valid. The stack currently has
                    //  the eqz result for each pointer so we can stash a bailout inside of an if
                    builder.appendU8(WasmOpcode.i32_or);
                    builder.block(WasmValtype.void, WasmOpcode.if_); // if null
                    append_bailout(builder, ip, BailoutReason.NullCheck);
                    builder.endBlock(); // if null

                    if (
                        (typeof (constantSize) !== "number") ||
                        !try_append_memmove_fast(builder, 0, 0, constantSize, false, "dest_ptr", "src_ptr")
                    ) {
                        // We passed the null check so now prepare the stack
                        builder.local("dest_ptr");
                        builder.local("src_ptr");
                        builder.local("count");
                        // wasm memmove with stack layout dest, src, count
                        builder.appendU8(WasmOpcode.PREFIX_sat);
                        builder.appendU8(10);
                        builder.appendU8(0);
                        builder.appendU8(0);
                    }

                    if (typeof (constantSize) !== "number")
                        builder.endBlock(); // if size
                }
                break;
            }
            case MintOpcode.MINT_INITBLK: {
                const sizeOffset = getArgU16(ip, 3),
                    valueOffset = getArgU16(ip, 2),
                    destOffset = getArgU16(ip, 1);

                // TODO: Handle constant size initblks. Not sure if they matter though
                // FIXME: This will cause an erroneous bailout if dest and size are both 0
                //  but that really shouldn't ever happen, and it will only cause a slowdown
                // dest
                append_ldloc_cknull(builder, destOffset, ip, true);
                // value
                append_ldloc(builder, valueOffset, WasmOpcode.i32_load);
                // size (FIXME: uint32 not int32)
                append_ldloc(builder, sizeOffset, WasmOpcode.i32_load);
                // spec: pop n, pop val, pop d, fill from d[0] to d[n] with value val
                builder.appendU8(WasmOpcode.PREFIX_sat);
                builder.appendU8(11);
                builder.appendU8(0);
                break;
            }

            // Other conditional branch types are handled by the relop table.
            case MintOpcode.MINT_BRFALSE_I4_S:
            case MintOpcode.MINT_BRTRUE_I4_S:
            case MintOpcode.MINT_BRFALSE_I4_SP:
            case MintOpcode.MINT_BRTRUE_I4_SP:
            case MintOpcode.MINT_BRFALSE_I8_S:
            case MintOpcode.MINT_BRTRUE_I8_S:
                if (!emit_branch(builder, ip, frame, opcode))
                    ip = abort;
                else
                    isConditionallyExecuted = true;
                break;

            case MintOpcode.MINT_BR_S:
            case MintOpcode.MINT_CALL_HANDLER:
            case MintOpcode.MINT_CALL_HANDLER_S:
                if (!emit_branch(builder, ip, frame, opcode))
                    ip = abort;
                else {
                    // Technically incorrect, but the instructions following this one may not be executed
                    //  since we might have skipped over them.
                    // FIXME: Identify when we should actually set the conditionally executed flag, perhaps
                    //  by doing a simple static flow analysis based on the displacements. Update heuristic too!
                    isConditionallyExecuted = true;
                }
                break;

            case MintOpcode.MINT_CKNULL: {
                // if (locals[ip[2]]) locals[ip[1]] = locals[ip[2]] else throw
                const src = getArgU16(ip, 2),
                    dest = getArgU16(ip, 1);
                // locals[n] = cknull(locals[n]) is a common pattern, and we don't
                //  need to do the write for it since it can't change the value
                if (src !== dest) {
                    builder.local("pLocals");
                    append_ldloc_cknull(builder, src, ip, true);
                    append_stloc_tail(builder, dest, WasmOpcode.i32_store);
                } else {
                    append_ldloc_cknull(builder, src, ip, false);
                }
                // We will have bailed out if the object was null
                if (builder.allowNullCheckOptimization) {
                    if (traceNullCheckOptimizations)
                        mono_log_info(`(0x${(<any>ip).toString(16)}) locals[${dest}] passed cknull`);
                    notNullSince.set(dest, <any>ip);
                }
                skipDregInvalidation = true;
                break;
            }

            case MintOpcode.MINT_TIER_ENTER_METHOD:
            case MintOpcode.MINT_TIER_PATCHPOINT: {
                // We need to make sure to notify the interpreter about tiering opcodes
                //  so that tiering up will still happen
                const iMethod = getU32_unaligned(<any>frame + getMemberOffset(JiterpMember.Imethod));
                builder.ptr_const(iMethod);
                // increase_entry_count will return 1 if we can continue, otherwise
                //  we need to bail out into the interpreter so it can perform tiering
                builder.callImport("entry");
                builder.block(WasmValtype.void, WasmOpcode.if_);
                append_bailout(builder, ip, BailoutReason.InterpreterTiering);
                builder.endBlock();
                break;
            }

            case MintOpcode.MINT_TIER_ENTER_JITERPRETER:
                opcodeValue = 0;
                break;

            case MintOpcode.MINT_SAFEPOINT:
                append_safepoint(builder, ip);
                break;

            case MintOpcode.MINT_LDLOCA_S: {
                // Pre-load locals for the store op
                builder.local("pLocals");
                // locals[ip[1]] = &locals[ip[2]]
                const offset = getArgU16(ip, 2),
                    flag = isAddressTaken(builder, offset),
                    destOffset = getArgU16(ip, 1);
                if (!flag)
                    mono_log_error(`${traceName}: Expected local ${offset} to have address taken flag`);
                append_ldloca(builder, offset);
                append_stloc_tail(builder, destOffset, WasmOpcode.i32_store);
                // Record this ldloca as a known constant so that later uses of it turn into a lea,
                //  and the wasm runtime can constant fold them with other constants. It's not uncommon
                //  to have code that does '&x + c', which (if this optimization works) should
                //  turn into '&locals + offsetof(x) + c' and get constant folded to have the same cost
                //  as a regular ldloc
                knownConstants.set(destOffset, { type: "ldloca", offset: offset });
                // dreg invalidation would blow the known constant away, so disable it
                skipDregInvalidation = true;
                break;
            }

            case MintOpcode.MINT_LDSTR:
            case MintOpcode.MINT_LDFTN:
            case MintOpcode.MINT_LDFTN_ADDR:
            case MintOpcode.MINT_LDPTR: {
                // Pre-load locals for the store op
                builder.local("pLocals");

                // frame->imethod->data_items [ip [2]]
                let data = get_imethod_data(frame, getArgU16(ip, 2));
                if (opcode === MintOpcode.MINT_LDFTN)
                    data = <any>cwraps.mono_jiterp_imethod_to_ftnptr(<any>data);

                builder.ptr_const(data);

                append_stloc_tail(builder, getArgU16(ip, 1), WasmOpcode.i32_store);
                break;
            }

            case MintOpcode.MINT_CPOBJ_VT: {
                const klass = get_imethod_data(frame, getArgU16(ip, 3));
                append_ldloc(builder, getArgU16(ip, 1), WasmOpcode.i32_load);
                append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                builder.ptr_const(klass);
                builder.callImport("value_copy");
                break;
            }
            case MintOpcode.MINT_CPOBJ_VT_NOREF: {
                const sizeBytes = getArgU16(ip, 3);
                append_ldloc(builder, getArgU16(ip, 1), WasmOpcode.i32_load);
                append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                append_memmove_dest_src(builder, sizeBytes);
                break;
            }
            case MintOpcode.MINT_LDOBJ_VT: {
                const size = getArgU16(ip, 3);
                append_ldloca(builder, getArgU16(ip, 1), size);
                append_ldloc_cknull(builder, getArgU16(ip, 2), ip, true);
                append_memmove_dest_src(builder, size);
                break;
            }
            case MintOpcode.MINT_STOBJ_VT: {
                const klass = get_imethod_data(frame, getArgU16(ip, 3));
                append_ldloc(builder, getArgU16(ip, 1), WasmOpcode.i32_load);
                append_ldloca(builder, getArgU16(ip, 2), 0);
                builder.ptr_const(klass);
                builder.callImport("value_copy");
                break;
            }
            case MintOpcode.MINT_STOBJ_VT_NOREF: {
                const sizeBytes = getArgU16(ip, 3);
                append_ldloc(builder, getArgU16(ip, 1), WasmOpcode.i32_load);
                append_ldloca(builder, getArgU16(ip, 2), 0);
                append_memmove_dest_src(builder, sizeBytes);
                break;
            }

            case MintOpcode.MINT_STRLEN: {
                builder.local("pLocals");
                append_ldloc_cknull(builder, getArgU16(ip, 2), ip, true);
                builder.appendU8(WasmOpcode.i32_load);
                builder.appendMemarg(getMemberOffset(JiterpMember.StringLength), 2);
                append_stloc_tail(builder, getArgU16(ip, 1), WasmOpcode.i32_store);
                break;
            }

            case MintOpcode.MINT_GETCHR: {
                builder.block();
                // index
                append_ldloc(builder, getArgU16(ip, 3), WasmOpcode.i32_load);
                // stash it, we'll be using it multiple times
                builder.local("index", WasmOpcode.tee_local);

                /*
                const constantIndex = get_known_constant_value(getArgU16(ip, 3));
                if (typeof (constantIndex) === "number")
                    console.log(`getchr in ${builder.functions[0].name} with constant index ${constantIndex}`);
                */

                // str
                let ptrLocal = "cknull_ptr";
                if (builder.options.zeroPageOptimization && isZeroPageReserved()) {
                    // load string ptr and stash it
                    // if the string ptr is null, the length check will fail and we will bail out,
                    //  so the null check is not necessary
                    modifyCounter(JiterpCounter.NullChecksFused, 1);
                    append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                    ptrLocal = "src_ptr";
                    builder.local(ptrLocal, WasmOpcode.tee_local);
                } else
                    append_ldloc_cknull(builder, getArgU16(ip, 2), ip, true);

                // current stack layout is [index, ptr]
                // get string length
                builder.appendU8(WasmOpcode.i32_load);
                builder.appendMemarg(getMemberOffset(JiterpMember.StringLength), 2);
                // current stack layout is [index, length]
                // index < length
                builder.appendU8(WasmOpcode.i32_lt_s);
                // index >= 0
                builder.local("index");
                builder.i32_const(0);
                builder.appendU8(WasmOpcode.i32_ge_s);
                // (index >= 0) && (index < length)
                builder.appendU8(WasmOpcode.i32_and);
                // If either of the index checks failed we will fall through to the bailout
                builder.appendU8(WasmOpcode.br_if);
                builder.appendULeb(0);
                append_bailout(builder, ip, BailoutReason.StringOperationFailed);
                builder.endBlock();

                // The null check and range check both passed so we can load the character now
                // Pre-load destination for the stloc at the end (we can't do this inside the block above)
                builder.local("pLocals");
                // (index * 2) + offsetof(MonoString, chars) + pString
                builder.local("index");
                builder.i32_const(2);
                builder.appendU8(WasmOpcode.i32_mul);
                builder.local(ptrLocal);
                builder.appendU8(WasmOpcode.i32_add);
                // Load char
                builder.appendU8(WasmOpcode.i32_load16_u);
                builder.appendMemarg(getMemberOffset(JiterpMember.StringData), 1);
                // Store into result
                append_stloc_tail(builder, getArgU16(ip, 1), WasmOpcode.i32_store);
                break;
            }

            case MintOpcode.MINT_GETITEM_SPAN:
            case MintOpcode.MINT_GETITEM_LOCALSPAN: {
                const elementSize = getArgI16(ip, 4);
                builder.block();
                // Load index and stash it
                append_ldloc(builder, getArgU16(ip, 3), WasmOpcode.i32_load);
                builder.local("index", WasmOpcode.tee_local);

                // Load address of the span structure
                let ptrLocal = "cknull_ptr";
                if (opcode === MintOpcode.MINT_GETITEM_SPAN) {
                    // span = *(MonoSpanOfVoid *)locals[2]
                    append_ldloc_cknull(builder, getArgU16(ip, 2), ip, true);
                } else {
                    // span = (MonoSpanOfVoid)locals[2]
                    append_ldloca(builder, getArgU16(ip, 2), 0);
                    ptrLocal = "src_ptr";
                    builder.local(ptrLocal, WasmOpcode.tee_local);
                }

                // length = span->length
                builder.appendU8(WasmOpcode.i32_load);
                builder.appendMemarg(getMemberOffset(JiterpMember.SpanLength), 2);
                // index < length
                builder.appendU8(WasmOpcode.i32_lt_u);
                // index >= 0
                // FIXME: It would be nice to optimize this down to a single (index < length) comparison
                //  but interp.c doesn't do it - presumably because a span could be bigger than 2gb?
                builder.local("index");
                builder.i32_const(0);
                builder.appendU8(WasmOpcode.i32_ge_s);
                // (index >= 0) && (index < length)
                builder.appendU8(WasmOpcode.i32_and);
                builder.appendU8(WasmOpcode.br_if);
                builder.appendULeb(0);
                append_bailout(builder, ip, BailoutReason.SpanOperationFailed);
                builder.endBlock();

                // We successfully null checked and bounds checked. Now compute
                //  the address and store it to the destination
                builder.local("pLocals");

                // src = span->_reference + (index * element_size);
                builder.local(ptrLocal);
                builder.appendU8(WasmOpcode.i32_load);
                builder.appendMemarg(getMemberOffset(JiterpMember.SpanData), 2);

                builder.local("index");
                builder.i32_const(elementSize);
                builder.appendU8(WasmOpcode.i32_mul);
                builder.appendU8(WasmOpcode.i32_add);

                append_stloc_tail(builder, getArgU16(ip, 1), WasmOpcode.i32_store);
                break;
            }

            case MintOpcode.MINT_INTRINS_SPAN_CTOR: {
                // if (len < 0) bailout
                builder.block();
                // int len = LOCAL_VAR (ip [3], gint32);
                append_ldloc(builder, getArgU16(ip, 3), WasmOpcode.i32_load);
                builder.local("count", WasmOpcode.tee_local);
                builder.i32_const(0);
                builder.appendU8(WasmOpcode.i32_ge_s);
                builder.appendU8(WasmOpcode.br_if);
                builder.appendULeb(0);
                append_bailout(builder, ip, BailoutReason.SpanOperationFailed);
                builder.endBlock();
                // gpointer span = locals + ip [1];
                append_ldloca(builder, getArgU16(ip, 1), 16);
                builder.local("dest_ptr", WasmOpcode.tee_local);
                // *(gpointer*)span = ptr;
                append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                builder.appendU8(WasmOpcode.i32_store);
                builder.appendMemarg(0, 0);
                // *(gint32*)((gpointer*)span + 1) = len;
                builder.local("dest_ptr");
                builder.local("count");
                builder.appendU8(WasmOpcode.i32_store);
                builder.appendMemarg(4, 0);
                break;
            }

            case MintOpcode.MINT_LD_DELEGATE_METHOD_PTR: {
                // FIXME: ldloca invalidation size
                append_ldloca(builder, getArgU16(ip, 1), 8);
                append_ldloca(builder, getArgU16(ip, 2), 8);
                builder.callImport("ld_del_ptr");
                break;
            }
            case MintOpcode.MINT_LDTSFLDA: {
                append_ldloca(builder, getArgU16(ip, 1), 4);
                // This value is unsigned but I32 is probably right
                builder.ptr_const(getArgI32(ip, 2));
                builder.callImport("ldtsflda");
                break;
            }
            case MintOpcode.MINT_INTRINS_GET_TYPE:
                builder.block();
                // dest, src
                append_ldloca(builder, getArgU16(ip, 1), 4);
                append_ldloca(builder, getArgU16(ip, 2), 0);
                builder.callImport("gettype");
                // bailout if gettype failed
                builder.appendU8(WasmOpcode.br_if);
                builder.appendULeb(0);
                append_bailout(builder, ip, BailoutReason.NullCheck);
                builder.endBlock();
                break;
            case MintOpcode.MINT_INTRINS_ENUM_HASFLAG: {
                const klass = get_imethod_data(frame, getArgU16(ip, 4));
                builder.ptr_const(klass);
                append_ldloca(builder, getArgU16(ip, 1), 4);
                append_ldloca(builder, getArgU16(ip, 2), 0);
                append_ldloca(builder, getArgU16(ip, 3), 0);
                builder.callImport("hasflag");
                break;
            }
            case MintOpcode.MINT_INTRINS_MEMORYMARSHAL_GETARRAYDATAREF: {
                const offset = getMemberOffset(JiterpMember.ArrayData);
                builder.local("pLocals");
                append_ldloc_cknull(builder, getArgU16(ip, 2), ip, true);
                builder.i32_const(offset);
                builder.appendU8(WasmOpcode.i32_add);
                append_stloc_tail(builder, getArgU16(ip, 1), WasmOpcode.i32_store);
                break;
            }
            case MintOpcode.MINT_INTRINS_GET_HASHCODE:
                builder.local("pLocals");
                append_ldloca(builder, getArgU16(ip, 2), 0);
                builder.callImport("hashcode");
                append_stloc_tail(builder, getArgU16(ip, 1), WasmOpcode.i32_store);
                break;
            case MintOpcode.MINT_INTRINS_TRY_GET_HASHCODE:
                builder.local("pLocals");
                append_ldloca(builder, getArgU16(ip, 2), 0);
                builder.callImport("try_hash");
                append_stloc_tail(builder, getArgU16(ip, 1), WasmOpcode.i32_store);
                break;
            case MintOpcode.MINT_INTRINS_RUNTIMEHELPERS_OBJECT_HAS_COMPONENT_SIZE:
                builder.local("pLocals");
                append_ldloca(builder, getArgU16(ip, 2), 0);
                builder.callImport("hascsize");
                append_stloc_tail(builder, getArgU16(ip, 1), WasmOpcode.i32_store);
                break;

            case MintOpcode.MINT_INTRINS_ORDINAL_IGNORE_CASE_ASCII: {
                builder.local("pLocals");
                // valueA (cache in lhs32, we need it again later)
                append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                builder.local("math_lhs32", WasmOpcode.tee_local);
                // valueB
                append_ldloc(builder, getArgU16(ip, 3), WasmOpcode.i32_load);
                // compute differentBits = (valueA ^ valueB) << 2
                builder.appendU8(WasmOpcode.i32_xor);
                builder.i32_const(2);
                builder.appendU8(WasmOpcode.i32_shl);
                builder.local("math_rhs32", WasmOpcode.set_local);
                // compute indicator
                builder.local("math_lhs32");
                builder.i32_const(0x00050005);
                builder.appendU8(WasmOpcode.i32_add);
                builder.i32_const(0x00A000A0);
                builder.appendU8(WasmOpcode.i32_or);
                builder.i32_const(0x001A001A);
                builder.appendU8(WasmOpcode.i32_add);
                builder.i32_const(-8388737); // 0xFF7FFF7F == 4286578559U == -8388737
                builder.appendU8(WasmOpcode.i32_or);
                // result = (differentBits & indicator) == 0
                builder.local("math_rhs32");
                builder.appendU8(WasmOpcode.i32_and);
                builder.appendU8(WasmOpcode.i32_eqz);
                append_stloc_tail(builder, getArgU16(ip, 1), WasmOpcode.i32_store);
                break;
            }

            case MintOpcode.MINT_ARRAY_RANK:
            case MintOpcode.MINT_ARRAY_ELEMENT_SIZE: {
                builder.block();
                // dest, src
                append_ldloca(builder, getArgU16(ip, 1), 4);
                append_ldloca(builder, getArgU16(ip, 2), 0);
                builder.callImport(opcode === MintOpcode.MINT_ARRAY_RANK ? "array_rank" : "a_elesize");
                // If the array was null we will bail out, otherwise continue
                builder.appendU8(WasmOpcode.br_if);
                builder.appendULeb(0);
                append_bailout(builder, ip, BailoutReason.NullCheck);
                builder.endBlock();
                break;
            }

            case MintOpcode.MINT_CASTCLASS_INTERFACE:
            case MintOpcode.MINT_ISINST_INTERFACE: {
                const klass = get_imethod_data(frame, getArgU16(ip, 3)),
                    isSpecialInterface = cwraps.mono_jiterp_is_special_interface(klass),
                    bailoutOnFailure = (opcode === MintOpcode.MINT_CASTCLASS_INTERFACE),
                    destOffset = getArgU16(ip, 1);
                if (!klass) {
                    record_abort(builder.traceIndex, ip, traceName, "null-klass");
                    ip = abort;
                    continue;
                }

                builder.block(); // depth x -> 0 (opcode block)

                if (builder.options.zeroPageOptimization && isZeroPageReserved()) {
                    // Null check fusion is possible, so (obj->vtable) will be 0 for !obj
                    append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                    builder.local("dest_ptr", WasmOpcode.tee_local);
                    modifyCounter(JiterpCounter.NullChecksFused, 1);
                } else {
                    builder.block(); // depth 0 -> 1 (null check block)
                    // src
                    append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                    builder.local("dest_ptr", WasmOpcode.tee_local);
                    // Null ptr check: If the ptr is non-null, skip this block
                    builder.appendU8(WasmOpcode.br_if);
                    builder.appendULeb(0);
                    builder.local("pLocals");
                    builder.i32_const(0);
                    append_stloc_tail(builder, destOffset, WasmOpcode.i32_store);
                    // at the end of this block (depth 0) we skip to the end of the opcode block (depth 1)
                    //  because we successfully zeroed the destination register
                    builder.appendU8(WasmOpcode.br);
                    builder.appendULeb(1);
                    builder.endBlock(); // depth 1 -> 0 (end null check block)
                    // Put ptr back on the stack
                    builder.local("dest_ptr");
                }

                // the special interface version signature is (obj, vtable, klass), but
                //  the fast signature is (vtable, klass)
                if (isSpecialInterface) {
                    // load a second copy of obj to build the helper arglist (obj, vtable, klass)
                    builder.local("dest_ptr");
                }

                builder.appendU8(WasmOpcode.i32_load); // obj->vtable
                builder.appendMemarg(getMemberOffset(JiterpMember.VTable), 0); // fixme: alignment

                builder.ptr_const(klass);
                builder.callImport(isSpecialInterface ? "imp_iface_s" : "imp_iface");

                if (bailoutOnFailure) {
                    // generate a 1 for null ptrs so we don't bail out and instead write the 0
                    //  to the destination
                    builder.local("dest_ptr");
                    builder.appendU8(WasmOpcode.i32_eqz);
                    builder.appendU8(WasmOpcode.i32_or);
                }

                builder.block(WasmValtype.void, WasmOpcode.if_); // if cast succeeded
                builder.local("pLocals");
                builder.local("dest_ptr");
                append_stloc_tail(builder, destOffset, WasmOpcode.i32_store);
                builder.appendU8(WasmOpcode.else_); // else cast failed
                if (bailoutOnFailure) {
                    // so bailout
                    append_bailout(builder, ip, BailoutReason.CastFailed);
                } else {
                    // this is isinst, so write 0 to destination instead
                    builder.local("pLocals");
                    builder.i32_const(0);
                    append_stloc_tail(builder, destOffset, WasmOpcode.i32_store);
                }
                builder.endBlock(); // endif

                builder.endBlock(); // depth 0 -> x (end opcode block)

                break;
            }

            case MintOpcode.MINT_CASTCLASS_COMMON:
            case MintOpcode.MINT_ISINST_COMMON:
            case MintOpcode.MINT_CASTCLASS:
            case MintOpcode.MINT_ISINST: {
                const klass = get_imethod_data(frame, getArgU16(ip, 3)),
                    canDoFastCheck = (opcode === MintOpcode.MINT_CASTCLASS_COMMON) ||
                        (opcode === MintOpcode.MINT_ISINST_COMMON),
                    bailoutOnFailure = (opcode === MintOpcode.MINT_CASTCLASS) ||
                        (opcode === MintOpcode.MINT_CASTCLASS_COMMON),
                    destOffset = getArgU16(ip, 1);
                if (!klass) {
                    record_abort(builder.traceIndex, ip, traceName, "null-klass");
                    ip = abort;
                    continue;
                }

                builder.block(); // depth x -> 0 (opcode block)

                if (builder.options.zeroPageOptimization && isZeroPageReserved()) {
                    // Null check fusion is possible, so (obj->vtable)->klass will be 0 for !obj
                    append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                    builder.local("dest_ptr", WasmOpcode.tee_local);
                    modifyCounter(JiterpCounter.NullChecksFused, 1);
                } else {
                    builder.block(); // depth 0 -> 1 (null check block)
                    // src
                    append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                    builder.local("dest_ptr", WasmOpcode.tee_local);
                    // Null ptr check: If the ptr is non-null, skip this block
                    builder.appendU8(WasmOpcode.br_if);
                    builder.appendULeb(0);
                    builder.local("pLocals");
                    builder.i32_const(0);
                    append_stloc_tail(builder, destOffset, WasmOpcode.i32_store);
                    // at the end of this block (depth 0) we skip to the end of the opcode block (depth 1)
                    //  because we successfully zeroed the destination register
                    builder.appendU8(WasmOpcode.br);
                    builder.appendULeb(1);
                    builder.endBlock(); // depth 1 -> 0 (end null check block)
                    // Put ptr back on the stack
                    builder.local("dest_ptr");
                }

                // If we're here the null check passed and we now need to type-check
                builder.appendU8(WasmOpcode.i32_load); // obj->vtable
                builder.appendMemarg(getMemberOffset(JiterpMember.VTable), 0); // fixme: alignment
                builder.appendU8(WasmOpcode.i32_load); // (obj->vtable)->klass
                builder.appendMemarg(getMemberOffset(JiterpMember.VTableKlass), 0); // fixme: alignment
                // Stash obj->vtable->klass so we can do a fast has_parent check later
                if (canDoFastCheck)
                    builder.local("src_ptr", WasmOpcode.tee_local);
                builder.i32_const(klass);
                builder.appendU8(WasmOpcode.i32_eq);
                builder.block(WasmValtype.void, WasmOpcode.if_); // if A

                // Fast type-check passed (exact match), so store the ptr and continue
                builder.local("pLocals");
                builder.local("dest_ptr");
                append_stloc_tail(builder, destOffset, WasmOpcode.i32_store);

                // Fast type-check failed, so call the helper function
                builder.appendU8(WasmOpcode.else_); // else A

                if (canDoFastCheck) {
                    // Fast path for ISINST_COMMON/CASTCLASS_COMMON. We know klass is a simple type
                    //  so all we need to do is a parentage check.
                    builder.local("src_ptr"); // obj->vtable->klass
                    builder.ptr_const(klass);
                    builder.callImport("hasparent");

                    if (bailoutOnFailure) {
                        // generate a 1 for null ptrs so we don't bail out and instead write the 0
                        //  to the destination
                        builder.local("dest_ptr");
                        builder.appendU8(WasmOpcode.i32_eqz);
                        builder.appendU8(WasmOpcode.i32_or);
                    }

                    builder.block(WasmValtype.void, WasmOpcode.if_); // if B
                    // mono_class_has_parent_fast returned 1 so *destination = obj
                    builder.local("pLocals");
                    builder.local("dest_ptr");
                    append_stloc_tail(builder, destOffset, WasmOpcode.i32_store);
                    builder.appendU8(WasmOpcode.else_); // else B
                    // mono_class_has_parent_fast returned 0
                    if (bailoutOnFailure) {
                        // so bailout
                        append_bailout(builder, ip, BailoutReason.CastFailed);
                    } else {
                        // this is isinst, so write 0 to destination instead
                        builder.local("pLocals");
                        builder.i32_const(0);
                        append_stloc_tail(builder, destOffset, WasmOpcode.i32_store);
                    }
                    builder.endBlock(); // endif B
                } else {
                    // Slow path for ISINST/CASTCLASS, handles things like generics and nullable.
                    // &dest
                    append_ldloca(builder, getArgU16(ip, 1), 4);
                    // src
                    builder.local("dest_ptr");
                    // klass
                    builder.ptr_const(klass);
                    // opcode
                    builder.i32_const(opcode);
                    builder.callImport("castv2");

                    // We don't need to do an explicit null check because mono_jiterp_cast_v2 does it

                    // Check whether the cast operation failed
                    builder.appendU8(WasmOpcode.i32_eqz);
                    builder.block(WasmValtype.void, WasmOpcode.if_); // if B
                    // Cast failed so bail out
                    append_bailout(builder, ip, BailoutReason.CastFailed);
                    builder.endBlock(); // endif B
                }

                builder.endBlock(); // endif A

                builder.endBlock(); // depth 0 -> x (end opcode block)

                break;
            }

            case MintOpcode.MINT_BOX:
            case MintOpcode.MINT_BOX_VT: {
                // MonoVTable *vtable = (MonoVTable*)frame->imethod->data_items [ip [3]];
                builder.ptr_const(get_imethod_data(frame, getArgU16(ip, 3)));
                // dest, src
                append_ldloca(builder, getArgU16(ip, 1), 4);
                append_ldloca(builder, getArgU16(ip, 2), 0);
                builder.i32_const(opcode === MintOpcode.MINT_BOX_VT ? 1 : 0);
                builder.callImport("box");
                break;
            }

            case MintOpcode.MINT_UNBOX: {
                const klass = get_imethod_data(frame, getArgU16(ip, 3)),
                    // The type check needs to examine the boxed value's rank and element class
                    elementClassOffset = getMemberOffset(JiterpMember.ClassElementClass),
                    destOffset = getArgU16(ip, 1),
                    // Get the class's element class, which is what we will actually type-check against
                    elementClass = getU32_unaligned(klass + elementClassOffset);

                if (!klass || !elementClass) {
                    record_abort(builder.traceIndex, ip, traceName, "null-klass");
                    ip = abort;
                    continue;
                }

                if (builder.options.zeroPageOptimization && isZeroPageReserved()) {
                    // Null check fusion is possible, so (obj->vtable)->klass will be 0 for !obj
                    append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                    builder.local("dest_ptr", WasmOpcode.tee_local);
                    modifyCounter(JiterpCounter.NullChecksFused, 1);
                } else {
                    append_ldloc_cknull(builder, getArgU16(ip, 2), ip, true);
                    builder.local("dest_ptr", WasmOpcode.tee_local);
                }

                // Fetch the object's klass so we can perform a type check
                builder.appendU8(WasmOpcode.i32_load); // obj->vtable
                builder.appendMemarg(getMemberOffset(JiterpMember.VTable), 0); // fixme: alignment
                builder.appendU8(WasmOpcode.i32_load); // (obj->vtable)->klass
                builder.appendMemarg(getMemberOffset(JiterpMember.VTableKlass), 0); // fixme: alignment

                // Stash obj->vtable->klass, then check klass->element_class == expected
                builder.local("src_ptr", WasmOpcode.tee_local);
                builder.appendU8(WasmOpcode.i32_load);
                builder.appendMemarg(elementClassOffset, 0);
                builder.i32_const(elementClass);
                builder.appendU8(WasmOpcode.i32_eq);

                // Check klass->rank == 0
                builder.local("src_ptr");
                builder.appendU8(WasmOpcode.i32_load8_u); // rank is a uint8
                builder.appendMemarg(getMemberOffset(JiterpMember.ClassRank), 0);
                builder.appendU8(WasmOpcode.i32_eqz);

                // (element_class == expected) && (rank == 0)
                builder.appendU8(WasmOpcode.i32_and);

                builder.block(WasmValtype.void, WasmOpcode.if_); // if type check passed

                // Type-check passed, so now compute the address of the object's data
                //  and store the address
                builder.local("pLocals");
                builder.local("dest_ptr");
                builder.i32_const(getMemberOffset(JiterpMember.BoxedValueData));
                builder.appendU8(WasmOpcode.i32_add);
                append_stloc_tail(builder, destOffset, WasmOpcode.i32_store);

                builder.appendU8(WasmOpcode.else_); // else type check failed

                //
                append_bailout(builder, ip, BailoutReason.UnboxFailed);

                builder.endBlock(); // endif A

                break;
            }

            case MintOpcode.MINT_NEWSTR: {
                builder.block();
                append_ldloca(builder, getArgU16(ip, 1), 4);
                append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load);
                builder.callImport("newstr");
                // If the newstr operation succeeded, continue, otherwise bailout
                // Note that this assumes the newstr operation will fail again when the interpreter does it
                //  (the only reason for a newstr to fail I can think of is an out-of-memory condition)
                builder.appendU8(WasmOpcode.br_if);
                builder.appendULeb(0);
                append_bailout(builder, ip, BailoutReason.AllocFailed);
                builder.endBlock();
                break;
            }

            case MintOpcode.MINT_NEWOBJ_INLINED: {
                builder.block();
                // MonoObject *o = mono_gc_alloc_obj (vtable, m_class_get_instance_size (vtable->klass));
                append_ldloca(builder, getArgU16(ip, 1), 4);
                builder.ptr_const(get_imethod_data(frame, getArgU16(ip, 2)));
                // LOCAL_VAR (ip [1], MonoObject*) = o;
                builder.callImport("newobj_i");
                // If the newobj operation succeeded, continue, otherwise bailout
                builder.appendU8(WasmOpcode.br_if);
                builder.appendULeb(0);
                append_bailout(builder, ip, BailoutReason.AllocFailed);
                builder.endBlock();
                break;
            }

            case MintOpcode.MINT_NEWOBJ:
            case MintOpcode.MINT_NEWOBJ_VT:
            case MintOpcode.MINT_CALLVIRT_FAST:
            case MintOpcode.MINT_CALL: {
                if (countCallTargets) {
                    const targetImethod = get_imethod_data(frame, getArgU16(ip, 3));
                    const targetMethod = <MonoMethod><any>getU32_unaligned(targetImethod);
                    const count = callTargetCounts[<any>targetMethod];
                    if (typeof (count) === "number")
                        callTargetCounts[<any>targetMethod] = count + 1;
                    else
                        callTargetCounts[<any>targetMethod] = 1;
                }
                if (isConditionallyExecuted) {
                    // We generate a bailout instead of aborting, because we don't want calls
                    //  to abort the entire trace if we have branch support enabled - the call
                    //  might be infrequently hit and as a result it's worth it to keep going.
                    append_exit(builder, ip, exitOpcodeCounter, BailoutReason.Call);
                    pruneOpcodes = true;
                    opcodeValue = 0;
                } else {
                    // We're in a block that executes unconditionally, and no branches have been
                    //  executed before now so the trace will always need to bail out into the
                    //  interpreter here. No point in compiling more.
                    ip = abort;
                }
                break;
            }

            // TODO: Verify that this isn't worse. I think these may only show up in wrappers?
            // case MintOpcode.MINT_JIT_CALL:
            case MintOpcode.MINT_CALLI:
            case MintOpcode.MINT_CALLI_NAT:
            case MintOpcode.MINT_CALLI_NAT_DYNAMIC:
            case MintOpcode.MINT_CALLI_NAT_FAST:
            case MintOpcode.MINT_CALL_DELEGATE:
                // See comments for MINT_CALL
                if (isConditionallyExecuted) {
                    append_exit(builder, ip, exitOpcodeCounter,
                        opcode == MintOpcode.MINT_CALL_DELEGATE
                            ? BailoutReason.CallDelegate
                            : BailoutReason.Call
                    );
                    pruneOpcodes = true;
                } else {
                    ip = abort;
                }
                break;

            // Unlike regular rethrow which will only appear in catch blocks,
            //  MONO_RETHROW appears to show up in other places, so it's worth conditional bailout
            case MintOpcode.MINT_MONO_RETHROW:
            case MintOpcode.MINT_THROW:
                // Not an exit, because throws are by definition unlikely
                // We shouldn't make optimization decisions based on them.
                append_bailout(builder, ip, BailoutReason.Throw);
                pruneOpcodes = true;
                break;

            // These are generated in place of regular LEAVEs inside of the body of a catch clause.
            // We can safely assume that during normal execution, catch clauses won't be running.
            case MintOpcode.MINT_LEAVE_CHECK:
            case MintOpcode.MINT_LEAVE_S_CHECK:
                append_bailout(builder, ip, BailoutReason.LeaveCheck);
                pruneOpcodes = true;
                break;

            case MintOpcode.MINT_ENDFINALLY: {
                if (
                    (builder.callHandlerReturnAddresses.length > 0) &&
                    (builder.callHandlerReturnAddresses.length <= maxCallHandlerReturnAddresses)
                ) {
                    // mono_log_info(`endfinally @0x${(<any>ip).toString(16)}. return addresses:`, builder.callHandlerReturnAddresses.map(ra => (<any>ra).toString(16)));
                    // FIXME: Clean this codegen up
                    // Load ret_ip
                    const clauseIndex = getArgU16(ip, 1),
                        clauseDataOffset = get_imethod_clause_data_offset(frame, clauseIndex);
                    builder.local("pLocals");
                    builder.appendU8(WasmOpcode.i32_load);
                    builder.appendMemarg(clauseDataOffset, 0);
                    // Stash it in a variable because we're going to need to use it multiple times
                    builder.local("index", WasmOpcode.set_local);
                    // Do a bunch of trivial comparisons to see if ret_ip is one of our expected return addresses,
                    //  and if it is, generate a branch back to the dispatcher at the top
                    for (let r = 0; r < builder.callHandlerReturnAddresses.length; r++) {
                        const ra = builder.callHandlerReturnAddresses[r];
                        builder.local("index");
                        builder.ptr_const(ra);
                        builder.appendU8(WasmOpcode.i32_eq);
                        builder.cfg.branch(ra, ra < ip, CfgBranchType.Conditional);
                    }
                    // If none of the comparisons succeeded we won't have branched anywhere, so bail out
                    // This shouldn't happen during non-exception-handling execution unless the trace doesn't
                    //  contain the CALL_HANDLER that led here
                    append_bailout(builder, ip, BailoutReason.UnexpectedRetIp);
                    // FIXME: prune opcodes?
                } else {
                    ip = abort;
                }
                break;
            }

            case MintOpcode.MINT_RETHROW:
            case MintOpcode.MINT_PROF_EXIT:
            case MintOpcode.MINT_PROF_EXIT_VOID:
                ip = abort;
                break;

            // Generating code for these is kind of complex due to the intersection of JS and int64,
            //  and it would bloat the implementation so we handle them all in C instead and match
            //  the interp implementation. Most of these are rare in runtime tests or browser bench
            case MintOpcode.MINT_CONV_OVF_I4_I8:
            case MintOpcode.MINT_CONV_OVF_U4_I8:
            case MintOpcode.MINT_CONV_OVF_I4_U8:
            case MintOpcode.MINT_CONV_OVF_I4_R8:
            case MintOpcode.MINT_CONV_OVF_I8_R8:
            case MintOpcode.MINT_CONV_OVF_I4_R4:
            case MintOpcode.MINT_CONV_OVF_I8_R4:
            case MintOpcode.MINT_CONV_OVF_U4_I4:
                builder.block();
                // dest, src
                append_ldloca(builder, getArgU16(ip, 1), 8);
                append_ldloca(builder, getArgU16(ip, 2), 0);
                builder.i32_const(opcode);
                builder.callImport("conv");
                // If the conversion succeeded, continue, otherwise bailout
                builder.appendU8(WasmOpcode.br_if);
                builder.appendULeb(0);
                append_bailout(builder, ip, BailoutReason.Overflow); // could be underflow but awkward to tell
                builder.endBlock();
                break;

            /*
             *  The native conversion opcodes for these are not specified for nan/inf, and v8
             *  chooses to throw, so we have to do some tricks to identify non-finite values
             *  and substitute INTnn_MIN, like clang would.
             *  This attempts to reproduce what clang does in -O3 with no special flags set:
             *
             *  f64 -> i64
             *
             *  block
             *  local.get       0
             *  f64.abs
             *  f64.const       0x1p63
             *  f64.lt
             *  i32.eqz
             *  br_if           0                               # 0: down to label0
             *  local.get       0
             *  i64.trunc_f64_s
             *  return
             *  end_block                               # label0:
             *  i64.const       -9223372036854775808
             *
             *  f32 -> i32
             *
             *  block
             *  local.get       0
             *  f32.abs
             *  f32.const       0x1p31
             *  f32.lt
             *  i32.eqz
             *  br_if           0                               # 0: down to label3
             *  local.get       0
             *  i32.trunc_f32_s
             *  return
             *  end_block                               # label3:
             *  i32.const       -2147483648
             */
            case MintOpcode.MINT_CONV_I4_R4:
            case MintOpcode.MINT_CONV_I4_R8:
            case MintOpcode.MINT_CONV_I8_R4:
            case MintOpcode.MINT_CONV_I8_R8: {
                const isF32 = (opcode === MintOpcode.MINT_CONV_I4_R4) ||
                    (opcode === MintOpcode.MINT_CONV_I8_R4),
                    isI64 = (opcode === MintOpcode.MINT_CONV_I8_R4) ||
                        (opcode === MintOpcode.MINT_CONV_I8_R8),
                    limit = isI64
                        ? 9223372036854775807 // this will round up to 0x1p63
                        : 2147483648, // this is 0x1p31 exactly
                    tempLocal = isF32 ? "temp_f32" : "temp_f64";

                // Pre-load locals for the result store at the end
                builder.local("pLocals");

                // Load src
                append_ldloc(builder, getArgU16(ip, 2), isF32 ? WasmOpcode.f32_load : WasmOpcode.f64_load);
                builder.local(tempLocal, WasmOpcode.tee_local);

                // Detect whether the value is within the representable range for the target type
                builder.appendU8(isF32 ? WasmOpcode.f32_abs : WasmOpcode.f64_abs);
                builder.appendU8(isF32 ? WasmOpcode.f32_const : WasmOpcode.f64_const);
                if (isF32)
                    builder.appendF32(limit);
                else
                    builder.appendF64(limit);
                builder.appendU8(isF32 ? WasmOpcode.f32_lt : WasmOpcode.f64_lt);

                // Select value via an if block that returns the result
                builder.block(isI64 ? WasmValtype.i64 : WasmValtype.i32, WasmOpcode.if_);
                // Value in range so truncate it to the appropriate type
                builder.local(tempLocal);
                builder.appendU8(floatToIntTable[opcode]);
                builder.appendU8(WasmOpcode.else_);
                // Value out of range so load the appropriate boundary value
                builder.appendU8(isI64 ? WasmOpcode.i64_const : WasmOpcode.i32_const);
                builder.appendBoundaryValue(isI64 ? 64 : 32, -1);
                builder.endBlock();

                append_stloc_tail(builder, getArgU16(ip, 1), isI64 ? WasmOpcode.i64_store : WasmOpcode.i32_store);

                break;
            }

            case MintOpcode.MINT_ADD_MUL_I4_IMM:
            case MintOpcode.MINT_ADD_MUL_I8_IMM: {
                const isI32 = opcode === MintOpcode.MINT_ADD_MUL_I4_IMM;
                builder.local("pLocals");
                append_ldloc(builder, getArgU16(ip, 2), isI32 ? WasmOpcode.i32_load : WasmOpcode.i64_load);
                const rhs = getArgI16(ip, 3),
                    multiplier = getArgI16(ip, 4);
                if (isI32)
                    builder.i32_const(rhs);
                else
                    builder.i52_const(rhs);
                builder.appendU8(isI32 ? WasmOpcode.i32_add : WasmOpcode.i64_add);
                if (isI32)
                    builder.i32_const(multiplier);
                else
                    builder.i52_const(multiplier);
                builder.appendU8(isI32 ? WasmOpcode.i32_mul : WasmOpcode.i64_mul);
                append_stloc_tail(builder, getArgU16(ip, 1), isI32 ? WasmOpcode.i32_store : WasmOpcode.i64_store);
                break;
            }

            case MintOpcode.MINT_MONO_CMPXCHG_I4:
                builder.local("pLocals");
                append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load); // dest
                append_ldloc(builder, getArgU16(ip, 3), WasmOpcode.i32_load); // newVal
                append_ldloc(builder, getArgU16(ip, 4), WasmOpcode.i32_load); // expected
                builder.callImport("cmpxchg_i32");
                append_stloc_tail(builder, getArgU16(ip, 1), WasmOpcode.i32_store);
                break;
            case MintOpcode.MINT_MONO_CMPXCHG_I8:
                // because i64 values can't pass through JS cleanly (c.f getRawCwrap and
                // EMSCRIPTEN_KEEPALIVE), we pass addresses of newVal, expected and the return value
                // to the helper function.  The "dest" for the compare-exchange is already a
                // pointer, so load it normally
                append_ldloc(builder, getArgU16(ip, 2), WasmOpcode.i32_load); // dest
                append_ldloca(builder, getArgU16(ip, 3), 0); // newVal
                append_ldloca(builder, getArgU16(ip, 4), 0); // expected
                append_ldloca(builder, getArgU16(ip, 1), 8); // oldVal
                builder.callImport("cmpxchg_i64");
                break;

            case MintOpcode.MINT_LOG2_I4:
            case MintOpcode.MINT_LOG2_I8: {
                const isI64 = (opcode === MintOpcode.MINT_LOG2_I8);

                builder.local("pLocals");

                append_ldloc(builder, getArgU16(ip, 2), isI64 ? WasmOpcode.i64_load : WasmOpcode.i32_load);
                if (isI64)
                    builder.i52_const(1);
                else
                    builder.i32_const(1);
                builder.appendU8(isI64 ? WasmOpcode.i64_or : WasmOpcode.i32_or);
                builder.appendU8(isI64 ? WasmOpcode.i64_clz : WasmOpcode.i32_clz);
                if (isI64)
                    builder.appendU8(WasmOpcode.i32_wrap_i64);
                builder.i32_const(isI64 ? 63 : 31);
                builder.appendU8(WasmOpcode.i32_xor);

                append_stloc_tail(builder, getArgU16(ip, 1), WasmOpcode.i32_store);
                break;
            }

            case MintOpcode.MINT_SHL_AND_I4:
            case MintOpcode.MINT_SHL_AND_I8: {
                const isI32 = (opcode === MintOpcode.MINT_SHL_AND_I4),
                    loadOp = isI32 ? WasmOpcode.i32_load : WasmOpcode.i64_load,
                    storeOp = isI32 ? WasmOpcode.i32_store : WasmOpcode.i64_store;

                builder.local("pLocals");

                append_ldloc(builder, getArgU16(ip, 2), loadOp);
                append_ldloc(builder, getArgU16(ip, 3), loadOp);
                if (isI32)
                    builder.i32_const(31);
                else
                    builder.i52_const(63);
                builder.appendU8(isI32 ? WasmOpcode.i32_and : WasmOpcode.i64_and);
                builder.appendU8(isI32 ? WasmOpcode.i32_shl : WasmOpcode.i64_shl);

                append_stloc_tail(builder, getArgU16(ip, 1), storeOp);
                break;
            }

            case MintOpcode.MINT_FMA:
            case MintOpcode.MINT_FMAF: {
                const isF32 = (opcode === MintOpcode.MINT_FMAF),
                    loadOp = isF32 ? WasmOpcode.f32_load : WasmOpcode.f64_load,
                    storeOp = isF32 ? WasmOpcode.f32_store : WasmOpcode.f64_store;

                builder.local("pLocals");

                // LOCAL_VAR (ip [1], double) = fma (LOCAL_VAR (ip [2], double), LOCAL_VAR (ip [3], double), LOCAL_VAR (ip [4], double));
                append_ldloc(builder, getArgU16(ip, 2), loadOp);
                append_ldloc(builder, getArgU16(ip, 3), loadOp);
                append_ldloc(builder, getArgU16(ip, 4), loadOp);

                builder.callImport(isF32 ? "fmaf" : "fma");

                append_stloc_tail(builder, getArgU16(ip, 1), storeOp);
                break;
            }

            default:
                if (
                    (
                        (opcode >= MintOpcode.MINT_RET) &&
                        (opcode <= MintOpcode.MINT_RET_U2)
                    ) ||
                    (
                        (opcode >= MintOpcode.MINT_RET_I4_IMM) &&
                        (opcode <= MintOpcode.MINT_RET_I8_IMM)
                    )
                ) {
                    if (isConditionallyExecuted || builder.options.countBailouts) {
                        // Not an exit, because returns are normal and we don't want to make them more expensive.
                        // FIXME: Or do we want to record them? Early conditional returns might reduce the value of a trace,
                        //  but the main problem is more likely to be calls early in traces. Worth testing later.
                        append_bailout(builder, ip, BailoutReason.Return);
                        pruneOpcodes = true;
                    } else
                        ip = abort;
                } else if (
                    (opcode >= MintOpcode.MINT_LDC_I4_0) &&
                    (opcode <= MintOpcode.MINT_LDC_R8)
                ) {
                    if (!emit_ldc(builder, ip, opcode))
                        ip = abort;
                    else
                        skipDregInvalidation = true;
                } else if (
                    (opcode >= MintOpcode.MINT_MOV_I4_I1) &&
                    (opcode <= MintOpcode.MINT_MOV_8_4)
                ) {
                    if (!emit_mov(builder, ip, opcode))
                        ip = abort;
                } else if (
                    // binops
                    (opcode >= MintOpcode.MINT_ADD_I4) &&
                    (opcode <= MintOpcode.MINT_CLT_UN_R8)
                ) {
                    if (!emit_binop(builder, ip, opcode))
                        ip = abort;
                } else if (unopTable[opcode]) {
                    if (!emit_unop(builder, ip, opcode))
                        ip = abort;
                } else if (relopbranchTable[opcode]) {
                    if (!emit_relop_branch(builder, ip, frame, opcode))
                        ip = abort;
                    else
                        isConditionallyExecuted = true;
                } else if (
                    // instance ldfld/stfld
                    (opcode >= MintOpcode.MINT_LDFLD_I1) &&
                    (opcode <= MintOpcode.MINT_STFLD_R8_UNALIGNED)
                ) {
                    if (!emit_fieldop(builder, frame, ip, opcode))
                        ip = abort;
                } else if (
                    // static ldfld/stfld
                    (opcode >= MintOpcode.MINT_LDSFLD_I1) &&
                    (opcode <= MintOpcode.MINT_LDTSFLDA)
                ) {
                    if (!emit_sfieldop(builder, frame, ip, opcode))
                        ip = abort;
                } else if (
                    // indirect load/store
                    (opcode >= MintOpcode.MINT_LDIND_I1) &&
                    (opcode <= MintOpcode.MINT_STIND_OFFSET_IMM_I8)
                ) {
                    if (!emit_indirectop(builder, ip, opcode))
                        ip = abort;
                } else if (
                    // math intrinsics
                    (opcode >= MintOpcode.MINT_ASIN) &&
                    (opcode <= MintOpcode.MINT_MAXF)
                ) {
                    if (!emit_math_intrinsic(builder, ip, opcode))
                        ip = abort;
                } else if (
                    (opcode >= MintOpcode.MINT_LDELEM_I1) &&
                    (opcode <= MintOpcode.MINT_LDLEN)
                ) {
                    if (!emit_arrayop(builder, frame, ip, opcode))
                        ip = abort;
                } else if (
                    (opcode >= MintOpcode.MINT_BRFALSE_I4_SP) &&
                    (opcode <= MintOpcode.MINT_BLT_UN_I8_IMM_SP)
                ) {
                    // NOTE: This elseif comes last so that specific safepoint branch
                    //  types can be handled by emit_branch or emit_relop_branch,
                    //  to only perform a conditional bailout
                    // complex safepoint branches, just generate a bailout
                    if (builder.branchTargets.size > 0) {
                        // FIXME: Try to reduce the number of these
                        append_exit(builder, ip, exitOpcodeCounter, BailoutReason.ComplexBranch);
                        pruneOpcodes = true;
                    } else
                        ip = abort;
                } else if (
                    (opcode >= MintOpcode.MINT_SIMD_V128_LDC) &&
                    (opcode <= MintOpcode.MINT_SIMD_INTRINS_P_PPP)
                ) {
                    if (!emit_simd(builder, ip, opcode, opname, simdIntrinsArgCount, simdIntrinsIndex))
                        ip = abort;
                    else {
                        containsSimd = true;
                        // We need to do dreg invalidation differently for simd, especially to handle ldc
                        skipDregInvalidation = true;
                    }
                } else if (opcodeValue === 0) {
                    // This means it was explicitly marked as no-value in the opcode value table
                    //  so we can just skip over it. This is done for things like nops.
                } else {
                    /*
                    if (opcodeValue > 0)
                        mono_log_info(`JITERP: aborting trace for opcode ${opname} with value ${opcodeValue}`);
                    */
                    ip = abort;
                }
                break;
        }

        if (ip) {
            if (!skipDregInvalidation) {
                // Invalidate cached values for all the instruction's destination registers.
                // This should have already happened, but it's possible there are opcodes where
                //  our invalidation is incorrect so it's best to do this for safety reasons
                const firstDreg = <any>ip + 2;
                for (let r = 0; r < numDregs; r++) {
                    const dreg = getU16(firstDreg + (r * 2));
                    invalidate_local(dreg);
                }
            }

            if ((trace > 1) || traceOnError || mostRecentOptions!.dumpTraces || instrumentedTraceId) {
                let stmtText = `${(<any>ip).toString(16)} ${opname} `;
                const firstDreg = <any>ip + 2;
                const firstSreg = firstDreg + (numDregs * 2);
                // print sregs
                for (let r = 0; r < numSregs; r++) {
                    if (r !== 0)
                        stmtText += ", ";
                    stmtText += getU16(firstSreg + (r * 2));
                }

                // print dregs
                if (numDregs > 0)
                    stmtText += " -> ";
                for (let r = 0; r < numDregs; r++) {
                    if (r !== 0)
                        stmtText += ", ";
                    stmtText += getU16(firstDreg + (r * 2));
                }

                builder.traceBuf.push(stmtText);
            }

            if (opcodeValue > 0) {
                if (isConditionallyExecuted)
                    conditionalOpcodeCounter++;
                else
                    prologueOpcodeCounter++;
                result += opcodeValue;
            } else if (opcodeValue < 0) {
                // mono_log_info(`JITERP: opcode ${opname} did not abort but had value ${opcodeValue}`);
            }

            ip += <any>(opLengthU16 * 2);
            if (<any>ip <= (<any>endOfBody))
                rip = ip;
            // For debugging
            if (emitPadding)
                builder.appendU8(WasmOpcode.nop);
        } else {
            if (instrumentedTraceId)
                mono_log_info(`instrumented trace ${traceName} aborted for opcode ${opname} @${(<any>_ip).toString(16)}`);
            record_abort(builder.traceIndex, _ip, traceName, opcode);
        }
    }

    if (emitPadding)
        builder.appendU8(WasmOpcode.nop);

    // We need to close any open blocks before generating our closing ret,
    //  because wasm would allow branching past the ret otherwise
    while (builder.activeBlocks > 0)
        builder.endBlock();

    builder.cfg.exitIp = rip;

    // mono_log_info(`estimated size: ${builder.size + builder.cfg.overheadBytes + builder.bytesGeneratedSoFar}`);

    // HACK: Traces containing simd will be *much* shorter than non-simd traces,
    //  which will cause both the heuristic and our length requirement outside
    //  to reject them. For now, just add a big constant to the length
    if (containsSimd)
        result += 10240;
    return result;
}