neo-decompiler 0.6.2

Minimal tooling for inspecting Neo N3 NEF bytecode
Documentation
import {
  convertTargetName,
  resolvePackedValue,
  stripOuterParens,
  wrapExpression,
} from "./high-level-utils.js";

export function tryCollectionExpression(state, instruction) {
  const mnemonic = instruction.opcode.mnemonic;

  if (mnemonic === "NEWARRAY0") {
    state.stack.push("[]");
    return true;
  }
  if (mnemonic === "NEWARRAY_T") {
    const size = stripOuterParens(state.stack.pop() ?? "???");
    const targetName = convertTargetName(instruction.operand) ?? "unknown";
    state.stack.push(`new_array_t(${size}, "${targetName}")`);
    return true;
  }
  if (mnemonic === "NEWMAP") {
    state.stack.push("{}");
    return true;
  }
  if (mnemonic === "NEWSTRUCT0") {
    state.stack.push("{}");
    return true;
  }
  if (mnemonic === "NEWSTRUCT") {
    const value = stripOuterParens(state.stack.pop() ?? "???");
    state.stack.push(`new_struct(${value})`);
    return true;
  }
  if (mnemonic === "PACK" || mnemonic === "PACKMAP" || mnemonic === "PACKSTRUCT") {
    return emitPackExpression(state, mnemonic, stripOuterParens);
  }
  if (mnemonic === "PICKITEM") {
    const index = stripOuterParens(state.stack.pop() ?? "???");
    const target = stripOuterParens(state.stack.pop() ?? "???");
    state.stack.push(`${wrapExpression(target)}[${index}]`);
    return true;
  }
  if (mnemonic === "HASKEY") {
    const key = stripOuterParens(state.stack.pop() ?? "???");
    const target = stripOuterParens(state.stack.pop() ?? "???");
    state.stack.push(`has_key(${wrapExpression(target)}, ${key})`);
    return true;
  }
  if (mnemonic === "SIZE") {
    const target = stripOuterParens(state.stack.pop() ?? "???");
    state.stack.push(`len(${wrapExpression(target)})`);
    return true;
  }
  if (mnemonic === "KEYS") {
    const target = stripOuterParens(state.stack.pop() ?? "???");
    state.stack.push(`keys(${wrapExpression(target)})`);
    return true;
  }
  if (mnemonic === "VALUES") {
    const target = stripOuterParens(state.stack.pop() ?? "???");
    state.stack.push(`values(${wrapExpression(target)})`);
    return true;
  }
  if (mnemonic === "POPITEM") {
    const target = stripOuterParens(state.stack.pop() ?? "???");
    state.stack.push(`pop_item(${wrapExpression(target)})`);
    return true;
  }
  if (mnemonic === "ISTYPE") {
    const value = stripOuterParens(state.stack.pop() ?? "???");
    const targetName = convertTargetName(instruction.operand);
    if (targetName !== null) {
      state.stack.push(`is_type_${targetName}(${value})`);
    } else {
      state.stack.push(`is_type(${value})`);
    }
    return true;
  }
  if (mnemonic === "UNPACK") {
    const source = stripOuterParens(state.stack.pop() ?? "???");
    const packed = resolvePackedValue(state, source);
    if (!packed) {
      const elementCount = inferUnpackElementCount(state, instruction);
      const elementsTemp = `unpack(${source})`;
      state.statements.push(`let t${state.nextTempId} = ${elementsTemp};`);
      const unpackTemp = `t${state.nextTempId}`;
      state.nextTempId += 1;
      for (let index = 0; index < elementCount; index += 1) {
        const itemTemp = `t${state.nextTempId}`;
        state.nextTempId += 1;
        state.statements.push(
          `let ${itemTemp} = unpack_item(${unpackTemp}, ${index});`,
        );
        state.stack.push(itemTemp);
      }
      const countTemp = `t${state.nextTempId}`;
      state.nextTempId += 1;
      state.statements.push(`let ${countTemp} = len(${source});`);
      state.stack.push(countTemp);
      return true;
    }
    for (const element of [...packed].reverse()) {
      state.stack.push(element);
    }
    state.stack.push(`${packed.length}`);
    return true;
  }
  return false;
}

