neo-decompiler 0.11.0

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::super::*;
use crate::instruction::OpCode;

#[test]
fn high_level_packs_literal_arrays() {
    // Script: PUSH1, PUSH2, PUSH2, PACK, RET
    // PACK uses the literal count (2) to build an array from stack values.
    let script = [
        OpCode::Push1.byte(),
        OpCode::Push2.byte(),
        OpCode::Push2.byte(),
        OpCode::Pack.byte(),
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("pack 2 element"),
        "expected literal PACK to be lifted: {high_level}"
    );
    assert!(
        !high_level.contains("pack_dynamic"),
        "literal PACK should not fall back to dynamic form: {high_level}"
    );
}

#[test]
fn high_level_rewrites_pickitem_as_indexing() {
    // Script: NEWARRAY0, PUSH0, PICKITEM, RET
    let script = [
        OpCode::Newarray0.byte(),
        OpCode::Push0.byte(),
        OpCode::Pickitem.byte(),
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("t0[t1]"),
        "expected PICKITEM to be rewritten as indexing: {high_level}"
    );
    assert!(
        !high_level.contains(" get "),
        "infix get should not appear after rewrite: {high_level}"
    );
}

#[test]
fn high_level_rewrites_setitem_as_index_assignment() {
    // Script: NEWMAP, PUSH0, PUSH1, SETITEM, RET
    let script = [
        OpCode::Newmap.byte(),
        OpCode::Push0.byte(),
        OpCode::Push1.byte(),
        OpCode::Setitem.byte(),
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("t0[t1] = t2;"),
        "expected SETITEM to be rewritten as indexing assignment: {high_level}"
    );
    assert!(
        !high_level.contains("set_item("),
        "set_item helper should not appear after rewrite: {high_level}"
    );
}

#[test]
fn high_level_rewrites_haskey_as_function_call() {
    // Script: NEWMAP, PUSH0, HASKEY, RET
    let script = [
        OpCode::Newmap.byte(),
        OpCode::Push0.byte(),
        OpCode::Haskey.byte(),
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    // Clean mode (single-use inlining) is the path that previously malformed
    // the output: it inlines the map temp into the RET, yielding
    // `return t0 has_key 0`, which the infix->call rewrite then mangled into
    // `has_key(return t0, 0)`.
    let decompilation = Decompiler::new()
        .with_inline_single_use_temps(true)
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("has_key("),
        "expected HASKEY to be rewritten as function call: {high_level}"
    );
    assert!(
        !high_level.contains(" has_key "),
        "infix has_key should not appear after rewrite: {high_level}"
    );
    // Regression: when the HASKEY result flows straight into RET, the call
    // must stay a tail expression (`return has_key(...)`), not have the
    // `return` keyword swallowed into its first argument
    // (`has_key(return t0, 0)` — malformed, value lost).
    assert!(
        !high_level.contains("has_key(return"),
        "return must not be nested inside the has_key call: {high_level}"
    );
    assert!(
        high_level.contains("return has_key("),
        "HASKEY result should flow into the return: {high_level}"
    );
}

#[test]
fn high_level_pickitem_inside_call_keeps_brackets_balanced() {
    // Script: PUSH1, PUSH1, PACK (=> [1]), PUSH0, PICKITEM (=> arr[0]), SIZE
    // (=> len(arr[0])), RET. With single-use inlining the index access is
    // embedded in the len(...) call. The infix `get` rewrite split on ` get `
    // regardless of parentheses, producing the malformed `len(arr[0)]`;
    // emitting bracket form at the source keeps it `len(arr[0])`.
    let script = [
        OpCode::Push1.byte(),
        OpCode::Push1.byte(),
        OpCode::Pack.byte(),
        OpCode::Push0.byte(),
        OpCode::Pickitem.byte(),
        OpCode::Size.byte(),
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let high_level = Decompiler::new()
        .with_inline_single_use_temps(true)
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds")
        .high_level
        .expect("high-level output");
    assert!(
        !high_level.contains("[0)]") && !high_level.contains(" get "),
        "index access inside a call must not interleave `)` and `]`: {high_level}"
    );
    assert!(
        high_level.contains("len(") && high_level.contains("[0])"),
        "PICKITEM result inside len() should render as len(arr[0]): {high_level}"
    );
}

#[test]
fn high_level_istype_respects_operand_tag() {
    // Script: PUSH1, ISTYPE array (0x40), RET
    let script = [
        OpCode::Push1.byte(),
        OpCode::Istype.byte(),
        0x40, // StackItemType::Array type-byte operand (not Ret)
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");

    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("is_type_array("),
        "ISTYPE should map to a helper named for the operand tag: {high_level}"
    );
    assert!(
        !high_level.contains("is_type("),
        "High-level output should not use the two-argument shorthand: {high_level}"
    );
}

#[test]
fn pack_literal_underflow_renders_elision_marker_not_synthetic_temps() {
    // Regression (adversarial parity): PACK n with fewer than n values on the
    // stack must render the elided remainder as a single `/* N more */` marker
    // (matching the JS port) rather than synthesizing missing_pack_item() temps.
    // Script: PUSH1 PUSH1 PUSH1 PUSH5 PACK RET (3 values, PACK asks for 5).
    let script = [
        OpCode::Push1.byte(),
        OpCode::Push1.byte(),
        OpCode::Push1.byte(),
        OpCode::Push5.byte(),
        OpCode::Pack.byte(),
        OpCode::Ret.byte(),
    ];
    let nef_bytes = build_nef(&script);
    let decompilation = Decompiler::new()
        .decompile_bytes(&nef_bytes)
        .expect("decompile succeeds");
    let high_level = decompilation
        .high_level
        .as_deref()
        .expect("high-level output");
    assert!(
        high_level.contains("/* 2 more elements */"),
        "underflowing PACK should render an elision marker: {high_level}"
    );
    assert!(
        !high_level.contains("missing_pack_item"),
        "underflowing PACK should not synthesize missing_pack_item temps: {high_level}"
    );
}