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 {
  isPureRhs,
  literalIndex,
  materialiseStackTopForDup,
  resolvePackedValue,
  stripOuterParens,
} from "./high-level-stack-shared.js";

function replaceDynamicStackSurvivors(state, helper, argument) {
  const base = `${helper}(${argument ?? "???"})`;
  const len = state.stack.length;
  for (let index = 0; index < state.stack.length; index++) {
    const value = state.stack[index];
    state.packedValuesByExpression.delete(value);
    state.pointerTargetsByExpression.delete(value);
    const depthFromTop = len - 1 - index;
    state.stack[index] = depthFromTop === 0 ? base : `${base}[${depthFromTop}]`;
  }
}

export function tryStackShapeOperation(state, instruction) {
  const mnemonic = instruction.opcode.mnemonic;
  switch (mnemonic) {
    case "DEPTH":
      state.stack.push(`${state.stack.length}`);
      return true;
    case "DROP": {
      const top = state.stack.pop();
      if (top !== undefined && !isPureRhs(top)) {
        const temp = `t${state.nextTempId}`;
        state.nextTempId += 1;
        state.statements.push(`let ${temp} = ${stripOuterParens(top)};`);
      }
      return true;
    }
    case "CLEAR":
      state.stack.length = 0;
      state.statements.push("// clear stack");
      return true;
    case "DUP": {
      const top = state.stack.at(-1);
      if (top === undefined) {
        state.stack.push("???");
      } else {
        const materialised = materialiseStackTopForDup(state, top);
        state.stack.push(materialised);
      }
      return true;
    }
    case "OVER": {
      if (state.stack.length < 2) {
        state.stack.push("???");
        return true;
      }
      const idx = state.stack.length - 2;
      const materialised = materialiseStackTopForDup(state, state.stack[idx], idx);
      state.stack.push(materialised);
      return true;
    }
    case "SWAP": {
      if (state.stack.length >= 2) {
        const last = state.stack.length - 1;
        [state.stack[last - 1], state.stack[last]] = [state.stack[last], state.stack[last - 1]];
      }
      return true;
    }
    case "NIP":
      if (state.stack.length >= 2) {
        state.stack.splice(state.stack.length - 2, 1);
      }
      return true;
    case "PICK": {
      const indexText = state.stack.pop();
      const index = literalIndex(indexText);
      if (!Number.isFinite(index) || index < 0 || index >= state.stack.length) {
        const temp = `t${state.nextTempId}`;
        state.nextTempId += 1;
        state.statements.push(`let ${temp} = pick(${indexText ?? "???"});`);
        state.stack.push(temp);
        return true;
      }
      const sourceSlot = state.stack.length - 1 - index;
      const source = materialiseStackTopForDup(state, state.stack[sourceSlot], sourceSlot);
      state.stack.push(source);
      const packed = resolvePackedValue(state, source);
      if (packed) {
        state.packedValuesByExpression.set(source, packed);
      }
      return true;
    }
    case "ROT":
      if (state.stack.length >= 3) {
        const [a, b, c] = state.stack.splice(state.stack.length - 3, 3);
        state.stack.push(b, c, a);
      }
      state.statements.push("// rotate top three stack values");
      return true;
    case "TUCK":
      if (state.stack.length >= 2) {
        const top = materialiseStackTopForDup(state, state.stack[state.stack.length - 1]);
        state.stack.splice(state.stack.length - 2, 0, top);
      }
      return true;
    case "ROLL": {
      const indexText = state.stack.pop();
      const index = literalIndex(indexText);
      if (Number.isFinite(index) && index >= 0 && index < state.stack.length) {
        const from = state.stack.length - 1 - index;
        const [value] = state.stack.splice(from, 1);
        state.stack.push(value);
      } else {
        const removed = state.stack.pop();
        if (removed !== undefined) {
          state.packedValuesByExpression.delete(removed);
          state.pointerTargetsByExpression.delete(removed);
        }
        replaceDynamicStackSurvivors(state, "roll_remaining", indexText);
        const temp = `t${state.nextTempId}`;
        state.nextTempId += 1;
        state.statements.push(`let ${temp} = roll(${indexText ?? "???"}); // dynamic roll`);
        state.stack.push(temp);
      }
      return true;
    }
    case "REVERSE3":
      if (state.stack.length >= 3) {
        const stack = state.stack;
        const last = stack.length - 1;
        const tmp = stack[last - 2];
        stack[last - 2] = stack[last];
        stack[last] = tmp;
      }
      state.statements.push("// reverse top 3 stack values");
      return true;
    case "REVERSE4":
      if (state.stack.length >= 4) {
        const stack = state.stack;
        const last = stack.length - 1;
        let tmp = stack[last - 3];
        stack[last - 3] = stack[last];
        stack[last] = tmp;
        tmp = stack[last - 2];
        stack[last - 2] = stack[last - 1];
        stack[last - 1] = tmp;
      }
      state.statements.push("// reverse top 4 stack values");
      return true;
    case "REVERSEN": {
      const countText = state.stack.pop();
      const count = literalIndex(countText);
      if (Number.isFinite(count) && count >= 0 && count <= state.stack.length) {
        const stack = state.stack;
        let i = stack.length - count;
        let j = stack.length - 1;
        while (i < j) {
          const tmp = stack[i];
          stack[i] = stack[j];
          stack[j] = tmp;
          i++;
          j--;
        }
        state.statements.push(`// reverse top ${count} stack values`);
      } else {
        state.statements.push(`// reverse top ${countText ?? "???"} stack values`);
        replaceDynamicStackSurvivors(state, "reverse_n_top", countText);
      }
      return true;
    }
    case "XDROP": {
      const indexText = state.stack.pop();
      const index = literalIndex(indexText);
      if (Number.isFinite(index) && index >= 0 && index < state.stack.length) {
        const removeAt = state.stack.length - 1 - index;
        state.stack.splice(removeAt, 1);
      } else {
        const removed = state.stack.pop();
        if (removed !== undefined) {
          state.packedValuesByExpression.delete(removed);
          state.pointerTargetsByExpression.delete(removed);
        }
        replaceDynamicStackSurvivors(state, "xdrop_remaining", indexText);
        state.statements.push(`// xdrop stack[${indexText ?? "???"}] (dynamic index, stack may be imprecise)`);
      }
      return true;
    }
    default:
      return false;
  }
}