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

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

impl HighLevelEmitter {
    pub(in super::super::super) fn emit_return(&mut self, instruction: &Instruction) {
        self.push_comment(instruction);
        if self.returns_void {
            // Void method: discard any leftover stack values.
            self.statements.push("return;".into());
        } else if let Some(value) = self.pop_stack_value() {
            self.statements.push(format!("return {value};"));
        } else {
            self.statements.push("return;".into());
        }
        self.stack.clear();
    }

    pub(in super::super::super) fn emit_syscall(&mut self, instruction: &Instruction) {
        self.push_comment(instruction);
        if let Some(Operand::Syscall(hash)) = instruction.operand {
            // `System.Contract.Call(scriptHash, method, callFlags, args)` is the
            // cross-contract-call syscalls the devpack emits. Lifting it into
            // `ContractName::method(args)` form is what lets a reader follow
            // the contract's logic without first mentally mapping every
            // `syscall("System.Contract.Call", ...)` back to its target.
            if hash == crate::syscalls::CONTRACT_CALL_HASH {
                self.emit_contract_call(instruction, hash);
                return;
            }
            if hash == crate::syscalls::CONTRACT_CALL_NATIVE_HASH {
                self.emit_contract_call_native(instruction, hash);
                return;
            }

            let info = crate::syscalls::lookup(hash);
            // When we do not have metadata, assume the syscall returns a value.
            // This is conservative (avoids stack underflow) and matches Neo's
            // "unknown syscalls push an item" convention.
            let returns_value = info.map(|i| i.returns_value).unwrap_or(true);
            let param_count = info.map(|i| i.param_count).unwrap_or(0) as usize;
            let syscall_name = info.map(|i| i.name).unwrap_or("unknown syscall");

            // Syscall arguments are pushed right-to-left (Cdecl) by the
            // devpack, so parameters[0] sits on top of the stack at SYSCALL
            // and `ApplicationEngine.OnSysCall` pops it first: pop order
            // already equals declaration order.
            let mut args: Vec<String> = Vec::with_capacity(param_count);
            let mut missing_argument = false;
            for _ in 0..param_count {
                match self.pop_stack_value() {
                    Some(value) => args.push(value),
                    None => {
                        missing_argument = true;
                        args.push("???".into());
                    }
                }
            }
            if missing_argument {
                let mut message =
                    format!("missing syscall argument values for {syscall_name} (substituted ???)");
                if let Some(context) =
                    self.missing_syscall_argument_context(instruction, syscall_name)
                {
                    message.push_str("; ");
                    message.push_str(&context);
                }
                // Use `warn(...)` so the inline `// XXXX: missing syscall
                // argument values for Foo (substituted ???)` comment fires
                // regardless of trace mode — earlier this delegated to
                // `note(...)` which gated on `emit_trace_comments`, so a
                // reader of the clean-mode rendering saw `???` placeholders
                // with no inline explanation. The JS port has always
                // emitted the comment unconditionally.
                self.warn(instruction, &message);
            }
            // Do NOT reverse: unlike `emit_call`'s VM intrinsics (operands
            // pushed left-to-right), syscall pop order is declaration order.
            // This matches the internal-call/CALLT convention documented in
            // `control_flow/jumps.rs`.
            let arg_list = args.join(", ");

            if let Some(info) = info {
                let call = if arg_list.is_empty() {
                    format!("{}()", info.name)
                } else {
                    format!("{}({})", info.name, arg_list)
                };
                // For known syscalls the name already identifies the call;
                // the 32-bit hash adds no information and clutters output.
                // Keep it only as a debug aid when trace comments are on.
                let trailing = if self.emit_trace_comments {
                    format!(" // 0x{hash:08X}")
                } else {
                    String::new()
                };
                if returns_value {
                    let temp = self.next_temp();
                    self.statements
                        .push(format!("let {temp} = {call};{trailing}"));
                    self.stack.push(temp);
                } else {
                    self.statements.push(format!("{call};{trailing}"));
                }
            } else {
                // Unknown syscall: keep the `syscall(0xHASH)` wrapper
                // because we have no human-readable name to substitute.
                let call = format!("syscall(0x{hash:08X})");
                self.warn(instruction, &format!("unknown syscall 0x{hash:08X}"));
                if returns_value {
                    let temp = self.next_temp();
                    self.statements.push(format!("let {temp} = {call};"));
                    self.stack.push(temp);
                } else {
                    self.statements.push(format!("{call};"));
                }
            }
        } else {
            self.statements.push(format!(
                "// {:04X}: missing syscall operand",
                instruction.offset
            ));
        }
    }

