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 {
  collectDerivedWarnings,
  rewriteForLoops,
} from "./high-level-control-flow-shared.js";
import { createMalformedTryLifter } from "./high-level-try-malformed.js";
import { findMnemonicFrom, findTryFrom, tryHandlerTargets } from "./high-level-try-shared.js";
import { jumpTarget } from "./high-level-utils.js";

export function createTryHelpers(runtime) {
  const { createState, cloneState, executeStraightLine } = runtime;
  const liftMalformedTryBlock = createMalformedTryLifter(runtime);

  function tryLiftSimpleTryBlock(instructions, manifestMethod, context, methodOffset) {
    const tryIndex = findTryFrom(instructions, 0);
    if (tryIndex < 0) {
      return null;
    }

    const tryInstruction = instructions[tryIndex];
    const handlerTargets = tryHandlerTargets(tryInstruction);
    if (handlerTargets === null) {
      return null;
    }

    const { bodyStart, catchTarget, finallyTarget } = handlerTargets;
    const indexByOffset = new Map();
    for (let i = 0; i < instructions.length; i++) {
      indexByOffset.set(instructions[i].offset, i);
    }

    const bodyStartIndex = indexByOffset.get(bodyStart);
    if (bodyStartIndex === undefined) {
      return null;
    }

    const catchIndex = catchTarget === null ? null : indexByOffset.get(catchTarget);
    const finallyIndex = finallyTarget === null ? null : indexByOffset.get(finallyTarget);
    const hasMissingHandlers =
      (catchTarget !== null && catchIndex === undefined) ||
      (finallyTarget !== null && finallyIndex === undefined);

    const endtryGlobalIndex = findMnemonicFrom(instructions, bodyStartIndex, "ENDTRY");
    if (hasMissingHandlers || endtryGlobalIndex < 0) {
      return liftMalformedTryBlock({
        instructions,
        manifestMethod,
        context,
        methodOffset,
        tryIndex,
        bodyStartIndex,
        catchIndex,
        finallyIndex,
      });
    }
    let catchSlice = [];
    let finallySlice = [];
    let resumeSlice = [];
    let allowBareTry = false;

    if (catchTarget !== null && finallyTarget !== null) {
      const finallyEndGlobalIndex = findMnemonicFrom(
        instructions,
        endtryGlobalIndex + 1,
        "ENDFINALLY",
      );

      catchSlice = instructions.slice(catchIndex, finallyIndex);
      if (finallyEndGlobalIndex >= 0) {
        finallySlice = instructions.slice(finallyIndex, finallyEndGlobalIndex);
        resumeSlice = instructions.slice(finallyEndGlobalIndex + 1);
      }
    } else if (catchTarget !== null) {
      const catchEndGlobalIndex = findMnemonicFrom(
        instructions,
        endtryGlobalIndex + 1,
        "ENDTRY",
      );
      if (catchEndGlobalIndex >= 0) {
        const catchEndInstruction = instructions[catchEndGlobalIndex];
        const catchEndTarget = jumpTarget(catchEndInstruction);
        if (catchEndTarget !== null && catchEndTarget > catchTarget) {
          catchSlice = instructions.slice(catchIndex, indexByOffset.get(catchEndTarget));
          resumeSlice = instructions.slice(catchEndGlobalIndex + 1);
        }
      }

      if (
        catchSlice.length === 0 &&
        instructions[catchIndex]?.opcode.mnemonic === "ENDFINALLY"
      ) {
        allowBareTry = true;
        resumeSlice = instructions.slice(catchIndex + 1);
      }
    } else if (finallyTarget !== null) {
      const finallyEndGlobalIndex = findMnemonicFrom(
        instructions,
        finallyIndex,
        "ENDFINALLY",
      );
      if (finallyEndGlobalIndex >= 0) {
        finallySlice = instructions.slice(finallyIndex, finallyEndGlobalIndex);
        // Resume picks up after ENDFINALLY. The finally body's stack
        // effects are propagated by cloning the resume state from
        // `finallyState` further below - re-slicing the finally bytes
        // into the resume would duplicate them and trip the
        // unstructured ENDFINALLY renderer.
        resumeSlice = instructions.slice(finallyEndGlobalIndex + 1);
      }
    }

    if (catchSlice.length === 0 && finallySlice.length === 0 && !allowBareTry) {
      return null;
    }

    const prefixState = createState(manifestMethod, context, methodOffset, instructions);
    executeStraightLine(prefixState, instructions.slice(0, tryIndex));
    const tryBodyState = cloneState(prefixState);
    executeStraightLine(tryBodyState, instructions.slice(bodyStartIndex, endtryGlobalIndex));

    const statements = [...prefixState.statements];
    statements.push("try {");
    statements.push(...tryBodyState.statements.slice(prefixState.statements.length));
    let catchState = null;
    let finallyState = null;
    let resumeState = null;

    if (catchSlice.length > 0) {
      catchState = cloneState(prefixState);
      catchState.stack.push("exception");
      executeStraightLine(catchState, catchSlice);
      statements.push("} catch {");
      statements.push(...catchState.statements.slice(prefixState.statements.length));
    }

    if (finallySlice.length > 0) {
      finallyState = cloneState(prefixState);
      executeStraightLine(finallyState, finallySlice);
      statements.push("} finally {");
      statements.push(...finallyState.statements.slice(prefixState.statements.length));
    }

    statements.push("}");

    if (resumeSlice.length > 0) {
      // Stack values that the try / finally body left on the operand
      // stack flow through to the resume target on the normal path
      // (NEO VM preserves the stack across try-context exits). Clone
      // from the most-recently-finished branch so the resume sees the
      // full stack a downstream RET / consumer expects: finally if it
      // ran, else the try body, else the prefix.
      const upstream = finallyState ?? tryBodyState ?? prefixState;
      resumeState = cloneState(upstream);
      executeStraightLine(resumeState, resumeSlice);
      statements.push(...resumeState.statements.slice(upstream.statements.length));
    }

    return rewriteForLoops({
      statements,
      warnings: collectDerivedWarnings(
        prefixState,
        tryBodyState,
        catchState,
        finallyState,
        resumeState,
      ),
    });
  }

  return { tryLiftSimpleTryBlock };
}