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::super::analysis::call_graph::CallGraph;
use super::super::analysis::types::TypeInfo;
use super::super::helpers::{
    build_call_targets_by_offset, build_calla_targets_by_offset, build_method_arg_counts_by_offset,
    build_method_labels_by_offset, inferred_method_starts, inferred_type_to_pseudo,
    sanitize_identifier,
};
use crate::decompiler::output_format::RenderOptions;
use crate::instruction::Instruction;
use crate::manifest::ContractManifest;
use crate::native_contracts;
use crate::nef::NefFile;

use std::collections::{BTreeMap, BTreeSet, HashSet};

mod body;
mod entry;
pub(super) mod header;
mod manifest_summary;
mod method_tokens;
mod methods;

pub(crate) struct HighLevelRender {
    pub(crate) text: String,
    pub(crate) warnings: Vec<String>,
}

/// Render the high-level pseudo-contract view.
pub(crate) fn render_high_level(
    nef: &NefFile,
    instructions: &[Instruction],
    manifest: Option<&ContractManifest>,
    call_graph: &CallGraph,
    types: &TypeInfo,
    opts: &RenderOptions,
) -> HighLevelRender {
    use std::fmt::Write;

    let inline_single_use_temps = opts.inline_single_use_temps;
    let emit_trace_comments = opts.emit_trace_comments;

    let mut output = String::new();
    let mut warnings = Vec::new();
    let mut used_method_names = HashSet::new();

    // Pre-resolve CALLT method-token labels so the emitter can emit friendly names.
    let callt_labels: Vec<String> = nef
        .method_tokens
        .iter()
        .map(|token| {
            if let Some(hint) = native_contracts::describe_method_token(&token.hash, &token.method)
            {
                hint.formatted_label(&token.method)
            } else {
                token.method.clone()
            }
        })
        .collect();
    let callt_param_counts: Vec<usize> = nef
        .method_tokens
        .iter()
        .map(|token| token.parameters_count as usize)
        .collect();
    let callt_returns_value: Vec<bool> = nef
        .method_tokens
        .iter()
        .map(|token| token.has_return_value)
        .collect();

    header::write_contract_header(&mut output, nef, manifest);

    let inferred_starts = inferred_method_starts(instructions, manifest);
    let method_labels_by_offset = build_method_labels_by_offset(
        instructions,
        &inferred_starts,
        manifest,
        sanitize_identifier,
        "script_entry",
    );
    let method_arg_counts_by_offset =
        build_method_arg_counts_by_offset(instructions, &inferred_starts, manifest);
    let call_targets_by_offset = if manifest.is_some() {
        build_call_targets_by_offset(call_graph)
    } else {
        BTreeMap::new()
    };
    let calla_targets_by_offset = build_calla_targets_by_offset(call_graph);
    let noreturn_method_offsets = build_noreturn_method_offsets(instructions, &inferred_starts);
    let slot_types_by_offset = build_slot_types_by_offset(types);
    let body_context = body::MethodBodyContext {
        method_labels_by_offset: &method_labels_by_offset,
        method_arg_counts_by_offset: &method_arg_counts_by_offset,
        call_targets_by_offset: &call_targets_by_offset,
        calla_targets_by_offset: &calla_targets_by_offset,
        noreturn_method_offsets: &noreturn_method_offsets,
        inline_single_use_temps,
        emit_trace_comments,
        typed_declarations: opts.typed_declarations,
        slot_types_by_offset: &slot_types_by_offset,
        callt_labels: &callt_labels,
        callt_param_counts: &callt_param_counts,
        callt_returns_value: &callt_returns_value,
    };
    let methods_context = methods::MethodsContext {
        instructions,
        inferred_method_starts: &inferred_starts,
        body_context: &body_context,
    };
    let entry_method = entry::write_entry_method(
        &mut output,
        instructions,
        &inferred_starts,
        manifest,
        &body_context,
        &mut warnings,
        &mut used_method_names,
    );
    if let Some(manifest) = manifest {
        methods::write_manifest_methods(
            &mut output,
            &methods_context,
            manifest,
            entry_method.as_ref(),
            &mut warnings,
            &mut used_method_names,
        );
    }
    methods::write_inferred_methods(
        &mut output,
        &methods_context,
        manifest,
        entry_method.as_ref(),
        &mut warnings,
        &mut used_method_names,
    );
    writeln!(output, "}}").unwrap();

    HighLevelRender {
        text: output,
        warnings,
    }
}

/// Build per-method high-level slot types from the inferred [`TypeInfo`].
fn build_slot_types_by_offset(types: &TypeInfo) -> BTreeMap<usize, body::SlotTypes> {
    let statics: Vec<&'static str> = types
        .statics
        .iter()
        .map(|t| inferred_type_to_pseudo(*t))
        .collect();
    types
        .methods
        .iter()
        .map(|method| {
            let arguments = method
                .arguments
                .iter()
                .map(|t| inferred_type_to_pseudo(*t))
                .collect();
            let locals = method
                .locals
                .iter()
                .map(|t| inferred_type_to_pseudo(*t))
                .collect();
            (
                method.method.offset,
                body::SlotTypes {
                    arguments,
                    locals,
                    statics: statics.clone(),
                },
            )
        })
        .collect()
}

/// Compute the set of method start offsets whose bodies always terminate
/// without returning (every exit path ends with ABORT, ABORTMSG, or THROW).
///
/// This is a conservative heuristic: a method is considered noreturn only if
/// its last instruction is a terminating opcode and it contains no RET.
fn build_noreturn_method_offsets(
    instructions: &[Instruction],
    inferred_starts: &[usize],
) -> BTreeSet<usize> {
    use crate::instruction::OpCode;

    let mut noreturn = BTreeSet::new();
    let all_starts: Vec<usize> = inferred_starts.to_vec();

    for (idx, &start) in all_starts.iter().enumerate() {
        let end = all_starts
            .get(idx + 1)
            .copied()
            .or_else(|| instructions.last().map(|i| i.offset + 1))
            .unwrap_or(start);

        let lo = instructions.partition_point(|ins| ins.offset < start);
        let hi = instructions.partition_point(|ins| ins.offset < end);
        let slice = &instructions[lo..hi];
        if slice.is_empty() {
            continue;
        }

        let has_ret = slice.iter().any(|ins| ins.opcode == OpCode::Ret);
        let last_is_terminator = slice.last().is_some_and(|ins| {
            matches!(ins.opcode, OpCode::Abort | OpCode::Abortmsg | OpCode::Throw)
        });

        if !has_ret && last_is_terminator {
            noreturn.insert(start);
        }
    }

    noreturn
}