    /// Lift `System.Contract.Call(scriptHash, method, callFlags, args)` into a
    /// direct `ContractName::method(args)` call.
    ///
    /// The devpack pushes the four arguments right-to-left (Cdecl), so at the
    /// SYSCALL the stack top-to-bottom is `scriptHash, method, callFlags,
    /// args`. We pop in that order — the popped values already match the
    /// C#-style `ContractName::method(args)` argument order. The contract
    /// hash is looked up against the bundled native-contract table; when
    /// the hash isn't recognised (custom contracts) we fall back to the
    /// hex form `0xHASH::method(args)` so the reader still sees the target
    /// without having to grep the manifest.
    ///
    /// Fall-back behaviour: if the contract hash on the stack isn't a tracked
    /// 20-byte literal (synthetic test bytecode, or a hash produced by a
    /// runtime computation), the expansion would render the call as
    /// `t0::t1(t3)` — syntactically valid but unreadable. In that case we
    /// fall back to the legacy `syscall("System.Contract.Call", hash, …)`
    /// form so the reader still sees the devpack's intended syscall.
    fn emit_contract_call(&mut self, instruction: &Instruction, hash: u32) {
        let contract_hash = self.pop_stack_value_with_literal();
        let method = self.pop_stack_value_with_literal();
        let flags = self.pop_stack_value();
        let args_array = self.pop_stack_value();

        let args_str = args_array.as_deref().unwrap_or("???");
        let trailing = if self.emit_trace_comments {
            format!(" // 0x{hash:08X}")
        } else {
            String::new()
        };

        // Hash isn't a tracked 20-byte literal — fall back to the syscall
        // form so the reader still sees the devpack's intended syscall.
        let contract_hash_is_tracked = matches!(
            contract_hash.as_ref().map(|(_, lit)| lit),
            Some(Some(LiteralValue::ContractHash(_)))
        );
        if !contract_hash_is_tracked {
            let hash_str = contract_hash
                .as_ref()
                .map(|(s, _)| s.as_str())
                .unwrap_or("???");
            let method_str = method.as_ref().map(|(s, _)| s.as_str()).unwrap_or("???");
            let flags_str = flags.as_deref().unwrap_or("???");
            let call =
                format!("System.Contract.Call({hash_str}, {method_str}, {flags_str}, {args_str})");
            self.warn(
                instruction,
                "contract-call hash was not a tracked 20-byte literal; fell back to syscall form",
            );
            self.statements.push(format!("{call};{trailing}"));
            return;
        }

        let call_label = Self::contract_call_label(contract_hash.as_ref(), method.as_ref());

        // The call always returns a value on the evaluation stack — even a
        // `void` target method leaves its return on the stack for the
        // caller to drop or use. Match the surrounding emitter's
        // convention of pushing a fresh temp for any value-producing call.
        let temp = self.next_temp();
        self.statements
            .push(format!("let {temp} = {call_label}({args_str});{trailing}"));
        self.stack.push(temp);
    }

    /// Resolve the `ContractName::method` (or `0xHASH::method` for unknown
    /// custom contracts) display label for a `System.Contract.Call`.
    ///
    /// Both the contract hash and the method name are taken from the popped
    /// stack values' literal metadata: a literal hash lifts to a bundled
    /// native contract name, and a literal string method lifts to the
    /// human-readable method identifier. When either literal is missing we
    /// fall back to the temp name so the call site still references the
    /// actual value instead of an opaque syscall wrapper.
    fn contract_call_label(
        contract_hash: Option<&(String, Option<LiteralValue>)>,
        method_value: Option<&(String, Option<LiteralValue>)>,
    ) -> String {
        let contract_part = match contract_hash {
            Some((_, Some(LiteralValue::ContractHash(bytes)))) => {
                if let Some(contract) = crate::native_contracts::lookup(bytes) {
                    contract.name.to_string()
                } else {
                    format!("0x{}", hex::encode_upper(bytes))
                }
            }
            Some((s, _)) => s.clone(),
            None => "???".to_string(),
        };
        // Note: the test hex above uses 0xHEXUPPER (matching format_pushdata
        // and the disassembler's operand Display) so the reader can grep
        // the manifest for the exact literal.
        let method_part = match method_value {
            Some((_, Some(LiteralValue::String(s)))) => s.clone(),
            Some((s, _)) => s.clone(),
            None => "?".to_string(),
        };
        format!("{contract_part}.{method_part}")
    }

