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 {
  findBlockEnd,
  isBlank,
  isIfOpen,
  nextCodeLine,
} from "./postprocess-shared.js";

const GOTO_LABEL_RE = /^goto\s+(label_0x[\da-f]+);$/i;
const LEAVE_LABEL_RE = /^leave\s+(label_0x[\da-f]+);$/i;
const LABEL_LINE_RE = /^(label_0x[\da-f]+):$/i;
const IF_GOTO_RE = /^if\s+.+\{\s*goto\s+(label_0x[\da-f]+);\s*\}$/i;
const DO_WHILE_END_RE = /^}\s+while\s+(?:!\((.+)\)|(.+))\);?$/;

export function rewriteGotoDoWhile(statements) {
  let index = 0;
  while (index < statements.length) {
    const trimmed = statements[index].trim();
    const labelMatch = GOTO_LABEL_RE.exec(trimmed);
    if (!labelMatch) {
      index++;
      continue;
    }
    const label = labelMatch[1];

    const doIdx = nextCodeLine(statements, index);
    if (doIdx < 0 || statements[doIdx].trim() !== "do {") {
      index++;
      continue;
    }

    const endIdx = findBlockEnd(statements, doIdx);
    if (endIdx < 0) {
      index++;
      continue;
    }
    const endTrimmed = statements[endIdx].trim();
    const condMatch = DO_WHILE_END_RE.exec(endTrimmed);
    if (!condMatch) {
      index++;
      continue;
    }
    const condition = condMatch[1] ? `!(${condMatch[1]})` : condMatch[2];

    const labelLine = `${label}:`;
    let labelIdx = -1;
    for (let i = doIdx + 1; i < endIdx; i++) {
      if (statements[i].trim() === labelLine) {
        labelIdx = i;
        break;
      }
    }
    if (labelIdx < 0) {
      index++;
      continue;
    }

    const setupLines = [];
    for (let i = labelIdx + 1; i < endIdx; i++) {
      if (!isBlank(statements[i])) setupLines.push(i);
    }

    statements[index] = "";
    statements[doIdx] = `while ${condition} {`;
    statements[labelIdx] = "";

    if (setupLines.length === 0) {
      statements[endIdx] = "}";
    } else {
      const copies = setupLines.map((i) => statements[i]);
      for (let j = 0; j < copies.length; j++) {
        statements.splice(doIdx + j, 0, copies[j]);
      }
      statements[endIdx + copies.length] = "}";
    }
    index++;
  }
}

export function rewriteIfGotoToWhile(statements) {
  let index = 0;
  while (index < statements.length) {
    const trimmed = statements[index].trim();
    const labelMatch = LABEL_LINE_RE.exec(trimmed);
    if (!labelMatch) {
      index++;
      continue;
    }
    const label = labelMatch[1];

    let ifIdx = -1;
    for (let i = index + 1; i < statements.length; i++) {
      const t = statements[i].trim();
      if (isIfOpen(t)) {
        ifIdx = i;
        break;
      }
    }
    if (ifIdx < 0) {
      index++;
      continue;
    }

    const endIdx = findBlockEnd(statements, ifIdx);
    if (endIdx < 0 || statements[endIdx].trim() !== "}") {
      index++;
      continue;
    }

    const gotoTarget = `goto ${label};`;
    let gotoIdx = -1;
    for (let i = ifIdx + 1; i < endIdx; i++) {
      if (statements[i].trim() === gotoTarget) {
        gotoIdx = i;
        break;
      }
    }
    if (gotoIdx < 0) {
      index++;
      continue;
    }

    const setupLines = [];
    for (let i = index + 1; i < ifIdx; i++) {
      if (!isBlank(statements[i])) setupLines.push(statements[i]);
    }

    statements[index] = "";
    statements[ifIdx] = statements[ifIdx].trim().replace(/^if /, "while ");
    statements[gotoIdx] = "";

    if (setupLines.length > 0) {
      for (let j = 0; j < setupLines.length; j++) {
        statements.splice(endIdx + j, 0, setupLines[j]);
      }
    }
    index++;
  }
}

export function eliminateFallthroughGotos(statements) {
  for (let i = 0; i < statements.length; i++) {
    const trimmed = statements[i].trim();
    const labelMatch =
      GOTO_LABEL_RE.exec(trimmed) ??
      LEAVE_LABEL_RE.exec(trimmed) ??
      IF_GOTO_RE.exec(trimmed);
    if (!labelMatch) continue;
    const label = labelMatch[1];
    const labelLine = `${label}:`;

    let probe = i + 1;
    while (probe < statements.length) {
      const t = statements[probe].trim();
      if (t === "" || t.startsWith("//") || t === "}") {
        probe++;
        continue;
      }
      if (t === labelLine) {
        statements[i] = "";
      }
      break;
    }
  }
}

export function rewriteLabelGotoToLoop(statements) {
  const standaloneGotos = new Set();
  for (const stmt of statements) {
    const t = stmt.trim();
    if (t.startsWith("goto label_") && t.endsWith(";")) standaloneGotos.add(t);
  }
  if (standaloneGotos.size === 0) return;

  let index = 0;
  while (index < statements.length) {
    const trimmed = statements[index].trim();
    const labelMatch = LABEL_LINE_RE.exec(trimmed);
    if (!labelMatch) {
      index++;
      continue;
    }
    const label = labelMatch[1];
    const gotoTarget = `goto ${label};`;
    if (!standaloneGotos.has(gotoTarget)) {
      index++;
      continue;
    }

    let depth = 0;
    let gotoIdx = -1;
    for (let j = index + 1; j < statements.length; j++) {
      const t = statements[j].trim();
      if (t === gotoTarget && depth === 0) {
        gotoIdx = j;
        break;
      }
      if (t.endsWith("{")) depth++;
      if (t === "}" || t.startsWith("} ")) {
        depth--;
        if (depth < 0) break;
      }
    }
    if (gotoIdx < 0) {
      index++;
      continue;
    }

    const labelDecl = `${label}:`;
    let hasOtherReference = false;
    for (let i = 0; i < statements.length; i++) {
      if (i === index || i === gotoIdx) continue;
      const t = statements[i].trim();
      if (t === labelDecl || t.includes(gotoTarget)) {
        hasOtherReference = true;
        break;
      }
    }
    if (hasOtherReference) {
      index++;
      continue;
    }

    const labelIndent = statements[index].slice(0, statements[index].length - statements[index].trimStart().length);
    const gotoIndent = statements[gotoIdx].slice(0, statements[gotoIdx].length - statements[gotoIdx].trimStart().length);
    statements[index] = `${labelIndent}loop {`;
    statements[gotoIdx] = `${gotoIndent}}`;
    index++;
  }
}

export function removeOrphanedLabels(statements) {
  const referenced = new Set();
  for (const stmt of statements) {
    const t = stmt.trim();
    const gotoM = GOTO_LABEL_RE.exec(t);
    if (gotoM) referenced.add(gotoM[1]);
    const leaveM = LEAVE_LABEL_RE.exec(t);
    if (leaveM) referenced.add(leaveM[1]);
    const ifGotoM = IF_GOTO_RE.exec(t);
    if (ifGotoM) referenced.add(ifGotoM[1]);
  }

  for (let i = 0; i < statements.length; i++) {
    const m = LABEL_LINE_RE.exec(statements[i].trim());
    if (m && !referenced.has(m[1])) {
      statements[i] = "";
    }
  }
}