neo-decompiler 0.10.1

Neo N3 NEF decompiler: parse, disassemble, lift bytecode to high-level pseudocode and C# skeletons, with a CLI, JSON reports, and optional WebAssembly bindings.
Documentation
use super::*;
use crate::decompiler::cfg::ssa::form::{SsaExpr, SsaStmt};
use crate::decompiler::cfg::{BasicBlock, BlockId, CfgBuilder, EdgeKind, Terminator};
use crate::decompiler::ir::BinOp;
use crate::instruction::{Instruction, OpCode, Operand};

/// Build instructions + a matching CFG for a straight-line program.
fn linear(instrs: Vec<Instruction>) -> (Vec<Instruction>, Cfg) {
    let cfg = CfgBuilder::new(&instrs).build();
    (instrs, cfg)
}

fn instr(off: usize, op: OpCode) -> Instruction {
    Instruction::new(off, op, None)
}

#[test]
fn linear_compute_produces_real_binary_expr() {
    // PUSH1, PUSH2, ADD, RET
    let ins = vec![
        instr(0, OpCode::Push1),
        instr(1, OpCode::Push2),
        instr(2, OpCode::Add),
        Instruction::new(3, OpCode::Ret, None),
    ];
    let (ins, cfg) = linear(ins);
    let ssa = SsaBuilder::new(&cfg, &ins).build();

    // At least one block exists; find a block with assignments.
    let (_id, block) = ssa
        .blocks_iter()
        .find(|(_, b)| b.stmt_count() >= 3)
        .expect("a block with >= 3 assignments should exist");

    // v0 = 1, v1 = 2, v2 = (v0 + v2)
    assert_eq!(block.stmt_count(), 3, "expected 3 push/compute assignments");
    let add = &block.stmts[2];
    let SsaStmt::Assign { value, .. } = add else {
        panic!("third stmt should be the ADD assignment: {add:?}");
    };
    let SsaExpr::Binary { op, left, right } = value else {
        panic!("ADD should lower to a binary expr, got {value:?}");
    };
    assert_eq!(*op, BinOp::Add);
    // Operands reference the two push defs (deep on the left, top right).
    assert!(
        matches!(left.as_ref(), SsaExpr::Variable(_)),
        "left operand"
    );
    assert!(
        matches!(right.as_ref(), SsaExpr::Variable(_)),
        "right operand"
    );
}

#[test]
fn dup_creates_a_copy_definition() {
    // PUSH1, DUP, RET
    let ins = vec![
        instr(0, OpCode::Push1),
        instr(1, OpCode::Dup),
        Instruction::new(2, OpCode::Ret, None),
    ];
    let (ins, cfg) = linear(ins);
    let ssa = SsaBuilder::new(&cfg, &ins).build();
    let block = ssa.blocks_iter().next().expect("a block exists").1;

    // Two assignments: v0 = 1, v1 = v0 (the DUP copy).
    assert_eq!(block.stmt_count(), 2);
    let copy = &block.stmts[1];
    let SsaStmt::Assign { value, .. } = copy else {
        panic!("DUP should produce an assignment: {copy:?}");
    };
    assert!(
        matches!(value, SsaExpr::Variable(_)),
        "DUP copy should reference its source var, got {value:?}"
    );
}

#[test]
fn store_local_emits_a_slot_assignment() {
    // PUSH10 ; STLOC0 ; RET  →  the store must define a loc0 SSA var.
    let ins = vec![
        Instruction::new(0, OpCode::Push10, None),
        Instruction::new(1, OpCode::Stloc0, None),
        Instruction::new(2, OpCode::Ret, None),
    ];
    let (ins, cfg) = linear(ins);
    let ssa = SsaBuilder::new(&cfg, &ins).build();
    let block = ssa.blocks_iter().next().expect("a block exists").1;

    let has_loc0_assign = block.stmts.iter().any(|s| match s {
        SsaStmt::Assign { target, .. } => target.base == "loc0",
        _ => false,
    });
    assert!(
        has_loc0_assign,
        "STLOC0 should define a loc0 SSA variable; got {:?}",
        block.stmts
    );
}

