celox-backend-x86 0.8.2

Celox x86-64 machine-code backend
Documentation
use super::*;

#[test]
fn traced_disassembly_labels_exact_basic_block_offsets() {
    let text =
        disassemble_with_block_offsets(&[0x90, 0xc3], 0x1000, &[(BlockId(9), 1), (BlockId(3), 0)]);

    assert_eq!(text, "bb3:\n  0x00001000  nop\nbb9:\n  0x00001001  ret\n");
    assert_eq!(
        disassemble(&[0x90, 0xc3], 0x1000),
        "  0x00001000  nop\n  0x00001001  ret\n"
    );
}

#[test]
fn emission_layout_pulls_a_backedge_chain_next_to_its_latch() {
    let mut vregs = VRegAllocator::new();
    let outer_condition = vregs.alloc();
    let loop_condition = vregs.alloc();
    let edge_value = vregs.alloc();
    let mut func = MFunction::new(vregs, vec![SpillDesc::transient(); 3]);

    let mut entry = MBlock::new(BlockId(0));
    entry.push(MInst::Jump { target: BlockId(1) });
    let mut header = MBlock::new(BlockId(1));
    header.push(MInst::Branch {
        cond: outer_condition,
        true_bb: BlockId(2),
        false_bb: BlockId(3),
    });
    let mut latch = MBlock::new(BlockId(2));
    latch.push(MInst::Branch {
        cond: loop_condition,
        true_bb: BlockId(4),
        false_bb: BlockId(3),
    });
    let mut exit = MBlock::new(BlockId(3));
    exit.push(MInst::Return);
    let mut first_edge = MBlock::new(BlockId(4));
    first_edge.push(MInst::Jump { target: BlockId(5) });
    let mut second_edge = MBlock::new(BlockId(5));
    second_edge.push(MInst::Mov {
        dst: edge_value,
        src: outer_condition,
    });
    second_edge.push(MInst::Jump { target: BlockId(1) });
    func.blocks = vec![entry, header, latch, exit, first_edge, second_edge];

    let order = emission_block_order(&func)
        .into_iter()
        .map(|index| func.blocks[index].id)
        .collect::<Vec<_>>();

    assert_eq!(
        order,
        vec![
            BlockId(0),
            BlockId(1),
            BlockId(2),
            BlockId(4),
            BlockId(5),
            BlockId(3),
        ]
    );
}

#[test]
fn branch_inverts_when_true_successor_is_the_physical_fallthrough() {
    let mut vregs = VRegAllocator::new();
    let condition = vregs.alloc();
    let mut func = MFunction::new(vregs, vec![SpillDesc::transient()]);
    let mut entry = MBlock::new(BlockId(0));
    entry.push(MInst::LoadImm {
        dst: condition,
        value: 1,
    });
    entry.push(MInst::Branch {
        cond: condition,
        true_bb: BlockId(1),
        false_bb: BlockId(2),
    });
    let mut true_block = MBlock::new(BlockId(1));
    true_block.push(MInst::Return);
    let mut false_block = MBlock::new(BlockId(2));
    false_block.push(MInst::Return);
    func.blocks = vec![entry, true_block, false_block];

    let mut assignment = AssignmentMap::default();
    assignment.set(condition, PhysReg::RAX);
    let emitted = emit(&func, &assignment, 0).unwrap();
    let mut decoder = Decoder::new(64, &emitted.code, DecoderOptions::NONE);
    let mut conditional_branches = Vec::new();
    while decoder.can_decode() {
        let instruction = decoder.decode();
        if matches!(instruction.mnemonic(), Mnemonic::Je | Mnemonic::Jne) {
            conditional_branches.push(instruction.mnemonic());
        }
    }

    assert_eq!(conditional_branches, vec![Mnemonic::Je]);
}

