recompilation: extend tolerant emit to print_branch fall-through path

Found a third emit-non-compilable-code path during Pokemon Stadium
runner build: print_branch lambda (line 371) checks for tail-call
candidates when the branch target is outside the function, prints
a warning if not found — then falls through to emit
`goto L_<branch_target>` to a label that doesn't exist in the
generated function. Linker compiles fail with "use of undeclared
label L_XXXXXXXX".

Fixed by emitting the same recomp_unhandled_branch runtime call as
the other tolerant-emit paths, plus an early return so the fall-
through to emit_goto doesn't run.

Also exposes recomp_unhandled_* declarations in include/recomp.h
(after the existing recomp_context definition + recomp_syscall_handler
decl) so generated C compiles cleanly without consumer-side header
patches.

171 → ~600+ tolerant-emit warnings on Pokemon Stadium pass; full
build pipeline now produces a linked PokemonStadiumRecomp.exe
that loads librecomp/ultramodern and runs cleanly to a "link-only
entry" message. Real boot still needs runtime wiring (renderer,
scheduler, init).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley 2026-04-26 12:21:13 -07:00
parent 3fe70f94a3
commit 5c0f771c59
2 changed files with 35 additions and 1 deletions

View File

@ -1,6 +1,8 @@
#ifndef __RECOMP_H__
#define __RECOMP_H__
#include <stdint.h>
#include <stdlib.h>
#include <stdint.h>
#include <math.h>
@ -468,6 +470,24 @@ void recomp_syscall_handler(uint8_t* rdram, recomp_context* ctx, int32_t instruc
void pause_self(uint8_t *rdram);
/* Tolerant-emit runtime entry points. The engine emits calls to these
* for instructions/branches/calls it can't translate at compile time;
* consumers implement them in a runtime hook file (e.g. extras.c).
* See src/recompilation.cpp for the emit sites and PRINCIPLES.md #12
* for why these exist (loud runtime aborts, NOT stubs). */
void recomp_unhandled_branch(uint8_t *rdram, recomp_context *ctx,
uint32_t instr_vram, uint32_t branch_target);
void recomp_unhandled_call(uint8_t *rdram, recomp_context *ctx,
uint32_t instr_vram, uint32_t target);
void recomp_unhandled_jalr(uint8_t *rdram, recomp_context *ctx,
uint32_t instr_vram, uint64_t target_value, int rd);
uint64_t recomp_unhandled_cop0_read(uint8_t *rdram, recomp_context *ctx,
uint32_t instr_vram, int cop0_reg);
void recomp_unhandled_cop0_write(uint8_t *rdram, recomp_context *ctx,
uint32_t instr_vram, int cop0_reg, uint64_t value);
void recomp_unhandled_instruction(uint8_t *rdram, recomp_context *ctx,
uint32_t instr_vram, const char *opcode_name);
#ifdef __cplusplus
}
#endif

View File

@ -387,7 +387,21 @@ bool process_instruction(GeneratorType& generator, const N64Recomp::Context& con
return true;
}
fmt::print(stderr, "[Warn] Function {} is branching outside of the function (to 0x{:08X})\n", func.name, branch_target);
// Branch out of function with no symbol at target. Emit a
// runtime abort instead of `goto L_<vram>` to a label that
// never exists. Per project principles: no stub, an
// unimplementable hole surfaced loudly at runtime.
fmt::print(stderr, "[Warn] Function {} is branching outside of the function (to 0x{:08X}) — emitting runtime abort\n", func.name, branch_target);
if (!process_delay_slot(true)) {
return false;
}
print_indent();
print_indent();
fmt::print(output_file, "recomp_unhandled_branch(rdram, ctx, 0x{:08X}u, 0x{:08X}u);\n", instr_vram, branch_target);
print_indent();
print_indent();
generator.emit_return(context, func_index);
return true;
}
if (!process_delay_slot(true)) {