#[test]
fn convert_preserves_lower_stack_values() {
    // PUSH2 ; PUSH1 ; CONVERT ByteString ; DROP ; STLOC0 ; RET
    //
    // CONVERT is unary: the type tag is an instruction operand, not a
    // stack value. After dropping the converted result, STLOC0 should still
    // store the PUSH2 value underneath it.
    let ins = vec![
        Instruction::new(0, OpCode::Push2, None),
        Instruction::new(1, OpCode::Push1, None),
        Instruction::new(2, OpCode::Convert, Some(Operand::U8(0x28))),
        Instruction::new(4, OpCode::Drop, None),
        Instruction::new(5, OpCode::Stloc0, None),
        Instruction::new(6, OpCode::Ret, None),
    ];
    let (ins, cfg) = linear(ins);
    let ssa = SsaBuilder::new(&cfg, &ins).build();
    let block = ssa.blocks_iter().next().expect("a block exists").1;

    let loc0_store = block
        .stmts
        .iter()
        .find(|stmt| matches!(stmt, SsaStmt::Assign { target, .. } if target.base == "loc0"))
        .expect("STLOC0 should define loc0");
    let SsaStmt::Assign { value, .. } = loc0_store else {
        panic!("loc0 store should be an assignment: {loc0_store:?}");
    };
    let SsaExpr::Variable(stored) = value else {
        panic!("loc0 should store the lower PUSH2 variable, got {value:?}");
    };
    assert_ne!(
        stored.base, "?",
        "CONVERT must not consume the lower PUSH2 stack value: {:?}",
        block.stmts
    );
}

#[test]
fn store_then_load_connects_within_a_block() {
    // PUSH10 ; STLOC0 ; LDLOC0 ; RET
    //   store defines a loc0 var; the load must read that var, not stay opaque.
    let ins = vec![
        Instruction::new(0, OpCode::Push10, None),
        Instruction::new(1, OpCode::Stloc0, None),
        Instruction::new(2, OpCode::Ldloc0, None),
        Instruction::new(3, OpCode::Ret, None),
    ];
    let (ins, cfg) = linear(ins);
    let ssa = SsaBuilder::new(&cfg, &ins).build();
    let block = ssa.blocks_iter().next().expect("a block exists").1;

    // loc0 defs in order: [store, load].
    let loc0_defs: Vec<&SsaStmt> = block
        .stmts
        .iter()
        .filter(|s| matches!(s, SsaStmt::Assign { target, .. } if target.base == "loc0"))
        .collect();
    assert!(
        loc0_defs.len() >= 2,
        "expected a store def and a load def for loc0; got {:?}",
        block.stmts
    );
    // The last loc0 def is the load: it must reference the stored var, NOT
    // be an opaque ldloc0() Call.
    let load_def = loc0_defs.last().copied().unwrap();
    let SsaStmt::Assign { value, .. } = load_def else {
        panic!("load def should be an Assign: {load_def:?}");
    };
    assert!(
        matches!(value, SsaExpr::Variable(_)),
        "LDLOC0 after STLOC0 should read the stored var; got {value:?}"
    );
    assert!(
        !matches!(value, SsaExpr::Call { .. }),
        "LDLOC0 should not stay an opaque ldloc0() call once a store exists; got {value:?}"
    );
}

