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
#!/usr/bin/env python3
"""
Regenerate src/opcodes_generated.rs from Neo project's OpCode definition.

Usage:
    python tools/generate_opcodes.py

The script expects tools/OpCode.cs to be present (downloaded from
https://github.com/neo-project/neo). The generated Rust source is written to
src/opcodes_generated.rs.
"""

import re
from pathlib import Path
from textwrap import dedent

REPO_ROOT = Path(__file__).resolve().parents[1]
OPCODE_SOURCE = REPO_ROOT / "tools" / "OpCode.cs"
LOCAL_OPCODE_SOURCE = REPO_ROOT / "neo_csharp" / "vm" / "src" / "Neo.VM" / "OpCode.cs"
OUTPUT = REPO_ROOT / "src" / "opcodes_generated.rs"


def to_rust_name(name: str) -> str:
    """Convert NEO opcode identifier to a CamelCase Rust variant name."""
    special_prefix = {
        "PUSHINT": lambda suffix: f"Pushint{suffix}",
        "PUSHDATA": lambda suffix: f"Pushdata{suffix}",
        "PUSH": lambda suffix: f"Push{suffix.title()}",
        "JMPIFNOT": lambda suffix: "Jmpifnot" + suffix.title(),
        "JMPIF": lambda suffix: "Jmpif" + suffix.title(),
        "JMP": lambda suffix: "Jmp" + suffix.title(),
        "CALL": lambda suffix: "Call" + suffix.title(),
        "INITSSLOT": lambda suffix: "Initsslot" + suffix.title(),
        "INITSLOT": lambda suffix: "Initslot" + suffix.title(),
    }
    for prefix, builder in special_prefix.items():
        if name.startswith(prefix):
            return builder(name[len(prefix) :])
    temp = name.lower().replace("_", " ")
    return temp.title().replace(" ", "")


def operand_encoding(name: str) -> str:
    """Return the OperandEncoding variant for the given opcode name."""
    if name.startswith("PUSHINT"):
        size = int(name[len("PUSHINT") :])
        mapping = {
            8: "OperandEncoding::I8",
            16: "OperandEncoding::I16",
            32: "OperandEncoding::I32",
            64: "OperandEncoding::I64",
        }
        if size in mapping:
            return mapping[size]
        return f"OperandEncoding::Bytes({size // 8})"
    if name == "PUSHA":
        # C# declares PUSHA's operand as a signed 32-bit relative offset
        # (backward pointers are legal), so decode it as I32 — not U32.
        return "OperandEncoding::I32"
    if name == "PUSHDATA1":
        return "OperandEncoding::Data1"
    if name == "PUSHDATA2":
        return "OperandEncoding::Data2"
    if name == "PUSHDATA4":
        return "OperandEncoding::Data4"
    short_targets = {
        "JMP",
        "JMPIF",
        "JMPIFNOT",
        "JMPEQ",
        "JMPNE",
        "JMPGT",
        "JMPGE",
        "JMPLT",
        "JMPLE",
        "CALL",
        "ENDTRY",
    }
    long_targets = {f"{base}_L" for base in short_targets}
    if name in short_targets:
        return "OperandEncoding::Jump8"
    if name in long_targets:
        return "OperandEncoding::Jump32"
    if name == "CALLT":
        return "OperandEncoding::U16"
    if name == "TRY":
        return "OperandEncoding::Bytes(2)"
    if name == "TRY_L":
        return "OperandEncoding::Bytes(8)"
    if name == "SYSCALL":
        return "OperandEncoding::Syscall"
    if name in {
        "INITSSLOT",
        "LDSFLD",
        "STSFLD",
        "LDLOC",
        "STLOC",
        "LDARG",
        "STARG",
        "NEWARRAY_T",
        "ISTYPE",
        "CONVERT",
    }:
        return "OperandEncoding::U8"
    if name == "INITSLOT":
        return "OperandEncoding::Bytes(2)"
    return "OperandEncoding::None"


def resolve_opcode_source() -> Path:
    if OPCODE_SOURCE.exists():
        return OPCODE_SOURCE
    if LOCAL_OPCODE_SOURCE.exists():
        return LOCAL_OPCODE_SOURCE
    return OPCODE_SOURCE


def parse_opcodes(source: Path) -> list[tuple[str, int]]:
    pattern = re.compile(
        r"(?:\[OperandSize\([^\]]*\)\]\s*)?(\w+)\s*=\s*0x([0-9A-Fa-f]{2})",
        re.MULTILINE,
    )
    matches = pattern.findall(source.read_text())
    return [(name, int(value, 16)) for name, value in matches]


def main() -> None:
    source = resolve_opcode_source()
    entries = parse_opcodes(source)

    variants = []
    from_byte = []
    to_byte = []
    mnemonics = []
    encodings = []

    for name, value in entries:
        rust_name = to_rust_name(name)
        variants.append(f"    {rust_name},")
        from_byte.append(f"            0x{value:02X} => OpCode::{rust_name},")
        to_byte.append(f"            OpCode::{rust_name} => 0x{value:02X},")
        mnemonics.append(f"            OpCode::{rust_name} => \"{name}\",")
        encodings.append(
            f"            OpCode::{rust_name} => {operand_encoding(name)},"
        )

    template = dedent(
        """\
        // This file is @generated by tools/generate_opcodes.py. Do not edit manually.
        #[allow(non_camel_case_types)]
        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
        pub enum OpCode {{
        {variants}
            Unknown(u8),
        }}

        impl OpCode {{
            pub fn from_byte(byte: u8) -> Self {{
                match byte {{
        {from_byte}
                    other => OpCode::Unknown(other),
                }}
            }}

            pub fn byte(self) -> u8 {{
                match self {{
        {to_byte}
                    OpCode::Unknown(value) => value,
                }}
            }}

            pub fn mnemonic(self) -> &'static str {{
                match self {{
        {mnemonics}
                    OpCode::Unknown(_) => "UNKNOWN",
                }}
            }}

            pub fn operand_encoding(self) -> OperandEncoding {{
                match self {{
        {encodings}
                    OpCode::Unknown(_) => OperandEncoding::None,
                }}
            }}
        }}
        """
    )

    generated = template.format(
        variants="\n".join(variants),
        from_byte="\n".join(from_byte),
        to_byte="\n".join(to_byte),
        mnemonics="\n".join(mnemonics),
        encodings="\n".join(encodings),
    )

    OUTPUT.write_text(generated)
    print(f"wrote {OUTPUT}")


if __name__ == "__main__":
    source = resolve_opcode_source()
    if not source.exists():
        raise SystemExit(
            f"missing opcode source. Looked for {OPCODE_SOURCE} and {LOCAL_OPCODE_SOURCE}."
        )
    main()