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 { tryCollectionExpression, tryCollectionStatement } from "./high-level-collections.js";
import { createControlFlowHelpers } from "./high-level-control-flow.js";
import { emitLabelIfNeeded, tryControlTransferFallback } from "./high-level-labels.js";
import { tryInternalCall, tryIndirectCall, trySyscall, tryTokenCall } from "./high-level-calls.js";
import { captureStoreInfo, cloneState, createState, emptyContext, finishInstruction } from "./high-level-state.js";
import {
  tryBinaryExpression,
  tryControlStatement,
  tryStackShapeOperation,
  tryUnaryExpression,
} from "./high-level-stack.js";
import {
  pushImmediate,
  tryLoadLocalOrArg,
  tryLoadStatic,
  trySlotDeclarations,
  tryStoreArgument,
  tryStoreLocal,
  tryStoreStatic,
} from "./high-level-slots.js";
import { renderHighLevelMethodGroups as renderHighLevelMethodGroupsWithLift } from "./high-level-render.js";
import { renderUntranslatedInstruction, stripOuterParens } from "./high-level-utils.js";
import { postprocess } from "./postprocess.js";

let CONTROL_FLOW;

function liftStructuredSlice(
  instructions,
  manifestMethod = null,
  context = emptyContext(),
  methodOffset = instructions[0]?.offset ?? 0,
) {
  if (instructions.length === 0) {
    return { statements: [], warnings: [] };
  }

  const result =
    CONTROL_FLOW.tryLiftSimpleSwitch(instructions, manifestMethod, context, methodOffset) ??
    CONTROL_FLOW.tryLiftSimpleLoop(instructions, manifestMethod, context, methodOffset) ??
    CONTROL_FLOW.tryLiftSimpleTryBlock(instructions, manifestMethod, context, methodOffset) ??
    CONTROL_FLOW.tryLiftSimpleBranch(instructions, manifestMethod, context, methodOffset) ??
    liftStraightLineMethodBody(instructions, manifestMethod, context, undefined, methodOffset);
  return result;
}

CONTROL_FLOW = createControlFlowHelpers({
  createState,
  cloneState,
  executeStraightLine,
  liftStructuredSlice,
});

export function renderHighLevelMethodGroups(groups, manifest, context = null) {
  return renderHighLevelMethodGroupsWithLift(groups, manifest, context, liftMethodBody);
}

// Maximum number of instructions a single method body may have before
// high-level lifting is skipped in favour of a fallback note. The stack-lifting
// and postprocess passes are worst-case O(n²) in the per-method statement
// count, so a maliciously crafted in-cap NEF packed with crossing jumps would
// otherwise take a very long time to lift. Real Neo contract methods are
// gas-bounded and far smaller; the raw instruction stream stays available via
// the disassembler. Mirrors the Rust port's MAX_HIGH_LEVEL_METHOD_INSTRUCTIONS.
export const MAX_HIGH_LEVEL_METHOD_INSTRUCTIONS = 16384;

export function liftMethodBody(
  instructions,
  manifestMethod = null,
  context = emptyContext(),
  methodOffset = instructions[0]?.offset ?? 0,
) {
  if (instructions.length > MAX_HIGH_LEVEL_METHOD_INSTRUCTIONS) {
    const offsetHex = (instructions[0]?.offset ?? 0)
      .toString(16)
      .padStart(4, "0")
      .toUpperCase();
    return {
      statements: [
        `// method body too large for high-level lifting: ${instructions.length} instructions ` +
          `exceeds the ${MAX_HIGH_LEVEL_METHOD_INSTRUCTIONS}-instruction limit; use the disassembler for the full listing`,
      ],
      warnings: [
        `high-level: method at 0x${offsetHex} skipped — ${instructions.length} instructions ` +
          `exceeds the high-level lifting limit (${MAX_HIGH_LEVEL_METHOD_INSTRUCTIONS})`,
      ],
    };
  }
  let result;
  const switchLift = CONTROL_FLOW.tryLiftSimpleSwitch(instructions, manifestMethod, context, methodOffset);
  if (switchLift !== null) {
    result = switchLift;
  } else {
    const loopLift = CONTROL_FLOW.tryLiftSimpleLoop(instructions, manifestMethod, context, methodOffset);
    if (loopLift !== null) {
      result = loopLift;
    } else {
      const tryLift = CONTROL_FLOW.tryLiftSimpleTryBlock(instructions, manifestMethod, context, methodOffset);
      if (tryLift !== null) {
        result = tryLift;
      } else {
        const branchLift = CONTROL_FLOW.tryLiftSimpleBranch(instructions, manifestMethod, context, methodOffset);
        if (branchLift !== null) {
          result = branchLift;
        } else {
          result = liftStraightLineMethodBody(instructions, manifestMethod, context, undefined, methodOffset);
        }
      }
    }
  }

  postprocess(result.statements, context.postprocessOptions);
  return result;
}

