import { SYSCALLS } from "./generated/syscalls.js";
import { describeCallFlags } from "./nef.js";
import {
finalizeCallGraph,
resolveMethodTarget,
} from "./call-graph-methods.js";
import {
pointerTargetBeforeIndex,
pointerTargetFromSlotFlow,
relativePointerTarget,
} from "./call-graph-pointers.js";
import {
isLoadArgument,
isLoadLocal,
isLoadStatic,
isStoreArgument,
isStoreLocal,
isStoreStatic,
slotIndex,
} from "./call-graph-slots.js";
import {
ensureArgValueArray,
integerValue,
isImmediateInteger,
mergeValues,
pointerValue,
popMany,
popValue,
propagateCallArguments,
valueToPointer,
} from "./call-graph-values.js";
import { upperHex } from "./util.js";
export function buildCallGraph(nef, instructions, methodGroups) {
const methods = methodGroups.map((group) => ({
offset: group.start,
name: group.name,
}));
const methodByOffset = new Map(methods.map((method) => [method.offset, method]));
const methodStartOffsets = new Set(methods.map((method) => method.offset));
const instructionOffsets = new Set(instructions.map((ins) => ins.offset));
const methodArgCountsByOffset = new Map(
methodGroups.map((group) => [group.start, inferMethodArgCount(group)]),
);
const methodArgValues = new Map();
const edges = [];
const localValues = new Map();
const staticValues = new Map();
let valueStack = [];
let currentMethodOffset = methods[0]?.offset ?? 0;
let currentArgValues = methodArgValues.get(currentMethodOffset) ?? [];
let methodCursor = 0;
const fallbackCaller = methods[0] ?? { offset: 0, name: "script_entry" };
for (let index = 0; index < instructions.length; index += 1) {
const instruction = instructions[index];
while (
methodCursor + 1 < methods.length &&
methods[methodCursor + 1].offset <= instruction.offset
) {
methodCursor += 1;
}
const caller = methods[methodCursor] ?? fallbackCaller;
const mnemonic = instruction.opcode.mnemonic;
if (index > 0 && methodStartOffsets.has(instruction.offset)) {
localValues.clear();
valueStack = [];
currentMethodOffset = caller.offset;
currentArgValues = methodArgValues.get(currentMethodOffset) ?? [];
}
if (mnemonic === "NOP") {
continue;
}
if (mnemonic === "PUSHA" && instruction.operand?.kind === "I32") {
const target = relativePointerTarget(instruction);
valueStack.push(target !== null ? pointerValue(target) : null);
continue;
}
if (isImmediateInteger(mnemonic, instruction)) {
valueStack.push(integerValue(mnemonic, instruction));
continue;
}
if (mnemonic === "NEWARRAY0") {
valueStack.push({ kind: "array", items: [] });
continue;
}
if (mnemonic === "DUP") {
valueStack.push(valueStack.at(-1) ?? null);
continue;
}
if (mnemonic === "DROP") {
valueStack.pop();
continue;
}
if (isLoadArgument(mnemonic)) {
const slot = slotIndex(mnemonic, instruction);
valueStack.push(slot !== null ? currentArgValues[slot] ?? null : null);
continue;
}
if (isLoadLocal(mnemonic)) {
const slot = slotIndex(mnemonic, instruction);
valueStack.push(slot !== null ? localValues.get(slot) ?? null : null);
continue;
}
if (isLoadStatic(mnemonic)) {
const slot = slotIndex(mnemonic, instruction);
valueStack.push(slot !== null ? staticValues.get(slot) ?? null : null);
continue;
}
if (isStoreLocal(mnemonic)) {
const slot = slotIndex(mnemonic, instruction);
const value = popValue(valueStack);
const target =
valueToPointer(value) ??
pointerTargetBeforeIndex(instructions, index, localValues, staticValues);
if (slot !== null && value !== null) {
localValues.set(slot, value);
} else if (slot !== null && target !== null) {
localValues.set(slot, pointerValue(target));
} else if (slot !== null) {
localValues.delete(slot);
}
continue;
}
if (isStoreStatic(mnemonic)) {
const slot = slotIndex(mnemonic, instruction);
const value = popValue(valueStack);
const target =
valueToPointer(value) ??
pointerTargetBeforeIndex(instructions, index, localValues, staticValues);
if (slot !== null && value !== null) {
staticValues.set(slot, value);
} else if (slot !== null && target !== null) {
staticValues.set(slot, pointerValue(target));
} else if (slot !== null) {
staticValues.delete(slot);
}
continue;
}
if (isStoreArgument(mnemonic)) {
const slot = slotIndex(mnemonic, instruction);
const value = popValue(valueStack);
if (slot !== null) {
currentArgValues = ensureArgValueArray(
methodArgValues,
currentMethodOffset,
Math.max(currentArgValues.length, slot + 1),
);
currentArgValues[slot] = mergeValues(currentArgValues[slot], value);
}
continue;
}
if (mnemonic === "APPEND") {
const item = popValue(valueStack);
const target = popValue(valueStack);
if (target?.kind === "array") {
target.items.push(item);
}
continue;
}
if (mnemonic === "PICKITEM") {
const indexValue = popValue(valueStack);
const target = popValue(valueStack);
if (
target?.kind === "array" &&
indexValue?.kind === "int" &&
indexValue.value >= 0 &&
indexValue.value < target.items.length
) {
valueStack.push(target.items[indexValue.value] ?? null);
} else {
valueStack.push(null);
}
continue;
}
if (mnemonic === "SYSCALL" && instruction.operand?.kind === "Syscall") {
const info = SYSCALLS.get(instruction.operand.value) ?? null;
popMany(valueStack, info?.param_count ?? 0);
if (info?.returns_value ?? true) {
valueStack.push(null);
}
edges.push({
caller,
callOffset: instruction.offset,
opcode: mnemonic,
target: {
kind: "Syscall",
hash: instruction.operand.value,
name: info?.name ?? null,
returnsValue: info?.returns_value ?? true,
},
});
continue;
}
if ((mnemonic === "CALL" || mnemonic === "CALL_L") && isJumpOperand(instruction.operand)) {
const targetOffset = instruction.offset + instruction.operand.value;
if (targetOffset < 0 || !instructionOffsets.has(targetOffset)) {
edges.push({
caller,
callOffset: instruction.offset,
opcode: mnemonic,
target: { kind: "UnresolvedInternal", target: targetOffset },
});
continue;
}
propagateCallArguments(
methodArgValues,
methodArgCountsByOffset,
targetOffset,
valueStack,
);
edges.push({
caller,
callOffset: instruction.offset,
opcode: mnemonic,
target: {
kind: "Internal",
method: resolveMethodTarget(methodByOffset, targetOffset),
},
});
continue;
}
if (mnemonic === "CALLT" && instruction.operand?.kind === "U16") {
const token = nef.methodTokens[instruction.operand.value] ?? null;
popMany(valueStack, token?.parametersCount ?? 0);
if (token?.hasReturnValue ?? false) {
valueStack.push(null);
}
edges.push({
caller,
callOffset: instruction.offset,
opcode: mnemonic,
target: token
? {
kind: "MethodToken",
index: instruction.operand.value,
hashLe: upperHex(token.hash),
hashBe: upperHex([...token.hash].reverse()),
method: token.method,
parametersCount: token.parametersCount,
hasReturnValue: token.hasReturnValue,
callFlags: token.callFlags,
callFlagsDescription: describeCallFlags(token.callFlags),
}
: {
kind: "Indirect",
opcode: mnemonic,
operand: instruction.operand.value,
},
});
continue;
}
if (mnemonic === "CALLA") {
const stackTarget = valueToPointer(popValue(valueStack));
const rawTarget =
stackTarget ??
pointerTargetBeforeIndex(instructions, index, localValues, staticValues) ??
pointerTargetFromSlotFlow(instructions[index - 1], localValues, staticValues);
const resolved =
rawTarget !== null && instructionOffsets.has(rawTarget) ? rawTarget : null;
if (resolved !== null) {
propagateCallArguments(
methodArgValues,
methodArgCountsByOffset,
resolved,
valueStack,
);
}
edges.push({
caller,
callOffset: instruction.offset,
opcode: mnemonic,
target:
resolved !== null
? {
kind: "Internal",
method: resolveMethodTarget(methodByOffset, resolved),
}
: {
kind: "Indirect",
opcode: mnemonic,
operand: null,
},
});
}
}
return finalizeCallGraph(methods, edges);
}
function isJumpOperand(operand) {
return operand?.kind === "Jump" || operand?.kind === "Jump32";
}
function inferMethodArgCount(group) {
if (group.source?.parameters) {
return group.source.parameters.length;
}
const first = group.instructions[0];
if (
first?.opcode?.mnemonic === "INITSLOT" &&
first.operand?.kind === "Bytes" &&
first.operand.value.length >= 2
) {
return first.operand.value[1];
}
let maxArg = -1;
for (const instruction of group.instructions) {
if (isLoadArgument(instruction.opcode.mnemonic) || isStoreArgument(instruction.opcode.mnemonic)) {
const slot = slotIndex(instruction.opcode.mnemonic, instruction);
if (slot !== null) {
maxArg = Math.max(maxArg, slot);
}
}
}
if (maxArg >= 0) {
return maxArg + 1;
}
return 0;
}