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::expressions::csharpize_expression;
use super::slots::SlotTypes;
use super::syntax::split_top_level_comma;

pub(in crate::decompiler) fn csharpize_statement(line: &str) -> String {
    csharpize_statement_typed(line, &SlotTypes::default())
}

/// Typed-declaration variant of [`csharpize_statement`].
///
/// When `types` knows a C# type for the declared local/static slot, the `let`
/// line is rendered as `<type> name = ...;` instead of `var name = ...;`.
/// Passing an empty [`SlotTypes`] reproduces the historical `var` behaviour
/// exactly.
pub(in crate::decompiler) fn csharpize_statement_typed(line: &str, types: &SlotTypes) -> String {
    let trimmed = line.trim();
    if trimmed.is_empty() {
        return String::new();
    }
    if trimmed.starts_with("//") {
        return trimmed.to_string();
    }
    if let Some(stripped) = trimmed.strip_prefix("let ") {
        // The declared name is the first token of the initialiser (up to the
        // first whitespace or `=`); consult the inferred-type map and prefer a
        // concrete C# type over `var` when one is known.
        let name = stripped
            .split(|c: char| c.is_whitespace() || c == '=')
            .next()
            .unwrap_or("");
        let body = csharpize_expression(stripped);
        match types.declaration_type(name) {
            Some(ty) => return format!("{ty} {body}"),
            None => return format!("var {body}"),
        }
    }
    if let Some(header) = csharpize_for_header(trimmed, Some(types)) {
        return header;
    }
    csharpize_statement_untyped(trimmed)
}

/// Historical line-level rewrite that does not depend on slot types.
fn csharpize_statement_untyped(trimmed: &str) -> String {
    if trimmed == "loop {" {
        return "while (true) {".to_string();
    }
    if let Some(condition) = trimmed
        .strip_prefix("if ")
        .and_then(|rest| rest.strip_suffix(" {"))
    {
        return format!("if ({}) {{", csharpize_expression(condition.trim()));
    }
    if let Some(condition) = trimmed
        .strip_prefix("else if ")
        .and_then(|rest| rest.strip_suffix(" {"))
    {
        return format!("else if ({}) {{", csharpize_expression(condition.trim()));
    }
    if let Some(condition) = trimmed
        .strip_prefix("} else if ")
        .and_then(|rest| rest.strip_suffix(" {"))
    {
        return format!("}} else if ({}) {{", csharpize_expression(condition.trim()));
    }
    if let Some(condition) = trimmed
        .strip_prefix("while ")
        .and_then(|rest| rest.strip_suffix(" {"))
    {
        return format!("while ({}) {{", csharpize_expression(condition.trim()));
    }
    if let Some(header) = csharpize_for_header(trimmed, None) {
        return header;
    }
    if let Some(scrutinee) = trimmed
        .strip_prefix("switch ")
        .and_then(|rest| rest.strip_suffix(" {"))
    {
        return format!("switch ({}) {{", csharpize_expression(scrutinee.trim()));
    }
    if let Some(value) = trimmed
        .strip_prefix("case ")
        .and_then(|rest| rest.strip_suffix(" {"))
    {
        return format!("case {}: {{", value.trim());
    }
    if trimmed == "default {" {
        return "default: {".to_string();
    }
    if let Some(target) = trimmed.strip_prefix("leave ") {
        return format!("goto {target}");
    }
    // The high-level emitter renders NEO's `THROW` opcode as
    // `throw(value);` -- NEO can throw any stack value (int, string, byte[],
    // etc.). `System.Exception`'s constructor takes a `string`, so non-string
    // operands need to be coerced.
    if let Some(rest) = trimmed
        .strip_prefix("throw(")
        .and_then(|r| r.strip_suffix(");"))
    {
        let operand = csharpize_expression(rest);
        return format!(
            "throw new Exception({});",
            wrap_exception_operand_for_csharp(&operand)
        );
    }
    // NEO `ABORT` / `ABORTMSG` are uncatchable VM aborts. C# has no direct
    // equivalent, but `throw new Exception(...)` terminates execution the same
    // way and reads naturally for a post-decompile reader.
    if trimmed == "abort();" {
        return "throw new Exception();".to_string();
    }
    if let Some(rest) = trimmed
        .strip_prefix("abort(")
        .and_then(|r| r.strip_suffix(");"))
    {
        let operand = csharpize_expression(rest);
        return format!(
            "throw new Exception({});",
            wrap_exception_operand_for_csharp(&operand)
        );
    }
    // NEO's `ASSERT` / `ASSERTMSG` are runtime checks: throw if the condition
    // is false. C# has no `assert(...)` keyword/function in scope, so render a
    // universal `if (!(cond)) throw new Exception(...);` form.
    if let Some(args) = trimmed
        .strip_prefix("assert(")
        .and_then(|r| r.strip_suffix(");"))
    {
        if let Some((cond, message)) = split_top_level_comma(args) {
            let message_expr = csharpize_expression(message.trim());
            return format!(
                "if (!({})) throw new Exception({});",
                csharpize_expression(cond.trim()),
                wrap_exception_operand_for_csharp(&message_expr)
            );
        }
        return format!(
            "if (!({})) throw new Exception();",
            csharpize_expression(args.trim())
        );
    }
    csharpize_expression(trimmed)
}

