import { makeUniqueIdentifier, sanitizeIdentifier } from "./manifest.js";
import { jumpTarget } from "./high-level-utils.js";
import { hexOffset } from "./util.js";
const TERMINATOR_MNEMONICS = new Set(["RET", "THROW", "ABORT", "ABORTMSG"]);
const BRANCH_MNEMONICS = new Set([
"JMP", "JMP_L", "JMPIF", "JMPIF_L", "JMPIFNOT", "JMPIFNOT_L",
"JMPEQ", "JMPEQ_L", "JMPNE", "JMPNE_L",
"JMPGT", "JMPGT_L", "JMPGE", "JMPGE_L",
"JMPLT", "JMPLT_L", "JMPLE", "JMPLE_L",
"ENDTRY", "ENDTRY_L",
]);
function collectControlFlowEdges(instructions, knownOffsets) {
const edges = [];
const pushEdge = (source, delta) => {
const target = source + delta;
if (target >= 0 && knownOffsets.has(target)) {
edges.push([source, target]);
}
};
for (const instruction of instructions) {
const mnemonic = instruction.opcode.mnemonic;
if (BRANCH_MNEMONICS.has(mnemonic)) {
const operand = instruction.operand;
if (operand?.kind === "Jump" || operand?.kind === "Jump32") {
pushEdge(instruction.offset, operand.value);
}
} else if (
mnemonic === "TRY" &&
instruction.operand?.kind === "Bytes" &&
instruction.operand.value.length === 2
) {
for (const byte of instruction.operand.value) {
pushEdge(instruction.offset, byte > 127 ? byte - 256 : byte);
}
} else if (
mnemonic === "TRY_L" &&
instruction.operand?.kind === "Bytes" &&
instruction.operand.value.length === 8
) {
const v = instruction.operand.value;
for (const base of [0, 4]) {
const delta =
v[base] | (v[base + 1] << 8) | (v[base + 2] << 16) | (v[base + 3] << 24);
pushEdge(instruction.offset, delta);
}
}
}
return edges;
}
function collectPostTerminatorStarts(instructions, starts) {
const knownOffsets = new Set(instructions.map((instruction) => instruction.offset));
const edges = collectControlFlowEdges(instructions, knownOffsets);
const edgesByTarget = new Map();
for (const [source, target] of edges) {
const sources = edgesByTarget.get(target);
if (sources) sources.push(source);
else edgesByTarget.set(target, [source]);
}
const baselineStartsSorted = [...starts].sort((a, b) => a - b);
const lastStartLE = (offset) => {
let lo = 0, hi = baselineStartsSorted.length;
while (lo < hi) {
const mid = (lo + hi) >> 1;
if (baselineStartsSorted[mid] <= offset) lo = mid + 1;
else hi = mid;
}
return lo > 0 ? baselineStartsSorted[lo - 1] : null;
};
const firstStartGT = (offset) => {
for (const s of baselineStartsSorted) if (s > offset) return s;
return Number.POSITIVE_INFINITY;
};
for (let i = 0; i + 1 < instructions.length; i++) {
const current = instructions[i];
const next = instructions[i + 1];
if (!TERMINATOR_MNEMONICS.has(current.opcode.mnemonic)) continue;
const methodStart = lastStartLE(current.offset) ?? current.offset;
const methodEnd = firstStartGT(methodStart);
const incoming = edgesByTarget.get(next.offset) ?? [];
const hasIncomingFromSameBaselineMethod = incoming.some(
(source) => source >= methodStart && source < methodEnd,
);
const hasIncomingFromOtherBaselineMethod = incoming.some(
(source) => source < methodStart || source >= methodEnd,
);
const hasSameBaselineIncomingLaterInRange = edges.some(
([source, target]) =>
source >= methodStart && source < methodEnd &&
target > next.offset && target < methodEnd,
);
const detachedTailAfterTerminator =
hasIncomingFromOtherBaselineMethod ||
(!hasIncomingFromSameBaselineMethod && !hasSameBaselineIncomingLaterInRange);
if (detachedTailAfterTerminator) {
starts.add(next.offset);
}
}
}
export function buildMethodGroups(instructions, manifest, options = {}) {
const includePostTerminatorTails = options.includePostTerminatorTails ?? true;
const entryOffset = instructions[0]?.offset ?? 0;
const used = new Set();
const methods = manifest?.abi?.methods ?? [];
const manifestOffsets = methods
.map((method, index) => ({ index, offset: method.offset }))
.filter((entry) => Number.isInteger(entry.offset) && entry.offset >= 0)
.sort((left, right) => left.offset - right.offset);
const knownOffsets = new Set(instructions.map((instruction) => instruction.offset));
const starts = new Set([entryOffset]);
for (const { offset } of manifestOffsets) {
starts.add(offset);
}
for (const instruction of instructions) {
if (instruction.opcode.mnemonic === "INITSLOT") {
starts.add(instruction.offset);
}
if (instruction.opcode.mnemonic === "CALL" || instruction.opcode.mnemonic === "CALL_L") {
const target = jumpTarget(instruction);
if (target !== null && knownOffsets.has(target)) {
starts.add(target);
}
}
}
if (includePostTerminatorTails) {
collectPostTerminatorStarts(instructions, starts);
}
const groups = [];
const offsets = [...starts].sort((left, right) => left - right);
const manifestOffsetMap = new Map(manifestOffsets.map(({ offset, index }) => [offset, index]));
const allManifestOffsetsMissing = manifestOffsets.length === 0;
for (const offset of offsets) {
if (offset === entryOffset && allManifestOffsetsMissing && methods[0]) {
groups.push({
start: offset,
name: makeUniqueIdentifier(sanitizeIdentifier(methods[0].name), used),
source: methods[0],
});
continue;
}
const manifestIndex = manifestOffsetMap.get(offset);
if (manifestIndex !== undefined) {
groups.push({
start: offset,
name: makeUniqueIdentifier(sanitizeIdentifier(methods[manifestIndex].name), used),
source: methods[manifestIndex],
});
continue;
}
groups.push({
start: offset,
name: makeUniqueIdentifier(
offset === entryOffset ? "script_entry" : `sub_0x${hexOffset(offset)}`,
used,
),
source: null,
});
}
groups.sort((left, right) => left.start - right.start);
let instrIdx = 0;
return groups.map((group, index) => {
const nextStart = groups[index + 1]?.start ?? Number.POSITIVE_INFINITY;
const end = groups[index + 1]?.start ?? (instructions.at(-1)?.offset ?? group.start) + 1;
const groupInstructions = [];
while (instrIdx < instructions.length && instructions[instrIdx].offset < group.start) {
instrIdx++;
}
while (instrIdx < instructions.length && instructions[instrIdx].offset < nextStart) {
groupInstructions.push(instructions[instrIdx++]);
}
return { ...group, end, instructions: groupInstructions };
});
}