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
//! Lightweight type inference for lifted Neo N3 bytecode.
//!
//! The Neo VM is dynamically typed and most syscalls do not encode argument
//! signatures in the bytecode. The goal of this module is therefore to provide
//! a best-effort type recovery pass that is:
//!
//! - conservative (falls back to `unknown`/`any` rather than guessing)
//! - useful for collection recovery and readability improvements
//! - deterministic and panic-free on malformed input

// Stack depth → i64 and type-tag byte reinterpretation casts are structurally
// safe: stack depth fits in i64, and the i8→u8 cast is intentional.
#![allow(
    clippy::cast_possible_truncation,
    clippy::cast_possible_wrap,
    clippy::cast_sign_loss
)]

mod call_effects;
mod return_depth;
mod return_effects;
mod simulation;
mod slot_ops;
mod stack_ops;
mod support;

use serde::Serialize;

use crate::instruction::Instruction;
use crate::manifest::ContractManifest;
use crate::nef::MethodToken;

use super::{MethodRef, MethodTable};
use call_effects::build_direct_call_effects_by_offset;
use simulation::infer_types_in_slice;
use support::{scan_slot_counts, scan_static_slot_count, type_from_manifest};

/// Primitive/value types inferred from the instruction stream.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[non_exhaustive]
pub enum ValueType {
    /// Unknown or not yet inferred.
    #[serde(rename = "unknown")]
    Unknown,
    /// Dynamic `any` value.
    #[serde(rename = "any")]
    Any,
    /// Null literal.
    #[serde(rename = "null")]
    Null,
    /// Boolean.
    #[serde(rename = "bool")]
    Boolean,
    /// Integer.
    #[serde(rename = "integer")]
    Integer,
    /// ByteString.
    #[serde(rename = "bytestring")]
    ByteString,
    /// Buffer.
    #[serde(rename = "buffer")]
    Buffer,
    /// Array.
    #[serde(rename = "array")]
    Array,
    /// Struct.
    #[serde(rename = "struct")]
    Struct,
    /// Map.
    #[serde(rename = "map")]
    Map,
    /// Interop interface.
    #[serde(rename = "interopinterface")]
    InteropInterface,
    /// Pointer.
    #[serde(rename = "pointer")]
    Pointer,
}

impl ValueType {
    fn join(self, other: Self) -> Self {
        use ValueType::*;
        if self == other {
            return self;
        }
        match (self, other) {
            (Unknown, x) | (x, Unknown) => x,
            (Null, _) | (_, Null) => Any,
            _ => Any,
        }
    }
}

/// Per-method inferred types.
#[derive(Debug, Clone, Serialize)]
pub struct MethodTypes {
    /// Method whose slots were analyzed.
    pub method: MethodRef,
    /// Inferred argument types indexed by argument slot.
    pub arguments: Vec<ValueType>,
    /// Inferred local types indexed by local slot.
    pub locals: Vec<ValueType>,
}

/// Aggregated type inference results.
#[derive(Debug, Clone, Default, Serialize)]
pub struct TypeInfo {
    /// Per-method inferred locals/arguments.
    pub methods: Vec<MethodTypes>,
    /// Inferred static slot types indexed by static slot.
    pub statics: Vec<ValueType>,
}

/// Infer primitive types and collection kinds from the instruction stream.
#[must_use]
pub fn infer_types(instructions: &[Instruction], manifest: Option<&ContractManifest>) -> TypeInfo {
    infer_types_with_method_tokens(instructions, manifest, &[])
}

/// Infer types with NEF method-token metadata available for CALLT stack effects.
#[must_use]
pub fn infer_types_with_method_tokens(
    instructions: &[Instruction],
    manifest: Option<&ContractManifest>,
    method_tokens: &[MethodToken],
) -> TypeInfo {
    let table = MethodTable::new(instructions, manifest);
    let static_count = scan_static_slot_count(instructions).unwrap_or(0);
    let mut statics = vec![ValueType::Unknown; static_count];
    let direct_call_effects =
        build_direct_call_effects_by_offset(&table, instructions, manifest, method_tokens);

    let mut methods = Vec::new();
    // `instructions` is sorted by offset and `table.spans()` is sorted by start
    // with contiguous ranges, so sweep a single forward cursor instead of
    // re-filtering the whole instruction stream per span (O(instructions *
    // spans), quadratic on call-dense bytecode). See build_xrefs.
    let mut cursor = 0usize;
    for span in table.spans() {
        while cursor < instructions.len() && instructions[cursor].offset < span.start {
            cursor += 1;
        }
        let begin = cursor;
        while cursor < instructions.len() && instructions[cursor].offset < span.end {
            cursor += 1;
        }
        let slice: Vec<&Instruction> = instructions[begin..cursor].iter().collect();

        let (locals_count, args_count) = scan_slot_counts(&slice).unwrap_or((0, 0));
        let mut locals = vec![ValueType::Unknown; locals_count];
        let mut arguments = vec![ValueType::Unknown; args_count];

        if let Some(manifest) = manifest {
            if let Some(index) = table.manifest_index_for_start(span.start) {
                if let Some(method) = manifest.abi.methods.get(index) {
                    if arguments.len() < method.parameters.len() {
                        arguments.resize(method.parameters.len(), ValueType::Unknown);
                    }
                    for (idx, param) in method.parameters.iter().enumerate() {
                        arguments[idx] = arguments[idx].join(type_from_manifest(&param.kind));
                    }
                }
            }
        }

        infer_types_in_slice(
            &slice,
            &mut locals,
            &mut arguments,
            &mut statics,
            method_tokens,
            &direct_call_effects,
        );

        methods.push(MethodTypes {
            method: span.method.clone(),
            arguments,
            locals,
        });
    }

    TypeInfo { methods, statics }
}