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 {
  braceDelta,
  containsIdentifier,
  extractWhileCondition,
  findBlockEnd,
  isBlank,
  nextCodeLine,
  parseAssignment,
  parseForParts,
  prevCodeLine,
  replaceIdentifier,
} from "./postprocess-shared.js";

export function rewriteForLoops(statements) {
  let index = 0;
  while (index < statements.length) {
    const condition = extractWhileCondition(statements[index]);
    if (condition === null) {
      index++;
      continue;
    }

    const end = findBlockEnd(statements, index);
    if (end < 0) {
      index++;
      continue;
    }

    const initIdx = findInitializerIndex(statements, index);
    if (initIdx < 0) {
      index++;
      continue;
    }
    const initAssign = parseAssignment(statements[initIdx]);
    if (!initAssign) {
      index++;
      continue;
    }

    const increment = findIncrementAssignment(statements, index, end, initAssign.lhs);
    if (!increment) {
      index++;
      continue;
    }

    statements[index] = `for (${initAssign.full}; ${condition}; ${increment.expr}) {`;
    statements[initIdx] = "";
    statements[increment.incrementIdx] = "";
    if (increment.tempIdx !== null) {
      statements[increment.tempIdx] = "";
    }
    index++;
  }
}

function findInitializerIndex(statements, start) {
  let index = start;
  while (index > 0) {
    index--;
    const line = statements[index].trim();
    if (isBlank(line)) continue;
    if (line === "}" || line.endsWith("{")) break;
    if (line.includes("=") && line.endsWith(";")) {
      const a = parseAssignment(line);
      if (a && (a.lhs.startsWith("loc") || a.lhs.startsWith("arg") || a.lhs.startsWith("static"))) {
        return index;
      }
    }
  }
  return -1;
}

function findIncrementAssignment(statements, start, end, varName) {
  let index = end;
  while (index > start) {
    index--;
    const line = statements[index].trim();
    if (isBlank(line) || line === "}") continue;
    const assign = parseAssignment(line);
    if (!assign || assign.lhs !== varName) return null;

    if (assign.rhs.startsWith(varName)) {
      return { incrementIdx: index, tempIdx: null, expr: assign.full };
    }
    const prevIdx = prevCodeLine(statements, index);
    if (prevIdx < 0) return null;
    const prevAssign = parseAssignment(statements[prevIdx]);
    if (!prevAssign) return null;

    if (prevAssign.lhs === assign.rhs) {
      return {
        incrementIdx: index,
        tempIdx: prevIdx,
        expr: `${varName} = ${prevAssign.rhs}`,
      };
    }
    if (containsIdentifier(assign.rhs, prevAssign.lhs)) {
      const replaced = replaceIdentifier(assign.rhs, prevAssign.lhs, prevAssign.rhs);
      return {
        incrementIdx: index,
        tempIdx: prevIdx,
        expr: `${varName} = ${replaced}`,
      };
    }
    return null;
  }
  return null;
}

// Resolve the inlined condition text for a single-use temp whose value feeds a
// loop/if header. A bare `t` inlines to its rhs; a negated `!t` inlines to
// `!(rhs)` so the `!` binds the whole expression. Mirrors the Rust port's
// condition_inline_candidate. Returns null when the condition is neither form.
function inlinedCondition(condition, assign) {
  if (assign.lhs === condition) {
    return assign.rhs;
  }
  if (condition.startsWith("!") && condition.slice(1).trim() === assign.lhs) {
    return `!(${assign.rhs})`;
  }
  return null;
}

export function inlineConditionTemps(statements) {
  let index = 0;
  while (index < statements.length) {
    const trimmed = statements[index].trim();

    let cond = null;
    let forParts = null;
    let kind = null;

    if (trimmed.endsWith(" {")) {
      if (trimmed.startsWith("while ")) {
        cond = trimmed.slice(6, -2).trim();
        kind = "while";
      } else if (trimmed.startsWith("for (")) {
        let inner = trimmed.slice(5, -2).trim();
        if (inner.endsWith(")")) inner = inner.slice(0, -1).trim();
        const parts = inner.split(";");
        if (parts.length === 3) {
          forParts = {
            init: parts[0].trim(),
            condition: parts[1].trim(),
            increment: parts[2].trim(),
          };
          kind = "for";
        }
      } else if (trimmed.startsWith("if ")) {
        cond = trimmed.slice(3, -2).trim();
        kind = "if";
      }
    }

    if (kind !== null) {
      const idx = prevCodeLine(statements, index);
      if (idx >= 0) {
        const assign = parseAssignment(statements[idx]);
        if (assign && shouldInlineCondition(assign.rhs)) {
          if (kind === "while") {
            const inlined = inlinedCondition(cond, assign);
            if (inlined !== null) {
              statements[index] = `while ${inlined} {`;
              statements[idx] = "";
            }
          } else if (kind === "for") {
            const inlined = inlinedCondition(forParts.condition, assign);
            if (inlined !== null) {
              statements[index] = `for (${forParts.init}; ${inlined}; ${forParts.increment}) {`;
              statements[idx] = "";
            }
          } else if (kind === "if") {
            const inlined = inlinedCondition(cond, assign);
            if (inlined !== null) {
              statements[index] = `if ${inlined} {`;
              statements[idx] = "";
            }
          }
        }
      }
    }

    index++;
  }
}

function shouldInlineCondition(rhs) {
  if (rhs === "true" || rhs === "false") return true;
  if (rhs.includes(" ")) return true;
  return /[<>!=+\-*\/&|]/.test(rhs);
}

export function inlineForIncrementTemps(statements) {
  let index = 0;
  while (index < statements.length) {
    const forParts = parseForParts(statements[index]);
    if (forParts === null) {
      index++;
      continue;
    }

    let depth = 1;
    let cursor = index + 1;
    while (cursor < statements.length && depth > 0) {
      depth += braceDelta(statements[cursor]);
      if (depth <= 0) break;
      const line = statements[cursor].trim();
      if (line.startsWith("let ")) {
        const assign = parseAssignment(line);
        if (assign && containsIdentifier(forParts.increment, assign.lhs)) {
          // Only inline+delete the definition when the temp is used nowhere
          // else (besides its own def and the for-header increment) AND its
          // value is pure (no call). Otherwise clearing the definition would
          // dangle a still-live reference, and moving a side-effecting RHS into
          // the increment would change evaluation order. Mirrors the Rust port.
          const usedElsewhere = statements.some(
            (stmt, i) => i !== cursor && i !== index && containsIdentifier(stmt, assign.lhs),
          );
          if (usedElsewhere || assign.rhs.includes("(")) {
            cursor++;
            continue;
          }
          const replaced = replaceIdentifier(forParts.increment, assign.lhs, assign.rhs);
          statements[index] = `for (${forParts.init}; ${forParts.condition}; ${replaced}) {`;
          statements[cursor] = "";
          break;
        }
      }
      cursor++;
    }
    index++;
  }
}