summaryrefslogtreecommitdiff
path: root/crates/parser/src/optimize.rs
blob: 80dfa0d23fbc2a24789a98a5d9ebd400609c083b (plain)
use crate::macros::optimize::*;
use crate::{ParseError, ParserOptions, Result};
use alloc::vec::Vec;
use core::ops::{Deref, DerefMut};
use tinywasm_types::{BinOp, BinOp128, CmpOp, ConstIdx, Instruction, ValueCounts, WasmFunctionData};

pub(crate) struct OptimizeResult {
    pub(crate) instructions: Vec<Instruction>,
    pub(crate) uses_local_memory: bool,
}

struct CompactOutput {
    instructions: Vec<Instruction>,
    block_start: usize,
    tail_rewritten: bool,
}

impl Deref for CompactOutput {
    type Target = Vec<Instruction>;

    fn deref(&self) -> &Self::Target {
        &self.instructions
    }
}

impl DerefMut for CompactOutput {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.instructions
    }
}

pub(crate) fn optimize_instructions(
    instructions: Vec<Instruction>,
    function_data: &mut WasmFunctionData,
    options: &ParserOptions,
    function_results: ValueCounts,
    self_func_addr: u32,
    imported_memory_count: u32,
) -> Result<OptimizeResult> {
    let (mut instructions, old_to_new) = if options.optimize_rewrite() {
        let boundaries = target_boundaries(&instructions, function_data)?;
        let (instructions, old_to_new) = rewrite(instructions, &boundaries, function_results, self_func_addr);
        (instructions, Some(old_to_new))
    } else {
        (instructions, None)
    };
    let uses_local_memory = finalize(&mut instructions, function_data, old_to_new.as_deref(), imported_memory_count)?;
    Ok(OptimizeResult { instructions, uses_local_memory })
}

