Skip to content

fix(z80): calls preserve live values and return all results correctly (P7) - #62

Merged
oisee merged 4 commits into
mainfrom
fix/P7-calls
Oct 2, 2026
Merged

oisee merged 4 commits into
mainfrom
fix/P7-calls

Conversation

@oisee

@oisee oisee commented Oct 2, 2026

Copy link
Copy Markdown
Owner

P7: function calls return wrong values on the default Z80 path

Bug. On the default PBQP path, calls corrupted values. With the MIR2 VM correct in every case, main produced:

Case Wrong on main
fc (two calls) 65,280 of 65,536 inputs
h_c2 (argument reused after the call) 65,280 of 65,536
h_c4 (word arg, word live across the call) 65,536 of 65,536
7-register argument permutations 470 of 1,536
Worker's call fuzzer 2,112 of 2,200 programs

Fixes (pkg/mir2/z80codegen.go, pipeline/CLI)

  • Live caller registers are saved before the argument parallel copies, including arguments reused after the call. Liveness is backward per instruction plus CFG live-out.
  • computeClobbers conservatively includes every allocatable register and register half.
  • Result pickup:
    • all return values are taken from the return contract, and every one of them is protected from the caller-save POPs (multi-value returns);
    • bytes are merged into saved pairs;
    • flag returns survive POP AF;
    • no writes into code: the old inline JP …; DS n temporary is gone. Moves, a local scratch register or a reentrant stack snapshot replace it, so it is ROM-safe.
  • saveAccAcrossCall is removed. Its scratch override outlived the call and was clobbered.
  • Scratch selection also changed:
    • it respects CFG liveness and word halves;
    • IX/IY conflict with their byte halves;
    • __mul8 argument setup uses parallel copies.
  • Full 7-register byte permutations use stack snapshots, and word-pair cycles are resolved.
  • Indirect calls with values live across them are rejected with an explicit error, but only when Z80 code is emitted. Previously they miscompiled.
    • The indirect ABI is still unsupported, and full support is a follow-up.
    • examples/c89/func_ptr.c still runs its MIR2 asserts. It prints a warning and produces no .a80.
  • CLI changes:
    • -b c now really uses the C emitter. It used to fall through to Z80 output.
    • Without -o it writes <stem>.generated.c, refuses to overwrite an existing file and accepts --force.
    • The default .a80 output is unchanged.

Evidence

Check Main Branch
Call fuzzer, 2,200 + 1,000 + 600 seeds ~95% fail 0 fail
Critic's typed fuzzers, 3,600 + 1,600 new seeds — 0 cases where main passes and the branch fails; +1,562 / +563 newly pass
Per-assert corpus, 978 asserts forced to z80 — 0 newly failing, +23 newly passing (c99_ctype, fatfs_lowlevel, 18_tail_recursion fib/pow2, hashmap, widemath)
  • Native assert modes are 951/951 on both sides.
  • CLI sweep, done twice independently: 510–521 tracked sources × 6–7 modes, about 6,000 compiles. No exit-code change, no lost .a80 and no overwritten file, except the documented func_ptr.c.
  • Each fix was reverted to show its test going red, then restored.

Size. +2,914 bytes on 128 shared files (+4.2%). The critic attributes it as:

Source Share
@extern callees now treated as clobbering everything 53%
Remaining correctness saves 29%
Callee-precise clobbers could recover ~3%

The @extern share is the cost of a previously unsafe assumption; main assumed externs clobber nothing.

Known debt (follow-ups)

  • ; clobbers: annotations now list every register, which makes mzd --verify-abi clobber checks meaningless.
  • Declared clobbers for @extern.
  • Callee-precise clobber sets.
  • Full indirect-call ABI.
  • The loosened genBinOp lhs != "A" costs a few leaf instructions (ADD A,H → LD A,H / ADD A,B).

Process. Code by codex gpt-6.1-sol, with cross-family Claude critics:

  • Round 1, AFTER FIXES: 22 regressions (accumulator scratch lifetime, multi-return POP, indirect calls, inline temporary).
  • Round 2, AFTER FIXES: CLI regressions (missing default .a80, -b c overwrite).
  • Round 3, MERGE GO.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wrwo36SzxKRZYTgpDo7hzo

oisee and others added 4 commits October 2, 2026 11:05
Save callers before argument setup, preserve reused arguments and implicit
callee clobbers, and pick up results before restoring overlapping registers.
Resolve saturated byte permutations and word-register cycles safely.
Add exhaustive and sampled production CPU judges for calls and loops.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwo36SzxKRZYTgpDo7hzo
Use caller AF saves instead of persistent call scratch overrides. Pick up
all returned values around pair restoration without writable inline code.
Fix related accumulator, multiply and index-half preservation failures.
Reject indirect calls with live caller values until their ABI is reliable.

Add independent seeded CPU regressions and require zero mismatches for
the now-correct L0 call judges. All four gates pass; critic and worker
fuzzers and the per-assert corpus show no new main-pass failures.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwo36SzxKRZYTgpDo7hzo
Keep MIR2 assertions and intermediate/C output available for modules with
unsupported live values across indirect calls. Skip incidental Z80 emission
while retaining the explicit error for Z80 output and assertion products.
Route the C backend through the existing MIR2 C emitter.

Add CLI and sandbox regressions with demonstrated red/green runs. All four
gates pass. All 978 native and forced assertions have zero new failures;
the 521-source compile sweep only rejects func_ptr.c for Z80 emission.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwo36SzxKRZYTgpDo7hzo
Emit default assembly for MIR2-only assertions, falling back with a warning
only for unsupported indirect-call ABI validation. Use .generated.c for
implicit C output and refuse replacement unless --force is requested.
Load stack-snapshot byte results into IY halves through A.

Add demonstrated red/green CLI and forced-allocation regressions. All four
gates pass, 978 assertions have no new failures in either mode, and 600 new
call seeds pass. The paired 521-source sweep has no unexpected regressions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwo36SzxKRZYTgpDo7hzo
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

PR tests: 🟢 pass

Commit: c0374e4d · Full test log

Check Result
Build mz, mza, mze, mzv 🟢 pass
Stable Go package tests 🟢 pass
Fibonacci compile + assemble 🟢 pass
Nanz language regressions 🟢 pass
Z3 West of House demo 🟢 pass
Z3 small conformance fixtures 🟢 pass

The external CZECH and full Zork I stories are not part of this PR gate.

@oisee
oisee merged commit 6ba1bce into main Oct 2, 2026
2 checks passed
@oisee
oisee deleted the fix/P7-calls branch October 2, 2026 12:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant