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
import { isSimpleConditional, popConditionForLoop } from "./high-level-control-flow-shared.js";
import { warnStackUnderflow } from "./high-level-state.js";
import { jumpTarget } from "./high-level-utils.js";
import { hexOffset } from "./util.js";

export function collectLabelTargets(instructions) {
  const knownOffsets = new Set(instructions.map((instruction) => instruction.offset));
  const labelTargets = new Set();

  for (const instruction of instructions) {
    const target = jumpTarget(instruction);
    if (target === null) {
      continue;
    }
    if (
      instruction.opcode.mnemonic === "JMP" ||
      instruction.opcode.mnemonic === "JMP_L" ||
      instruction.opcode.mnemonic === "ENDTRY" ||
      instruction.opcode.mnemonic === "ENDTRY_L" ||
      isSimpleConditional(instruction.opcode.mnemonic)
    ) {
      if (knownOffsets.has(target)) {
        labelTargets.add(target);
      }
    }
  }

  return labelTargets;
}

export function emitLabelIfNeeded(state, offset) {
  if (!state.labelTargets.has(offset) || state.emittedLabels.has(offset)) {
    return;
  }
  state.statements.push(`${labelName(offset)}:`);
  state.emittedLabels.add(offset);
}

export function tryControlTransferFallback(state, instruction) {
  const target = jumpTarget(instruction);
  if (target === null) {
    return false;
  }

  const mnemonic = instruction.opcode.mnemonic;
  if (
    (mnemonic === "ENDTRY" || mnemonic === "ENDTRY_L") &&
    (target < 0 || !state.instructionOffsets.has(target))
  ) {
    const message = "end-try with unsupported operand (skipping)";
    state.statements.push(`// warning: ${message}`);
    state.warnings.push(
      `high-level: 0x${instruction.offset.toString(16).padStart(4, "0").toUpperCase()}: ${message}`,
    );
    return true;
  }

  if (isSimpleConditional(mnemonic)) {
    const needed = conditionalOperandCount(mnemonic);
    if (state.stack.length < needed) {
      warnStackUnderflow(state, instruction, needed);
      return true;
    }
    const condition = popConditionForLoop(state.stack, mnemonic);
    if (condition === null) {
      return false;
    }
    state.statements.push(`if ${condition} { goto ${labelName(target)}; }`);
    return true;
  }

  if (mnemonic === "JMP" || mnemonic === "JMP_L") {
    state.statements.push(`goto ${labelName(target)};`);
    state.stack.length = 0;
    return true;
  }

  if (mnemonic === "ENDTRY" || mnemonic === "ENDTRY_L") {
    state.statements.push(`leave ${labelName(target)};`);
    // ENDTRY leaves the try context but preserves the VM operand stack. When
    // the transfer is a fallthrough, postprocess removes the visual `leave`
    // while the following consumer still sees the carried value.
    return true;
  }

  return false;
}

export function labelName(offset) {
  return `label_0x${hexOffset(offset)}`;
}

function conditionalOperandCount(mnemonic) {
  return mnemonic === "JMPIF" ||
    mnemonic === "JMPIF_L" ||
    mnemonic === "JMPIFNOT" ||
    mnemonic === "JMPIFNOT_L"
    ? 1
    : 2;
}