fn rewrite(
    source: Vec<Instruction>,
    boundaries: &[bool],
    function_results: ValueCounts,
    self_func_addr: u32,
) -> (Vec<Instruction>, Vec<u32>) {
    use Instruction::*;
    let mut instrs =
        CompactOutput { instructions: Vec::with_capacity(source.len()), block_start: 0, tail_rewritten: false };
    let mut old_to_new = alloc::vec![0; source.len() + 1];
    let mut after_terminator = false;
    let return_instr = match function_results {
        ValueCounts { c32: 0, c64: 0, c128: 0 } => Some(ReturnVoid),
        ValueCounts { c32: 1, c64: 0, c128: 0 } => Some(Return32),
        ValueCounts { c32: 0, c64: 1, c128: 0 } => Some(Return64),
        ValueCounts { c32: 0, c64: 0, c128: 1 } => Some(Return128),
        _ => None,
    };

    for (old_idx, instr) in source.iter().copied().enumerate() {
        if boundaries[old_idx] || after_terminator {
            instrs.block_start = instrs.len();
        }
        old_to_new[old_idx] = instrs.len() as u32;
        instrs.tail_rewritten = false;
        instrs.push(instr);
        let mut i = instrs.len() - 1;
        match instrs[i] {
            LocalCopy32(a, b) if a == b => {
                instrs.pop();
            }
            LocalCopy64(a, b) if a == b => {
                instrs.pop();
            }
            LocalCopy128(a, b) if a == b => {
                instrs.pop();
            }
            Call(addr) if addr == self_func_addr => instrs[i] = CallSelf,
            ReturnCall(addr) if addr == self_func_addr => instrs[i] = ReturnCallSelf,
            Return if let Some(return_instr) = return_instr => instrs[i] = return_instr,
            instr @ (I32Add | I32Mul | I32And | I32Or | I32Xor) => {
                let Some(op) = int_bin_op(instr) else { unreachable!() };
                rewrite!(instrs, i, [LocalGet32(a), LocalGet32(b)] => BinOpLocalLocal32(op, a, b));
                rewrite!(instrs, i, [LocalGet32(local), Const32(c)] => BinOpLocalConst32(op, local, c));
                rewrite!(instrs, i, [Const32(c), LocalGet32(local)] => BinOpLocalConst32(op, local, c));
                rewrite!(instrs, i, [GlobalGet(global)] => BinOpStackGlobal32(op, global));
                if matches!(op, BinOp::IAdd) {
                    rewrite!(instrs, i, [Const32(c)] => AddConst32(c));
                    rewrite!(instrs, i, [I32Add] => I32Add3);
                }
            }
            instr @ (I32Sub | I32Shl | I32ShrS | I32ShrU | I32Rotl | I32Rotr) => {
                let Some(op) = int_bin_op(instr) else { unreachable!() };
                rewrite!(instrs, i, [LocalGet32(a), LocalGet32(b)] => BinOpLocalLocal32(op, a, b));
                rewrite!(instrs, i, [LocalGet32(local), Const32(c)] => BinOpLocalConst32(op, local, c));
                rewrite!(instrs, i, [GlobalGet(global)] => BinOpStackGlobal32(op, global));
                if matches!(op, BinOp::IShrS) {
                    rewrite!(instrs, i, [BinOpLocalConst32(BinOp::IShl, local, 8), Const32(8)] => [LocalGet32(local), I32Extend8S]);
                    rewrite!(instrs, i, [BinOpLocalConst32(BinOp::IShl, local, 16), Const32(16)] => [LocalGet32(local), I32Extend16S]);
                }
            }
            instr @ (I64Add | I64Mul | I64And | I64Or | I64Xor) => {
                let Some(op) = int_bin_op(instr) else { unreachable!() };
                rewrite!(instrs, i, [LocalGet64(a), LocalGet64(b)] => BinOpLocalLocal64(op, a, b));
                rewrite!(instrs, i, [LocalGet64(local), Const64(c)] => BinOpLocalConst64(op, local, c));
                rewrite!(instrs, i, [Const64(c), LocalGet64(local)] => BinOpLocalConst64(op, local, c));
                rewrite!(instrs, i, [GlobalGet(global)] => BinOpStackGlobal64(op, global));
                if matches!(op, BinOp::IAdd) {
                    rewrite!(instrs, i, [Const64(c)] => AddConst64(c));
                    rewrite!(instrs, i, [I64Add] => I64Add3);
                }
            }
            instr @ (I64Sub | I64Shl | I64ShrS | I64ShrU | I64Rotl | I64Rotr) => {
                let Some(op) = int_bin_op(instr) else { unreachable!() };
                rewrite!(instrs, i, [LocalGet64(a), LocalGet64(b)] => BinOpLocalLocal64(op, a, b));
                rewrite!(instrs, i, [LocalGet64(local), Const64(c)] => BinOpLocalConst64(op, local, c));
                rewrite!(instrs, i, [GlobalGet(global)] => BinOpStackGlobal64(op, global));
                if matches!(op, BinOp::IShrS) {
                    rewrite!(instrs, i, [BinOpLocalConst64(BinOp::IShl, local, 8), Const64(8)] => [LocalGet64(local), I64Extend8S]);
                    rewrite!(instrs, i, [BinOpLocalConst64(BinOp::IShl, local, 16), Const64(16)] => [LocalGet64(local), I64Extend16S]);
                    rewrite!(instrs, i, [BinOpLocalConst64(BinOp::IShl, local, 32), Const64(32)] => [LocalGet64(local), I64Extend32S]);
                }
            }
            instr @ (F32Add | F32Mul | F32Min | F32Max) => {
                let Some(op) = float_bin_op(instr) else { unreachable!() };
                rewrite!(instrs, i, [LocalGet32(a), LocalGet32(b)] => BinOpLocalLocal32(op, a, b));
                rewrite!(instrs, i, [LocalGet32(local), Const32(c)] => BinOpLocalConst32(op, local, c));
                rewrite!(instrs, i, [Const32(c), LocalGet32(local)] => BinOpLocalConst32(op, local, c));
            }
            instr @ (F32Sub | F32Div | F32Copysign) => {
                let Some(op) = float_bin_op(instr) else { unreachable!() };
                rewrite!(instrs, i, [LocalGet32(a), LocalGet32(b)] => BinOpLocalLocal32(op, a, b));
                rewrite!(instrs, i, [LocalGet32(local), Const32(c)] => BinOpLocalConst32(op, local, c));
            }
            instr @ (F64Add | F64Mul | F64Min | F64Max) => {
                let Some(op) = float_bin_op(instr) else { unreachable!() };
                rewrite!(instrs, i, [LocalGet64(a), LocalGet64(b)] => BinOpLocalLocal64(op, a, b));
                rewrite!(instrs, i, [LocalGet64(local), Const64(c)] => BinOpLocalConst64(op, local, c));
                rewrite!(instrs, i, [Const64(c), LocalGet64(local)] => BinOpLocalConst64(op, local, c));
            }
            instr @ (F64Sub | F64Div | F64Copysign) => {
                let Some(op) = float_bin_op(instr) else { unreachable!() };
                rewrite!(instrs, i, [LocalGet64(a), LocalGet64(b)] => BinOpLocalLocal64(op, a, b));
                rewrite!(instrs, i, [LocalGet64(local), Const64(c)] => BinOpLocalConst64(op, local, c));
            }
            instr @ (V128And | V128Or | V128Xor | I64x2Add | I64x2Mul) => {
                let Some(op) = bin_op_128(instr) else { unreachable!() };
                rewrite!(instrs, i, [LocalGet128(a), LocalGet128(b)] => BinOpLocalLocal128(op, a, b));
                rewrite!(instrs, i, [LocalGet128(local), Const128(c)] => BinOpLocalConst128(op, local, c));
                rewrite!(instrs, i, [Const128(c), LocalGet128(local)] => BinOpLocalConst128(op, local, c));
            }
            V128AndNot => {
                rewrite!(instrs, i, [LocalGet128(a), LocalGet128(b)] => BinOpLocalLocal128(BinOp128::AndNot, a, b));
                rewrite!(instrs, i, [LocalGet128(local), Const128(c)] => BinOpLocalConst128(BinOp128::AndNot, local, c));
            }
            I32Store(memarg) | F32Store(memarg) => {
                rewrite!(instrs, i,
                    [LocalGet32(addr), LoadLocal32(load_memarg, load_addr), AddConst32(1)] if
                    (load_memarg == memarg && addr == u16::from(load_addr)) =>
                    IncMemoryLocal32(memarg, load_addr)
                );
                rewrite!(instrs, i,
                    [LocalGet64(addr), LoadLocal32(load_memarg, load_addr), AddConst32(1)] if
                    (load_memarg == memarg && addr == u16::from(load_addr)) =>
                    IncMemoryLocal32(memarg, load_addr)
                );
                rewrite!(instrs, i, [F32Mul, F32Add] => FMaStoreF32(memarg));
                rewrite!(instrs, i,
                    [LocalGet32(addr_local), LocalGet32(value_local)] if
                    (let (Ok(addr_local), Ok(value_local)) = (u8::try_from(addr_local), u8::try_from(value_local))) =>
                    StoreLocalLocal32(memarg, addr_local, value_local)
                );
            }
            I64Store(memarg) | F64Store(memarg) => {
                rewrite!(instrs, i,
                    [LocalGet32(addr), LoadLocal64(load_memarg, load_addr), AddConst64(1)] if
                    (load_memarg == memarg && addr == u16::from(load_addr)) =>
                    IncMemoryLocal64(memarg, load_addr)
                );
                rewrite!(instrs, i,
                    [LocalGet64(addr), LoadLocal64(load_memarg, load_addr), AddConst64(1)] if
                    (load_memarg == memarg && addr == u16::from(load_addr)) =>
                    IncMemoryLocal64(memarg, load_addr)
                );
                rewrite!(instrs, i, [F64Mul, F64Add] => FMaStoreF64(memarg));
                rewrite!(instrs, i,
                    [LocalGet32(addr_local), LocalGet64(value_local)] if
                    (let (Ok(addr_local), Ok(value_local)) = (u8::try_from(addr_local), u8::try_from(value_local))) =>
                    StoreLocalLocal64(memarg, addr_local, value_local)
                );
            }
            V128Store(memarg) => {
                rewrite!(instrs, i,
                    [LocalGet32(addr_local), LocalGet128(value_local)] if
                    (let (Ok(addr_local), Ok(value_local)) = (u8::try_from(addr_local), u8::try_from(value_local))) =>
                    StoreLocalLocal128(memarg, addr_local, value_local)
                );
            }
            I32Load(memarg) | F32Load(memarg) => {
                rewrite!(instrs, i,
                    [LocalGet32(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal32(memarg, addr_local)
                );
                rewrite!(instrs, i,
                    [LocalGet64(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal32(memarg, addr_local)
                );
            }
            I64Load(memarg) | F64Load(memarg) => {
                rewrite!(instrs, i,
                    [LocalGet32(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal64(memarg, addr_local)
                );
                rewrite!(instrs, i,
                    [LocalGet64(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal64(memarg, addr_local)
                );
            }
            I32Load8S(memarg) => {
                rewrite!(instrs, i,
                    [LocalGet32(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal8S32(memarg, addr_local)
                );
                rewrite!(instrs, i,
                    [LocalGet64(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal8S32(memarg, addr_local)
                );
            }
            I32Load8U(memarg) => {
                rewrite!(instrs, i,
                    [LocalGet32(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal8U32(memarg, addr_local)
                );
                rewrite!(instrs, i,
                    [LocalGet64(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal8U32(memarg, addr_local)
                );
            }
            I32Load16S(memarg) => {
                rewrite!(instrs, i,
                    [LocalGet32(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal16S32(memarg, addr_local)
                );
                rewrite!(instrs, i,
                    [LocalGet64(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal16S32(memarg, addr_local)
                );
            }
            I32Load16U(memarg) => {
                rewrite!(instrs, i,
                    [LocalGet32(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal16U32(memarg, addr_local)
                );
                rewrite!(instrs, i,
                    [LocalGet64(addr_local)] if (let Ok(addr_local) = u8::try_from(addr_local)) =>
                    LoadLocal16U32(memarg, addr_local)
                );
            }
            MemoryFill(mem) => {
                rewrite!(instrs, i, [Const32(val), Const32(size)] => MemoryFillImm(mem, val as u8, size))
            }
            LocalGet32(dst) => rewrite!(instrs, i, [LocalSet32(src)] if (src == dst) => LocalTee32(src)),
            LocalGet64(dst) => rewrite!(instrs, i, [LocalSet64(src)] if (src == dst) => LocalTee64(src)),
            LocalGet128(dst) => rewrite!(instrs, i, [LocalSet128(src)] if (src == dst) => LocalTee128(src)),
            LocalSet32(dst) => {
                fold_local_binop!(
                    instrs, i, dst,
                    source = resolve_local_source_32,
                    op = scalar_bin_op,
                    const = scalar_const_32,
                    local_local = BinOpLocalLocalSet32,
                    local_const = |dst, lhs, op, imm| match (dst == lhs, op) {
                        (true, BinOp::IAdd) => Instruction::IncLocal32(dst, imm),
                        (true, BinOp::ISub) => Instruction::IncLocal32(dst, imm.wrapping_neg()),
                        _ => Instruction::BinOpLocalConstSet32(op, lhs, imm, dst),
                    }
                );
                rewrite!(instrs, i, [I32Mul, LocalGet32(acc), I32Add] if (acc == dst) => MulAccLocal32(dst));
                rewrite!(instrs, i, [F32Mul, LocalGet32(acc), F32Add] if (acc == dst) => FMulAccLocal32(dst));
                rewrite_local_set_direct!(
                    instrs,
                    i,
                    dst,
                    get = LocalGet32,
                    copy = LocalCopy32,
                    binop_local_local = BinOpLocalLocal32,
                    binop_local_local_set = BinOpLocalLocalSet32,
                    binop_local_const = BinOpLocalConst32,
                    binop_local_const_set = |dst, src, op, c| match (dst == src, op) {
                        (true, BinOp::IAdd) => IncLocal32(dst, c),
                        (true, BinOp::ISub) => IncLocal32(dst, c.wrapping_neg()),
                        _ => BinOpLocalConstSet32(op, src, c, dst),
                    },
                    const_instr = Const32,
                    set_local_const = SetLocalConst32
                );
                rewrite!(instrs, i, [LoadLocal32(memarg, addr)] if (let Ok(dst) = u8::try_from(dst)) => LoadLocalSet32(memarg, addr, dst));
                rewrite!(instrs, i, [LoadLocal8S32(memarg, addr)] if (let Ok(dst) = u8::try_from(dst)) => LoadLocalSet8S32(memarg, addr, dst));
                rewrite!(instrs, i, [LoadLocal8U32(memarg, addr)] if (let Ok(dst) = u8::try_from(dst)) => LoadLocalSet8U32(memarg, addr, dst));
                rewrite!(instrs, i, [LoadLocal16S32(memarg, addr)] if (let Ok(dst) = u8::try_from(dst)) => LoadLocalSet16S32(memarg, addr, dst));
                rewrite!(instrs, i, [LoadLocal16U32(memarg, addr)] if (let Ok(dst) = u8::try_from(dst)) => LoadLocalSet16U32(memarg, addr, dst));
                rewrite!(instrs, i,
                    [LocalGet32(addr), I32Load(memarg)] if
                    (let (Ok(addr), Ok(dst)) = (u8::try_from(addr), u8::try_from(dst))) =>
                    LoadLocalSet32(memarg, addr, dst)
                );
            }
            LocalSet64(dst) => {
                fold_local_binop!(
                    instrs, i, dst,
                    source = resolve_local_source_64,
                    op = scalar_bin_op,
                    const = scalar_const_64,
                    local_local = BinOpLocalLocalSet64,
                    local_const = |dst, lhs, op, imm| match (dst == lhs, op) {
                        (true, BinOp::IAdd) => Instruction::IncLocal64(dst, imm),
                        (true, BinOp::ISub) => Instruction::IncLocal64(dst, imm.wrapping_neg()),
                        _ => Instruction::BinOpLocalConstSet64(op, lhs, imm, dst),
                    }
                );
                rewrite!(instrs, i, [I64Mul, LocalGet64(acc), I64Add] if (acc == dst) => MulAccLocal64(dst));
                rewrite!(instrs, i, [F64Mul, LocalGet64(acc), F64Add] if (acc == dst) => FMulAccLocal64(dst));
                rewrite_local_set_direct!(
                    instrs,
                    i,
                    dst,
                    get = LocalGet64,
                    copy = LocalCopy64,
                    binop_local_local = BinOpLocalLocal64,
                    binop_local_local_set = BinOpLocalLocalSet64,
                    binop_local_const = BinOpLocalConst64,
                    binop_local_const_set = |dst, src, op, c| match (dst == src, op) {
                        (true, BinOp::IAdd) => IncLocal64(dst, c),
                        (true, BinOp::ISub) => IncLocal64(dst, c.wrapping_neg()),
                        _ => BinOpLocalConstSet64(op, src, c, dst),
                    },
                    const_instr = Const64,
                    set_local_const = SetLocalConst64
                );
            }
            LocalSet128(dst) => {
                fold_local_binop!(
                    instrs, i, dst,
                    source = resolve_local_source_128,
                    op = bin_op_128,
                    const = const_128,
                    local_local = BinOpLocalLocalSet128,
                    local_const = |dst, lhs, op, imm| Instruction::BinOpLocalConstSet128(op, lhs, imm, dst)
                );
                rewrite_local_set_direct!(
                    instrs,
                    i,
                    dst,
                    get = LocalGet128,
                    copy = LocalCopy128,
                    binop_local_local = BinOpLocalLocal128,
                    binop_local_local_set = BinOpLocalLocalSet128,
                    binop_local_const = BinOpLocalConst128,
                    binop_local_const_set = |dst, src, op, c| BinOpLocalConstSet128(op, src, c, dst),
                    const_instr = Const128,
                    set_local_const = SetLocalConst128
                );
                rewrite!(instrs, i,
                    [LocalGet32(addr), V128Load(memarg)] if
                    (let (Ok(addr), Ok(dst)) = (u8::try_from(addr), u8::try_from(dst))) =>
                    LoadLocalSet128(memarg, addr, dst)
                );
            }
            LocalTee32(dst) => {
                fold_local_binop!(
                    instrs, i, dst,
                    source = resolve_local_source_32,
                    op = scalar_bin_op,
                    const = scalar_const_32,
                    local_local = BinOpLocalLocalTee32,
                    local_const = |dst, lhs, op, imm| Instruction::BinOpLocalConstTee32(op, lhs, imm, dst)
                );
                rewrite_local_tee_direct!(
                    instrs,
                    i,
                    dst,
                    get = LocalGet32,
                    binop_local_local = BinOpLocalLocal32,
                    binop_local_local_tee = BinOpLocalLocalTee32,
                    binop_local_const = BinOpLocalConst32,
                    binop_local_const_tee = BinOpLocalConstTee32
                );
                rewrite!(instrs, i, [Const32(c), I32And] => AndConstTee32(c, dst));
                rewrite!(instrs, i, [Const32(c), I32Sub] => SubConstTee32(c, dst));
                rewrite!(instrs, i,
                    [LocalGet32(addr), I32Load(memarg)] if
                    (let (Ok(addr), Ok(dst)) = (u8::try_from(addr), u8::try_from(dst))) =>
                    LoadLocalTee32(memarg, addr, dst)
                );
                rewrite!(instrs, i,
                    [LocalGet32(addr), F32Load(memarg)] if
                    (let (Ok(addr), Ok(dst)) = (u8::try_from(addr), u8::try_from(dst))) =>
                    LoadLocalTee32(memarg, addr, dst)
                );
                rewrite!(instrs, i,
                    [LoadLocal32(memarg, addr)] if (let Ok(dst) = u8::try_from(dst)) =>
                    LoadLocalTee32(memarg, addr, dst)
                );
                rewrite!(instrs, i,
                    [LoadLocal8S32(memarg, addr)] if (let Ok(dst) = u8::try_from(dst)) =>
                    LoadLocalTee8S32(memarg, addr, dst)
                );
                rewrite!(instrs, i,
                    [LoadLocal8U32(memarg, addr)] if (let Ok(dst) = u8::try_from(dst)) =>
                    LoadLocalTee8U32(memarg, addr, dst)
                );
                rewrite!(instrs, i,
                    [LoadLocal16S32(memarg, addr)] if (let Ok(dst) = u8::try_from(dst)) =>
                    LoadLocalTee16S32(memarg, addr, dst)
                );
                rewrite!(instrs, i,
                    [LoadLocal16U32(memarg, addr)] if (let Ok(dst) = u8::try_from(dst)) =>
                    LoadLocalTee16U32(memarg, addr, dst)
                );
            }
            LocalTee64(dst) => {
                fold_local_binop!(
                    instrs, i, dst,
                    source = resolve_local_source_64,
                    op = scalar_bin_op,
                    const = scalar_const_64,
                    local_local = BinOpLocalLocalTee64,
                    local_const = |dst, lhs, op, imm| Instruction::BinOpLocalConstTee64(op, lhs, imm, dst)
                );
                rewrite_local_tee_direct!(
                    instrs,
                    i,
                    dst,
                    get = LocalGet64,
                    binop_local_local = BinOpLocalLocal64,
                    binop_local_local_tee = BinOpLocalLocalTee64,
                    binop_local_const = BinOpLocalConst64,
                    binop_local_const_tee = BinOpLocalConstTee64
                );
                rewrite!(instrs, i, [Const64(c), I64And] => AndConstTee64(c, dst));
                rewrite!(instrs, i, [Const64(c), I64Sub] => SubConstTee64(c, dst));
            }
            LocalTee128(dst) => {
                fold_local_binop!(
                    instrs, i, dst,
                    source = resolve_local_source_128,
                    op = bin_op_128,
                    const = const_128,
                    local_local = BinOpLocalLocalTee128,
                    local_const = |dst, lhs, op, imm| Instruction::BinOpLocalConstTee128(op, lhs, imm, dst)
                );
                rewrite_local_tee_direct!(
                    instrs,
                    i,
                    dst,
                    get = LocalGet128,
                    binop_local_local = BinOpLocalLocal128,
                    binop_local_local_tee = BinOpLocalLocalTee128,
                    binop_local_const = BinOpLocalConst128,
                    binop_local_const_tee = BinOpLocalConstTee128
                );
                rewrite!(instrs, i,
                    [LocalGet32(addr), V128Load(memarg)] if
                    (let (Ok(addr), Ok(dst)) = (u8::try_from(addr), u8::try_from(dst))) =>
                    LoadLocalTee128(memarg, addr, dst)
                );
            }
            Drop32 => rewrite_drop_tee_direct!(
                instrs,
                i,
                tee = LocalTee32,
                set = LocalSet32,
                binop_local_local_tee = BinOpLocalLocalTee32,
                binop_local_local_set = BinOpLocalLocalSet32,
                binop_local_const_tee = BinOpLocalConstTee32,
                binop_local_const_set = BinOpLocalConstSet32
            ),
            Drop64 => rewrite_drop_tee_direct!(
                instrs,
                i,
                tee = LocalTee64,
                set = LocalSet64,
                binop_local_local_tee = BinOpLocalLocalTee64,
                binop_local_local_set = BinOpLocalLocalSet64,
                binop_local_const_tee = BinOpLocalConstTee64,
                binop_local_const_set = BinOpLocalConstSet64
            ),
            Drop128 => rewrite_drop_tee_direct!(
                instrs,
                i,
                tee = LocalTee128,
                set = LocalSet128,
                binop_local_local_tee = BinOpLocalLocalTee128,
                binop_local_local_set = BinOpLocalLocalSet128,
                binop_local_const_tee = BinOpLocalConstTee128,
                binop_local_const_set = BinOpLocalConstSet128
            ),
            Jump(ip) => {
                let target = resolve_jump_target(&source, ip);
                let exit = old_idx as u32 + 1;
                let body = target + 1;

                match source.get(target as usize).copied() {
                    Some(JumpCmpLocalLocal32 { target_ip, left, right, op })
                        if resolve_jump_target(&source, target_ip) == exit && body > target =>
                    {
                        instrs[i] = JumpCmpLocalLocal32 { target_ip: body, left, right, op: inverse_cmp_op(op) };
                    }
                    Some(JumpCmpLocalLocal64 { target_ip, left, right, op })
                        if resolve_jump_target(&source, target_ip) == exit && body > target =>
                    {
                        instrs[i] = JumpCmpLocalLocal64 { target_ip: body, left, right, op: inverse_cmp_op(op) };
                    }
                    _ => canonicalize_jump_like_with_target(&mut instrs, i, target, exit),
                }
            }
            JumpIfZero32(ip) => {
                let target = resolve_jump_target(&source, ip);
                rewrite!(instrs, i, [LocalGet32(local), I32Eqz] => {
                    replace!(instrs, i, 2 => JumpIfLocalNonZero32 { target_ip: target, local });
                    continue;
                });
                rewrite!(instrs, i, [I32Eqz] => {
                    replace!(instrs, i, 1 => JumpIfNonZero32(target));
                    continue;
                });
                rewrite!(instrs, i, [LocalGet32(local)] => JumpIfLocalZero32 { target_ip: target, local });
                rewrite!(instrs, i, [LocalGet64(local), I64Eqz] => {
                    replace!(instrs, i, 2 => JumpIfLocalNonZero64 { target_ip: target, local });
                    continue;
                });
                rewrite!(instrs, i, [I64Eqz] => {
                    replace!(instrs, i, 1 => JumpIfNonZero64(target));
                    continue;
                });
                rewrite!(instrs, i,
                    [LocalGet32(local), Const32(imm), cmp] if (let Some(op) = cmp_op(cmp)) =>
                    match (imm, inverse_cmp_op(op)) {
                        (0, CmpOp::Eq) => JumpIfLocalZero32 { target_ip: target, local },
                        (0, CmpOp::Ne) => JumpIfLocalNonZero32 { target_ip: target, local },
                        (imm, op) => JumpCmpLocalConst32 { target_ip: target, local, imm, op },
                    }
                );
                rewrite!(instrs, i,
                    [LocalGet64(local), Const64(imm), cmp] if
                    (let Some(op) = cmp_op(cmp) && let Ok(imm) = i32::try_from(imm)) =>
                    match (imm, inverse_cmp_op(op)) {
                        (0, CmpOp::Eq) => JumpIfLocalZero64 { target_ip: target, local },
                        (0, CmpOp::Ne) => JumpIfLocalNonZero64 { target_ip: target, local },
                        (imm, op) => JumpCmpLocalConst64 { target_ip: target, local, imm, op },
                    }
                );
                rewrite!(instrs, i,
                    [LocalGet32(left), LocalGet32(right), cmp] if (let Some(op) = cmp_op(cmp)) =>
                    JumpCmpLocalLocal32 { target_ip: target, left, right, op: inverse_cmp_op(op) }
                );
                rewrite!(instrs, i,
                    [LocalGet64(left), LocalGet64(right), cmp] if (let Some(op) = cmp_op(cmp)) =>
                    JumpCmpLocalLocal64 { target_ip: target, left, right, op: inverse_cmp_op(op) }
                );
                rewrite!(instrs, i, [Const32(imm), cmp] if (let Some(op) = cmp_op(cmp)) => match (imm, inverse_cmp_op(op)) {
                    (0, CmpOp::Eq) => JumpIfZero32(target),
                    (0, CmpOp::Ne) => JumpIfNonZero32(target),
                    (imm, op) => JumpCmpStackConst32 { target_ip: target, imm, op },
                });
                rewrite!(instrs, i, [Const64(imm), cmp] if (let Some(op) = cmp_op(cmp)) => match (imm, inverse_cmp_op(op)) {
                    (0, CmpOp::Eq) => JumpIfZero64(target),
                    (0, CmpOp::Ne) => JumpIfNonZero64(target),
                    (imm, op) => JumpCmpStackConst64 { target_ip: target, imm, op },
                });
                canonicalize_jump_like_with_target(&mut instrs, i, target, old_idx as u32 + 1);
            }
            JumpIfNonZero32(ip) => {
                let target = resolve_jump_target(&source, ip);
                rewrite!(instrs, i, [LocalGet32(local), I32Eqz] => {
                    replace!(instrs, i, 2 => JumpIfLocalZero32 { target_ip: target, local });
                    continue;
                });
                rewrite!(instrs, i, [I32Eqz] => {
                    replace!(instrs, i, 1 => JumpIfZero32(target));
                    continue;
                });
                rewrite!(instrs, i, [LocalGet32(local)] => JumpIfLocalNonZero32 { target_ip: target, local });
                rewrite!(instrs, i, [LocalGet64(local), I64Eqz] => {
                    replace!(instrs, i, 2 => JumpIfLocalZero64 { target_ip: target, local });
                    continue;
                });
                rewrite!(instrs, i, [I64Eqz] => {
                    replace!(instrs, i, 1 => JumpIfZero64(target));
                    continue;
                });
                rewrite!(instrs, i,
                    [LocalGet32(local), Const32(imm), cmp] if (let Some(op) = cmp_op(cmp)) =>
                    match (imm, op) {
                        (0, CmpOp::Eq) => JumpIfLocalZero32 { target_ip: target, local },
                        (0, CmpOp::Ne) => JumpIfLocalNonZero32 { target_ip: target, local },
                        (imm, op) => JumpCmpLocalConst32 { target_ip: target, local, imm, op },
                    }
                );
                rewrite!(instrs, i,
                    [LocalGet64(local), Const64(imm), cmp] if
                    (let Some(op) = cmp_op(cmp) && let Ok(imm) = i32::try_from(imm)) =>
                    match (imm, op) {
                        (0, CmpOp::Eq) => JumpIfLocalZero64 { target_ip: target, local },
                        (0, CmpOp::Ne) => JumpIfLocalNonZero64 { target_ip: target, local },
                        (imm, op) => JumpCmpLocalConst64 { target_ip: target, local, imm, op },
                    }
                );
                rewrite!(instrs, i,
                    [LocalGet32(left), LocalGet32(right), cmp] if (let Some(op) = cmp_op(cmp)) =>
                    JumpCmpLocalLocal32 { target_ip: target, left, right, op }
                );
                rewrite!(instrs, i,
                    [LocalGet64(left), LocalGet64(right), cmp] if (let Some(op) = cmp_op(cmp)) =>
                    JumpCmpLocalLocal64 { target_ip: target, left, right, op }
                );
                rewrite!(instrs, i, [Const32(imm), cmp] if (let Some(op) = cmp_op(cmp)) => match (imm, op) {
                    (0, CmpOp::Eq) => JumpIfZero32(target),
                    (0, CmpOp::Ne) => JumpIfNonZero32(target),
                    (imm, op) => JumpCmpStackConst32 { target_ip: target, imm, op },
                });
                rewrite!(instrs, i, [Const64(imm), cmp] if (let Some(op) = cmp_op(cmp)) => match (imm, op) {
                    (0, CmpOp::Eq) => JumpIfZero64(target),
                    (0, CmpOp::Ne) => JumpIfNonZero64(target),
                    (imm, op) => JumpCmpStackConst64 { target_ip: target, imm, op },
                });
                canonicalize_jump_like_with_target(&mut instrs, i, target, old_idx as u32 + 1);
            }
            JumpIfZero64(ip) => {
                let target = resolve_jump_target(&source, ip);
                rewrite!(instrs, i, [LocalGet64(local)] => JumpIfLocalZero64 { target_ip: target, local });
                canonicalize_jump_like_with_target(&mut instrs, i, target, old_idx as u32 + 1);
            }
            JumpIfNonZero64(ip) => {
                let target = resolve_jump_target(&source, ip);
                rewrite!(instrs, i, [LocalGet64(local)] => JumpIfLocalNonZero64 { target_ip: target, local });
                canonicalize_jump_like_with_target(&mut instrs, i, target, old_idx as u32 + 1);
            }
            JumpCmpStackConst32 { target_ip, imm: 0, op } => {
                match op {
                    CmpOp::Eq => instrs[i] = JumpIfZero32(target_ip),
                    CmpOp::Ne => instrs[i] = JumpIfNonZero32(target_ip),
                    _ => {}
                }
                canonicalize_jump_like(&source, &mut instrs, i, old_idx as u32 + 1);
            }
            JumpCmpStackConst64 { target_ip, imm: 0, op } => {
                match op {
                    CmpOp::Eq => instrs[i] = JumpIfZero64(target_ip),
                    CmpOp::Ne => instrs[i] = JumpIfNonZero64(target_ip),
                    _ => {}
                }
                canonicalize_jump_like(&source, &mut instrs, i, old_idx as u32 + 1);
            }
            JumpCmpLocalConst32 { target_ip, local, imm: 0, op } => {
                match op {
                    CmpOp::Eq => instrs[i] = JumpIfLocalZero32 { target_ip, local },
                    CmpOp::Ne => instrs[i] = JumpIfLocalNonZero32 { target_ip, local },
                    _ => {}
                }
                canonicalize_jump_like(&source, &mut instrs, i, old_idx as u32 + 1);
            }
            JumpCmpLocalConst64 { target_ip, local, imm: 0, op } => {
                match op {
                    CmpOp::Eq => instrs[i] = JumpIfLocalZero64 { target_ip, local },
                    CmpOp::Ne => instrs[i] = JumpIfLocalNonZero64 { target_ip, local },
                    _ => {}
                }
                canonicalize_jump_like(&source, &mut instrs, i, old_idx as u32 + 1);
            }
            JumpCmpStackConst32 { .. }
            | JumpCmpStackConst64 { .. }
            | JumpCmpLocalConst32 { .. }
            | JumpCmpLocalConst64 { .. }
            | JumpCmpLocalLocal32 { .. }
            | JumpCmpLocalLocal64 { .. }
            | JumpIfLocalZero32 { .. }
            | JumpIfLocalNonZero32 { .. }
            | JumpIfLocalZero64 { .. }
            | JumpIfLocalNonZero64 { .. } => {
                canonicalize_jump_like(&source, &mut instrs, i, old_idx as u32 + 1);
            }
            _ => {}
        }

        after_terminator = is_unconditional_terminator(instr);
    }

    old_to_new[source.len()] = instrs.len() as u32;
    (instrs.instructions, old_to_new)
}

fn cmp_op(instr: Instruction) -> Option<CmpOp> {
    Some(match instr {
        Instruction::I32Eq | Instruction::I64Eq => CmpOp::Eq,
        Instruction::I32Ne | Instruction::I64Ne => CmpOp::Ne,
        Instruction::I32LtS | Instruction::I64LtS => CmpOp::LtS,
        Instruction::I32LtU | Instruction::I64LtU => CmpOp::LtU,
        Instruction::I32GtS | Instruction::I64GtS => CmpOp::GtS,
        Instruction::I32GtU | Instruction::I64GtU => CmpOp::GtU,
        Instruction::I32LeS | Instruction::I64LeS => CmpOp::LeS,
        Instruction::I32LeU | Instruction::I64LeU => CmpOp::LeU,
        Instruction::I32GeS | Instruction::I64GeS => CmpOp::GeS,
        Instruction::I32GeU | Instruction::I64GeU => CmpOp::GeU,
        _ => return None,
    })
}

fn int_bin_op(instr: Instruction) -> Option<BinOp> {
    Some(match instr {
        Instruction::I32Add | Instruction::I64Add => BinOp::IAdd,
        Instruction::I32Sub | Instruction::I64Sub => BinOp::ISub,
        Instruction::I32Mul | Instruction::I64Mul => BinOp::IMul,
        Instruction::I32And | Instruction::I64And => BinOp::IAnd,
        Instruction::I32Or | Instruction::I64Or => BinOp::IOr,
        Instruction::I32Xor | Instruction::I64Xor => BinOp::IXor,
        Instruction::I32Shl | Instruction::I64Shl => BinOp::IShl,
        Instruction::I32ShrS | Instruction::I64ShrS => BinOp::IShrS,
        Instruction::I32ShrU | Instruction::I64ShrU => BinOp::IShrU,
        Instruction::I32Rotl | Instruction::I64Rotl => BinOp::IRotl,
        Instruction::I32Rotr | Instruction::I64Rotr => BinOp::IRotr,
        _ => return None,
    })
}

fn float_bin_op(instr: Instruction) -> Option<BinOp> {
    Some(match instr {
        Instruction::F32Add | Instruction::F64Add => BinOp::FAdd,
        Instruction::F32Sub | Instruction::F64Sub => BinOp::FSub,
        Instruction::F32Mul | Instruction::F64Mul => BinOp::FMul,
        Instruction::F32Div | Instruction::F64Div => BinOp::FDiv,
        Instruction::F32Min | Instruction::F64Min => BinOp::FMin,
        Instruction::F32Max | Instruction::F64Max => BinOp::FMax,
        Instruction::F32Copysign | Instruction::F64Copysign => BinOp::FCopysign,
        _ => return None,
    })
}

fn scalar_bin_op(instr: Instruction) -> Option<BinOp> {
    int_bin_op(instr).or_else(|| float_bin_op(instr))
}

fn scalar_const_32(instr: Instruction, op_instr: Instruction) -> Option<i32> {
    match instr {
        Instruction::Const32(c) if scalar_bin_op(op_instr).is_some() => Some(c),
        _ => None,
    }
}

fn scalar_const_64(instr: Instruction, op_instr: Instruction) -> Option<i64> {
    match instr {
        Instruction::Const64(c) if scalar_bin_op(op_instr).is_some() => Some(c),
        _ => None,
    }
}

fn const_128(instr: Instruction, op_instr: Instruction) -> Option<ConstIdx> {
    match instr {
        Instruction::Const128(c) if bin_op_128(op_instr).is_some() => Some(c),
        _ => None,
    }
}

define_local_source_resolver!(
    resolve_local_source_32,
    get = LocalGet32,
    tee = LocalTee32,
    set = LocalSet32,
    binop_local_local_tee = BinOpLocalLocalTee32,
    binop_local_local_set = BinOpLocalLocalSet32,
    binop_local_const_tee = BinOpLocalConstTee32,
    binop_local_const_set = BinOpLocalConstSet32,
    load_local_tee = LoadLocalTee32,
    load_local_set = LoadLocalSet32
);

define_local_source_resolver!(
    resolve_local_source_64,
    get = LocalGet64,
    tee = LocalTee64,
    set = LocalSet64,
    binop_local_local_tee = BinOpLocalLocalTee64,
    binop_local_local_set = BinOpLocalLocalSet64,
    binop_local_const_tee = BinOpLocalConstTee64,
    binop_local_const_set = BinOpLocalConstSet64
);

define_local_source_resolver!(
    resolve_local_source_128,
    get = LocalGet128,
    tee = LocalTee128,
    set = LocalSet128,
    binop_local_local_tee = BinOpLocalLocalTee128,
    binop_local_local_set = BinOpLocalLocalSet128,
    binop_local_const_tee = BinOpLocalConstTee128,
    binop_local_const_set = BinOpLocalConstSet128,
    load_local_tee = LoadLocalTee128,
    load_local_set = LoadLocalSet128
);

fn bin_op_128(instr: Instruction) -> Option<BinOp128> {
    Some(match instr {
        Instruction::V128And => BinOp128::And,
        Instruction::V128AndNot => BinOp128::AndNot,
        Instruction::V128Or => BinOp128::Or,
        Instruction::V128Xor => BinOp128::Xor,
        Instruction::I64x2Add => BinOp128::I64x2Add,
        Instruction::I64x2Mul => BinOp128::I64x2Mul,
        _ => return None,
    })
}

fn inverse_cmp_op(op: CmpOp) -> CmpOp {
    match op {
        CmpOp::Eq => CmpOp::Ne,
        CmpOp::Ne => CmpOp::Eq,
        CmpOp::LtS => CmpOp::GeS,
        CmpOp::LtU => CmpOp::GeU,
        CmpOp::GtS => CmpOp::LeS,
        CmpOp::GtU => CmpOp::LeU,
        CmpOp::LeS => CmpOp::GtS,
        CmpOp::LeU => CmpOp::GtU,
        CmpOp::GeS => CmpOp::LtS,
        CmpOp::GeU => CmpOp::LtU,
    }
}

fn resolve_jump_target(instrs: &[Instruction], target: u32) -> u32 {
    let mut idx = target as usize;
    let mut steps = 0usize;

    while let Some(Instruction::Jump(next)) = instrs.get(idx)
        && steps < instrs.len()
    {
        idx = *next as usize;
        steps += 1;
    }

    idx as u32
}

fn instruction_target_mut(instr: &mut Instruction) -> Option<&mut u32> {
    Some(match instr {
        Instruction::Jump(ip)
        | Instruction::JumpIfZero32(ip)
        | Instruction::JumpIfNonZero32(ip)
        | Instruction::JumpIfZero64(ip)
        | Instruction::JumpIfNonZero64(ip)
        | Instruction::JumpCmpStackConst32 { target_ip: ip, .. }
        | Instruction::JumpCmpStackConst64 { target_ip: ip, .. }
        | Instruction::JumpIfLocalZero32 { target_ip: ip, .. }
        | Instruction::JumpIfLocalNonZero32 { target_ip: ip, .. }
        | Instruction::JumpIfLocalZero64 { target_ip: ip, .. }
        | Instruction::JumpIfLocalNonZero64 { target_ip: ip, .. }
        | Instruction::JumpCmpLocalConst32 { target_ip: ip, .. }
        | Instruction::JumpCmpLocalConst64 { target_ip: ip, .. }
        | Instruction::JumpCmpLocalLocal32 { target_ip: ip, .. }
        | Instruction::JumpCmpLocalLocal64 { target_ip: ip, .. } => ip,
        Instruction::BranchTable(ip, _, _) => ip,
        _ => return None,
    })
}

fn instruction_target(instr: &Instruction) -> Option<u32> {
    let mut instr = *instr;
    instruction_target_mut(&mut instr).copied()
}

fn canonicalize_jump_like(source: &[Instruction], instrs: &mut Vec<Instruction>, idx: usize, fallthrough: u32) {
    let Some(target) = instruction_target(&instrs[idx]) else {
        return;
    };

    canonicalize_jump_like_with_target(instrs, idx, resolve_jump_target(source, target), fallthrough);
}

fn canonicalize_jump_like_with_target(instrs: &mut Vec<Instruction>, idx: usize, target: u32, fallthrough: u32) {
    if matches!(instrs[idx], Instruction::Jump(_)) && target == fallthrough {
        instrs.truncate(idx);
    } else if let Some(ip) = instruction_target_mut(&mut instrs[idx]) {
        *ip = target;
    }
}

fn is_unconditional_terminator(instr: Instruction) -> bool {
    matches!(
        instr,
        Instruction::Unreachable
            | Instruction::Jump(_)
            | Instruction::BranchTable(..)
            | Instruction::Return
            | Instruction::ReturnVoid
            | Instruction::Return32
            | Instruction::Return64
            | Instruction::Return128
            | Instruction::ReturnCall(_)
            | Instruction::ReturnCallSelf
            | Instruction::ReturnCallIndirect(..)
    )
}

fn target_boundaries(instructions: &[Instruction], function_data: &WasmFunctionData) -> Result<Vec<bool>> {
    let mut boundaries = alloc::vec![false; instructions.len() + 1];
    for instr in instructions {
        if let Some(target) = instruction_target(instr) {
            let boundary = boundaries
                .get_mut(target as usize)
                .ok_or_else(|| ParseError::Other(alloc::format!("instruction target out of bounds: {target}")))?;
            *boundary = true;
        }
        if let Instruction::BranchTable(_, start, count) = *instr {
            let end =
                start.checked_add(count).ok_or_else(|| ParseError::Other("branch table range overflow".into()))?;
            let targets = function_data
                .branch_table_targets
                .get(start as usize..end as usize)
                .ok_or_else(|| ParseError::Other("branch table range out of bounds".into()))?;
            for &target in targets {
                let boundary = boundaries
                    .get_mut(target as usize)
                    .ok_or_else(|| ParseError::Other(alloc::format!("branch table target out of bounds: {target}")))?;
                *boundary = true;
            }
        }
    }
    Ok(boundaries)
}

/// Remaps rewritten targets, then validates targets and ranges while detecting local memory use.
fn finalize(
    instructions: &mut [Instruction],
    function_data: &mut WasmFunctionData,
    old_to_new: Option<&[u32]>,
    imported_memory_count: u32,
) -> Result<bool> {
    let len = instructions.len() as u32;
    for target in &mut function_data.branch_table_targets {
        if let Some(old_to_new) = old_to_new {
            *target = *old_to_new
                .get(*target as usize)
                .ok_or_else(|| ParseError::Other(alloc::format!("instruction target out of bounds: {target}")))?;
        }
        if *target >= len {
            return Err(ParseError::Other(alloc::format!("branch table target out of bounds: {target}")));
        }
    }

    let mut uses_local_memory = false;
    for instr in instructions {
        if let Some(target) = instruction_target_mut(instr) {
            if let Some(old_to_new) = old_to_new {
                *target = *old_to_new
                    .get(*target as usize)
                    .ok_or_else(|| ParseError::Other(alloc::format!("instruction target out of bounds: {target}")))?;
            }
            if *target >= len {
                return Err(ParseError::Other(alloc::format!("instruction target out of bounds: {target}")));
            }
        }
        if let Instruction::BranchTable(_, start, count) = *instr {
            let end =
                start.checked_add(count).ok_or_else(|| ParseError::Other("branch table range overflow".into()))?;
            function_data
                .branch_table_targets
                .get(start as usize..end as usize)
                .ok_or_else(|| ParseError::Other("branch table range out of bounds".into()))?;
        }
        uses_local_memory |= instr.memory_addr().is_some_and(|mem| mem >= imported_memory_count);
    }
    Ok(uses_local_memory)
}