recompilation: tolerant emit mode for untranslatable opcodes/branches

Convert five fatal "engine cannot translate" code paths from
process_instruction-returns-false (which kills the whole recompile)
into per-call runtime-abort emits that compile cleanly:

  1. Unhandled branch (target not in functions_by_vram, not a label)
     → recomp_unhandled_branch(rdram, ctx, instr_vram, target)
        + emit_return so subsequent code is unreachable

  2. JAL to unresolved target (no symbol in lookup table)
     → recomp_unhandled_call(rdram, ctx, instr_vram, target)

  3. JALR with non-RA link register
     → recomp_unhandled_jalr(rdram, ctx, instr_vram, target_value, rd)

  4. Unhandled MFC0 / MTC0 register
     → ctx->rN = recomp_unhandled_cop0_read(rdram, ctx, instr_vram, reg)
     → recomp_unhandled_cop0_write(rdram, ctx, instr_vram, reg, value)

  5. Unhandled instruction opcode (no decoder match)
     → recomp_unhandled_instruction(rdram, ctx, instr_vram, opcode_name)

The function still compiles. Other instructions still execute
normally. Only when the unsupported instruction is reached at
runtime does the abort fire — with full diagnostic context
(function, vram, opcode/target/register). Consumers implement the
runtime entry points and decide policy (abort, log+continue,
escalate to host emulator, etc.).

