neo-decompiler 0.10.1

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 {
  containsIdentifier,
  isTempIdent,
  leadingWhitespace,
  nextCodeLine,
  parseAssignment,
  replaceIdentifier,
  TEMP_TOKEN_RE,
} from "./postprocess-shared.js";

export function stripStackComments(statements) {
  for (let i = 0; i < statements.length; i++) {
    const trimmed = statements[i].trim();
    if (
      trimmed.startsWith("// drop ") ||
      trimmed.startsWith("// remove second") ||
      // `// xdrop stack[N] (removed X)` and the dynamic-index variant
      // are accurate but read as VM-internal noise — Rust strips them
      // already; mirror that so the two ports stay byte-identical on
      // contracts that hit XDROP.
      trimmed.startsWith("// xdrop stack") ||
      // ROT / TUCK / REVERSEN / CLEAR also leave purely descriptive
      // VM-mechanics annotations; the data-flow is captured in the
      // subsequent variable references, so the comment is redundant.
      trimmed.startsWith("// rotate top") ||
      trimmed.startsWith("// tuck top") ||
      trimmed.startsWith("// reverse top") ||
      trimmed === "// clear stack"
    ) {
      statements[i] = "";
      continue;
    }
    for (const suffix of [" // duplicate top of stack", " // copy second stack value"]) {
      const pos = statements[i].indexOf(suffix);
      if (pos >= 0) {
        statements[i] = statements[i].slice(0, pos);
      }
    }
  }
}

export function eliminateIdentityTemps(statements) {
  // Pre-scan: record first and last occurrence of each temp identifier.
  // This avoids O(n) backward/forward scans per temp.
  const firstSeen = new Map(); // temp -> first line index
  const lastSeen = new Map(); // temp -> last line index
  for (let i = 0; i < statements.length; i++) {
    const t = statements[i].trim();
    if (t === "") continue;
    const matches = t.match(TEMP_TOKEN_RE);
    if (matches) {
      for (const m of matches) {
        if (!firstSeen.has(m)) firstSeen.set(m, i);
        lastSeen.set(m, i);
      }
    }
  }

  let index = 0;
  while (index < statements.length) {
    const trimmed = statements[index].trim();
    if (!trimmed.startsWith("let ")) {
      index++;
      continue;
    }
    const assign = parseAssignment(trimmed);
    if (!assign || !isTempIdent(assign.lhs) || !isTempIdent(assign.rhs)) {
      index++;
      continue;
    }

    // Self-assignment is dead code.
    if (assign.lhs === assign.rhs) {
      statements[index] = "";
      index++;
      continue;
    }

    // Don't substitute if lhs appeared earlier (O(1) check via pre-scan).
    const first = firstSeen.get(assign.lhs);
    if (first !== undefined && first < index) {
      index++;
      continue;
    }

    // Substitute lhs -> rhs in all subsequent lines.
    for (let j = index + 1; j < statements.length; j++) {
      if (containsIdentifier(statements[j], assign.lhs)) {
        statements[j] = replaceIdentifier(statements[j], assign.lhs, assign.rhs);
      }
    }
    statements[index] = "";
    index++;
  }
}

export function collapseTempIntoStore(statements) {
  // Pre-scan: count how many lines each temp appears on.
  // If a temp appears on exactly 2 lines (definition + single use), it's safe
  // to collapse without scanning forward. This replaces O(n) per-temp scans.
  const tempLineCounts = new Map();
  for (let i = 0; i < statements.length; i++) {
    const t = statements[i].trim();
    if (t === "") continue;
    const matches = t.match(TEMP_TOKEN_RE);
    if (matches) {
      const seen = new Set(matches); // dedupe within same line
      for (const m of seen) {
        tempLineCounts.set(m, (tempLineCounts.get(m) || 0) + 1);
      }
    }
  }

  let index = 0;
  while (index + 1 < statements.length) {
    const trimmed = statements[index].trim();
    if (!trimmed.startsWith("let ")) {
      index++;
      continue;
    }
    const a1 = parseAssignment(trimmed);
    if (!a1 || !isTempIdent(a1.lhs)) {
      index++;
      continue;
    }

    const next = nextCodeLine(statements, index + 1);
    if (next < 0) {
      index++;
      continue;
    }

    const temp = a1.lhs;
    const trimmedNext = statements[next].trim();

    // Assignment pattern: [let] X = tN;
    const a2 = parseAssignment(trimmedNext);
    if (a2 && a2.rhs === temp) {
      // Temp on exactly 2 lines (definition + this use) means not used later.
      const usedLater = (tempLineCounts.get(temp) || 0) > 2;
      if (!usedLater) {
        const indent = leadingWhitespace(statements[next]);
        const prefix = a2.hasLet ? "let " : "";
        statements[next] = `${indent}${prefix}${a2.lhs} = ${a1.rhs};`;
        statements[index] = "";
        index = next + 1;
        continue;
      }
    }

    // Return pattern: return tN;
    if (trimmedNext === `return ${temp};`) {
      const usedLater = (tempLineCounts.get(temp) || 0) > 2;
      if (!usedLater) {
        const indent = leadingWhitespace(statements[next]);
        statements[next] = `${indent}return ${a1.rhs};`;
        statements[index] = "";
        index = next + 1;
        continue;
      }
    }
    index++;
  }
}

export function eliminateBareTempUses(statements) {
  for (let index = 0; index + 1 < statements.length; index++) {
    const assign = parseAssignment(statements[index]);
    if (!assign || !isTempIdent(assign.lhs)) {
      continue;
    }

    const next = nextCodeLine(statements, index + 1);
    if (next < 0 || statements[next].trim() !== `${assign.lhs};`) {
      continue;
    }

    statements[next] = "";
  }
}