neo-decompiler 0.10.2

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::instruction::OpCode;

#[test]
fn csharp_translates_loop_to_while_true() {
    // Script: INITSLOT; PUSH0; STLOC0; (loop top:) LDLOC0; PUSH3; LT;
    // JMPIFNOT to JMP; NOP; LDLOC0; PUSH1; ADD; STLOC0; JMP back-to-PUSH0; RET.
    // The JMP here targets the `STLOC0` initialization (an infinite reset
    // loop), so the high-level post-pass collapses the `label: ... goto label;`
    // pattern into `loop { ... }`. The C# emitter must rewrite that into a
    // valid C# `while (true)`.
    let script = [
        OpCode::Initslot.byte(),
        0x01,
        0x00,
        OpCode::Push0.byte(),
        OpCode::Stloc0.byte(),
        OpCode::Ldloc0.byte(),
        OpCode::Push3.byte(),
        OpCode::Lt.byte(),
        OpCode::Jmpifnot.byte(),
        0x07,
        OpCode::Nop.byte(),
        OpCode::Ldloc0.byte(),
        OpCode::Push1.byte(),
        OpCode::Add.byte(),
        OpCode::Stloc0.byte(),
        OpCode::Jmp.byte(),
        0xF4,
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("while (true) {"),
        "C# output should translate `loop {{` to `while (true) {{`: {csharp}"
    );
    assert!(
        !csharp.contains("loop {"),
        "C# output should not retain the high-level `loop` keyword: {csharp}"
    );
}

#[test]
fn csharp_translates_switch_to_idiomatic_c_sharp() {
    // Script: INITSLOT; STLOC0(1); equality chain on loc0 with cases 0,1,default
    let script = [
        OpCode::Initslot.byte(),
        0x01,
        0x00,
        OpCode::Push1.byte(),
        OpCode::Stloc0.byte(),
        OpCode::Ldloc0.byte(),
        OpCode::Push0.byte(),
        OpCode::Equal.byte(),
        OpCode::Jmpifnot.byte(),
        0x06,
        OpCode::Push10.byte(),
        OpCode::Stloc0.byte(),
        OpCode::Jmp.byte(),
        0x0D,
        OpCode::Ldloc0.byte(),
        OpCode::Push1.byte(),
        OpCode::Equal.byte(),
        OpCode::Jmpifnot.byte(),
        0x06,
        OpCode::Push11.byte(),
        OpCode::Stloc0.byte(),
        OpCode::Jmp.byte(),
        0x04,
        OpCode::Push12.byte(),
        OpCode::Stloc0.byte(),
        OpCode::Ldloc0.byte(),
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("switch (loc0) {"),
        "switch scrutinee should be parenthesised: {csharp}"
    );
    assert!(
        csharp.contains("case 0: {"),
        "case label should use C# `: {{` form: {csharp}"
    );
    assert!(
        csharp.contains("case 1: {"),
        "second case should also use `: {{`: {csharp}"
    );
    assert!(
        csharp.contains("default: {"),
        "default label should use `default: {{`: {csharp}"
    );
    // Each case body must end in a control-transfer statement; with the
    // simple PUSH/STLOC bodies here the emitter inserts `break;` before the
    // matching close brace so the switch compiles under C#.
    let break_count = csharp.matches("break;").count();
    assert!(
        break_count >= 3,
        "each case (including default) should end with `break;` (found {break_count}): {csharp}"
    );
    assert!(
        !csharp.contains("case 0 {"),
        "C# output should not retain the high-level `case X {{` form: {csharp}"
    );
    assert!(
        !csharp.contains("default {"),
        "C# output should not retain the high-level `default {{` form: {csharp}"
    );
}

#[test]
fn csharp_else_if_chain_uses_parenthesised_conditions() {
    // Same script as switch test — high-level emitter may or may not promote
    // it to a switch depending on heuristics; either way, any surviving
    // `else if` chain must be parenthesised in C#.
    let script = [
        OpCode::Initslot.byte(),
        0x01,
        0x00,
        OpCode::Push1.byte(),
        OpCode::Stloc0.byte(),
        OpCode::Ldloc0.byte(),
        OpCode::Push0.byte(),
        OpCode::Equal.byte(),
        OpCode::Jmpifnot.byte(),
        0x06,
        OpCode::Push10.byte(),
        OpCode::Stloc0.byte(),
        OpCode::Jmp.byte(),
        0x0D,
        OpCode::Ldloc0.byte(),
        OpCode::Push1.byte(),
        OpCode::Equal.byte(),
        OpCode::Jmpifnot.byte(),
        0x06,
        OpCode::Push11.byte(),
        OpCode::Stloc0.byte(),
        OpCode::Jmp.byte(),
        0x04,
        OpCode::Push12.byte(),
        OpCode::Stloc0.byte(),
        OpCode::Ldloc0.byte(),
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    // No bare `else if X {` (without parens) should survive.
    for line in csharp.lines() {
        let trimmed = line.trim();
        assert!(
            !trimmed.starts_with("else if ") || trimmed.starts_with("else if ("),
            "C# else-if must use parenthesised condition: {trimmed}"
        );
        assert!(
            !trimmed.starts_with("} else if ") || trimmed.starts_with("} else if ("),
            "C# `}} else if` must use parenthesised condition: {trimmed}"
        );
    }
}

#[test]
fn csharp_uses_label_style_for_transfer_placeholders() {
    // Script: ENDTRY +6 (jumps past intermediate code to the final RET),
    //         PUSH1, PUSH2, ADD, DROP, RET. The intermediate stack ops
    //         keep the `leave` from being a fallthrough so the lift
    //         exercises the label-style transfer path the C# emitter
    //         lowers to `goto label_X;`.
    let nef_bytes = build_nef(&[
        OpCode::Endtry.byte(),
        0x06,
        OpCode::Push1.byte(),
        OpCode::Push2.byte(),
        OpCode::Add.byte(),
        OpCode::Stloc5.byte(),
        OpCode::Ret.byte(),
    ]);

    let decompilation = Decompiler::new()
        .decompile_bytes_with_manifest(&nef_bytes, None, OutputFormat::All)
        .expect("decompile succeeds");

    let csharp = decompilation.csharp.as_deref().expect("csharp output");

    assert!(
        csharp.contains("goto label_0x0006;"),
        "C# should normalize leave-transfers to goto label style: {csharp}"
    );
    assert!(
        csharp.contains("label_0x0006:"),
        "C# should emit label declaration for transfer targets: {csharp}"
    );
    assert!(
        !csharp.contains("leave label_"),
        "C# should not emit non-C# leave statements: {csharp}"
    );
    assert!(
        !csharp.contains("leave_0x"),
        "C# should not emit legacy function-style transfer placeholders: {csharp}"
    );
}