//! A post-legalization rewriting pass.

#![allow(non_snake_case)]

use crate::cursor::{Cursor, EncCursor};
use crate::ir::condcodes::{CondCode, FloatCC, IntCC};
use crate::ir::dfg::ValueDef;
use crate::ir::immediates::{Imm64, Offset32};
use crate::ir::instructions::{Opcode, ValueList};
use crate::ir::{Block, Function, Inst, InstBuilder, InstructionData, MemFlags, Type, Value};
use crate::isa::TargetIsa;
use crate::timing;

/// Information collected about a compare+branch sequence.
struct CmpBrInfo {
    /// The branch instruction.
    br_inst: Inst,
    /// The icmp, icmp_imm, or fcmp instruction.
    cmp_inst: Inst,
    /// The destination of the branch.
    destination: Block,
    /// The arguments of the branch.
    args: ValueList,
    /// The first argument to the comparison. The second is in the `kind` field.
    cmp_arg: Value,
    /// If the branch is `brz` rather than `brnz`, we need to invert the condition
    /// before the branch.
    invert_branch_cond: bool,
    /// The kind of comparison, and the second argument.
    kind: CmpBrKind,
}

enum CmpBrKind {
    Icmp { cond: IntCC, arg: Value },
    IcmpImm { cond: IntCC, imm: Imm64 },
    Fcmp { cond: FloatCC, arg: Value },
}

/// Optimize comparisons to use flags values, to avoid materializing conditions
/// in integer registers.
///
/// For example, optimize icmp/fcmp brz/brnz sequences into ifcmp/ffcmp brif/brff
/// sequences.
fn optimize_cpu_flags(
    pos: &mut EncCursor,
    inst: Inst,
    last_flags_clobber: Option<Inst>,
    isa: &dyn TargetIsa,
) {
    // Look for compare and branch patterns.
    // This code could be considerably simplified with non-lexical lifetimes.
    let info = match pos.func.dfg[inst] {
        InstructionData::Branch {
            opcode,
            destination,
            ref args,
        } => {
            let first_arg = args.first(&pos.func.dfg.value_lists).unwrap();
            let invert_branch_cond = match opcode {
                Opcode::Brz => true,
                Opcode::Brnz => false,
                _ => panic!(),
            };
            if let ValueDef::Result(cond_inst, _) = pos.func.dfg.value_def(first_arg) {
                match pos.func.dfg[cond_inst] {
                    InstructionData::IntCompare {
                        cond,
                        args: cmp_args,
                        ..
                    } => CmpBrInfo {
                        br_inst: inst,
                        cmp_inst: cond_inst,
                        destination,
                        args: args.clone(),
                        cmp_arg: cmp_args[0],
                        invert_branch_cond,
                        kind: CmpBrKind::Icmp {
                            cond,
                            arg: cmp_args[1],
                        },
                    },
                    InstructionData::IntCompareImm {
                        cond,
                        arg: cmp_arg,
                        imm: cmp_imm,
                        ..
                    } => CmpBrInfo {
                        br_inst: inst,
                        cmp_inst: cond_inst,
                        destination,
                        args: args.clone(),
                        cmp_arg,
                        invert_branch_cond,
                        kind: CmpBrKind::IcmpImm { cond, imm: cmp_imm },
                    },
                    InstructionData::FloatCompare {
                        cond,
                        args: cmp_args,
                        ..
                    } => CmpBrInfo {
                        br_inst: inst,
                        cmp_inst: cond_inst,
                        destination,
                        args: args.clone(),
                        cmp_arg: cmp_args[0],
                        invert_branch_cond,
                        kind: CmpBrKind::Fcmp {
                            cond,
                            arg: cmp_args[1],
                        },
                    },
                    _ => return,
                }
            } else {
                return;
            }
        }
        // TODO: trapif, trueif, selectif, and their ff counterparts.
        _ => return,
    };

    // If any instructions clobber the flags between the comparison and the branch,
    // don't optimize them.
    if last_flags_clobber != Some(info.cmp_inst) {
        return;
    }

    // We found a compare+branch pattern. Transform it to use flags.
    let args = info.args.as_slice(&pos.func.dfg.value_lists)[1..].to_vec();
    pos.goto_inst(info.cmp_inst);
    pos.use_srcloc(info.cmp_inst);
    match info.kind {
        CmpBrKind::Icmp { mut cond, arg } => {
            let flags = pos.ins().ifcmp(info.cmp_arg, arg);
            pos.func.dfg.replace(info.cmp_inst).trueif(cond, flags);
            if info.invert_branch_cond {
                cond = cond.inverse();
            }
            pos.func
                .dfg
                .replace(info.br_inst)
                .brif(cond, flags, info.destination, &args);
        }
        CmpBrKind::IcmpImm { mut cond, imm } => {
            let flags = pos.ins().ifcmp_imm(info.cmp_arg, imm);
            pos.func.dfg.replace(info.cmp_inst).trueif(cond, flags);
            if info.invert_branch_cond {
                cond = cond.inverse();
            }
            pos.func
                .dfg
                .replace(info.br_inst)
                .brif(cond, flags, info.destination, &args);
        }
        CmpBrKind::Fcmp { mut cond, arg } => {
            let flags = pos.ins().ffcmp(info.cmp_arg, arg);
            pos.func.dfg.replace(info.cmp_inst).trueff(cond, flags);
            if info.invert_branch_cond {
                cond = cond.inverse();
            }
            pos.func
                .dfg
                .replace(info.br_inst)
                .brff(cond, flags, info.destination, &args);
        }
    }
    let ok = pos.func.update_encoding(info.cmp_inst, isa).is_ok();
    debug_assert!(ok);
    let ok = pos.func.update_encoding(info.br_inst, isa).is_ok();
    debug_assert!(ok);
}