#[test]
fn diamond_places_a_phi_at_the_merge() {
    // Build a diamond by hand so we control predecessor exit stacks:
    //   BB0 (entry) pushes 1, branches to BB1 / BB2
    //   BB1 pushes 10  -> jmp BB3
    //   BB2 pushes 20  -> jmp BB3
    //   BB3 (merge): the incoming slot should be a φ(BB1: 10, BB2: 20).
    let ins = vec![
        Instruction::new(0, OpCode::Push1, None), // BB0: push 1 (condition-ish)
        Instruction::new(0, OpCode::Pushint8, Some(Operand::I8(10))), // BB1
        Instruction::new(0, OpCode::Pushint8, Some(Operand::I8(20))), // BB2
        Instruction::new(0, OpCode::Ret, None),   // BB3
    ];

    let mut cfg = Cfg::new();
    cfg.add_block(BasicBlock::new(
        BlockId(0),
        0,
        1,
        0..1,
        Terminator::Branch {
            then_target: BlockId(1),
            else_target: BlockId(2),
        },
    ));
    cfg.add_block(BasicBlock::new(
        BlockId(1),
        1,
        2,
        1..2,
        Terminator::Jump { target: BlockId(3) },
    ));
    cfg.add_block(BasicBlock::new(
        BlockId(2),
        2,
        3,
        2..3,
        Terminator::Jump { target: BlockId(3) },
    ));
    cfg.add_block(BasicBlock::new(BlockId(3), 3, 4, 3..4, Terminator::Return));
    cfg.add_edge(BlockId(0), BlockId(1), EdgeKind::ConditionalTrue);
    cfg.add_edge(BlockId(0), BlockId(2), EdgeKind::ConditionalFalse);
    cfg.add_edge(BlockId(1), BlockId(3), EdgeKind::Unconditional);
    cfg.add_edge(BlockId(2), BlockId(3), EdgeKind::Unconditional);

    let ssa = SsaBuilder::new(&cfg, &ins).build();
    let merge = ssa.block(BlockId(3)).expect("merge block exists");
    assert!(
        merge.phi_count() >= 1,
        "merge block should have a phi node for the incoming value slot"
    );
    let phi = &merge.phi_nodes[0];
    assert_eq!(
        phi.operands.len(),
        2,
        "phi should have one operand per predecessor"
    );
}

#[test]
fn diamond_places_a_phi_for_a_slot() {
    // Two arms store different values to the same local; the merge loads it
    // and so needs a slot φ(loc0) over BB1 / BB2.
    //   BB0: Push1, STLOC0, Branch -> BB1 / BB2
    //   BB1: PUSH11, STLOC0 -> jmp BB3
    //   BB2: PUSH12, STLOC0 -> jmp BB3
    //   BB3: LDLOC0, RET
    let ins = vec![
        Instruction::new(0, OpCode::Push1, None),
        Instruction::new(1, OpCode::Stloc0, None),
        Instruction::new(0, OpCode::Push11, None),
        Instruction::new(1, OpCode::Stloc0, None),
        Instruction::new(0, OpCode::Push12, None),
        Instruction::new(1, OpCode::Stloc0, None),
        Instruction::new(0, OpCode::Ldloc0, None),
        Instruction::new(0, OpCode::Ret, None),
    ];

    let mut cfg = Cfg::new();
    cfg.add_block(BasicBlock::new(
        BlockId(0),
        0,
        1,
        0..2,
        Terminator::Branch {
            then_target: BlockId(1),
            else_target: BlockId(2),
        },
    ));
    cfg.add_block(BasicBlock::new(
        BlockId(1),
        1,
        2,
        2..4,
        Terminator::Jump { target: BlockId(3) },
    ));
    cfg.add_block(BasicBlock::new(
        BlockId(2),
        2,
        3,
        4..6,
        Terminator::Jump { target: BlockId(3) },
    ));
    cfg.add_block(BasicBlock::new(BlockId(3), 3, 4, 6..8, Terminator::Return));
    cfg.add_edge(BlockId(0), BlockId(1), EdgeKind::ConditionalTrue);
    cfg.add_edge(BlockId(0), BlockId(2), EdgeKind::ConditionalFalse);
    cfg.add_edge(BlockId(1), BlockId(3), EdgeKind::Unconditional);
    cfg.add_edge(BlockId(2), BlockId(3), EdgeKind::Unconditional);

    let ssa = SsaBuilder::new(&cfg, &ins).build();
    let merge = ssa.block(BlockId(3)).expect("merge block exists");

    let has_slot_phi = merge.phi_nodes.iter().any(|phi| phi.target.base == "loc0");
    assert!(
        has_slot_phi,
        "merge of two STLOC0 arms should place a loc0 φ; got {:?}",
        merge.phi_nodes
    );
}