Conditional branches no longer end compiled blocks. The not-taken path
falls through into the next inline instruction; a taken branch whose
target is an instruction boundary inside the same block becomes a
native jump to that instruction's label (per-insn labels, patched via
jit_patch_at). Backward edges - loops - pass a guard that returns to
the dispatcher when the per-dispatch loop budget (JIT_LOOP_BUDGET,
default 256, -D-overridable) is exhausted, a host stop is requested,
or the IRQ line is asserted, bounding tick/stop/IRQ latency inside
native loops. Taken branches to external targets store PC and return
as before. No cross-block links are created, so SMC invalidation
still destroys exactly one self-contained Lightning function; the
differential SMC fuzzer and the whole suite stay green.
Blocks also end at STP/WAI now (bytes after them never execute before
a re-dispatch, and decoding past them extended covered spans into
adjacent data). The back-edge guard accumulates loop spans into
stats.insns_executed (exact for straight lines and back-edges,
approximate when an early-exit branch leaves mid-block).
The functional-test monitors needed hardening, not the JIT: PC-only
stability detection aliases with long loops sampled at dispatch
boundaries (chaining makes those PCs highly repetitive - at tick
interval 64 even the interpreter false-detected a trap). Traps are now
recognized only when the full register file is frozen AND the stable
PC holds a branch-to-self or jmp-to-self instruction.
Measured (medians of 3, tick 1024 for ROMs; baselines from the
attribution commit):
6502_functional: 2.59x -> 4.28x (26.5 insns/block, was 4.9; 102 MIPS)
65C02_extended: 3.28x -> 5.36x (27-32 insns/block; 141 MIPS)
MS BASIC bench: 2.5x -> 3.0x (0.85s vs 2.57s; 8.96 insns/block)
bench_loop: 5.0x -> 8.1x (whole benchmark is one native block)
smcdata: 6.5x -> 8.4x
bench_mmu: unchanged within noise
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>