function liftStraightLineMethodBody(
  instructions,
  manifestMethod = null,
  context,
  initialState,
  methodOffset = instructions[0]?.offset ?? 0,
) {
  const state = initialState ?? createState(manifestMethod, context, methodOffset, instructions);
  executeStraightLine(state, instructions);
  if (instructions.at(-1)?.opcode?.mnemonic !== "RET" && state.stack.length > 0) {
    for (const expression of state.stack) {
      state.statements.push(`${stripOuterParens(expression)};`);
    }
    state.stack.length = 0;
  }
  return { statements: state.statements, warnings: state.warnings };
}

function executeStraightLine(state, instructions) {
  // Destructure stable references once. state.stack / state.statements / the
  // four pointer-and-packed Maps are mutated in place but never reassigned,
  // so caching the references avoids per-instruction property loads. The
  // assigned-to state.previousStoreInfo / state.previousInstruction stay on
  // `state` since those are slot writes, not mutations.
  const {
    statements,
    initializedLocals,
    initializedStatics,
    parameterNames,
    returnsVoid,
    stack,
    pointerTargetsByExpression,
    pointerTargetsBySlot,
    packedValuesByExpression,
    packedValuesBySlot,
  } = state;

  for (const instruction of instructions) {
    const mnemonic = instruction.opcode.mnemonic;
    emitLabelIfNeeded(state, instruction.offset);

    if (trySlotDeclarations(statements, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (pushImmediate(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (mnemonic === "NOP") {
      finishInstruction(state, instruction);
      continue;
    }

    // ENDFINALLY without a structured try-block lift wrapping it
    // (i.e. one that already absorbed the matching ENDTRY) is
    // best treated as a pass-through: the JS port has no
    // verbose-mode trace comment to emit and the bytecode-level
    // `endfinally` semantics are subtle enough that flagging it
    // as "not yet translated" misleads. Match the Rust port's
    // clean-mode behaviour (silently consume).
    if (mnemonic === "ENDFINALLY") {
      finishInstruction(state, instruction);
      continue;
    }

    if (tryLoadLocalOrArg(stack, mnemonic, parameterNames, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (tryLoadStatic(stack, mnemonic, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (
      tryStoreLocal(
        statements,
        state,
        initializedLocals,
        pointerTargetsByExpression,
        pointerTargetsBySlot,
        packedValuesByExpression,
        packedValuesBySlot,
        mnemonic,
        instruction,
      )
    ) {
      state.previousStoreInfo = captureStoreInfo(instruction, state);
      finishInstruction(state, instruction);
      continue;
    }

    if (tryStoreArgument(statements, state, parameterNames, mnemonic, instruction)) {
      state.previousStoreInfo = captureStoreInfo(instruction, state);
      finishInstruction(state, instruction);
      continue;
    }

    if (
      tryStoreStatic(
        statements,
        state,
        initializedStatics,
        pointerTargetsByExpression,
        pointerTargetsBySlot,
        packedValuesByExpression,
        packedValuesBySlot,
        mnemonic,
        instruction,
      )
    ) {
      state.previousStoreInfo = captureStoreInfo(instruction, state);
      finishInstruction(state, instruction);
      continue;
    }

    if (tryBinaryExpression(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (tryInternalCall(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (tryIndirectCall(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (tryTokenCall(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (trySyscall(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (tryCollectionExpression(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (tryCollectionStatement(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (tryStackShapeOperation(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (tryUnaryExpression(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (tryControlStatement(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (tryControlTransferFallback(state, instruction)) {
      finishInstruction(state, instruction);
      continue;
    }

    if (mnemonic === "RET") {
      if (returnsVoid || stack.length === 0) {
        statements.push("return;");
      } else {
        statements.push(`return ${stripOuterParens(stack.pop())};`);
      }
      finishInstruction(state, instruction);
      continue;
    }

    statements.push(renderUntranslatedInstruction(instruction));
    // Surface the not-yet-translated opcode through the structured
    // `warnings` array too, so a programmatic caller (CI, IDE
    // integration) sees the hazard without having to grep the
    // rendered source. Mirrors the Rust port's `self.warn(...)` for
    // unhandled opcodes.
    const opcodeName =
      instruction.opcode.mnemonic === "UNKNOWN"
        ? `UNKNOWN_0x${instruction.opcode.byte.toString(16).padStart(2, "0").toUpperCase()}`
        : instruction.opcode.mnemonic;
    state.warnings.push(
      `high-level: 0x${instruction.offset.toString(16).padStart(4, "0").toUpperCase()}: ${opcodeName} (not yet translated)`,
    );
    finishInstruction(state, instruction);
  }
}