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

#[test]
fn csharp_synthetic_script_entry_exposes_initslot_args_and_preserves_return() {
    // Bytecode: INITSLOT 0,1; LDARG0; ISNULL; NOT; RET — declares one
    // argument and returns its non-null-ness. Without a manifest, the
    // C# emitter must (a) surface the arg as a parameter so the body's
    // `arg0` reference resolves, and (b) preserve the lifted return
    // value (the previous hardcoded `void` signature silently dropped
    // it).
    let nef_bytes = build_nef(&[
        OpCode::Initslot.byte(),
        0x00,
        0x01,
        OpCode::Ldarg0.byte(),
        OpCode::Isnull.byte(),
        OpCode::Not.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("public static object ScriptEntry(object arg0)"),
        "synthetic ScriptEntry should declare INITSLOT-counted args: {csharp}"
    );
    // Verbose-mode (lib API default) keeps the temp; clean-mode would
    // inline. Either form preserves the return value, which is the
    // bug we care about — the previous `void` signature dropped it.
    assert!(
        csharp.contains("return !t0;") || csharp.contains("return !(arg0 is null);"),
        "lifted return value should be preserved (not dropped via void signature): {csharp}"
    );
    // Also verify the body actually references arg0 (not just the
    // signature) so the param isn't unused boilerplate.
    assert!(
        csharp.contains("(arg0 is null)") || csharp.contains("is_null(arg0)"),
        "body should reference arg0 from the new parameter: {csharp}"
    );
}

#[test]
fn csharp_omits_trailing_return_in_void_methods() {
    // Smallest script: a single RET. Without a manifest the synthetic
    // ScriptEntry now defaults to `object` return (so any pushed value
    // is preserved instead of dropped) — for a bare RET that produces
    // `return default;` in the body. The historical behavior (`void`
    // signature with `return;` stripped) was buggy for non-void scripts
    // (it silently discarded the lifted return value).
    let nef_bytes = build_nef(&[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("public static object ScriptEntry()"),
        "synthetic ScriptEntry should default to object return when no manifest is provided: {csharp}"
    );
    // Bare RET on empty stack lifts to `return;`. With a non-void
    // signature the C# render rewrites that to `return default;` to
    // satisfy the type system.
    assert!(
        csharp.contains("return default;"),
        "synthetic ScriptEntry with bare RET should yield `return default;`: {csharp}"
    );
}

#[test]
fn csharp_keeps_explicit_return_value_in_non_void_methods() {
    // Script: PUSH1 RET — high-level lifts as `return 1;`, which the C#
    // emitter must preserve since the method is non-void.
    let nef_bytes = build_nef(&[OpCode::Push1.byte(), OpCode::Ret.byte()]);
    let manifest = ContractManifest::from_json_str(
        r#"
            {
                "name": "ReturnsInt",
                "abi": {
                    "methods": [
                        {
                            "name": "main",
                            "parameters": [],
                            "returntype": "Integer",
                            "offset": 0
                        }
                    ],
                    "events": []
                },
                "permissions": [],
                "trusts": "*"
            }
            "#,
    )
    .expect("manifest parsed");

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

    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("return 1;"),
        "non-void method should keep its computed return: {csharp}"
    );
}

#[test]
fn csharp_non_void_method_with_empty_body_throws_not_implemented() {
    // `prologue` spans [0,3) = INITSLOT only, which lifts to no statements.
    // A non-void (Integer) return with no body is C# error CS0161, so the
    // renderer must emit a throwing stub rather than a bare comment.
    let nef_bytes = build_nef(&[
        OpCode::Initslot.byte(),
        0x00,
        0x00,
        OpCode::Push1.byte(),
        OpCode::Push2.byte(),
        OpCode::Add.byte(),
        OpCode::Ret.byte(),
        OpCode::Push16.byte(),
        OpCode::Ret.byte(),
    ]);
    let manifest = ContractManifest::from_json_str(
        r#"{"name":"Demo","abi":{"methods":[
            {"name":"prologue","returntype":"Integer","offset":0,"parameters":[],"safe":false},
            {"name":"body","returntype":"Integer","offset":3,"parameters":[],"safe":false}
        ],"events":[]}}"#,
    )
    .expect("manifest parsed");
    // Clean mode (trace comments off) is the user-facing compilable C# output —
    // the mode the CLI `decompile --format csharp` emits. There INITSLOT lifts
    // to no statements, so `prologue`'s body is empty.
    let decompilation = Decompiler::new()
        .with_trace_comments(false)
        .decompile_bytes_with_manifest(&nef_bytes, Some(manifest), OutputFormat::All)
        .expect("decompile succeeds");
    let csharp = decompilation.csharp.as_deref().expect("csharp output");
    assert!(
        csharp.contains("BigInteger prologue()"),
        "prologue should render with its declared return type: {csharp}"
    );
    assert!(
        csharp.contains("throw new NotImplementedException();"),
        "non-void method with an empty lifted body must throw, not emit a bare comment: {csharp}"
    );
}