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
import { SYSCALLS } from "./generated/syscalls.js";
import { handleSlotInstruction } from "./type-slot-ops.js";
import { handleStackInstruction } from "./type-stack-ops.js";
import { asCount, constrainOrigins, convertTargetType, directCallTarget, intLiteral, pushaTarget } from "./type-support.js";

export function inferInSlice(instructions, locals, args, statics, methodTokens = [], directCallEffects = new Map()) {
  const stack = [];
  const aliases = { locals: [], args: [], statics: [] };
  const pointerTargets = { locals: [], args: [], statics: [] };
  const unk = () => ({ ty: "unknown", lit: null, ptr: null, origins: [] });
  const cloneValue = (value) => ({ ...value, origins: [...(value.origins ?? [])] });
  const popU = () => stack.pop() ?? unk();
  const popExpected = (expected) => {
    const value = popU();
    constrainOrigins(value.origins, expected, locals, args, statics);
    return value;
  };

  for (const ins of instructions) {
    const m = ins.opcode.mnemonic;

    if (handleSlotInstruction(ins, stack, locals, args, statics, aliases, pointerTargets, unk)) {
      continue;
    }
    if (handleStackInstruction(ins, stack, unk, cloneValue, popU)) {
      continue;
    }

    switch (m) {
      // Literals
      case "PUSHNULL":
        stack.push({ ty: "null", lit: null, ptr: null, origins: [] });
        break;
      case "PUSHT":
      case "PUSHF":
        stack.push({ ty: "bool", lit: null, ptr: null, origins: [] });
        break;
      case "PUSHDATA1":
      case "PUSHDATA2":
      case "PUSHDATA4":
        stack.push({ ty: "bytestring", lit: null, ptr: null, origins: [] });
        break;
      case "PUSHINT8":
      case "PUSHINT16":
      case "PUSHINT32":
      case "PUSHINT64":
      case "PUSHINT128":
      case "PUSHINT256":
      case "PUSHM1":
      case "PUSH0":
      case "PUSH1":
      case "PUSH2":
      case "PUSH3":
      case "PUSH4":
      case "PUSH5":
      case "PUSH6":
      case "PUSH7":
      case "PUSH8":
      case "PUSH9":
      case "PUSH10":
      case "PUSH11":
      case "PUSH12":
      case "PUSH13":
      case "PUSH14":
      case "PUSH15":
      case "PUSH16":
        stack.push({ ty: "integer", lit: intLiteral(ins.operand), ptr: null, origins: [] });
        break;
      case "PUSHA":
        stack.push({ ty: "pointer", lit: null, ptr: pushaTarget(ins), origins: [] });
        break;

      // Collections
      case "NEWARRAY0":
        stack.push({ ty: "array", lit: null, origins: [] });
        break;
      case "NEWARRAY":
      case "NEWARRAY_T":
        popU();
        stack.push({ ty: "array", lit: null, origins: [] });
        break;
      case "NEWMAP":
        stack.push({ ty: "map", lit: null, origins: [] });
        break;
      case "NEWSTRUCT0":
        stack.push({ ty: "struct", lit: null, origins: [] });
        break;
      case "NEWSTRUCT":
        popU();
        stack.push({ ty: "struct", lit: null, origins: [] });
        break;
      case "NEWBUFFER":
        popU();
        stack.push({ ty: "buffer", lit: null, origins: [] });
        break;
      case "MEMCPY":
        popU(); // count
        popU(); // source index
        popU(); // source
        popU(); // destination index
        popU(); // destination
        break;
      case "CAT":
        popExpected("bytestring");
        popExpected("bytestring");
        stack.push({ ty: "bytestring", lit: null, origins: [] });
        break;
      case "SUBSTR":
        popExpected("integer");
        popExpected("integer");
        popExpected("bytestring");
        stack.push({ ty: "bytestring", lit: null, origins: [] });
        break;
      case "LEFT":
      case "RIGHT":
        popExpected("integer");
        popExpected("bytestring");
        stack.push({ ty: "bytestring", lit: null, origins: [] });
        break;
      case "PACK": {
        const count = asCount(popU().lit);
        if (count !== null) for (let k = 0; k < Math.min(count, stack.length); k += 1) popU();
        stack.push({ ty: "array", lit: null, origins: [] });
        break;
      }
      case "PACKMAP": {
        const count = asCount(popU().lit);
        if (count !== null) for (let k = 0; k < Math.min(count * 2, stack.length); k += 1) popU();
        stack.push({ ty: "map", lit: null, origins: [] });
        break;
      }
      case "PACKSTRUCT": {
        const count = asCount(popU().lit);
        if (count !== null) for (let k = 0; k < Math.min(count, stack.length); k += 1) popU();
        stack.push({ ty: "struct", lit: null, origins: [] });
        break;
      }
      case "UNPACK":
        popU();
        stack.push({ ty: "integer", lit: null, ptr: null, origins: [] });
        break;
      case "PICKITEM":
        popU();
        popU();
        stack.push(unk());
        break;
      case "SETITEM":
        popU();
        popU();
        popU();
        break;
      case "APPEND":
      case "REMOVE":
        popU();
        popU();
        break;
      case "CLEARITEMS":
        popU();
        break;
      case "REVERSEITEMS":
        popU();
        break;
      case "POPITEM":
        popU();
        stack.push(unk());
        break;
      case "SIZE":
        popU();
        stack.push({ ty: "integer", lit: null, origins: [] });
        break;
      case "HASKEY":
        popU();
        popU();
        stack.push({ ty: "bool", lit: null, origins: [] });
        break;
      case "KEYS":
      case "VALUES":
        popU();
        stack.push({ ty: "array", lit: null, origins: [] });
        break;
      case "ISNULL":
        popU();
        stack.push({ ty: "bool", lit: null, origins: [] });
        break;
      case "ISTYPE":
        popU();
        stack.push({ ty: "bool", lit: null, origins: [] });
        break;
      case "CONVERT":
        popU();
        stack.push({ ty: convertTargetType(ins.operand) ?? "any", lit: null, origins: [] });
        break;

      case "SYSCALL": {
        const hash = ins.operand?.kind === "Syscall" ? ins.operand.value : null;
        if (hash !== null) {
          const info = SYSCALLS.get(hash) ?? null;
          const argCount = info?.param_count ?? 0;
          for (let index = 0; index < argCount; index += 1) {
            popU();
          }
          if (info?.returns_value ?? true) {
            stack.push({ ty: "unknown", lit: null, origins: [] });
          }
        }
        break;
      }
      case "CALLT": {
        const index = ins.operand?.kind === "U16" ? ins.operand.value : null;
        if (index !== null) {
          const token = methodTokens[index] ?? null;
          if (token) {
            for (let argIndex = 0; argIndex < token.parametersCount; argIndex += 1) {
              popU();
            }
            if (token.hasReturnValue) {
              stack.push({ ty: "unknown", lit: null, origins: [] });
            }
          } else {
            stack.push({ ty: "unknown", lit: null, origins: [] });
          }
        }
        break;
      }
      case "CALL":
      case "CALL_L": {
        const target = directCallTarget(ins);
        const effect = target !== null ? directCallEffects.get(target) ?? null : null;
        if (effect) {
          for (let argIndex = 0; argIndex < effect.paramCount; argIndex += 1) {
            popU();
          }
          if (effect.returnsValue) {
            stack.push({ ty: "unknown", lit: null, origins: [] });
          }
        } else {
          stack.push({ ty: "unknown", lit: null, origins: [] });
        }
        break;
      }
      case "CALLA": {
        const pointer = popU();
        const target = pointer.ptr ?? null;
        const effect = target !== null ? directCallEffects.get(target) ?? null : null;
        if (effect) {
          for (let argIndex = 0; argIndex < effect.paramCount; argIndex += 1) {
            popU();
          }
          if (effect.returnsValue) {
            stack.push({ ty: "unknown", lit: null, ptr: null, origins: [] });
          }
        } else {
          stack.push({ ty: "unknown", lit: null, ptr: null, origins: [] });
        }
        break;
      }

      // Control flow consumers.
      case "JMPIF":
      case "JMPIF_L":
      case "JMPIFNOT":
      case "JMPIFNOT_L":
        popExpected("bool");
        break;
      case "JMPEQ":
      case "JMPEQ_L":
      case "JMPNE":
      case "JMPNE_L":
        popU();
        popU();
        break;
      case "JMPGT":
      case "JMPGT_L":
      case "JMPGE":
      case "JMPGE_L":
      case "JMPLT":
      case "JMPLT_L":
      case "JMPLE":
      case "JMPLE_L":
        popExpected("integer");
        popExpected("integer");
        break;
      case "ASSERT":
        popExpected("bool");
        break;
      case "ASSERTMSG":
        popU(); // message
        popExpected("bool");
        break;
      case "ABORT":
        stack.length = 0;
        break;
      case "ABORTMSG":
        popU(); // message
        stack.length = 0;
        break;
      case "THROW":
        popU();
        stack.length = 0;
        break;

      // Arithmetic + comparisons (subset)
      case "ADD":
      case "SUB":
      case "MUL":
      case "DIV":
      case "MOD":
      case "POW":
      case "MIN":
      case "MAX":
      case "SHL":
      case "SHR":
      case "AND":
      case "OR":
      case "XOR":
        popExpected("integer");
        popExpected("integer");
        stack.push({ ty: "integer", lit: null, origins: [] });
        break;
      case "MODMUL":
      case "MODPOW":
        popExpected("integer");
        popExpected("integer");
        popExpected("integer");
        stack.push({ ty: "integer", lit: null, origins: [] });
        break;
      case "WITHIN":
        popExpected("integer");
        popExpected("integer");
        popExpected("integer");
        stack.push({ ty: "bool", lit: null, origins: [] });
        break;
      case "SQRT":
      case "ABS":
      case "SIGN":
      case "INC":
      case "DEC":
      case "NEGATE":
      case "INVERT":
        popExpected("integer");
        stack.push({ ty: "integer", lit: null, origins: [] });
        break;
      case "NOT":
        popExpected("bool");
        stack.push({ ty: "bool", lit: null, origins: [] });
        break;
      case "BOOLAND":
      case "BOOLOR":
        popExpected("bool");
        popExpected("bool");
        stack.push({ ty: "bool", lit: null, origins: [] });
        break;
      case "EQUAL":
      case "NOTEQUAL":
        popU();
        popU();
        stack.push({ ty: "bool", lit: null, origins: [] });
        break;
      case "NUMEQUAL":
      case "NUMNOTEQUAL":
      case "GT":
      case "GE":
      case "LT":
      case "LE":
        popExpected("integer");
        popExpected("integer");
        stack.push({ ty: "bool", lit: null, origins: [] });
        break;
      case "NZ":
        // NZ is unary, but treating it as "pop 1" is enough for type recovery.
        popExpected("integer");
        stack.push({ ty: "bool", lit: null, origins: [] });
        break;

      // Everything else is a no-op for typing purposes.
      default:
        break;
    }
  }
}