#[test]
fn dense_jump_table_executes_every_masked_index() {
    let mut vregs = VRegAllocator::new();
    let loaded = vregs.alloc();
    let index = vregs.alloc();
    let table_base = vregs.alloc();
    let target = vregs.alloc();
    let mut func = MFunction::new(vregs, vec![SpillDesc::transient(); 4]);

    let mut entry = MBlock::new(BlockId(0));
    entry.push(MInst::Load {
        dst: loaded,
        base: BaseReg::SimState,
        offset: 0,
        size: OpSize::S8,
    });
    entry.push(MInst::AndImm32 {
        dst: index,
        src: loaded,
        imm: 3,
    });
    entry.push(MInst::Scratch { dst: table_base });
    entry.push(MInst::Scratch { dst: target });
    entry.push(MInst::JumpTable {
        index,
        table_base,
        target,
        targets: (1..=4).map(BlockId).collect(),
    });
    func.blocks.push(entry);
    for code in 1..=4 {
        let mut arm = MBlock::new(BlockId(code));
        arm.push(MInst::ReturnError {
            code: i64::from(code),
        });
        func.blocks.push(arm);
    }
    func.verify();

    let mut assignment = AssignmentMap::default();
    assignment.set(loaded, PhysReg::RAX);
    assignment.set(index, PhysReg::RCX);
    assignment.set(table_base, PhysReg::RDX);
    assignment.set(target, PhysReg::RBX);
    let emitted = emit(&func, &assignment, 0).unwrap();
    let jit = JitCode::new(&emitted.code).unwrap();
    for index in 0..4u8 {
        let mut state = [0xfcu8 | index];
        assert_eq!(unsafe { jit.call(&mut state) }, i64::from(index) + 1);
    }
}

#[test]
fn memory_branch_predicate_compares_in_place_for_every_width() {
    for size in [OpSize::S8, OpSize::S16, OpSize::S32, OpSize::S64] {
        let mut func = MFunction::new(VRegAllocator::new(), Vec::new());
        let mut entry = MBlock::new(BlockId(0));
        entry.push(MInst::BranchPred {
            predicate: BranchPredicate::MemoryNonZero {
                base: BaseReg::SimState,
                offset: 8,
                size,
            },
            true_bb: BlockId(1),
            false_bb: BlockId(2),
        });
        let mut true_block = MBlock::new(BlockId(1));
        true_block.push(MInst::ReturnError { code: 7 });
        let mut false_block = MBlock::new(BlockId(2));
        false_block.push(MInst::ReturnError { code: 9 });
        func.blocks = vec![entry, true_block, false_block];
        func.verify();

        let emitted = emit(&func, &AssignmentMap::default(), 0).unwrap();
        let mut decoder = Decoder::new(64, &emitted.code, DecoderOptions::NONE);
        let mut mnemonics = Vec::new();
        while decoder.can_decode() {
            mnemonics.push(decoder.decode().mnemonic());
        }
        assert!(
            mnemonics.contains(&Mnemonic::Cmp),
            "{size:?}: {mnemonics:?}"
        );
        assert!(
            !mnemonics.contains(&Mnemonic::Movzx),
            "{size:?}: {mnemonics:?}"
        );

        let jit = JitCode::new(&emitted.code).unwrap();
        let mut state = [0u8; 16];
        assert_eq!(unsafe { jit.call(&mut state) }, 9);
        state[8] = 1;
        assert_eq!(unsafe { jit.call(&mut state) }, 7);
    }
}

#[test]
fn materialized_compare_branch_preserves_condition_used_after_the_branch() {
    let mut vregs = VRegAllocator::new();
    let zero = vregs.alloc();
    let alternative = vregs.alloc();
    let condition = vregs.alloc();
    let merged = vregs.alloc();
    let mut func = MFunction::new(vregs, vec![SpillDesc::transient(); 4]);

    let mut entry = MBlock::new(BlockId(0));
    entry.push(MInst::LoadImm {
        dst: zero,
        value: 0,
    });
    entry.push(MInst::LoadImm {
        dst: alternative,
        value: 7,
    });
    entry.push(MInst::CmpImm {
        dst: condition,
        lhs: zero,
        imm: 0,
        kind: CmpKind::Eq,
    });
    entry.push(MInst::Branch {
        cond: condition,
        true_bb: BlockId(1),
        false_bb: BlockId(2),
    });

    let mut true_block = MBlock::new(BlockId(1));
    true_block.push(MInst::Jump { target: BlockId(3) });
    let mut false_block = MBlock::new(BlockId(2));
    false_block.push(MInst::Jump { target: BlockId(3) });

    let mut join = MBlock::new(BlockId(3));
    join.phis.push(PhiNode {
        dst: merged,
        sources: vec![(BlockId(1), condition), (BlockId(2), alternative)],
    });
    join.push(MInst::Store {
        base: BaseReg::SimState,
        offset: 0,
        src: merged,
        size: OpSize::S64,
    });
    join.push(MInst::Return);
    func.blocks = vec![entry, true_block, false_block, join];
    func.verify();

    let allocation = regalloc::run_regalloc(&mut func).unwrap();
    let emitted = emit(&func, &allocation.assignment, allocation.spill_frame_size).unwrap();
    let jit = JitCode::new(&emitted.code).unwrap();
    let mut state = [0u8; 8];

    assert_eq!(unsafe { jit.call(&mut state) }, 0);
    assert_eq!(u64::from_le_bytes(state), 1);
}