    /// Lift `System.Contract.CallNative(nativeId)` into a placeholder
    /// `native_call_<id>()` call. `CallNative` is the deprecated
    /// integer-indexed path; the modern devpack uses `System.Contract.Call`
    /// with the native hash instead, so this branch only fires on legacy
    /// bytecode.
    fn emit_contract_call_native(&mut self, _instruction: &Instruction, hash: u32) {
        let _ = self.pop_stack_value();
        let trailing = if self.emit_trace_comments {
            format!(" // 0x{hash:08X}")
        } else {
            String::new()
        };
        self.statements
            .push(format!("System.Contract.CallNative();{trailing}"));
    }

    fn missing_syscall_argument_context(
        &self,
        instruction: &Instruction,
        syscall_name: &str,
    ) -> Option<String> {
        let &instruction_index = self.index_by_offset.get(&instruction.offset)?;
        let previous = instruction_index
            .checked_sub(1)
            .and_then(|index| self.program.get(index))?;
        let (kind, index) = Self::store_slot_context(previous)?;
        let slot_name = Self::format_slot_label(kind, index);
        let stored_value = if self.packed_values_by_name.contains_key(&slot_name) {
            "a packed value"
        } else {
            "the last produced value"
        };
        Some(format!(
            "preceding {} stored {stored_value} into {slot_name}; no value remains on the evaluation stack before {syscall_name}",
            previous.opcode
        ))
    }

    fn store_slot_context(instruction: &Instruction) -> Option<(SlotKind, usize)> {
        use OpCode::{
            Starg, Starg0, Starg1, Starg2, Starg3, Starg4, Starg5, Starg6, Stloc, Stloc0, Stloc1,
            Stloc2, Stloc3, Stloc4, Stloc5, Stloc6, Stsfld, Stsfld0, Stsfld1, Stsfld2, Stsfld3,
            Stsfld4, Stsfld5, Stsfld6,
        };

        match instruction.opcode {
            Stloc0 => Some((SlotKind::Local, 0)),
            Stloc1 => Some((SlotKind::Local, 1)),
            Stloc2 => Some((SlotKind::Local, 2)),
            Stloc3 => Some((SlotKind::Local, 3)),
            Stloc4 => Some((SlotKind::Local, 4)),
            Stloc5 => Some((SlotKind::Local, 5)),
            Stloc6 => Some((SlotKind::Local, 6)),
            Stloc => Self::operand_slot_index(instruction).map(|index| (SlotKind::Local, index)),
            Starg0 => Some((SlotKind::Argument, 0)),
            Starg1 => Some((SlotKind::Argument, 1)),
            Starg2 => Some((SlotKind::Argument, 2)),
            Starg3 => Some((SlotKind::Argument, 3)),
            Starg4 => Some((SlotKind::Argument, 4)),
            Starg5 => Some((SlotKind::Argument, 5)),
            Starg6 => Some((SlotKind::Argument, 6)),
            Starg => Self::operand_slot_index(instruction).map(|index| (SlotKind::Argument, index)),
            Stsfld0 => Some((SlotKind::Static, 0)),
            Stsfld1 => Some((SlotKind::Static, 1)),
            Stsfld2 => Some((SlotKind::Static, 2)),
            Stsfld3 => Some((SlotKind::Static, 3)),
            Stsfld4 => Some((SlotKind::Static, 4)),
            Stsfld5 => Some((SlotKind::Static, 5)),
            Stsfld6 => Some((SlotKind::Static, 6)),
            Stsfld => Self::operand_slot_index(instruction).map(|index| (SlotKind::Static, index)),
            _ => None,
        }
    }

    fn operand_slot_index(instruction: &Instruction) -> Option<usize> {
        match instruction.operand {
            Some(Operand::U8(value)) => Some(value as usize),
            _ => None,
        }
    }

    fn format_slot_label(kind: SlotKind, index: usize) -> String {
        match kind {
            SlotKind::Local => format!("loc{index}"),
            SlotKind::Argument => format!("arg{index}"),
            SlotKind::Static => format!("static{index}"),
        }
    }
}