export function tryCollectionStatement(state, instruction) {
  const mnemonic = instruction.opcode.mnemonic;

  if (mnemonic === "MEMCPY") {
    const args = [];
    for (let index = 0; index < 5; index += 1) {
      args.push(stripOuterParens(state.stack.pop() ?? "???"));
    }
    args.reverse();
    state.statements.push(`memcpy(${args.join(", ")});`);
    return true;
  }
  if (mnemonic === "SETITEM") {
    const value = stripOuterParens(state.stack.pop() ?? "???");
    const index = stripOuterParens(state.stack.pop() ?? "???");
    const target = stripOuterParens(state.stack.pop() ?? "???");
    state.statements.push(`${wrapExpression(target)}[${index}] = ${value};`);
    return true;
  }
  if (mnemonic === "APPEND") {
    const value = stripOuterParens(state.stack.pop() ?? "???");
    const target = stripOuterParens(state.stack.pop() ?? "???");
    state.statements.push(`append(${wrapExpression(target)}, ${value});`);
    return true;
  }
  if (mnemonic === "REMOVE") {
    const key = stripOuterParens(state.stack.pop() ?? "???");
    const target = stripOuterParens(state.stack.pop() ?? "???");
    state.statements.push(`remove_item(${wrapExpression(target)}, ${key});`);
    return true;
  }
  if (mnemonic === "CLEARITEMS") {
    const target = stripOuterParens(state.stack.pop() ?? "???");
    state.statements.push(`clear_items(${wrapExpression(target)});`);
    return true;
  }
  if (mnemonic === "REVERSEITEMS") {
    const target = stripOuterParens(state.stack.pop() ?? "???");
    state.statements.push(`reverse_items(${wrapExpression(target)});`);
    return true;
  }
  return false;
}

function emitPackExpression(state, mnemonic, stripOuterParens) {
  const countText = state.stack.pop();
  const count = countText !== undefined ? Number.parseInt(countText, 10) : Number.NaN;
  if (!Number.isFinite(count) || count < 0) {
    state.stack.push(`pack_dynamic(${countText ?? "???"})`);
    return true;
  }

  const elements = [];
  for (let index = 0; index < count; index += 1) {
    elements.push(stripOuterParens(state.stack.pop() ?? "???"));
  }

  const expression = renderPackedExpression(mnemonic, elements);
  state.stack.push(expression);
  state.packedValuesByExpression.set(expression, [...elements]);
  return true;
}

function renderPackedExpression(mnemonic, elements) {
  const body = elements.join(", ");
  if (mnemonic === "PACKMAP") {
    return `Map(${body})`;
  }
  if (mnemonic === "PACKSTRUCT") {
    return `Struct(${body})`;
  }
  return `[${body}]`;
}

function inferUnpackElementCount(state, instruction) {
  const DEFAULT_COUNT = 4;
  const unpackIndex = state.program.findIndex(
    (candidate) => candidate.offset === instruction.offset,
  );
  if (unpackIndex < 0) {
    return DEFAULT_COUNT;
  }

  let cursor = unpackIndex + 1;
  if (cursor >= state.program.length) {
    return DEFAULT_COUNT;
  }
  if (state.program[cursor].opcode.mnemonic !== "DROP") {
    return DEFAULT_COUNT;
  }
  cursor += 1;

  let pops = 0;
  while (
    cursor < state.program.length &&
    isSinglePopMnemonic(state.program[cursor].opcode.mnemonic)
  ) {
    pops += 1;
    cursor += 1;
  }

  if (pops === 0) {
    return DEFAULT_COUNT;
  }

  const hasDupBefore =
    unpackIndex > 0 && state.program[unpackIndex - 1].opcode.mnemonic === "DUP";
  const count = hasDupBefore ? Math.max(0, pops - 1) : pops;
  return count === 0 ? DEFAULT_COUNT : count;
}

function isSinglePopMnemonic(mnemonic) {
  return (
    mnemonic === "DROP" ||
    mnemonic.startsWith("STLOC") ||
    mnemonic.startsWith("STARG") ||
    mnemonic.startsWith("STSFLD")
  );
}