struct MemOpInfo {
    opcode: Opcode,
    itype: Type,
    arg: Value,
    st_arg: Option<Value>,
    flags: MemFlags,
    offset: Offset32,
}

fn optimize_complex_addresses(pos: &mut EncCursor, inst: Inst, isa: &dyn TargetIsa) {
    // Look for simple loads and stores we can optimize.
    let info = match pos.func.dfg[inst] {
        InstructionData::Load {
            opcode,
            arg,
            flags,
            offset,
        } => MemOpInfo {
            opcode,
            itype: pos.func.dfg.ctrl_typevar(inst),
            arg,
            st_arg: None,
            flags,
            offset,
        },
        InstructionData::Store {
            opcode,
            args,
            flags,
            offset,
        } => MemOpInfo {
            opcode,
            itype: pos.func.dfg.ctrl_typevar(inst),
            arg: args[1],
            st_arg: Some(args[0]),
            flags,
            offset,
        },
        _ => return,
    };

    // Examine the instruction that defines the address operand.
    if let ValueDef::Result(result_inst, _) = pos.func.dfg.value_def(info.arg) {
        match pos.func.dfg[result_inst] {
            InstructionData::Binary {
                opcode: Opcode::Iadd,
                args,
            } => match info.opcode {
                // Operand is an iadd. Fold it into a memory address with a complex address mode.
                Opcode::Load => {
                    pos.func.dfg.replace(inst).load_complex(
                        info.itype,
                        info.flags,
                        &args,
                        info.offset,
                    );
                }
                Opcode::Uload8 => {
                    pos.func.dfg.replace(inst).uload8_complex(
                        info.itype,
                        info.flags,
                        &args,
                        info.offset,
                    );
                }
                Opcode::Sload8 => {
                    pos.func.dfg.replace(inst).sload8_complex(
                        info.itype,
                        info.flags,
                        &args,
                        info.offset,
                    );
                }
                Opcode::Uload16 => {
                    pos.func.dfg.replace(inst).uload16_complex(
                        info.itype,
                        info.flags,
                        &args,
                        info.offset,
                    );
                }
                Opcode::Sload16 => {
                    pos.func.dfg.replace(inst).sload16_complex(
                        info.itype,
                        info.flags,
                        &args,
                        info.offset,
                    );
                }
                Opcode::Uload32 => {
                    pos.func
                        .dfg
                        .replace(inst)
                        .uload32_complex(info.flags, &args, info.offset);
                }
                Opcode::Sload32 => {
                    pos.func
                        .dfg
                        .replace(inst)
                        .sload32_complex(info.flags, &args, info.offset);
                }
                Opcode::Uload8x8 => {
                    pos.func
                        .dfg
                        .replace(inst)
                        .uload8x8_complex(info.flags, &args, info.offset);
                }
                Opcode::Sload8x8 => {
                    pos.func
                        .dfg
                        .replace(inst)
                        .sload8x8_complex(info.flags, &args, info.offset);
                }
                Opcode::Uload16x4 => {
                    pos.func
                        .dfg
                        .replace(inst)
                        .uload16x4_complex(info.flags, &args, info.offset);
                }
                Opcode::Sload16x4 => {
                    pos.func
                        .dfg
                        .replace(inst)
                        .sload16x4_complex(info.flags, &args, info.offset);
                }
                Opcode::Uload32x2 => {
                    pos.func
                        .dfg
                        .replace(inst)
                        .uload32x2_complex(info.flags, &args, info.offset);
                }
                Opcode::Sload32x2 => {
                    pos.func
                        .dfg
                        .replace(inst)
                        .sload32x2_complex(info.flags, &args, info.offset);
                }
                Opcode::Store => {
                    pos.func.dfg.replace(inst).store_complex(
                        info.flags,
                        info.st_arg.unwrap(),
                        &args,
                        info.offset,
                    );
                }
                Opcode::Istore8 => {
                    pos.func.dfg.replace(inst).istore8_complex(
                        info.flags,
                        info.st_arg.unwrap(),
                        &args,
                        info.offset,
                    );
                }
                Opcode::Istore16 => {
                    pos.func.dfg.replace(inst).istore16_complex(
                        info.flags,
                        info.st_arg.unwrap(),
                        &args,
                        info.offset,
                    );
                }
                Opcode::Istore32 => {
                    pos.func.dfg.replace(inst).istore32_complex(
                        info.flags,
                        info.st_arg.unwrap(),
                        &args,
                        info.offset,
                    );
                }
                _ => panic!("Unsupported load or store opcode"),
            },
            InstructionData::BinaryImm64 {
                opcode: Opcode::IaddImm,
                arg,
                imm,
            } => match pos.func.dfg[inst] {
                // Operand is an iadd_imm. Fold the immediate into the offset if possible.
                InstructionData::Load {
                    arg: ref mut load_arg,
                    ref mut offset,
                    ..
                } => {
                    if let Some(imm) = offset.try_add_i64(imm.into()) {
                        *load_arg = arg;
                        *offset = imm;
                    } else {
                        // Overflow.
                        return;
                    }
                }
                InstructionData::Store {
                    args: ref mut store_args,
                    ref mut offset,
                    ..
                } => {
                    if let Some(imm) = offset.try_add_i64(imm.into()) {
                        store_args[1] = arg;
                        *offset = imm;
                    } else {
                        // Overflow.
                        return;
                    }
                }
                _ => panic!(),
            },
            _ => {
                // Address value is defined by some other kind of instruction.
                return;
            }
        }
    } else {
        // Address value is not the result of an instruction.
        return;
    }

    let ok = pos.func.update_encoding(inst, isa).is_ok();
    debug_assert!(
        ok,
        "failed to update encoding for `{}`",
        pos.func.dfg.display_inst(inst, isa)
    );
}

//----------------------------------------------------------------------
//
// The main post-opt pass.

pub fn do_postopt(func: &mut Function, isa: &dyn TargetIsa) {
    let _tt = timing::postopt();
    let mut pos = EncCursor::new(func, isa);
    let is_mach_backend = isa.get_mach_backend().is_some();
    while let Some(_block) = pos.next_block() {
        let mut last_flags_clobber = None;
        while let Some(inst) = pos.next_inst() {
            if !is_mach_backend && isa.uses_cpu_flags() {
                // Optimize instructions to make use of flags.
                optimize_cpu_flags(&mut pos, inst, last_flags_clobber, isa);

                // Track the most recent seen instruction that clobbers the flags.
                if let Some(constraints) = isa
                    .encoding_info()
                    .operand_constraints(pos.func.encodings[inst])
                {
                    if constraints.clobbers_flags {
                        last_flags_clobber = Some(inst)
                    }
                }
            }

            if isa.uses_complex_addresses() {
                optimize_complex_addresses(&mut pos, inst, isa);
            }
        }
    }
}
