Skip to content

feat(nanz): declared @extern clobbers + working extern ABI (P9) - #66

Merged
oisee merged 6 commits into
mainfrom
feat/P9-extern-clobbers
Oct 2, 2026
Merged

oisee merged 6 commits into
mainfrom
feat/P9-extern-clobbers

Conversation

@oisee

@oisee oisee commented Oct 2, 2026

Copy link
Copy Markdown
Owner

P9: declared clobbers for @extern calls, and a working extern ABI

Since #62, every call to a callee without a known body has been treated as clobbering every register. That is safe but costs bytes. This PR lets the author of an external routine declare what it clobbers:

@extern(clobbers: "A, HL, F") fun f(a: u8) -> u8
@extern(0x1234, clobbers: "A") fun rom_call(...)

Caller-saves are narrowed only when the call resolves to a single extern with no body. Everything else stays conservative, as before:

  • externs with no declaration
  • compiled functions
  • indirect calls
  • unresolved calls
  • ambiguous names

Unknown register names are a compile error. AF' is rejected. The docs explain that EXX shadows, I and R are not tracked.

The extern ABI on main was broken, and this PR fixes it

On origin/main, extern calls never received their arguments in the ABI registers, and POP AF overwrote the result. The critic's probe returns 25 on main and 55 here, and ps1 returns 3 on main and 6 here. hir/lower.go now gives externs a parameter and return contract.

Known gap (follow-up): if a signature has several parameters of the same class, they map to the same register, e.g. (u16,u16,u16) becomes HL, DE, DE, so an argument is lost. Main lost all of them, so this is not a regression. It is documented in the report.

Safety hardening, found by the critics

  • Duplicate @extern declarations: identical redeclarations (repeated or diamond imports) are merged into one. Conflicting ones are a compile error, as is an @extern that shares its name with a function body.
  • Symbol collisions: checked on the emitted assembly symbol (after sanitizing, e.g. f→v_f), and runtime labels such as __mul8 are reserved. A collision is a compile error. Before this, the second label was silently dropped.
  • Frill duplicate emission: Frill emitted some curried or composed helpers twice. That is fixed at the source. As a side effect, importing the same module twice now gives correct results (main returned 1 instead of 4).
  • No panics: Z80Codegen now returns (string, error) instead of panicking.

Numbers (vs origin/main 05402e1)

  • All-frontend sweep: 510 sources, 0 compile/assemble status changes (231 compile and assemble). 51 asm files change, mostly from extern argument setup and conservative saves.
  • Isolated --asserts-force z80 checks: 682 critic checks (nanz + frill) and 3,737 worker checks, 0 newly failing.
  • Size: +569 bytes across the corpus. This comes from restoring the missing extern ABI and from saves around unresolved callees, not from narrowing.
  • Mutation audit: every fix has a test that fails without it.

Gate

  • Cross-family critic: MERGE GO after 4 rounds.
  • Go gates: build; hir, mir2, nanz and frill tests; -short pipeline and c89.
  • pkg/vir (offline oracle): the same 11 failures as on main.

Report: reports/2026-10-02-P9-Extern-Clobbers.md.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wrwo36SzxKRZYTgpDo7hzo

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

PR tests: 🟢 pass

Commit: 170344d9 · 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 and others added 6 commits October 2, 2026 14:24
Carry explicit physical clobber contracts through HIR and MIR2 and preserve
live callers using Z80 register overlap. Keep unknown calls conservative,
retain external parameter/return ABIs, and account for caller setup and
result pickup writes. Document contracts and corpus findings, with assembly
judges and mutation-verified regressions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwo36SzxKRZYTgpDo7hzo
Require unique callee names and bodyless externs before narrowing caller saves. Reject extern/body collisions and conflicting extern ABIs, improve attribute errors, and document untracked registers. Add regression judges and record FIX1 validation in the dated report.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwo36SzxKRZYTgpDo7hzo
Deduplicate identical bodyless extern declarations after imports merge and compare clobber contracts as sets. Reject colliding emitted Z80 symbols and ambiguous calls instead of dropping argument, return, and address contracts. Add executable regression tests and record critic, mutation, corpus, and gate results, including the unresolved-extern compatibility limitation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wrwo36SzxKRZYTgpDo7hzo
Transfer adopted Frill helpers into one declaration and retain first import
bindings. Reserve runtime and generated Z80 symbols, and propagate collision
errors through codegen, pipelines, and VIR's PBQP fallback. Add regression tests
and document cross-frontend extern ABI effects and the complete corpus audit.

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

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

oisee commented Oct 2, 2026

Copy link
Copy Markdown
Owner Author

Rebased onto main with #65 (J2a). Re-ran gates: Go build/tests green; scripts/test_judges.py OK; assert_matrix.py vs current main, -j16, controls on: Z80 1,311 pass both / 0 newly fail; MIR2 1,485 pass both / 0 newly fail; 1,673 controls per backend, 0 unexpected passes.

@oisee
oisee force-pushed the feat/P9-extern-clobbers branch from 95edba5 to 170344d Compare October 2, 2026 14:29
@oisee
oisee merged commit 04e0ba1 into main Oct 2, 2026
2 checks passed
@oisee
oisee deleted the feat/P9-extern-clobbers branch October 2, 2026 14:35
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