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 {
  extractElseIfCondition,
  extractIfCondition,
  extractWhileCondition,
  isComment,
  isValidIdentifier,
  leadingWhitespace,
  parseAssignment,
  parseForParts,
} from "./postprocess-shared.js";

export function rewriteCompoundAssignments(statements) {
  for (let i = 0; i < statements.length; i++) {
    const trimmed = statements[i].trim();
    if (trimmed === "" || isComment(trimmed)) continue;

    if (trimmed.startsWith("for (") && trimmed.endsWith("{")) {
      const parts = parseForParts(trimmed);
      if (parts) {
        const rewritten = rewriteIncrement(parts.increment);
        if (rewritten) {
          statements[i] = `for (${parts.init}; ${parts.condition}; ${rewritten}) {`;
        }
      }
      continue;
    }

    if (trimmed.startsWith("let ")) continue;

    const assign = parseAssignment(trimmed);
    if (!assign) continue;

    const result = rewriteRhs(assign.lhs, assign.rhs);
    if (result) {
      const indent = leadingWhitespace(statements[i]);
      statements[i] = `${indent}${assign.lhs} ${result.op} ${result.tail};`;
    }
  }
}

function rewriteIncrement(increment) {
  const eqIdx = increment.indexOf(" = ");
  if (eqIdx < 0) return null;
  const lhs = increment.slice(0, eqIdx).trim();
  const rhs = increment.slice(eqIdx + 3).trim();
  if (!isValidIdentifier(lhs)) return null;
  const result = rewriteRhs(lhs, rhs);
  if (!result) return null;
  return `${lhs} ${result.op} ${result.tail}`;
}

function rewriteRhs(lhs, rhs) {
  const plusPrefix = `${lhs} + `;
  if (rhs.startsWith(plusPrefix)) return { op: "+=", tail: rhs.slice(plusPrefix.length) };
  const minusPrefix = `${lhs} - `;
  if (rhs.startsWith(minusPrefix)) return { op: "-=", tail: rhs.slice(minusPrefix.length) };
  return null;
}

export function rewriteIndexingSyntax(statements) {
  for (let i = 0; i < statements.length; i++) {
    const trimmed = statements[i].trim();
    if (trimmed === "" || isComment(trimmed)) continue;

    const setItem = rewriteSetItem(trimmed);
    if (setItem) {
      statements[i] = setItem;
      continue;
    }

    if (trimmed.startsWith("for (") && trimmed.endsWith("{")) {
      const parts = parseForParts(trimmed);
      if (parts) {
        statements[i] =
          `for (${rewriteExpr(parts.init)}; ${rewriteExpr(parts.condition)}; ${rewriteExpr(parts.increment)}) {`;
      }
      continue;
    }

    const ifCond = extractIfCondition(trimmed);
    if (ifCond !== null) {
      statements[i] = `if ${rewriteExpr(ifCond)} {`;
      continue;
    }

    const elseIfCond = extractElseIfCondition(trimmed);
    if (elseIfCond !== null) {
      const prefix = trimmed.startsWith("} ") ? "} " : "";
      statements[i] = `${prefix}else if ${rewriteExpr(elseIfCond)} {`;
      continue;
    }

    const whileCond = extractWhileCondition(trimmed);
    if (whileCond !== null) {
      statements[i] = `while ${rewriteExpr(whileCond)} {`;
      continue;
    }

    const assign = parseAssignment(trimmed);
    if (assign) {
      const indent = leadingWhitespace(statements[i]);
      const prefix = assign.hasLet ? "let " : "";
      statements[i] = `${indent}${prefix}${assign.lhs} = ${rewriteExpr(assign.rhs)};`;
      continue;
    }

    if (trimmed.endsWith(";")) {
      const indent = leadingWhitespace(statements[i]);
      statements[i] = `${indent}${rewriteExpr(trimmed.slice(0, -1))};`;
    }
  }
}

function rewriteSetItem(line) {
  const trimmed = line.trim();
  if (!trimmed.startsWith("set_item(") || !trimmed.endsWith(");")) return null;
  const body = trimmed.slice(9, -2);
  const args = splitArgs(body);
  if (args.length !== 3) return null;
  return `${rewriteExpr(args[0])}[${rewriteExpr(args[1])}] = ${rewriteExpr(args[2])};`;
}

function rewriteExpr(expr) {
  expr = expr.trim();
  if (expr === "") return "";

  // Find the leftmost " get " / " has_key " operator that sits outside a
  // string literal, so a literal like "a get b" is not mistaken for an index.
  const op = findExprOp(expr);
  if (!op) return expr;
  const { pos, kind } = op;

  const left = expr.slice(0, pos).trim();
  const right = expr.slice(pos + (kind === "get" ? 5 : 10));
  if (kind === "get") return `${left}[${rewriteExpr(right)}]`;
  return `has_key(${left}, ${rewriteExpr(right)})`;
}

function findExprOp(s) {
  let inString = false;
  for (let i = 0; i < s.length; i += 1) {
    const c = s[i];
    if (inString) {
      if (c === "\\") {
        i += 1;
        continue;
      }
      if (c === '"') inString = false;
      continue;
    }
    if (c === '"') {
      inString = true;
      continue;
    }
    if (s.startsWith(" get ", i)) return { pos: i, kind: "get" };
    if (s.startsWith(" has_key ", i)) return { pos: i, kind: "has_key" };
  }
  return null;
}

function splitArgs(text) {
  const out = [];
  let depth = 0;
  let current = "";
  for (const ch of text) {
    if ("([{".includes(ch)) {
      depth++;
      current += ch;
    } else if (")]}".includes(ch)) {
      depth = Math.max(0, depth - 1);
      current += ch;
    } else if (ch === "," && depth === 0) {
      out.push(current.trim());
      current = "";
    } else {
      current += ch;
    }
  }
  if (current.trim()) out.push(current.trim());
  return out;
}