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 crate::instruction::{Instruction, Operand};

use super::super::super::{HighLevelEmitter, LiteralValue};
use super::MAX_INTERNAL_CALL_ENTRY_DELTA;

impl HighLevelEmitter {
    pub(super) fn emit_internal_call(&mut self, instruction: &Instruction, target: usize) {
        let mut target = self.normalize_internal_call_target(target);
        if let Some(required_args) = self.method_arg_counts_by_offset.get(&target).copied() {
            if self.stack.len() < required_args {
                if let Some((candidate, _)) =
                    self.method_arg_counts_by_offset.range(..target).rev().find(
                        |(candidate, candidate_args)| {
                            target.saturating_sub(**candidate) <= MAX_INTERNAL_CALL_ENTRY_DELTA
                                && **candidate_args <= self.stack.len()
                                && self
                                    .method_labels_by_offset
                                    .get(*candidate)
                                    .map(|label| label.as_str() != "script_entry")
                                    .unwrap_or(false)
                        },
                    )
                {
                    target = *candidate;
                }
            }
        }
        let callee = self
            .resolve_internal_call_name(target)
            .map(str::to_string)
            .unwrap_or_else(|| format!("call_0x{target:04X}"));
        if let Some(arg_count) = self.method_arg_counts_by_offset.get(&target).copied() {
            self.push_comment(instruction);
            if self.stack.len() < arg_count {
                self.stack_underflow(instruction, arg_count);
                return;
            }
            let mut args = Vec::with_capacity(arg_count);
            for _ in 0..arg_count {
                if let Some(value) = self.pop_stack_value() {
                    // Internal calls use right-to-left push order (C
                    // convention), so popping yields display-order arguments.
                    args.push(value);
                }
            }
            let args = args.join(", ");
            let temp = self.next_temp();
            self.statements
                .push(format!("let {temp} = {callee}({args});"));
            self.stack.push(temp);
            return;
        }

        self.push_comment(instruction);
        let temp = self.next_temp();
        self.statements.push(format!("let {temp} = {callee}();"));
        self.stack.push(temp);
    }

    pub(in super::super::super) fn emit_relative_call(&mut self, instruction: &Instruction) {
        if let Some(target) = self.jump_target(instruction) {
            let resolved_target = self
                .call_targets_by_offset
                .get(&instruction.offset)
                .copied()
                .unwrap_or(target);
            self.emit_internal_call(instruction, resolved_target);
        } else if let Some(target) = self
            .call_targets_by_offset
            .get(&instruction.offset)
            .copied()
        {
            self.emit_internal_call(instruction, target);
        } else {
            self.warn(instruction, "call with unsupported operand (skipping)");
        }
    }

    pub(in super::super::super) fn emit_indirect_call(
        &mut self,
        instruction: &Instruction,
        label: &str,
    ) {
        match instruction.operand {
            Some(Operand::U16(value)) => self.emit_callt(instruction, label, value),
            None => self.emit_calla(instruction, label),
            _ => self.warn(instruction, &format!("{label} (unexpected operand)")),
        }
    }

    fn normalize_internal_call_target(&self, target: usize) -> usize {
        if self.method_labels_by_offset.contains_key(&target)
            || self.method_arg_counts_by_offset.contains_key(&target)
        {
            return target;
        }

        if let Some((candidate, _)) = self
            .method_arg_counts_by_offset
            .range(..=target)
            .next_back()
        {
            if target.saturating_sub(*candidate) <= MAX_INTERNAL_CALL_ENTRY_DELTA
                && self
                    .method_labels_by_offset
                    .get(candidate)
                    .map(|label| label.as_str() != "script_entry")
                    .unwrap_or(false)
            {
                return *candidate;
            }
        }

        if let Some((candidate, label)) = self.method_labels_by_offset.range(..=target).next_back()
        {
            if target.saturating_sub(*candidate) <= MAX_INTERNAL_CALL_ENTRY_DELTA
                && label.as_str() != "script_entry"
            {
                return *candidate;
            }
        }

        target
    }

    fn resolve_internal_call_name(&self, target: usize) -> Option<&str> {
        self.method_labels_by_offset
            .get(&target)
            .map(std::string::String::as_str)
    }

    fn emit_callt(&mut self, instruction: &Instruction, label: &str, value: u16) {
        // CALLT: token-based indirect call with a U16 operand. Resolve token
        // metadata so argument consumption and return behavior match the
        // declared method signature.
        let index = value as usize;
        let resolved = self.callt_labels.get(index).cloned();
        if let Some(name) = resolved {
            let arg_count = self.callt_param_counts.get(index).copied().unwrap_or(0);
            let returns_value = self.callt_returns_value.get(index).copied().unwrap_or(true);
            self.push_comment(instruction);
            // Always emit the token call, substituting `???` for missing args
            // when malformed bytecode or an earlier missed lift underflows.
            if self.stack.len() < arg_count {
                self.stack_underflow(instruction, arg_count);
            }
            let args = self
                .pop_call_arguments_with_placeholders(arg_count)
                .join(", ");
            if returns_value {
                let temp = self.next_temp();
                self.statements
                    .push(format!("let {temp} = {name}({args});"));
                self.stack.push(temp);
            } else {
                self.statements.push(format!("{name}({args});"));
            }
        } else {
            self.push_comment(instruction);
            let temp = self.next_temp();
            self.statements
                .push(format!("let {temp} = {label}(0x{value:04X});"));
            self.stack.push(temp);
        }
    }

    fn emit_calla(&mut self, instruction: &Instruction, label: &str) {
        // CALLA: stack-based indirect call; pop one stack entry as the target.
        let (target, literal) = self
            .pop_stack_value_with_literal()
            .unwrap_or_else(|| ("??".to_string(), None));
        if let Some(LiteralValue::Pointer(offset)) = literal {
            self.emit_internal_call(instruction, offset);
        } else if let Some(offset) = self
            .calla_targets_by_offset
            .get(&instruction.offset)
            .copied()
        {
            self.emit_internal_call(instruction, offset);
        } else {
            self.push_comment(instruction);
            let temp = self.next_temp();
            self.statements
                .push(format!("let {temp} = {label}({target});"));
            self.stack.push(temp);
        }
    }

    fn pop_call_arguments_with_placeholders(&mut self, arg_count: usize) -> Vec<String> {
        let mut args = Vec::with_capacity(arg_count);
        for _ in 0..arg_count {
            if let Some(value) = self.pop_stack_value() {
                args.push(value);
            } else {
                args.push("???".to_string());
            }
        }
        args
    }
}