Files
psxrecomp/recompiler/src/function_discovery.h
T
Matthew Stanley 7c40d33db3 Phase 1 complete: recompiler proven correct on SCPH1001.BIN
Phase 1a (boot slice): 30 instructions at 0xBFC00000, compiles clean.
Phase 1b (instruction coverage): 54 opcodes implemented in strict_translator,
  44,629 walker-reachable instructions covered, 0 missing.
Phase 1c (function discovery): 76 functions from 2 seeds, 2,988 instructions,
  199 call edges, 27 indirect jumps. All validated through StrictTranslator.
Phase 1d (indirect control flow): 27 sites classified — 21 A0/B0/C0 dispatch
  stubs, 4 computed calls, 2 computed tail calls. Ghidra cross-referenced
  with 0 disagreements.
Phase 1e (relocation): ROM-to-RAM copy at FUN_bfc00420 proven with both
  Ghidra (static) and DuckStation (runtime). 69 address aliases, 68/69
  function entries match byte-for-byte in RAM. Normalization rule documented.

Includes PRINCIPLES.md and DEBUG.md adapted from recomp-template.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 21:26:04 -07:00

171 lines
6.4 KiB
C++

// function_discovery.h
// ----------------------------------------------------------------------------
// Phase 1c: deterministic function discovery pipeline.
//
// Given a set of seed addresses and a flat ROM image, discovers functions
// transitively via direct JAL targets. Each function is walked exactly
// once using BFS over basic blocks. Emits structured artifacts:
// - function_manifest.json (per-function metadata)
// - function_edges.json (caller->callee edges)
// - discovery_run.log.json (structured run statistics)
//
// Design constraints (from PLAN.md Phase 1c):
// - Every seed must come from an explicit seed file.
// - Functions are identified by normalized address (addr & 0x1FFFFFFF).
// - No function may be emitted twice under the same normalized address.
// - Indirect jumps (jr non-$ra, jalr) terminate the walk for that path
// and are recorded for Phase 1d. They are NOT resolved.
// - Every instruction is validated through StrictTranslator. An
// unsupported opcode is a hard failure.
// - Direct JAL targets outside the ROM range are recorded as edges
// but NOT walked (they may be RAM-side copies; Phase 1e handles that).
#pragma once
#include <cstdint>
#include <map>
#include <set>
#include <string>
#include <vector>
#include "strict_translator.h"
namespace PSXRecompV4 {
struct Seed {
uint32_t address;
std::string label;
std::string rationale;
};
struct BasicBlockInfo {
uint32_t start_addr;
uint32_t end_addr; // address of last instruction (inclusive)
uint32_t instruction_count;
std::string termination; // "fall_through", "jr_ra", "jr_indirect", "jalr",
// "j_intra", "j_out", "branch", "jal", "rfe",
// "syscall", "break"
uint32_t branch_target; // target address for branches/jumps, 0 if N/A
};
struct DiscoveredFunction {
uint32_t entry_addr;
uint32_t normalized_addr; // entry_addr & 0x1FFFFFFF
uint32_t end_addr; // highest instruction address (inclusive)
uint32_t instruction_count;
std::string termination_reason; // primary termination: "jr_ra", "rfe",
// "j_out_of_function",
// "indirect_jump_recorded_for_1d",
// "mixed" (multiple exit types)
std::string discovered_by; // seed lineage description
std::vector<uint32_t> block_leaders; // sorted basic block start addresses
};
struct CallEdge {
uint32_t caller_func; // entry address of calling function
uint32_t caller_pc; // PC of the JAL instruction
uint32_t callee_addr; // target of the JAL
bool in_rom; // true if callee_addr is within ROM range
};
struct PrecedingInstr {
uint32_t address;
uint32_t raw;
std::string mnemonic;
};
struct IndirectJumpSite {
uint32_t func_addr; // entry address of containing function
uint32_t pc; // PC of the indirect jump
uint8_t reg; // register used (e.g. $t0 for jr $t0)
std::string kind; // "jr_non_ra", "jalr"
std::string classification; // "a0_b0_c0_dispatch", "computed_tail_call",
// "computed_call", "jump_table", "unknown"
std::vector<PrecedingInstr> context_window; // ~16 instructions before the jump
};
struct UnsupportedInstr {
uint32_t address;
uint32_t raw;
std::string reason;
};
struct DiscoveryResult {
bool ok = false;
std::vector<DiscoveredFunction> functions;
std::vector<CallEdge> edges;
std::vector<IndirectJumpSite> indirect_jumps;
std::vector<UnsupportedInstr> unsupported;
// Statistics
uint32_t total_instructions = 0;
uint32_t total_functions = 0;
uint32_t total_edges = 0;
uint32_t total_indirect_jumps = 0;
// Opcode histogram: mnemonic -> count
std::map<std::string, uint32_t> opcode_histogram;
};
class FunctionDiscovery {
public:
// Discover functions starting from the given seeds.
// rom: flat BIOS image bytes
// base_addr: virtual address of rom[0] (e.g. 0xBFC00000)
// rom_end: last valid address inclusive (e.g. 0xBFC7FFFF)
// seeds: explicit seed addresses with labels
static DiscoveryResult discover(
const std::vector<uint8_t>& rom,
uint32_t base_addr,
uint32_t rom_end,
const std::vector<Seed>& seeds);
private:
// Walk a single function via BFS. Returns the set of visited
// (addr, raw) pairs, basic block info, edges, and indirect jump sites.
struct SingleFunctionResult {
std::vector<std::pair<uint32_t, uint32_t>> instructions; // (addr, raw) sorted
std::vector<BasicBlockInfo> blocks;
std::vector<CallEdge> edges;
std::vector<IndirectJumpSite> indirect_jumps;
std::vector<UnsupportedInstr> unsupported;
std::set<std::string> exit_types; // "jr_ra", "rfe", etc.
uint32_t end_addr = 0;
};
static SingleFunctionResult walk_function(
const std::vector<uint8_t>& rom,
uint32_t base_addr,
uint32_t rom_end,
uint32_t entry,
uint32_t hard_cap,
const std::string& lineage);
static uint32_t read_u32_le(const std::vector<uint8_t>& rom, uint32_t offset);
// Control flow classification for a raw instruction at a given address.
enum class CFKind {
Normal, // falls through
CondBranch, // conditional branch (BEQ/BNE/BLEZ/BGTZ/BLTZ/BGEZ/BLTZAL/BGEZAL)
Jump, // unconditional J
Call, // JAL
JR_RA, // jr $ra (function return)
JR_Indirect, // jr $reg (not $ra)
JALR, // jalr (indirect call)
Syscall, // SYSCALL
Break, // BREAK
RFE, // return from exception
};
struct CFInfo {
CFKind kind;
uint32_t target; // computed target for CondBranch/Jump/Call, 0 otherwise
uint8_t reg; // register for JR_Indirect/JALR
};
static CFInfo classify_control_flow(uint32_t raw, uint32_t addr);
};
} // namespace PSXRecompV4