fn csharpize_for_header(trimmed: &str, types: Option<&SlotTypes>) -> Option<String> {
    if !trimmed.starts_with("for (") || !trimmed.ends_with(" {") {
        return None;
    }
    let inner = &trimmed[4..trimmed.len() - 2];
    let inner = inner.strip_prefix('(').unwrap_or(inner);
    let inner = inner.strip_suffix(')').unwrap_or(inner).trim();
    let converted = if let Some(stripped) = inner.strip_prefix("let ") {
        let name = stripped
            .split(|c: char| c.is_whitespace() || c == '=')
            .next()
            .unwrap_or("");
        let ty = types
            .and_then(|slot_types| slot_types.declaration_type(name))
            .unwrap_or("var");
        format!("{ty} {stripped}")
    } else {
        inner.to_string()
    };
    Some(format!("for ({}) {{", csharpize_expression(&converted)))
}

/// Coerce a `throw(...)` / `abort(...)` operand into something
/// `new Exception(string)` accepts. NEO bytecode can THROW any stack value;
/// non-string operands are wrapped in C# string interpolation.
fn wrap_exception_operand_for_csharp(operand: &str) -> String {
    let trimmed = operand.trim();
    if operand_appears_string_typed(trimmed) {
        operand.to_string()
    } else {
        format!("$\"{{{trimmed}}}\"")
    }
}

/// Return `true` when `text` plausibly evaluates to a `string` without further
/// coercion -- either a single string literal or any expression containing one.
fn operand_appears_string_typed(text: &str) -> bool {
    text.contains('"')
}

/// Returns true if `line` already terminates control flow such that a trailing
/// `break;` would be unreachable. Used by C# switch-case rendering to skip
/// inserting a redundant `break;` after `return`/`throw`/`goto`/`break`.
pub(in crate::decompiler) fn line_is_csharp_terminator(line: &str) -> bool {
    let trimmed = line.trim();
    trimmed.starts_with("return ")
        || trimmed == "return;"
        || trimmed.starts_with("return;")
        || trimmed.starts_with("throw ")
        || trimmed == "throw;"
        || trimmed.starts_with("goto ")
        || trimmed == "break;"
        || trimmed == "continue;"
}

#[cfg(test)]
mod tests {
    use super::{csharpize_statement_typed, SlotTypes};

    #[test]
    fn typed_for_headers_preserve_inferred_slot_types() {
        let types = SlotTypes {
            arguments: vec![],
            locals: vec!["BigInteger"],
            statics: vec![],
        };
        assert_eq!(
            csharpize_statement_typed("for (let loc0 = 0; loc0 < 3; loc0++) {", &types),
            "for (BigInteger loc0 = 0; loc0 < 3; loc0++) {"
        );
    }
}