Per project principles
(F:\Projects\recomp-template\NES\PRINCIPLES.md #12): NOT a stub.
No behavior is simulated. The unimplementable path is left as a
loud, contextful runtime hole — the inverse of silent failure.

Driven by the Pokemon Stadium port: 171 instances of these errors
cluster in IPL3/boot/cache-mgmt/asm helpers. Pre-fix: 1 fatal
error per run, ~hundreds of by-hand toml entries needed.
Post-fix: clean exit=0, 14747 functions processed, full overlay
table emitted.

Lambda capture fix: print_func_call_by_address now captures
&output_file and instr_vram so the JAL emit path can use them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley 2026-04-26 12:03:26 -07:00
parent 23e292bba8
commit 3fe70f94a3

View File

@ -260,7 +260,7 @@ bool process_instruction(GeneratorType& generator, const N64Recomp::Context& con
return true;
};
auto print_func_call_by_address = [&generator, reloc_target_section_offset, has_reloc, reloc_section, reloc_reference_symbol, reloc_type, &context, &func, &static_funcs_out, &needs_link_branch, &print_indent, &process_delay_slot, &print_link_branch]
auto print_func_call_by_address = [&generator, reloc_target_section_offset, has_reloc, reloc_section, reloc_reference_symbol, reloc_type, &context, &func, &static_funcs_out, &needs_link_branch, &print_indent, &process_delay_slot, &print_link_branch, &output_file, instr_vram]
(uint32_t target_func_vram, bool tail_call = false, bool indent = false)
{
bool call_by_lookup = false;
@ -306,8 +306,20 @@ bool process_instruction(GeneratorType& generator, const N64Recomp::Context& con
switch (jal_result) {
case JalResolutionResult::NoMatch:
fmt::print(stderr, "No function found for jal target: 0x{:08X}\n", target_func_vram);
return false;
// No symbol found for this jal target. Instead of failing
// the whole recompile (which would block ~hundreds of
// downstream functions for one missing symbol), emit a
// runtime call that aborts loudly with diagnostic info.
// The function still compiles; only the unsupported call
// path crashes if reached. Per project principles
// (F:\Projects\recomp-template\NES\PRINCIPLES.md #12),
// this is NOT a stub — no behavior is simulated; the
// call is left as an unimplementable hole that surfaces
// at runtime with full context.
fmt::print(stderr, "[Warn] No function found for jal target 0x{:08X} in {} — emitting runtime abort\n", target_func_vram, func.name);
if (indent) fmt::print(output_file, " ");
fmt::print(output_file, " recomp_unhandled_call(rdram, ctx, 0x{:08X}u, 0x{:08X}u);\n", instr_vram, target_func_vram);
return true;
case JalResolutionResult::Match:
jal_target_name = context.functions[matched_func_index].name;
break;
@ -425,8 +437,15 @@ bool process_instruction(GeneratorType& generator, const N64Recomp::Context& con
generator.emit_cop0_status_read(rt);
break;
default:
fmt::print(stderr, "Unhandled cop0 register in mfc0: {}\n", (int)reg);
return false;
// Engine doesn't model this cop0 register yet. Emit a
// runtime call instead of failing — the function still
// compiles; runtime aborts loudly if this read is hit.
// Per project principles: not a stub, just an
// unimplementable hole surfaced at runtime.
fmt::print(stderr, "[Warn] Unhandled cop0 register in mfc0: {} — emitting runtime abort\n", (int)reg);
print_indent();
fmt::print(output_file, "ctx->r{} = recomp_unhandled_cop0_read(rdram, ctx, 0x{:08X}u, {});\n", (int)rt, instr_vram, (int)reg);
break;
}
break;
}
@ -439,8 +458,10 @@ bool process_instruction(GeneratorType& generator, const N64Recomp::Context& con
generator.emit_cop0_status_write(rt);
break;
default:
fmt::print(stderr, "Unhandled cop0 register in mtc0: {}\n", (int)reg);
return false;
fmt::print(stderr, "[Warn] Unhandled cop0 register in mtc0: {} — emitting runtime abort\n", (int)reg);
print_indent();
fmt::print(output_file, "recomp_unhandled_cop0_write(rdram, ctx, 0x{:08X}u, {}, ctx->r{});\n", instr_vram, (int)reg, (int)rt);
break;
}
break;
}
@ -481,8 +502,15 @@ bool process_instruction(GeneratorType& generator, const N64Recomp::Context& con
case InstrId::cpu_jalr:
// jalr can only be handled with $ra as the return address register
if (rd != (int)rabbitizer::Registers::Cpu::GprO32::GPR_O32_ra) {
fmt::print(stderr, "Invalid return address reg for jalr: f{}\n", rd);
return false;
// Engine doesn't model jalr with non-RA link register. Emit a
// runtime call instead of failing — the function still
// compiles; runtime aborts loudly if this jalr is reached.
// Per project principles: not a stub, an unimplementable
// hole surfaced at runtime with full context.
fmt::print(stderr, "[Warn] Invalid return address reg for jalr: r{} in {} at 0x{:08X} — emitting runtime abort\n", rd, func.name, instr_vram);
print_indent();
fmt::print(output_file, "recomp_unhandled_jalr(rdram, ctx, 0x{:08X}u, ctx->r{}, {});\n", instr_vram, (int)rs, rd);
break;
}
needs_link_branch = true;
print_func_call_by_register(rs);
@ -520,8 +548,19 @@ bool process_instruction(GeneratorType& generator, const N64Recomp::Context& con
generator.emit_return(context, func_index);
}
else {
fmt::print(stderr, "Unhandled branch in {} at 0x{:08X} to 0x{:08X}\n", func.name, instr_vram, branch_target);
return false;
// Branch to an address the engine can't resolve (not in
// functions_by_vram, not a label inside this function).
// Emit a runtime abort with diagnostic info instead of
// failing the whole recompile. The branch transfers
// control, so we follow with a return so the C compiler
// doesn't fall through into the next instruction's emit.
// Per project principles: no stub, no simulated behavior;
// the unhandled branch surfaces at runtime if reached.
fmt::print(stderr, "[Warn] Unhandled branch in {} at 0x{:08X} to 0x{:08X} — emitting runtime abort\n", func.name, instr_vram, branch_target);
print_indent();
fmt::print(output_file, "recomp_unhandled_branch(rdram, ctx, 0x{:08X}u, 0x{:08X}u);\n", instr_vram, branch_target);
print_indent();
generator.emit_return(context, func_index);
}
}
break;
@ -747,8 +786,14 @@ bool process_instruction(GeneratorType& generator, const N64Recomp::Context& con
}
if (!handled) {
fmt::print(stderr, "Unhandled instruction: {}\n", instr.getOpcodeName());
return false;
// Engine doesn't have a decoder for this opcode. Emit a runtime
// call instead of failing — function still compiles; if execution
// reaches the unhandled instruction, it aborts loudly with the
// opcode name. Per project principles: not a stub, an
// unimplementable hole surfaced at runtime with full context.
fmt::print(stderr, "[Warn] Unhandled instruction '{}' in {} at 0x{:08X} — emitting runtime abort\n", instr.getOpcodeName(), func.name, instr_vram);
print_indent();
fmt::print(output_file, "recomp_unhandled_instruction(rdram, ctx, 0x{:08X}u, \"{}\");\n", instr_vram, instr.getOpcodeName());
}
// TODO is this used?