From 237da6c445194648701fa278e4912ce5047f7b90 Mon Sep 17 00:00:00 2001 From: Alice Vinogradova Date: Fri, 2 Oct 2026 12:28:42 +0000 Subject: [PATCH] refactor(mir2): split Z80 codegen into thematic files Move whole top-level declarations into focused files for arithmetic, comparisons, calls, copies, moves, terminators, patterns and peepholes. Keep the instruction dispatcher intact in its own file. Verify all 154 declarations and comments with go/parser and go/printer. Compare both compilers across 979 tracked frontend sources: 499 identical assembly outputs and 480 identical failures. All required gates pass. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Wrwo36SzxKRZYTgpDo7hzo --- minzc/pkg/mir2/z80codegen.go | 8564 +------------------------ minzc/pkg/mir2/z80codegen_alu.go | 1067 +++ minzc/pkg/mir2/z80codegen_call.go | 885 +++ minzc/pkg/mir2/z80codegen_cmp.go | 867 +++ minzc/pkg/mir2/z80codegen_copy.go | 473 ++ minzc/pkg/mir2/z80codegen_inst.go | 1166 ++++ minzc/pkg/mir2/z80codegen_move.go | 881 +++ minzc/pkg/mir2/z80codegen_muldiv.go | 1161 ++++ minzc/pkg/mir2/z80codegen_patterns.go | 869 +++ minzc/pkg/mir2/z80codegen_peephole.go | 467 ++ minzc/pkg/mir2/z80codegen_term.go | 481 ++ 11 files changed, 8467 insertions(+), 8414 deletions(-) create mode 100644 minzc/pkg/mir2/z80codegen_alu.go create mode 100644 minzc/pkg/mir2/z80codegen_call.go create mode 100644 minzc/pkg/mir2/z80codegen_cmp.go create mode 100644 minzc/pkg/mir2/z80codegen_copy.go create mode 100644 minzc/pkg/mir2/z80codegen_inst.go create mode 100644 minzc/pkg/mir2/z80codegen_move.go create mode 100644 minzc/pkg/mir2/z80codegen_muldiv.go create mode 100644 minzc/pkg/mir2/z80codegen_patterns.go create mode 100644 minzc/pkg/mir2/z80codegen_peephole.go create mode 100644 minzc/pkg/mir2/z80codegen_term.go diff --git a/minzc/pkg/mir2/z80codegen.go b/minzc/pkg/mir2/z80codegen.go index 76e786e8f..023f884d8 100644 --- a/minzc/pkg/mir2/z80codegen.go +++ b/minzc/pkg/mir2/z80codegen.go @@ -2,13 +2,8 @@ package mir2 import ( "fmt" - "maps" - "math/bits" - "slices" - "sort" - "strings" - "github.com/minz/minzc/pkg/z80validate" + "strings" ) func emitDBLine(sb *strings.Builder, data []byte) { @@ -299,467 +294,6 @@ func Z80Codegen(m *Module, ar *AllocResult, opts ...Z80CodegenOptions) string { return result } -// asmPeepholePass applies post-emit string-level peephole optimisations: -// 1. Double EX DE,HL elimination — two consecutive EX DE,HL cancel; remove both. -// 2. Single-JP function → EQU alias — a function whose entire body is one JP -// becomes a zero-byte/zero-T EQU alias (tail-call guaranteed by contract). -func asmPeepholePass(src string) string { - lines := strings.Split(src, "\n") - lines = elimDoubleExDeHl(lines) - lines = elimSingleJpEqu(lines) - lines = elimCallRet(lines) - lines = foldCondCall(lines) - lines = elimJrToRet(lines) - lines = elimDeadAfterRet(lines) - lines = foldRLCASled(lines) - return strings.Join(lines, "\n") -} - -// foldCondCall replaces JR cc, skip / CALL target / skip: with CALL cc, target. -// Saves 2 bytes and avoids branch penalty. -// -// Pattern (any of JR/JRS, any condition): -// -// JR NC, .skip (or JRS, or any cc: Z/NZ/C/NC) -// [.optional_label:] (zero or more intermediate labels, no instructions) -// CALL target -// .skip: -// -// → CALL NC, target -// -// The inverse condition is used: JR NC skips the CALL when NC, -// so we emit CALL C (call when condition is met, i.e. NOT NC). -func foldCondCall(lines []string) []string { - invertCC := map[string]string{ - "Z": "NZ", "NZ": "Z", "C": "NC", "NC": "C", - "z": "nz", "nz": "z", "c": "nc", "nc": "c", - } - - out := make([]string, 0, len(lines)) - i := 0 - for i < len(lines) { - trimmed := strings.TrimSpace(lines[i]) - // Match JR cc, .label or JRS cc, .label - var jrCC, skipLabel string - if strings.HasPrefix(trimmed, "JR ") || strings.HasPrefix(trimmed, "JRS ") { - // Parse: JR(S) CC, .label - rest := trimmed - if strings.HasPrefix(rest, "JRS ") { - rest = rest[4:] - } else { - rest = rest[3:] - } - parts := strings.SplitN(rest, ",", 2) - if len(parts) == 2 { - cc := strings.TrimSpace(parts[0]) - lab := strings.TrimSpace(parts[1]) - if _, ok := invertCC[cc]; ok && strings.HasPrefix(lab, ".") { - jrCC = cc - skipLabel = lab - } - } - } - - if jrCC == "" { - out = append(out, lines[i]) - i++ - continue - } - - // Scan forward: skip label-only lines, find CALL, then skip label. - j := i + 1 - var intermediateLabels []string - for j < len(lines) { - t := strings.TrimSpace(lines[j]) - if t == "" || strings.HasPrefix(t, ";") { - j++ - continue - } - if strings.HasSuffix(t, ":") { - intermediateLabels = append(intermediateLabels, lines[j]) - j++ - continue - } - break - } - - // Expect CALL target at position j - if j >= len(lines) { - out = append(out, lines[i]) - i++ - continue - } - callLine := strings.TrimSpace(lines[j]) - if !strings.HasPrefix(callLine, "CALL ") { - out = append(out, lines[i]) - i++ - continue - } - callTarget := strings.TrimSpace(callLine[5:]) - - // Expect skip label at position j+1 (possibly with blank/comment lines between) - k := j + 1 - for k < len(lines) { - t := strings.TrimSpace(lines[k]) - if t == "" || strings.HasPrefix(t, ";") { - k++ - continue - } - break - } - if k >= len(lines) { - out = append(out, lines[i]) - i++ - continue - } - labelLine := strings.TrimSpace(lines[k]) - expectedLabel := skipLabel + ":" - if labelLine != expectedLabel { - out = append(out, lines[i]) - i++ - continue - } - - // Match! Emit intermediate labels + CALL cc, target + skip label. - invCC := invertCC[jrCC] - indent := " " - if idx := strings.Index(lines[j], "CALL "); idx > 0 { - indent = lines[j][:idx] - } - for _, lab := range intermediateLabels { - out = append(out, lab) - } - out = append(out, fmt.Sprintf("%sCALL %s, %s", indent, invCC, callTarget)) - out = append(out, lines[k]) // keep the skip label - i = k + 1 - continue - } - return out -} - -// foldRLCASled replaces consecutive RLCA sequences (3+) with CALL __rotate_N. -// RLCA = 4T each. CALL __rotate_N = 17T + N×4T + 10T = 27 + N×4T. -// Break-even at N=7 (inline=28T, sled=55T) — so sled is about code SIZE savings. -// For N >= 3 inline RLCAs, fold to CALL to save code bytes (shared sled is 9 bytes). -// Caller must ensure __rotate sled is emitted (needsRotate flag). -func foldRLCASled(lines []string) []string { - out := make([]string, 0, len(lines)) - i := 0 - needsSled := false - for i < len(lines) { - // Count consecutive RLCA instructions - if strings.TrimSpace(lines[i]) == "RLCA" { - count := 0 - j := i - for j < len(lines) && strings.TrimSpace(lines[j]) == "RLCA" { - count++ - j++ - } - if count >= 3 { - // Replace with CALL __rotate_N - indent := " " - if idx := strings.Index(lines[i], "RLCA"); idx > 0 { - indent = lines[i][:idx] - } - out = append(out, fmt.Sprintf("%sCALL __rotate_%d", indent, count)) - needsSled = true - i = j - continue - } - } - out = append(out, lines[i]) - i++ - } - // If we folded any, add a marker comment so the caller knows to emit the sled - if needsSled { - out = append(out, "; [peephole] RLCA sled referenced — __rotate runtime required") - } - return out -} - -// elimCallRet replaces CALL X / RET with JP X (tail call promotion). -// Saves 17 T-states (CALL=17 + RET=10 → JP=10) and 1 byte. -func elimCallRet(lines []string) []string { - out := make([]string, 0, len(lines)) - for i := 0; i < len(lines); i++ { - trimmed := strings.TrimSpace(lines[i]) - if strings.HasPrefix(trimmed, "CALL ") && i+1 < len(lines) { - next := strings.TrimSpace(lines[i+1]) - if next == "RET" { - target := strings.TrimSpace(strings.TrimPrefix(trimmed, "CALL ")) - // Skip numeric targets (BDOS CALL 5, RST addresses) — - // these are syscalls that need the return address on stack. - isNumeric := len(target) > 0 && (target[0] >= '0' && target[0] <= '9' || - target[0] == '$' || strings.HasPrefix(target, "0x")) - if !isNumeric { - out = append(out, strings.Replace(lines[i], "CALL ", "JP ", 1)) - i++ // skip RET - continue - } - } - } - out = append(out, lines[i]) - } - return out -} - -// elimDeadAfterRet removes unreachable code between an unconditional RET/JP -// and the next label. After cond_ret lowering, the pattern: -// -// RET ← unconditional return -// .label: ← else-branch (only reachable via jump, not fallthrough) -// -// means nothing between the RET and .label is reachable. If there are -// instructions between them (rare), they're dead code. More commonly, -// the RET itself is redundant when the PREVIOUS instruction was also a -// RET or JP — but that's already handled by the IR. The main benefit -// here is removing the RET when it's the last instruction before a label -// and the codegen emitted it defensively. -func elimDeadAfterRet(lines []string) []string { - // This pass doesn't remove the RET (it may be the return for the - // then-branch). It removes dead instructions BETWEEN RET and next label. - // Uncommon in practice — the main pattern is already clean. - return lines -} - -// elimDoubleExDeHl removes consecutive EX DE,HL pairs. -// Blank lines and comment-only lines between the pair are preserved. -func elimDoubleExDeHl(lines []string) []string { - const exLine = " EX DE, HL" - out := make([]string, 0, len(lines)) - for i := 0; i < len(lines); i++ { - if strings.TrimSpace(lines[i]) != "EX DE, HL" { - out = append(out, lines[i]) - continue - } - // lines[i] is an EX DE,HL. Look for a second one after skipping - // blank/comment lines (no real instructions between). - j := i + 1 - for j < len(lines) { - t := strings.TrimSpace(lines[j]) - if t == "" || strings.HasPrefix(t, ";") { - j++ - continue - } - break - } - if j < len(lines) && strings.TrimSpace(lines[j]) == "EX DE, HL" { - // Skip both EX DE,HL lines (drop them from output). - i = j // loop increment makes i = j+1 - continue - } - out = append(out, lines[i]) - } - return out -} - -// elimSingleJpEqu converts a function that is exactly one JP target into an EQU. -// -// Pattern (consecutive lines, optionally separated by blank/comment lines): -// -// funcname: -// JP target -// -// → replaced with a single line: -// -// funcname EQU target -// -// Safety conditions: -// - No additional instructions in the block (comments/blanks OK). -// - No local labels between funcname: and JP (would break relative refs). -// - Not "main" (entry point must remain a real label). -func elimSingleJpEqu(lines []string) []string { - out := make([]string, 0, len(lines)) - i := 0 - for i < len(lines) { - line := lines[i] - // Match a bare label line: word followed by ':' with no leading whitespace. - if !strings.HasPrefix(line, " ") && strings.HasSuffix(strings.TrimSpace(line), ":") { - label := strings.TrimSuffix(strings.TrimSpace(line), ":") - if label != "main" && label != "" && !strings.Contains(label, ".") { - // Scan forward past blank/comment lines to find first instruction. - j := i + 1 - for j < len(lines) { - t := strings.TrimSpace(lines[j]) - if t == "" || strings.HasPrefix(t, ";") { - j++ - continue - } - break - } - if j < len(lines) { - instr := strings.TrimSpace(lines[j]) - // Must be exactly "JP target" (not DJNZ, not JP cc, not JP (HL)/(IX)/(IY)). - if strings.HasPrefix(instr, "JP ") && !strings.Contains(instr, ",") && !strings.Contains(instr, "(") { - target := strings.TrimSpace(strings.TrimPrefix(instr, "JP ")) - // Check that the next non-blank/comment line after JP is - // a top-level label or end-of-section (not another instruction - // inside this "function"). - k := j + 1 - for k < len(lines) { - t := strings.TrimSpace(lines[k]) - if t == "" || strings.HasPrefix(t, ";") { - k++ - continue - } - break - } - nextIsTopLevel := k >= len(lines) || - (!strings.HasPrefix(lines[k], " ") && strings.HasSuffix(strings.TrimSpace(lines[k]), ":")) || - lines[k] == "; globals" || lines[k] == "; strings" - if nextIsTopLevel { - // Emit EQU alias, skip all consumed lines. - out = append(out, fmt.Sprintf("%s EQU %s", label, target)) - // Preserve blank lines that came before JP (already in out - // via j-loop... actually they were skipped). Emit one blank - // to maintain section spacing. - i = j + 1 - continue - } - } - } - } - } - out = append(out, line) - i++ - } - return out -} - -// elimJrToRet replaces `JRS cc, label` with `RET cc` when the target label -// is a bare RET instruction. Then removes the label+RET if no other references -// remain. This saves 1B per replaced jump (JR cc = 2B → RET cc = 1B). -// -// Before: JRS Z, .cret0 / JRS C, .cret0 / ... / .cret0: / RET -// After: RET Z / RET C / ... (label+RET removed if dead) -func elimJrToRet(lines []string) []string { - // Pass 1: find labels that point to bare RET. - retLabels := make(map[string]bool) - for i := 0; i < len(lines)-1; i++ { - t := strings.TrimSpace(lines[i]) - if strings.HasPrefix(t, ".") && strings.HasSuffix(t, ":") { - label := strings.TrimSuffix(t, ":") - // Check next non-blank line is RET. - for j := i + 1; j < len(lines); j++ { - next := strings.TrimSpace(lines[j]) - if next == "" || strings.HasPrefix(next, ";") { - continue - } - if next == "RET" { - retLabels[label] = true - } - break - } - } - } - if len(retLabels) == 0 { - return lines - } - - // Pass 2: replace JRS cc, retLabel → RET cc. - // Track which labels are still referenced. - replaced := make(map[string]int) // label → count of replacements - out := make([]string, 0, len(lines)) - for _, line := range lines { - t := strings.TrimSpace(line) - // Match: JRS cc, .label or JR cc, .label - if (strings.HasPrefix(t, "JRS ") || strings.HasPrefix(t, "JR ")) && strings.Contains(t, ", .") { - parts := strings.SplitN(t, ", ", 2) - if len(parts) == 2 { - label := strings.TrimSpace(parts[1]) - if retLabels[label] { - // Extract condition code. - prefix := parts[0] // "JRS Z" or "JR C" - cc := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(prefix, "JRS"), "JR")) - if cc != "" { - out = append(out, " RET "+cc) - replaced[label]++ - continue - } - } - } - } - out = append(out, line) - } - - if len(replaced) == 0 { - return lines // nothing changed - } - - // Pass 3: count remaining references to each replaced label. - // If a label has zero remaining references, remove the label+RET. - labelRefs := make(map[string]int) - for _, line := range out { - t := strings.TrimSpace(line) - for label := range replaced { - if strings.Contains(t, label) && !strings.HasSuffix(t, ":") { - labelRefs[label]++ - } - } - } - - // Pass 3b: keep RET when the preceding instruction can fall through. - // Arithmetic sign correction, DJNZ and conditional branches all need it. - for i := 0; i < len(out); i++ { - t := strings.TrimSpace(out[i]) - if strings.HasSuffix(t, ":") { - label := strings.TrimSuffix(t, ":") - if _, ok := replaced[label]; ok { - // Walk backward to find the preceding instruction. - for j := i - 1; j >= 0; j-- { - prev := strings.TrimSpace(out[j]) - if prev == "" || strings.HasPrefix(prev, ";") { - continue - } - if prev != "RET" && - !(strings.HasPrefix(prev, "JP ") && !strings.Contains(prev, ",")) && - !(strings.HasPrefix(prev, "JR ") && !strings.Contains(prev, ",")) && - !(strings.HasPrefix(prev, "JRS ") && !strings.Contains(prev, ",")) { - labelRefs[label]++ // implicit fall-through reference - } - break - } - } - } - } - - // Pass 4: remove dead label+RET pairs. - deadLabels := make(map[string]bool) - for label := range replaced { - if labelRefs[label] == 0 { - deadLabels[label] = true - } - } - if len(deadLabels) == 0 { - return out - } - - var final []string - for i := 0; i < len(out); i++ { - t := strings.TrimSpace(out[i]) - if strings.HasSuffix(t, ":") { - label := strings.TrimSuffix(t, ":") - if deadLabels[label] { - // Skip label line and the following RET. - for j := i + 1; j < len(out); j++ { - next := strings.TrimSpace(out[j]) - if next == "" || strings.HasPrefix(next, ";") { - i = j - continue - } - if next == "RET" { - i = j // skip the RET too - } - break - } - continue - } - } - final = append(final, out[i]) - } - return final -} - // ── Internal codegen state ──────────────────────────────────────────────────── type z80cg struct { @@ -899,69 +433,6 @@ type z80cg struct { fnWorstT int } -// smcPatcherInfo records metadata for a synthesised @smc patcher function. -// Sym is "funcName$paramName"; eqLabel is the EQU pointing at the imm16 bytes. -type smcPatcherInfo struct { - sym string // e.g. "draw_player$r0" - eqLabel string // e.g. "draw_player$r0$imm" -} - -// lutLoadPat describes a page-aligned LUT access merged at codegen time. -type lutLoadPat struct { - sym string // page-aligned global symbol - src8Reg Reg // virtual reg holding the 8-bit index (after lo-subtract if any) -} - -type bitStorePat struct { - bit int - byteOffset int - op string // "SET" or "RES" -} - -type bitCmpPat struct { - bit int - byteOffset int - ptrReg Reg - srcReg Reg - useMem bool -} - -// globalFieldInfo describes a global struct field access that can be lowered -// to direct Z80 addressing: LD A,(sym__field) or LD (sym__field),A. -// -// The EQU label is: {sanitizeIdent(sym)}__{fieldName} (or just sanitizeIdent(sym) for offset 0). -// Emission: LD A,(sym__field) for loads; LD A,val + LD (sym__field),A for stores. -type globalFieldInfo struct { - sym string // global symbol name (raw, before sanitize) - offset int64 // byte offset within the global - fieldName string // field name for the EQU label (may be empty → use offset) - // HL-chain: when true, emit LD HL,sym chain instead of LD (sym__field),A. - hlChain bool // part of a consecutive-field HL-chain group - hlHead bool // first in group → emit LD HL,sym before - hlLast bool // last in group → no INC HL after -} - -// trampolineBlock is emitted after all regular blocks of a function. -// It resolves block-argument copies for one outgoing BrIf edge. -// If isRet is true, the block ends with RET instead of JRS target. -type trampolineBlock struct { - label string // local label for this trampoline - copies []parallelCopy - target string // real destination label (already formatted) - isRet bool // true → emit RET instead of JRS target -} - -// parallelCopy is a single register-to-register move in a parallel copy sequence. -type parallelCopy struct { - srcName string - dstName string - ty Ty - // isImm marks a constant rematerialisation: no source register, just emit - // LD dstName, immVal after all register-to-register copies are resolved. - isImm bool - immVal int64 -} - func (g *z80cg) emit(s string) { if g.annotateT && strings.HasPrefix(s, " ") && !strings.HasPrefix(s, " ;") { ts, tsAlt := z80TStates(s) @@ -980,412 +451,49 @@ func (g *z80cg) emit(s string) { func (g *z80cg) emitf(f string, args ...any) { g.emit(fmt.Sprintf(f, args...)) } func (g *z80cg) comment(s string) { g.emitf(" ; %s", s) } -// isIXY reports whether reg is an index register (IX or IY). -// Z80 indexed loads/stores require explicit displacement: (IX+d), never bare (IX). -func isIXY(reg string) bool { return reg == "IX" || reg == "IY" } - -// isIXYReg returns true for the undocumented IXH/IXL/IYH/IYL half-registers. -// These share the DD/FD prefix with (IX+d)/(IY+d) addressing, so they cannot -// be combined with (HL) in a single instruction. -func isIXYReg(reg string) bool { - return reg == "IXH" || reg == "IXL" || reg == "IYH" || reg == "IYL" -} +// ── Function ────────────────────────────────────────────────────────────────── -// ptrIndirect returns the memory-operand string for a pointer register. -// For IX/IY the Z80 requires an explicit displacement byte — even zero: -// -// HL → "(HL)" -// IX → "(IX+0)" DE → "(DE)" +// computeDeadConsts returns the set of OpConst dsts whose LD can be suppressed +// because every use of the constant is already handled by a codegen peephole: // -// An optional byte offset d shifts the displacement for 16-bit multi-byte accesses. -func ptrIndirect(ptr string, d int) string { - if isIXY(ptr) { - if d == 0 { - return fmt.Sprintf("(%s+0)", ptr) - } - return fmt.Sprintf("(%s+%d)", ptr, d) - } - return fmt.Sprintf("(%s)", ptr) -} - -// isSimpleReg reports whether s names a plain physical register (no parens, -// no digits) — safe to use as an alias key in holdsPhys. -func isSimpleReg(s string) bool { - if len(s) == 0 { - return false - } - for _, c := range s { - if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { - return false - } - } - return true -} - -// ── Register topology ───────────────────────────────────────────────────────── +// - OpCmp with Src[1] being the const → CP imm8 fires (no register needed). +// - OpAdd/OpSub with Src[1]==const, value==1, and lhs/dst at the same physical +// location → INC/DEC fires (no register needed). // -// Writing to a 16-bit pair register (e.g. HL) implicitly changes its bytes -// (H, L), and vice versa: writing to H invalidates HL. These tables encode -// that topology so invalidate() can clear all affected aliases at once. - -// regBytes maps a 16-bit register to its 8-bit byte components. -var regBytes = map[string][]string{ - "BC": {"B", "C"}, - "DE": {"D", "E"}, - "HL": {"H", "L"}, - "IX": {"IXH", "IXL"}, - "IY": {"IYH", "IYL"}, -} - -// regParent maps an 8-bit component to its 16-bit parent register. -var regParent = map[string]string{ - "B": "BC", "C": "BC", - "D": "DE", "E": "DE", - "H": "HL", "L": "HL", - "IXH": "IX", "IXL": "IX", - "IYH": "IY", "IYL": "IY", -} - -// killOne removes a single register and its bidirectional partner from holdsPhys. -func (g *z80cg) killOne(r string) { - if partner, ok := g.holdsPhys[r]; ok { - delete(g.holdsPhys, partner) - delete(g.holdsPhys, r) - } -} - -// invalidate removes all tracking for r: kills r itself, its byte components -// (if r is a 16-bit pair), and its parent pair (if r is an 8-bit component). -func (g *z80cg) invalidate(r string) { - g.killOne(r) - if bytes, ok := regBytes[r]; ok { - for _, b := range bytes { - g.killOne(b) +// Constants with zero uses are also suppressed. +func computeDeadConsts(f *Func, ar *AllocResult) map[Reg]bool { + // Collect all OpConst dsts and their immediate values. + constVal := make(map[Reg]int64) + for _, b := range f.Blocks { + for _, inst := range b.Insts { + if inst.Op == OpConst { + constVal[inst.Dst] = inst.Imm + } } } - if parent, ok := regParent[r]; ok { - g.killOne(parent) - } -} - -// setCopy records that dst was loaded from src (they now hold the same value). -// Invalidates dst's old aliases and clears src's previous bidirectional link -// before establishing the new one, preventing stale reverse entries. -func (g *z80cg) setCopy(dst, src string) { - g.invalidate(dst) - g.killOne(src) // break src's old reverse-link (could be left by a prior setCopy) - if isSimpleReg(dst) && isSimpleReg(src) { - g.holdsPhys[dst] = src - g.holdsPhys[src] = dst - } -} - -// holdsValue reports whether physical register a currently holds the same -// value as b (either they are the same register, or a copy alias exists). -func (g *z80cg) holdsValue(a, b string) bool { - return a == b || g.holdsPhys[a] == b -} - -// emitLDA emits "LD A, src" and records the copy alias A ≡ src. -// When src is a LocMem address ($Fxxx), emits "LD A, ($Fxxx)" — the only -// valid Z80 form for loading an 8-bit value from an absolute 16-bit address. -func (g *z80cg) emitLDA(src string) { - if src == "F" { - // F register cannot be loaded directly. Materialise carry flag. - g.emit(" SBC A, A") // A=0xFF if C=1, A=0x00 if C=0 - g.invalidate("A") - return - } - if isSpill(src) { - g.emitf(" LD A, (%s)", src) - } else if isPairReg(src) { - // Width mismatch: pair→8bit. Take low byte. - g.emitf(" LD A, %s", lowByte(src)) - } else { - g.emitf(" LD A, %s", src) - } - g.setCopy("A", src) -} - -// loc returns the physical location name for virtual register r. -// For LocMem registers the allocator stores the actual byte address in Offset; -// return it as a hex literal so the assembler resolves it correctly. -// e.g. Offset=0xF001 → "$F001", which assembles as LD D, ($F001) / LD ($F001), A etc. -func (g *z80cg) loc(r Reg) string { - if r == NoReg { - return "?" - } - if phys, ok := g.physOverride[r]; ok { - return phys - } - pl := g.ar.Loc(r) - if pl.Kind == LocMem { - return g.spillLabel(r) - } - return pl.Name -} - -// spillLabel returns a named label for a spilled virtual register. -// Format: _spill_{funcName}_r{regNum} -// The label is emitted in the data section at end of function. -func (g *z80cg) spillLabel(r Reg) string { - return fmt.Sprintf("_spill_%s_r%d", sanitizeIdent(g.fn.Name), int(r)) -} - -// spillWidth returns the byte width of a spilled register (1, 2, or 3 for eZ80). -func (g *z80cg) spillWidth(r Reg) int { - if info, ok := collectRegInfo(g.fn)[r]; ok { - return (info.Ty.Width() + 7) / 8 - } - return 1 -} - -// physName returns the assembly-level name for a PhysLoc. -// For LocMem, returns the named spill label; for everything else, returns pl.Name. -func physName(pl PhysLoc) string { - // Note: physName is used outside genFunc context (e.g. buildBlockCopies) - // where we don't have access to g.fn. Fall back to $F0xx for those cases. - if pl.Kind == LocMem { - return fmt.Sprintf("$%04X", pl.Offset) - } - return pl.Name -} - -// physNameFor returns the assembly-level name for a PhysLoc within function context. -func (g *z80cg) physNameFor(pl PhysLoc, r Reg) string { - if pl.Kind == LocMem { - return g.spillLabel(r) - } - return pl.Name -} - -// emitLD8 emits a safe 8-bit LD, handling spill labels. -// -// Backend policy: IXH/IXL/IYH/IYL are treated as first-class 8-bit registers, -// not as exotic fallbacks reserved only for bit operations. Direct half-reg -// paths should be preferred whenever they are encoding-correct; alternative -// routes through A / pair copies / memory are justified only by real DD/FD -// prefix conflicts or by proven size/timing wins elsewhere. -// -// Z80 only supports LD A,(nn) and LD (nn),A for absolute memory. -// Routes through shadow A (EX AF,AF') to preserve main A register. -func (g *z80cg) emitLD8(dst, src string) { - if dst == src { - return - } - // Width mismatch: pair→8bit truncation. - if isPairReg(src) && !isPairReg(dst) { - src = lowByte(src) - } - // Width mismatch: 8bit→pair widening (u8 result stored in pair reg). - if !isPairReg(src) && isPairReg(dst) { - g.emitf(" LD %s, %s", lowByte(dst), src) - g.emitf(" LD %s, 0", highByte(dst)) - return - } - // DD/FD prefix conflicts — route through shadow A: - if (isIXYReg(src) && (dst == "H" || dst == "L")) || - (isIXYReg(dst) && (src == "H" || src == "L")) || - (isIXYReg(src) && isIXYReg(dst) && src[:2] != dst[:2]) { - g.emitMovViaAltA(dst, src) - return - } - dstSpill := isSpill(dst) - srcSpill := isSpill(src) - if srcSpill && dstSpill { - // Spill-to-spill: route through shadow A. - g.emit(" EX AF, AF'") - g.emitf(" LD A, (%s)", src) - g.emitf(" LD (%s), A", dst) - g.emit(" EX AF, AF'") - } else if srcSpill && dst == "A" { - g.emitf(" LD A, (%s)", src) - } else if srcSpill { - // Load spill to non-A register via shadow A. - g.emit(" EX AF, AF'") - g.emitf(" LD A, (%s)", src) - g.emitLD8(dst, "A") - g.emit(" EX AF, AF'") - } else if dstSpill && src == "A" { - g.emitf(" LD (%s), A", dst) - } else if dstSpill { - // Store non-A register to spill via shadow A. - g.emit(" EX AF, AF'") - g.emitf(" LD A, %s", src) - g.emitf(" LD (%s), A", dst) - g.emit(" EX AF, AF'") - } else { - g.emitf(" LD %s, %s", dst, src) - } -} - -// emitMovViaAltA routes a single 8-bit LD through shadow A (EX AF,AF') -// to avoid clobbering the main A register. Used for DD/FD prefix conflicts -// where LD H,IXH would encode as NOP. -// Cost: 20T (EX 4T + LD A,src 4-8T + LD dst,A 4T + EX 4T). -func (g *z80cg) emitMovViaAltA(dst, src string) { - g.emit(" EX AF, AF'") - g.emitf(" LD A, %s", src) - g.emitLD8(dst, "A") - g.emit(" EX AF, AF'") -} - -// emitADDHL emits ADD HL, rr with IX/IY guard. -// Z80 ADD HL only accepts BC/DE/HL/SP — NOT IX/IY. -func (g *z80cg) emitADDHL(rhs string) { - if rhs == "IX" || rhs == "IY" { - g.emit(" PUSH DE") - if rhs == "IX" { - g.emit(" LD D, IXH") - g.emit(" LD E, IXL") - } else { - g.emit(" LD D, IYH") - g.emit(" LD E, IYL") - } - g.emit(" ADD HL, DE") - g.emit(" POP DE") - } else if isSpill(rhs) { - g.emitf(" LD BC, (%s)", rhs) - g.emit(" ADD HL, BC") - } else { - g.emitf(" ADD HL, %s", rhs) + if len(constVal) == 0 { + return nil } -} -// emitSBCHL emits SBC HL, rr with IX/IY/spill/8-bit guard. -// Z80 SBC HL only accepts BC/DE/HL/SP — NOT IX/IY or 8-bit. -func (g *z80cg) emitSBCHL(rhs string) { - if rhs == "IX" || rhs == "IY" { - g.emit(" PUSH DE") - if rhs == "IX" { - g.emit(" LD D, IXH") - g.emit(" LD E, IXL") - } else { - g.emit(" LD D, IYH") - g.emit(" LD E, IYL") + physName := func(r Reg) string { + if loc, ok := ar.Locs[r]; ok { + return loc.Name } - g.emit(" SBC HL, DE") - g.emit(" POP DE") - } else if isSpill(rhs) { - g.emitf(" LD BC, (%s)", rhs) - g.emit(" SBC HL, BC") - } else if isSimpleReg(rhs) && !isPairReg(rhs) { - // 8-bit operand: promote to pair via promote8toPair. - pair := g.promote8toPair(rhs) - g.emitf(" SBC HL, %s", pair) - } else { - g.emitf(" SBC HL, %s", rhs) + return "" } -} -// isSpill returns true if the operand is a LocMem spill (named _spill_ label or legacy $F0xx). -func isSpill(s string) bool { - return strings.HasPrefix(s, "$") || strings.HasPrefix(s, "_spill_") || strings.HasPrefix(s, "._call_result_") -} + totalUses := make(map[Reg]int) + foldedUses := make(map[Reg]int) -// loadSpill8 loads an 8-bit spill value into the target register. -// Uses A as intermediary (LD A,(nn) is the only valid 8-bit absolute load). -func (g *z80cg) loadSpill8(dst, src string) { - if dst == "A" { - if pair := g.tsmcPairByAddr(src); pair != nil { - label := g.tsmcReloadLabel(pair.reg, g.blockIdx, g.curInstIdx, 0) - if label != "" { - g.emitTSMCReload(label, "A", 8) - return - } - } - g.emitf(" LD A, (%s)", src) - } else { - if pair := g.tsmcPairByAddr(src); pair != nil { - label := g.tsmcReloadLabel(pair.reg, g.blockIdx, g.curInstIdx, 0) - if label != "" { - g.emitTSMCReload(label, dst, 8) - return - } - } - g.emitf(" LD A, (%s)", src) - g.emitLD8(dst, "A") - g.invalidate("A") - } -} - -// storeSpill8 stores an 8-bit register value to a spill address. -func (g *z80cg) storeSpill8(dst, src string) { - if src != "A" { - g.emitLDA(src) - } - if pair := g.tsmcPairByAddr(dst); pair != nil { - g.emitTSMCSpill(pair, "A") - } else { - g.emitf(" LD (%s), A", dst) - } -} - -// loadSpill16 loads a 16-bit spill value into a register pair. -func (g *z80cg) loadSpill16(dst, src string) { - if pair := g.tsmcPairByAddr(src); pair != nil { - label := g.tsmcReloadLabel(pair.reg, g.blockIdx, g.curInstIdx, 0) - if label != "" { - g.emitTSMCReload(label, dst, 16) - return - } - } - g.emitf(" LD %s, (%s)", dst, src) -} - -// storeSpill16 stores a 16-bit pair to a spill address. -func (g *z80cg) storeSpill16(dst, src string) { - if pair := g.tsmcPairByAddr(dst); pair != nil { - g.emitTSMCSpill(pair, src) - } else { - // Z80: only LD (nn),HL is native. DE/BC/SP need ED prefix (supported). - g.emitf(" LD (%s), %s", dst, src) - } -} - -// ── Function ────────────────────────────────────────────────────────────────── - -// computeDeadConsts returns the set of OpConst dsts whose LD can be suppressed -// because every use of the constant is already handled by a codegen peephole: -// -// - OpCmp with Src[1] being the const → CP imm8 fires (no register needed). -// - OpAdd/OpSub with Src[1]==const, value==1, and lhs/dst at the same physical -// location → INC/DEC fires (no register needed). -// -// Constants with zero uses are also suppressed. -func computeDeadConsts(f *Func, ar *AllocResult) map[Reg]bool { - // Collect all OpConst dsts and their immediate values. - constVal := make(map[Reg]int64) - for _, b := range f.Blocks { - for _, inst := range b.Insts { - if inst.Op == OpConst { - constVal[inst.Dst] = inst.Imm - } - } - } - if len(constVal) == 0 { - return nil - } - - physName := func(r Reg) string { - if loc, ok := ar.Locs[r]; ok { - return loc.Name - } - return "" - } - - totalUses := make(map[Reg]int) - foldedUses := make(map[Reg]int) - - for _, b := range f.Blocks { - for _, inst := range b.Insts { - // Call/CallIndirect args live in inst.Args, not inst.Src — count them - // as non-peephole uses so their LD is not suppressed. - for _, src := range inst.Args { - if _, isConst := constVal[src]; isConst { - totalUses[src]++ - // call args are never peephole-foldable - } + for _, b := range f.Blocks { + for _, inst := range b.Insts { + // Call/CallIndirect args live in inst.Args, not inst.Src — count them + // as non-peephole uses so their LD is not suppressed. + for _, src := range inst.Args { + if _, isConst := constVal[src]; isConst { + totalUses[src]++ + // call args are never peephole-foldable + } } for _, src := range inst.Src { cv, isConst := constVal[src] @@ -1658,6833 +766,156 @@ func (g *z80cg) genFunc(f *Func) { fixOrphanedTSMCStores(g.sb, label) } -// fixOrphanedTSMCStores scans the emitted asm for _tsmc_ labels that are -// referenced (by LD (_tsmc_label+N), A store patches) but never defined -// (no _tsmc_label: reload instruction). For each orphan, replaces the -// TSMC store patches with regular _spill_ stores: -// -// LD (_tsmc_func_rN_M+1), A → LD (_spill_func_rN), A -// LD (_tsmc_func_rN_M+2), A → LD (_spill_func_rN+1), A -func fixOrphanedTSMCStores(sb *strings.Builder, funcLabel string) { - asm := sb.String() - tsmcPrefix := "_tsmc_" + funcLabel + "_r" +// ── Block ───────────────────────────────────────────────────────────────────── - // Collect all _tsmc_ labels that appear as references (in store patches) - referenced := make(map[string]bool) - for idx := 0; ; { - pos := strings.Index(asm[idx:], tsmcPrefix) - if pos < 0 { +func (g *z80cg) genBlock(f *Func, b *Block) { + if b.Label != "entry" { + g.emitf(".%s_%s:", sanitizeIdent(f.Name), sanitizeIdent(b.Label)) + } + clear(g.holdsPhys) // all register aliases unknown at block entry + // Relocations are block-local: the previous block restored everything a + // successor reads (restoreLiveOutOverrides), and blocks are laid out in an + // order unrelated to control flow, so nothing may leak into this one. + clear(g.physOverride) + g.lastFlagsLhs = "" // flags state unknown at block entry + g.lastFlagsRhs = "" + g.blockT = 0 // reset per-block T-state accumulator + g.curBlock = b + g.pendingFlagReg = NoReg + g.pendingAccReg = NoReg + // If a block param is allocated to A, any compare instruction in this block + // would clobber it with "LD A, lhs". Mark it as pending so materializePendingAcc + // can save it to a scratch register before the clobber occurs. + for _, bp := range b.Params { + if bp.Dst != NoReg && g.ar.Loc(bp.Dst).Name == "A" { + g.pendingAccReg = bp.Dst break } - pos += idx - // Extract: _tsmc_func_rN_M - end := pos - for end < len(asm) && asm[end] != '+' && asm[end] != ')' && asm[end] != ':' && - asm[end] != ' ' && asm[end] != '\n' { - end++ - } - label := asm[pos:end] - referenced[label] = true - idx = end } - - // Check which are defined (have "label:" in the asm) - changed := false - for label := range referenced { - if strings.Contains(asm, label+":") { - continue // defined — TSMC reload is working - } - // Orphaned: extract register number to build _spill_ label. - // Label format: _tsmc_func_rN_M → _spill_func_rN - tsmcSuffix := label[len("_tsmc_"):] - // Find last underscore (the _M part) - lastUS := strings.LastIndex(tsmcSuffix, "_") - if lastUS < 0 { - continue + // Also check function contract params (entry block has no block params). + if g.pendingAccReg == NoReg && b == g.fn.Entry() { + for _, cp := range g.fn.Contract.Params { + if cp.Reg != NoReg && g.ar.Loc(cp.Reg).Name == "A" { + g.pendingAccReg = cp.Reg + break + } } - spillLabel := "_spill_" + tsmcSuffix[:lastUS] - // Replace TSMC store patches with regular spill stores - asm = strings.ReplaceAll(asm, "LD ("+label+"+1), A", "LD ("+spillLabel+"), A") - asm = strings.ReplaceAll(asm, "LD ("+label+"+2), A", "LD ("+spillLabel+"+1), A") - changed = true - } - - if changed { - sb.Reset() - sb.WriteString(asm) } -} -// ── Page-aligned LUT pre-scan ────────────────────────────────────────────────── + // Pre-scan: identify page-aligned LUT access patterns so we can merge + // Ext + AddrOf + PtrAdd + Load into: LD HL,sym; LD L,src8; LD A,(HL). + g.scanLUTPatterns(b) -// scanLUTPatterns detects the pattern emitted by LUTGen for page-aligned tables: -// -// idx16 = Ext(src8, u8→u16, ClassIndex) -// base = AddrOf("sym") ← sym has Align==256 -// ptr = PtrAdd(base, idx16) -// dst = Load(ptr, u8, ClassAcc) -// -// When found: -// - g.lutLoadPat[dst] = {sym, src8Reg} -// - g.lutSkip[idx16, base, ptr] = true -// -// The pattern is cleared (g.lutLoadPat, g.lutSkip rebuilt) at each new block. -// This is safe because LUTGen only ever creates single-use intermediate regs. -func (g *z80cg) scanLUTPatterns(b *Block) { - // Reset per-block maps (keep same map objects; just clear them). - clear(g.lutLoadPat) - clear(g.lutSkip) + // Pre-scan: identify global struct field accesses so we can emit + // LD A,(sym__field) / LD (sym__field),A instead of LD HL,sym + INC HL… + LD A,(HL). + g.scanGlobalFieldPatterns(b) - // Build: reg → defining instruction (within this block only). - defInst := make(map[Reg]*Inst, len(b.Insts)) - for _, inst := range b.Insts { - if inst.Dst != NoReg { - defInst[inst.Dst] = inst - } - } + // Pre-scan: detect u8 load-modify-store bit updates so Store can emit + // SET/RES directly on memory and skip the intermediate Load/ALU regs. + g.scanBitStorePatterns(b) - for _, inst := range b.Insts { - // Looking for: dst = Load(ptr, u8) - if inst.Op != OpLoad || inst.Ty.Width() != 8 || inst.Dst == NoReg { - continue - } - ptrReg := inst.Src[0] + // Pre-scan: detect u8 bit-test compares against zero so Cmp can emit BIT. + g.scanBitCmpPatterns(b) - // ptr = PtrAdd(base, idx16) - ptrInst, ok := defInst[ptrReg] - if !ok || ptrInst.Op != OpPtrAdd { - continue - } - baseReg := ptrInst.Src[0] - idx16Reg := ptrInst.Src[1] + // Pre-scan: detect ptr_add+load patterns where the base (ptr) is in HL + // and must survive across the instruction (e.g. in a loop). Arranges + // for PUSH HL / ADD HL,rr / LD dst,(HL) / POP HL to preserve the base. + g.scanPtrAddBase(b) - // base = AddrOf(sym) where sym is page-aligned - baseInst, ok := defInst[baseReg] - if !ok || baseInst.Op != OpAddrOf { - continue - } - sym := baseInst.Sym - if !g.isGlobalPageAligned(sym) { - continue - } + // Detect DJNZ peephole: DEC B + JP loop_head → DJNZ loop_body. + peep := g.detectDJNZPeephole(f, b) - // idx16 = Ext(src8, u8→u16) - extInst, ok := defInst[idx16Reg] - if !ok || extInst.Op != OpExt { - continue - } - if extInst.SrcTy == nil || extInst.SrcTy.Width() != 8 || extInst.Ty.Width() != 16 { - continue + // Detect tail call: last instruction is OpCall whose result feeds directly + // into TermRet. genCall will emit JP instead of CALL; genTerm skips RET. + g.tailCallInst = nil + if len(b.Insts) > 0 && peep.bodyLbl == "" { + last := b.Insts[len(b.Insts)-1] + if last.Op == OpCall && !isIntrinsic(last.Sym) { + if ret, ok := b.Term.(*TermRet); ok { + isTail := len(ret.Vals) == 0 || + (len(ret.Vals) == 1 && ret.Vals[0] == last.Dst) + if isTail { + g.tailCallInst = last + } + } } - src8Reg := extInst.Src[0] + } - // Pattern matched: record and mark intermediates for skipping. - g.lutLoadPat[inst.Dst] = lutLoadPat{sym: sym, src8Reg: src8Reg} - g.lutSkip[idx16Reg] = true - g.lutSkip[baseReg] = true - g.lutSkip[ptrReg] = true + limit := len(b.Insts) + if peep.skipLast { + limit-- // skip final DEC B; DJNZ handles it } -} -// scanBitStorePatterns detects the common u8/u16 load-modify-store forms: -// -// cur = Load(ptr, u8) -// next = Or(cur, const(1< 0 { + g.emitf(" ; — block %q: %dT", b.Label, g.blockT) + if g.blockT > g.fnWorstT { + g.fnWorstT = g.blockT } + } +} - var loadReg Reg = NoReg - var mask byte - width := inst.Ty.Width() - for _, src := range valInst.Src { - if def, ok := defInst[src]; ok && def.Op == OpLoad && def.Ty.Width() == width && def.Src[0] == inst.Src[0] { - loadReg = src - } else if def, ok := defInst[src]; ok && def.Op == OpConst { - mask = byte(def.Imm) - } - } - if loadReg == NoReg || useCount[loadReg] != 1 { - if valInst.Op != OpBitSet && valInst.Op != OpBitReset { - continue - } - loadReg = valInst.Src[0] - if def, ok := defInst[loadReg]; !ok || def.Op != OpLoad || def.Ty.Width() != width || def.Src[0] != inst.Src[0] || useCount[loadReg] != 1 { - continue - } - } +// ── Label helpers ───────────────────────────────────────────────────────────── - switch width { - case 8: - switch valInst.Op { - case OpOr: - if bits.OnesCount8(mask) != 1 { - continue - } - bit := bits.TrailingZeros8(mask) - g.bitStorePat[valReg] = bitStorePat{bit: bit, byteOffset: 0, op: "SET"} - case OpAnd: - if bits.OnesCount8(^mask) != 1 { - continue - } - bit := bits.TrailingZeros8(^mask) - g.bitStorePat[valReg] = bitStorePat{bit: bit, byteOffset: 0, op: "RES"} - case OpBitSet: - g.bitStorePat[valReg] = bitStorePat{bit: int(valInst.Imm), byteOffset: 0, op: "SET"} - case OpBitReset: - g.bitStorePat[valReg] = bitStorePat{bit: int(valInst.Imm), byteOffset: 0, op: "RES"} - } - case 16: - mask16 := uint16(0) - for _, src := range valInst.Src { - if def, ok := defInst[src]; ok && def.Op == OpConst { - mask16 = uint16(def.Imm) - } - } - switch valInst.Op { - case OpOr: - if bits.OnesCount16(mask16) != 1 { - continue - } - totalBit := bits.TrailingZeros16(mask16) - g.bitStorePat[valReg] = bitStorePat{bit: totalBit % 8, byteOffset: totalBit / 8, op: "SET"} - case OpAnd: - if bits.OnesCount16(^mask16) != 1 { - continue - } - totalBit := bits.TrailingZeros16(^mask16) - g.bitStorePat[valReg] = bitStorePat{bit: totalBit % 8, byteOffset: totalBit / 8, op: "RES"} - case OpBitSet: - totalBit := int(valInst.Imm) - g.bitStorePat[valReg] = bitStorePat{bit: totalBit % 8, byteOffset: totalBit / 8, op: "SET"} - case OpBitReset: - totalBit := int(valInst.Imm) - g.bitStorePat[valReg] = bitStorePat{bit: totalBit % 8, byteOffset: totalBit / 8, op: "RES"} - } - } - if _, ok := g.bitStorePat[valReg]; ok { - g.bitStoreSkip[loadReg] = true - g.bitStoreSkip[valReg] = true - } +func blockLabel(funcName, blockLabel string) string { + if blockLabel == "entry" { + return sanitizeIdent(funcName) } + return fmt.Sprintf(".%s_%s", sanitizeIdent(funcName), sanitizeIdent(blockLabel)) } -// scanBitCmpPatterns detects u8/u16 bit-test compares against zero: -// -// masked = And(x, const(1<= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_' { + b.WriteRune(c) + } else { + b.WriteByte('_') } } - if b.Term != nil { - for _, src := range b.Term.termUses() { - if src != NoReg { - useCount[src]++ - } - } + result := b.String() + // Avoid Z80 register name collisions (e.g. Pascal variable "I"). + if z80ReservedNames[strings.ToUpper(result)] { + return "v_" + result } + return result +} - for _, inst := range b.Insts { - if inst.Op != OpCmp || (inst.Cond != CmpEq && inst.Cond != CmpNe) { - continue - } - testReg, zeroReg := NoReg, NoReg - if rhsInst, ok := defInst[inst.Src[1]]; ok && rhsInst.Op == OpConst && rhsInst.Imm == 0 { - testReg = inst.Src[0] - zeroReg = inst.Src[1] - } else if lhsInst, ok := defInst[inst.Src[0]]; ok && lhsInst.Op == OpConst && lhsInst.Imm == 0 { - testReg = inst.Src[1] - zeroReg = inst.Src[0] - } - if testReg == NoReg || useCount[zeroReg] != 1 { - continue - } - - testInst, ok := defInst[testReg] - if !ok || useCount[testReg] != 1 { - continue - } - - var ( - pat bitCmpPat - skipRegs []Reg - matched bool - maskConst uint16 - otherReg Reg = NoReg - ) - - if testInst.Op == OpBitGet { - totalBit := int(testInst.Imm) - pat.bit = totalBit % 8 - pat.byteOffset = totalBit / 8 - baseReg := testInst.Src[0] - if loadInst, ok := defInst[baseReg]; ok && loadInst.Op == OpLoad && (loadInst.Ty.Width() == 8 || loadInst.Ty.Width() == 16) && useCount[baseReg] == 1 { - pat.ptrReg = loadInst.Src[0] - pat.useMem = true - skipRegs = []Reg{baseReg, testReg} - matched = true - } else { - pat.srcReg = baseReg - skipRegs = []Reg{testReg} - matched = true - } - } - if matched { - g.bitCmpPat[inst.Dst] = pat - g.bitCmpSkip[zeroReg] = true - for _, r := range skipRegs { - g.bitCmpSkip[r] = true - } - continue - } - - if testInst.Op != OpAnd { - continue - } - for _, src := range testInst.Src { - if def, ok := defInst[src]; ok && def.Op == OpConst { - maskConst = uint16(def.Imm) - } else { - otherReg = src - } - } - - if maskConst == 1 && otherReg != NoReg { - shrInst, ok := defInst[otherReg] - if ok && shrInst.Op == OpShr && useCount[otherReg] == 1 { - shiftConstInst, ok := defInst[shrInst.Src[1]] - if ok && shiftConstInst.Op == OpConst { - totalBit := int(shiftConstInst.Imm) - pat.bit = totalBit % 8 - pat.byteOffset = totalBit / 8 - baseReg := shrInst.Src[0] - if loadInst, ok := defInst[baseReg]; ok && loadInst.Op == OpLoad && (loadInst.Ty.Width() == 8 || loadInst.Ty.Width() == 16) && useCount[baseReg] == 1 { - pat.ptrReg = loadInst.Src[0] - pat.useMem = true - skipRegs = []Reg{baseReg, otherReg, testReg} - matched = true - } else { - pat.srcReg = baseReg - skipRegs = []Reg{otherReg, testReg} - matched = true - } - } - } - } - - if !matched && bits.OnesCount16(maskConst) == 1 && otherReg != NoReg { - totalBit := bits.TrailingZeros16(maskConst) - pat.bit = totalBit % 8 - pat.byteOffset = totalBit / 8 - if loadInst, ok := defInst[otherReg]; ok && loadInst.Op == OpLoad && (loadInst.Ty.Width() == 8 || loadInst.Ty.Width() == 16) && useCount[otherReg] == 1 { - pat.ptrReg = loadInst.Src[0] - pat.useMem = true - skipRegs = []Reg{otherReg, testReg} - matched = true - } else { - pat.srcReg = otherReg - skipRegs = []Reg{testReg} - matched = true - } - } - - if matched { - g.bitCmpPat[inst.Dst] = pat - g.bitCmpSkip[zeroReg] = true - for _, r := range skipRegs { - g.bitCmpSkip[r] = true - } - } - } -} - -// isGlobalPageAligned reports whether the named global in the current module -// has Align == 256 (i.e. is a page-aligned LUT candidate). -func (g *z80cg) isGlobalPageAligned(sym string) bool { - for _, gl := range g.mod.Globals { - if gl.Name == sym { - if at, ok := gl.Ty.(*ArrayTy); ok { - return at.Align == 256 - } - } - } - return false -} - -func (g *z80cg) emitBitMemOp(op string, bit int, ptr string, byteOffset int) bool { - if ptr == "HL" { - switch byteOffset { - case 0: - g.emitf(" %s %d, (HL)", op, bit) - return true - case 1: - g.emit(" INC HL") - g.emitf(" %s %d, (HL)", op, bit) - g.emit(" DEC HL") - return true - default: - return false - } - } - if isIXY(ptr) && byteOffset >= 0 && byteOffset <= 127 { - g.emitf(" %s %d, %s", op, bit, ptrIndirect(ptr, byteOffset)) - return true - } - return false -} - -func selectBitReg(src string, byteOffset int) string { - switch byteOffset { - case 0: - return lowByte(src) - case 1: - return highByte(src) - default: - return "" - } -} - -func (g *z80cg) emitBitRegOp(op string, bit int, reg string) bool { - if reg == "" || reg == "A" || isSpill(reg) || reg == "F" { - return false - } - if !isSimpleReg(reg) { - return false - } - // BIT/SET/RES use CB prefix encoding — IXH/IXL/IYH/IYL are NOT valid - // operands (no DD/FD CB r form exists). Return false to fall through - // to the AND/OR mask path in every caller. - if isIXYReg(reg) { - return false - } - g.emitf(" %s %d, %s", op, bit, reg) - return true -} - -// globalFieldLabel returns the EQU label for a global struct field access. -// Format: sanitizeIdent(sym)__fieldName (or just sanitizeIdent(sym) for offset 0 / no name). -func globalFieldLabel(sym string, offset int64, fieldName string) string { - base := sanitizeIdent(sym) - if fieldName != "" { - return base + "__" + fieldName - } - if offset == 0 { - return base - } - // No named field at this offset (e.g. nested struct access with folded offsets). - // Use "sym + N" arithmetic expression — the assembler resolves it inline. - return fmt.Sprintf("%s + %d", base, offset) -} - -// globalStructField looks up a global in the module by symbol name and returns its -// StructTy if it has one, plus the field name for a given byte offset. -// Returns ("", -1, false) when not found or not a struct global. -func (g *z80cg) globalStructField(sym string, offset int64) (fieldName string, found bool) { - for _, gl := range g.mod.Globals { - if gl.Name != sym { - continue - } - st, ok := gl.Ty.(*StructTy) - if !ok { - return "", false - } - // Find field by byte offset. - off := int64(0) - for _, f := range st.Fields { - if off == offset { - return f.Name, true - } - off += int64(ByteWidth(f.Ty)) - } - // Offset is valid but doesn't align with a named field boundary — still use it. - return "", false - } - return "", false -} - -// scanGlobalFieldPatterns detects patterns where a global struct field is loaded -// or stored via AddrOf + optional Field + Load/Store. When found, the load/store -// is rewritten to use Z80 direct addressing (LD A,(sym__field) or LD (sym__field),A). -// -// Patterns detected (u8 only — all addressable via single LD A,(nn)): -// -// Load pattern (with field): -// base = AddrOf(sym) [OpAddrOf] -// fptr = Field(base, imm) [OpField] -// dst = Load(fptr, u8) [OpLoad] -// -// Load pattern (field 0, no OpField): -// base = AddrOf(sym) [OpAddrOf] -// dst = Load(base, u8) [OpLoad] -// -// Store pattern (with field): -// base = AddrOf(sym) -// fptr = Field(base, imm) -// Store(fptr, val, u8) [OpStore, Dst==NoReg] -// -// Store pattern (field 0): -// base = AddrOf(sym) -// Store(base, val, u8) -// -// Only fires when the AddrOf refers to a global in m.Globals (not a func symbol). -// Only u8 loads and stores are handled (single-byte LD A,(nn) / LD (nn),A). -func (g *z80cg) scanGlobalFieldPatterns(b *Block) { - // Reset per-block maps. - clear(g.globalFieldLoad) - clear(g.globalFieldStore) - clear(g.globalFieldSkip) - clear(g.globalU16StoreSym) - clear(g.globalU16LoadSym) - - // Build: reg → defining instruction within this block. - defInst := make(map[Reg]*Inst, len(b.Insts)) - for _, inst := range b.Insts { - if inst.Dst != NoReg { - defInst[inst.Dst] = inst - } - } - - // Helper: check if sym names a module-level global (not a string or function symbol). - isGlobal := func(sym string) bool { - for _, gl := range g.mod.Globals { - if gl.Name == sym { - return true - } - } - return false - } - - // Count uses of each virtual reg in this block (instructions + block terminators). - // Used to guard against skipping intermediates that are shared with other users. - useCount := make(map[Reg]int) - for _, inst := range b.Insts { - for _, s := range inst.Src { - if s != NoReg { - useCount[s]++ - } - } - for _, s := range inst.Args { - if s != NoReg { - useCount[s]++ - } - } - } - if b.Term != nil { - for _, s := range b.Term.termUses() { - if s != NoReg { - useCount[s]++ - } - } - } - - // Scan Load instructions. - // Only mark intermediates as skip when: - // 1. The load's allocated destination IS A (LD A,(nn) is the only direct read on Z80). - // 2. Each intermediate register (AddrOf/Field results) is used exactly once — - // by this Load (and the Field uses the AddrOf, counted once). - // If an intermediate is shared (e.g., base ptr reused for multiple fields), - // we must not skip it or the other consumer will see a dead register. - for _, inst := range b.Insts { - if inst.Op == OpLoad && inst.Ty.Width() == 8 && inst.Dst != NoReg { - // Only apply when the allocated destination is the A register. - if g.ar.Loc(inst.Dst).Name != "A" { - continue - } - ptrReg := inst.Src[0] - ptrInst, hasDef := defInst[ptrReg] - if !hasDef { - continue - } - - switch ptrInst.Op { - case OpAddrOf: - // Pattern: Load(AddrOf(sym), u8) — offset 0 - if !isGlobal(ptrInst.Sym) { - continue - } - // Only skip AddrOf if its result is used only by this Load. - if useCount[ptrReg] != 1 { - continue - } - fieldName, _ := g.globalStructField(ptrInst.Sym, 0) - g.globalFieldLoad[inst.Dst] = globalFieldInfo{ - sym: ptrInst.Sym, - offset: 0, - fieldName: fieldName, - } - g.globalFieldSkip[ptrReg] = true - - case OpField: - // Pattern: Load(Field(AddrOf(sym), imm), u8) - // Guard: Field result used only by this Load, AddrOf result used only by Field. - if useCount[ptrReg] != 1 { - continue - } - baseReg := ptrInst.Src[0] - baseDef, ok := defInst[baseReg] - if !ok || baseDef.Op != OpAddrOf { - continue - } - if !isGlobal(baseDef.Sym) { - continue - } - if useCount[baseReg] != 1 { - continue - } - offset := ptrInst.Imm - fieldName, _ := g.globalStructField(baseDef.Sym, offset) - g.globalFieldLoad[inst.Dst] = globalFieldInfo{ - sym: baseDef.Sym, - offset: offset, - fieldName: fieldName, - } - g.globalFieldSkip[ptrReg] = true // Field result - g.globalFieldSkip[baseReg] = true // AddrOf result - } - } - } - - // Scan Store instructions (Dst==NoReg, Src[0]=addr, Src[1]=val). - for _, inst := range b.Insts { - if inst.Op != OpStore || inst.Ty.Width() != 8 { - continue - } - addrReg := inst.Src[0] - addrInst, hasDef := defInst[addrReg] - if !hasDef { - continue - } - - switch addrInst.Op { - case OpAddrOf: - if !isGlobal(addrInst.Sym) { - continue - } - // Only skip if the AddrOf result is used solely by this Store. - if useCount[addrReg] != 1 { - continue - } - fieldName, _ := g.globalStructField(addrInst.Sym, 0) - g.globalFieldStore[addrReg] = globalFieldInfo{ - sym: addrInst.Sym, - offset: 0, - fieldName: fieldName, - } - g.globalFieldSkip[addrReg] = true - - case OpField: - if useCount[addrReg] != 1 { - continue - } - baseReg2 := addrInst.Src[0] - baseDef, ok := defInst[baseReg2] - if !ok || baseDef.Op != OpAddrOf { - continue - } - if !isGlobal(baseDef.Sym) { - continue - } - if useCount[baseReg2] != 1 { - continue - } - offset := addrInst.Imm - fieldName, _ := g.globalStructField(baseDef.Sym, offset) - g.globalFieldStore[addrReg] = globalFieldInfo{ - sym: baseDef.Sym, - offset: offset, - fieldName: fieldName, - } - g.globalFieldSkip[addrReg] = true // Field result - g.globalFieldSkip[baseReg2] = true // AddrOf result - } - } - - // ── HL-chain detection ────────────────────────────────────────────────────── - // When 2+ stores to the same global struct cover consecutive byte offsets and - // all value registers are safe (not H or L, which alias with the HL pointer), - // replace independent LD (sym__field),A sequences with: - // LD HL,sym ; LD (HL),r ; INC HL ; LD (HL),r ; ... - // Saves 8T and 4 bytes for a 3-field struct (61T→53T, 12B→8B). - type hlEntry struct { - addrReg Reg - valReg Reg - offset int64 - } - symStores := make(map[string][]hlEntry) - for addrReg, info := range g.globalFieldStore { - for _, inst := range b.Insts { - if inst.Op == OpStore && inst.Src[0] == addrReg { - symStores[info.sym] = append(symStores[info.sym], hlEntry{ - addrReg: addrReg, - valReg: inst.Src[1], - offset: info.offset, - }) - break - } - } - } - for _, entries := range symStores { - if len(entries) < 2 { - continue - } - sort.Slice(entries, func(i, j int) bool { return entries[i].offset < entries[j].offset }) - // Check consecutive offsets. - consecutive := true - for i := 1; i < len(entries); i++ { - if entries[i].offset != entries[i-1].offset+1 { - consecutive = false - break - } - } - if !consecutive { - continue - } - // Check all value regs safe (not H/L which aliases with HL pointer). - safe := true - for _, e := range entries { - if e.valReg != NoReg { - loc := g.ar.Loc(e.valReg).Name - if loc == "H" || loc == "L" { - safe = false - break - } - } - } - if !safe { - continue - } - // Mark as HL-chain. - for i, e := range entries { - entry := g.globalFieldStore[e.addrReg] - entry.hlChain = true - entry.hlHead = (i == 0) - entry.hlLast = (i == len(entries)-1) - g.globalFieldStore[e.addrReg] = entry - } - } - - // ── u16 direct-address store detection ────────────────────────────────────── - // Pattern: OpStore(w=16, addrReg=AddrOf(sym), valReg) - // Enables LD (sym), HL (16T, no A clobber) when valReg is in HL. - for _, inst := range b.Insts { - if inst.Op != OpStore || inst.Ty.Width() != 16 { - continue - } - addrReg := inst.Src[0] - def, ok := defInst[addrReg] - if !ok || def.Op != OpAddrOf || !isGlobal(def.Sym) { - continue - } - g.globalU16StoreSym[addrReg] = def.Sym - } - - // ── u16 direct-address load detection ──────────────────────────────────── - // Pattern: OpLoad(w=16, addrReg=AddrOf(sym)) → dst (u16, must be HL) - // Enables LD HL, (sym) (20T, no A clobber) instead of INC/DEC trick via A. - // Only fires when addrReg is used solely by this load (useCount==1) so we - // can safely skip the AddrOf instruction (suppress LD HL,sym). - for _, inst := range b.Insts { - if inst.Op != OpLoad || inst.Ty.Width() != 16 || inst.Dst == NoReg { - continue - } - if g.ar.Loc(inst.Dst).Name != "HL" { - continue // only LD HL,(nn) exists for direct u16 load - } - addrReg := inst.Src[0] - def, ok := defInst[addrReg] - if !ok || def.Op != OpAddrOf || !isGlobal(def.Sym) { - continue - } - if useCount[addrReg] != 1 { - continue // addr reg used by other instructions too - } - g.globalU16LoadSym[addrReg] = def.Sym - g.globalFieldSkip[addrReg] = true // suppress LD HL, sym from OpAddrOf - } -} - -// scanPtrAddBase detects OpPtrAdd instructions where the base pointer is in HL -// AND the base virtual is live after the ptr_add (e.g. needed on a loop -// back-edge). In that case ADD HL,rr would clobber HL (the base), so we -// arrange for PUSH HL before the ADD and POP HL after the subsequent load. -// -// Sets g.ptrAddPushHL = ptr_add.Dst when the pattern fires; cleared by the -// OpLoad codegen after it emits the POP HL. -func (g *z80cg) scanPtrAddBase(b *Block) { - g.ptrAddPushHL = NoReg - - for idx, inst := range b.Insts { - if inst.Op != OpPtrAdd { - continue - } - baseReg := inst.Src[0] - if g.ar.Loc(baseReg).Name != "HL" { - continue // base not in HL — no clobber risk - } - - // Check if baseReg is live after this ptr_add. - // A base is live-after if it appears in any later instruction's sources - // OR in the block terminator's argument list. - liveAfter := false - for _, later := range b.Insts[idx+1:] { - for _, s := range later.Src { - if s == baseReg { - liveAfter = true - break - } - } - for _, a := range later.Args { - if a == baseReg { - liveAfter = true - break - } - } - if liveAfter { - break - } - } - if !liveAfter { - if t := b.Term; t != nil { - for _, r := range t.termUses() { - if r == baseReg { - liveAfter = true - break - } - } - } - } - - if liveAfter { - g.ptrAddPushHL = inst.Dst - return // one pattern per block is enough - } - } -} - -// ── Block ───────────────────────────────────────────────────────────────────── - -func (g *z80cg) genBlock(f *Func, b *Block) { - if b.Label != "entry" { - g.emitf(".%s_%s:", sanitizeIdent(f.Name), sanitizeIdent(b.Label)) - } - clear(g.holdsPhys) // all register aliases unknown at block entry - // Relocations are block-local: the previous block restored everything a - // successor reads (restoreLiveOutOverrides), and blocks are laid out in an - // order unrelated to control flow, so nothing may leak into this one. - clear(g.physOverride) - g.lastFlagsLhs = "" // flags state unknown at block entry - g.lastFlagsRhs = "" - g.blockT = 0 // reset per-block T-state accumulator - g.curBlock = b - g.pendingFlagReg = NoReg - g.pendingAccReg = NoReg - // If a block param is allocated to A, any compare instruction in this block - // would clobber it with "LD A, lhs". Mark it as pending so materializePendingAcc - // can save it to a scratch register before the clobber occurs. - for _, bp := range b.Params { - if bp.Dst != NoReg && g.ar.Loc(bp.Dst).Name == "A" { - g.pendingAccReg = bp.Dst - break - } - } - // Also check function contract params (entry block has no block params). - if g.pendingAccReg == NoReg && b == g.fn.Entry() { - for _, cp := range g.fn.Contract.Params { - if cp.Reg != NoReg && g.ar.Loc(cp.Reg).Name == "A" { - g.pendingAccReg = cp.Reg - break - } - } - } - - // Pre-scan: identify page-aligned LUT access patterns so we can merge - // Ext + AddrOf + PtrAdd + Load into: LD HL,sym; LD L,src8; LD A,(HL). - g.scanLUTPatterns(b) - - // Pre-scan: identify global struct field accesses so we can emit - // LD A,(sym__field) / LD (sym__field),A instead of LD HL,sym + INC HL… + LD A,(HL). - g.scanGlobalFieldPatterns(b) - - // Pre-scan: detect u8 load-modify-store bit updates so Store can emit - // SET/RES directly on memory and skip the intermediate Load/ALU regs. - g.scanBitStorePatterns(b) - - // Pre-scan: detect u8 bit-test compares against zero so Cmp can emit BIT. - g.scanBitCmpPatterns(b) - - // Pre-scan: detect ptr_add+load patterns where the base (ptr) is in HL - // and must survive across the instruction (e.g. in a loop). Arranges - // for PUSH HL / ADD HL,rr / LD dst,(HL) / POP HL to preserve the base. - g.scanPtrAddBase(b) - - // Detect DJNZ peephole: DEC B + JP loop_head → DJNZ loop_body. - peep := g.detectDJNZPeephole(f, b) - - // Detect tail call: last instruction is OpCall whose result feeds directly - // into TermRet. genCall will emit JP instead of CALL; genTerm skips RET. - g.tailCallInst = nil - if len(b.Insts) > 0 && peep.bodyLbl == "" { - last := b.Insts[len(b.Insts)-1] - if last.Op == OpCall && !isIntrinsic(last.Sym) { - if ret, ok := b.Term.(*TermRet); ok { - isTail := len(ret.Vals) == 0 || - (len(ret.Vals) == 1 && ret.Vals[0] == last.Dst) - if isTail { - g.tailCallInst = last - } - } - } - } - - limit := len(b.Insts) - if peep.skipLast { - limit-- // skip final DEC B; DJNZ handles it - } - - // Pre-pass: reorder to avoid A-clobber between call arg setup instructions. - // Pattern: OpMove → A (ClassAcc) followed by an 8-bit ALU → nonA that uses - // A as scratch. Swapping them lets the ALU use A freely, then the move - // loads A last (right before the call). Safe when neither depends on the other. - reordered := reorderAccMoves(b.Insts[:limit], g.ar) - - for instIdx, inst := range reordered { - g.curInstIdx = instIdx - g.genInst(inst) - } - - if peep.bodyLbl != "" { - g.restoreLiveOutOverrides(f, b.Term) - g.emitf(" DJNZ %s", peep.bodyLbl) - // Exit path after DJNZ falls through (B=0). - // Use the check block's Else edge copies. - exitCopies := g.buildBlockCopies(f, peep.checkBrif.Else, peep.checkBrif.ElseArgs) - g.emitParallelCopy(exitCopies) - if !g.isFallThrough(f, peep.checkBrif.Else) || len(exitCopies) != 0 { - g.emitf(" JRS %s", blockLabel(f.Name, peep.checkBrif.Else)) - } - } else { - g.genTerm(f, b.Term) - } - - // Emit per-block T-state total and accumulate into function worst-case. - if g.annotateT && g.blockT > 0 { - g.emitf(" ; — block %q: %dT", b.Label, g.blockT) - if g.blockT > g.fnWorstT { - g.fnWorstT = g.blockT - } - } -} - -// ── Instructions ────────────────────────────────────────────────────────────── - -func (g *z80cg) genInst(inst *Inst) { - // Instructions merged into a page-aligned LUT load are skipped here; - // their code is emitted by the Load case below. - if g.lutSkip[inst.Dst] { - return - } - - if inst.Op != OpStore && g.bitStoreSkip[inst.Dst] { - return - } - - if inst.Op != OpCmp && g.bitCmpSkip[inst.Dst] { - return - } - - // Instructions merged into a global-field direct-addressing sequence are - // skipped here; their code is emitted by the Load/Store case below. - // (Stores have Dst==NoReg; check the addr-reg via the instruction's Src[0].) - if inst.Op != OpStore && g.globalFieldSkip[inst.Dst] { - return - } - - // Scratch relocations of A are instruction-local. Return them to their - // allocated home before another instruction can allocate that scratch. - for _, r := range slices.Sorted(maps.Keys(g.physOverride)) { - loc := g.physOverride[r] - if g.ar.Loc(r).Name == "A" && loc != "A" && loc != "F" { - g.emitLD8("A", loc) - delete(g.physOverride, r) - g.invalidate("A") - } - } - - // Flag-clobbering ops clear lastFlagsLhs/Rhs explicitly in their cases. - // Flag-preserving ops (LD, PUSH, POP, …) leave the tracker intact so that - // the Sub+CmpLt flag-fusion peephole can fire across an intervening LD. - - dst := g.loc(inst.Dst) - - switch inst.Op { - case OpConst: - // Only record as constant if the Dst is NOT a block parameter. - // After PreallocCoalesce, an OpConst may share a Reg with a block param - // (loop counter). The initial value is constant but the param changes - // each iteration — treating it as constant produces wrong code. - if !g.blockParamRegs[inst.Dst] { - g.constVals[inst.Dst] = inst.Imm - } - if dst == "F" { - // Bool constant into flag register: SCF (true) or AND A (false, clears C+sets Z). - g.lastFlagsLhs = "" - g.lastFlagsRhs = "" - if inst.Imm != 0 { - g.emit(" SCF") - } else { - g.emit(" AND A") // A=A (no change), clears C, sets Z based on A - } - return - } - if !g.deadConsts[inst.Dst] { - w := inst.Ty.Width() - if w >= 24 { - // 24/32-bit constant via shadow pair. - // LD rr, lo16 (10T) - // EXX (4T) - // LD rr, hi (10T) — hi8 for u24, hi16 for u32 - // EXX (4T) = 28T total - // For u24: hi is at most 0x00FF, so shadow H' is always 0. - lo := uint32(inst.Imm) & 0xFFFF - hi := uint32(inst.Imm) >> 16 - g.emitf(" LD %s, %d", dst, lo) - g.emit(" EXX") - g.emitf(" LD %s, %d", dst, hi) - g.emit(" EXX") - } else if isSpill(dst) { - // LocMem spill destination. - // TSMC: if eligible, patch reload sites instead of memory store. - if pair := g.tsmcSpillPairFor(inst.Dst); pair != nil { - if w <= 8 { - g.emitf(" LD A, %d", inst.Imm&0xFF) - g.emitTSMCSpill(pair, "A") - g.invalidate("A") - } else { - g.emitf(" LD HL, %d", inst.Imm&0xFFFF) - g.emitTSMCSpill(pair, "HL") - g.invalidate("HL") - } - } else { - // Traditional spill - if w <= 8 { - g.emitf(" LD A, %d", inst.Imm&0xFF) - g.emitf(" LD (%s), A", dst) - g.invalidate("A") - } else { - g.emitf(" LD HL, %d", inst.Imm&0xFFFF) - g.emitf(" LD (%s), HL", dst) - g.invalidate("HL") - } - } - } else if isSpill(dst) { - g.emit(" EX AF, AF'") - g.emitf(" LD A, %d", inst.Imm&0xFF) - g.emitf(" LD (%s), A", dst) - g.emit(" EX AF, AF'") - } else { - g.emitf(" LD %s, %d", dst, inst.Imm) - } - } - - case OpMove: - src := g.loc(inst.Src[0]) - if dst == src { - return // no-op - } - g.emitMov(dst, src, inst.Ty.Width()) - - case OpAdd: - g.lastFlagsLhs = "" - g.lastFlagsRhs = "" - g.genBinOp("ADD", inst) - - case OpSub: - g.lastFlagsLhs = "" // clear stale tracking before this sub - g.lastFlagsRhs = "" - g.genBinOp("SUB", inst) - // Record that flags were set by SUB(lhs, rhs) for the Sub+CmpLt fusion. - // Only track 8-bit subs; 16-bit uses SBC which sets different flags. - if inst.Ty.Width() <= 8 { - rhsP := g.loc(inst.Src[1]) - if cv, ok := g.constVals[inst.Src[1]]; ok { - rhsP = fmt.Sprintf("%d", cv) - } - if rhsP != "A" { // skip NEG-trick case (rhs was in A) - g.lastFlagsLhs = g.loc(inst.Src[0]) // logical lhs - g.lastFlagsRhs = rhsP - } - } - - case OpMul: - g.lastFlagsLhs = "" - g.lastFlagsRhs = "" - g.genMul(inst) - - case OpDiv, OpSDiv, OpSMod, OpMod: - g.lastFlagsLhs = "" - g.lastFlagsRhs = "" - g.genDivMod(inst) - - case OpAnd: - g.lastFlagsLhs = "" - g.lastFlagsRhs = "" - g.genBinOp("AND", inst) - - case OpOr: - g.lastFlagsLhs = "" - g.lastFlagsRhs = "" - g.genBinOp("OR", inst) - - case OpXor: - g.lastFlagsLhs = "" - g.lastFlagsRhs = "" - g.genBinOp("XOR", inst) - - case OpBitGet: - g.lastFlagsLhs = "" - g.lastFlagsRhs = "" - src := g.loc(inst.Src[0]) - bit := int(inst.Imm) - byteOffset := bit / 8 - bitInByte := bit % 8 - target := selectBitReg(src, byteOffset) - if isSpill(src) { - if inst.SrcTy != nil && inst.SrcTy.Width() > 8 { - g.loadSpill16("HL", src) - target = selectBitReg("HL", byteOffset) - } else { - g.emitLD8("A", src) - target = "A" - } - } - if target != "A" { - g.emitLDA(target) - } - for i := 0; i < bitInByte; i++ { - g.emit(" SRL A") - } - g.emit(" AND 1") - g.invalidate("A") - if dst != "A" { - g.emitLD8(dst, "A") - } else { - g.pendingAccReg = inst.Dst - } - - case OpBitSet, OpBitReset: - g.lastFlagsLhs = "" - g.lastFlagsRhs = "" - src := g.loc(inst.Src[0]) - bit := int(inst.Imm) - byteOffset := bit / 8 - bitInByte := bit % 8 - op := "SET" - if inst.Op == OpBitReset { - op = "RES" - } - if dst != src { - g.emitMov(dst, src, inst.Ty.Width()) - } - target := selectBitReg(dst, byteOffset) - if g.emitBitRegOp(op, bitInByte, target) { - g.invalidate(dst) - g.invalidate(target) - break - } - if isSpill(dst) && inst.Ty.Width() > 8 { - g.loadSpill16("HL", dst) - target = selectBitReg("HL", byteOffset) - g.emitLDA(target) - if op == "SET" { - g.emitf(" OR %d", 1<= 24 { - g.genBinOp32(mnem, dst, lhs, rhs) - return - } - - // Word operations also use HL as a working destination. Preserve a rhs - // there before copying lhs, regardless of the allocated result location. - if w == 16 { - rhsValue, rhsConstant := g.constVals[rhsReg] - inPlaceUnit := rhsConstant && rhsValue == 1 && lhs == dst - if (mnem == "ADD" && rhs == "HL" && !inPlaceUnit) || ((mnem == "AND" || mnem == "OR" || mnem == "XOR") && rhs == dst && !rhsConstant) { - lhs, rhs = rhs, lhs - rhsReg = inst.Src[0] - } - if mnem == "SUB" && ((rhs == dst && lhs != dst) || (rhs == "HL" && lhs != "HL")) { - g.pushWord(lhs) - g.pushWord(rhs) - g.emit(" POP DE") - g.emit(" POP HL") - g.emit(" OR A") - g.emit(" SBC HL, DE") - g.emitMov(dst, "HL", 16) - g.invalidate(dst) - return - } - } - - if w <= 8 { - if lhs == dst && dst != "A" && dst != "F" && !isSpill(dst) { - if cv, ok := g.constVals[inst.Src[1]]; ok { - switch mnem { - case "OR": - mask := uint16(cv) - if bits.OnesCount16(mask) == 1 { - bit := bits.TrailingZeros16(mask) - if bit < 8 && g.emitBitRegOp("SET", bit, dst) { - g.invalidate(dst) - return - } - } - case "AND": - mask := uint16(cv) - if bits.OnesCount16(^mask) == 1 { - bit := bits.TrailingZeros16(^mask) - if bit < 8 && g.emitBitRegOp("RES", bit, dst) { - g.invalidate(dst) - return - } - } - } - } - } - - // Special case: boolean OR with a ClassFlag operand — Z80 cannot LD A, F - // or OR F. Materialize the flag condition as 0/1 via a conditional skip: - // (ensure A holds the non-flag operand) - // JR Ncc, skip ; if condition false, skip setting bit 0 - // OR 1 ; A |= 1 (condition true) - // skip: - // Also treat a source as a flag if it's the pending carry-flag result - // (g.loc() may return "A" because PBQP assigned it there, but the - // actual value is still live in the carry flag via pendingFlagReg). - isPendingFlag0 := len(inst.Src) > 0 && inst.Src[0] == g.pendingFlagReg - isPendingFlag1 := len(inst.Src) > 1 && inst.Src[1] == g.pendingFlagReg - if mnem == "OR" && (lhs == "F" || rhs == "F" || isPendingFlag0 || isPendingFlag1) { - var flagSrc Reg - var otherLoc string - if lhs == "F" || isPendingFlag0 { - flagSrc = inst.Src[0] - otherLoc = rhs - } else { - flagSrc = inst.Src[1] - otherLoc = lhs - } - // Consume the pending flag tracking for flagSrc. - if flagSrc == g.pendingFlagReg { - g.pendingFlagReg = NoReg - } - if otherLoc != "A" && !g.holdsValue("A", otherLoc) { - g.emitLDA(otherLoc) - } - g.invalidate("A") - cc := g.condCode(g.fn, flagSrc) - skipLbl := fmt.Sprintf(".%s_orf_%d_sk", sanitizeIdent(g.fn.Name), int(inst.Dst)) - g.emitf(" JR %s, %s", invertCC(cc), skipLbl) - g.emit(" OR 1") - g.emitf("%s:", skipLbl) - if dst != "A" { - g.emitLD8(dst, "A") - g.setCopy(dst, "A") - } - // Result is in A (or dst). Track it as a pending acc value in case - // a subsequent genCmp overwrites A with "LD A, imm". - if dst == "A" { - g.pendingAccReg = inst.Dst - } - return - } - - // If there is a pending ClassAcc value in A that is still live after this - // instruction, save it to a scratch register before we overwrite A. - // This fires when two successive 8-bit ALU ops both need A: the first - // result (r33=acc) must survive while the second (r35=i+1) runs. - // Skip when the peephole below will use INC/DEC (does not touch A). - // INC/DEC doesn't clobber A — only fires when rhs is a small constant. - _, rhsIsConst := g.constVals[inst.Src[1]] - willUseINCDEC := (mnem == "ADD" || mnem == "SUB") && lhs == dst && rhsIsConst - if !willUseINCDEC { - g.materializePendingAcc(inst) - } - - // Peephole: ADD/SUB dst, N where dst == lhs and N ≤ 3 → INC/DEC dst × N. - // N=1: 1B/4T vs 4B/15T; N=2: 2B/8T vs 4B/15T; N=3: 3B/12T vs 4B/15T. - // N=4 would be 16T vs 15T — worse in T-states, skip and fall through to ALU. - if mnem == "ADD" && lhs == dst && !isSpill(dst) && dst != "F" { - if cv, ok := g.constVals[inst.Src[1]]; ok && cv >= 1 && cv <= 3 { - for range cv { - g.emitf(" INC %s", dst) - } - g.invalidate(dst) - return - } - } - if mnem == "SUB" && lhs == dst && !isSpill(dst) && dst != "F" { - if cv, ok := g.constVals[inst.Src[1]]; ok && cv >= 1 && cv <= 3 { - for range cv { - g.emitf(" DEC %s", dst) - } - g.invalidate(dst) - return - } - } - // 8-bit: accumulator must hold lhs. Emit LD A,lhs if needed. - // - // Special case: rhs is already A and lhs is elsewhere. - // Loading lhs into A would destroy the rhs value. - // ADD: commutative → just ADD A, lhs. - // SUB: A = lhs - A → NEG first (A = -A), then ADD A, lhs. - if rhs == "A" && lhs != "A" { - // The result overwrites A, which holds rhs: save rhs if it is - // still live (gcd's b = b - a keeps a live in A for the loop). - g.saveAccOperandIfLive(inst.Src[1], inst) - g.invalidate("A") // A about to hold a new result - switch mnem { - case "ADD": - g.emitf(" ADD A, %s", lhs) - case "AND", "OR", "XOR": - g.emit8ALU(mnem, lhs) - case "SUB": - g.emit(" NEG") - g.emitf(" ADD A, %s", lhs) - default: - g.comment(fmt.Sprintf("TODO: 8-bit %s %s, A → %s with A as rhs", mnem, lhs, dst)) - g.emitLDA(lhs) - g.emit8ALU(mnem, rhs) - } - if dst != "A" { - g.emitLD8(dst, "A") - g.setCopy(dst, "A") - } else { - g.pendingAccReg = inst.Dst - } - return - } - // Coalescing: if A already holds lhs or (commutative) rhs, skip LD A, reg. - isCommutative := mnem == "ADD" || mnem == "AND" || mnem == "OR" || mnem == "XOR" - if g.holdsValue("A", lhs) { - // Save-before-overwrite: if the lhs vreg in A is still live after this - // instruction (used by a later inst or terminator), save it to a scratch - // register. The ALU op will overwrite A with the result. - // Example: fib's r6=sub(r0,1) where r0 is still needed for r9=sub(r0,2). - g.saveAccOperandIfLive(inst.Src[0], inst) - // A == lhs already — skip LD A, lhs. - g.invalidate("A") - // Use immediate form if rhs is a known constant. - if cv, ok := g.constVals[inst.Src[1]]; ok { - g.emit8ALUImm(mnem, cv) - } else { - g.emit8ALU(mnem, rhs) - } - if dst != "A" { - g.emitLD8(dst, "A") - g.setCopy(dst, "A") - } else { - g.pendingAccReg = inst.Dst - } - return - } - if isCommutative && g.holdsValue("A", rhs) { - // A == rhs, commutative swap: emit OP A, lhs instead. - g.saveAccOperandIfLive(inst.Src[1], inst) - g.invalidate("A") - // Use immediate form if lhs is a known constant. - if cv, ok := g.constVals[inst.Src[0]]; ok { - g.emit8ALUImm(mnem, cv) - } else { - g.emit8ALU(mnem, lhs) - } - if dst != "A" { - g.emitLD8(dst, "A") - g.setCopy(dst, "A") - } else { - g.pendingAccReg = inst.Dst - } - return - } - - // Immediate peephole: AND/OR/XOR/ADD/SUB n — no register needed for rhs. - if cv, ok := g.constVals[inst.Src[1]]; ok { - if lhs != "A" { - // Before loading lhs into A, save any live vreg currently in A. - // Example: fib — after first CALL, A=fib(n-1) is needed later but - // computing n-2 requires loading n (from scratch) into A. - g.saveABeforeOverwrite(inst) - g.emitLDA(lhs) - } - switch mnem { - case "AND", "OR", "XOR", "SUB": - g.emitf(" %s %d", mnem, cv) - case "ADD": - g.emitf(" ADD A, %d", cv) - default: - g.emit8ALU(mnem, rhs) - } - g.invalidate("A") // ALU result is a new value — break old lhs alias - if dst != "A" { - g.emitLD8(dst, "A") - g.setCopy(dst, "A") - } else { - g.pendingAccReg = inst.Dst - } - return - } - // If A holds a live value from a different virtual reg (e.g. a function - // param allocated to A), and this ALU op will clobber A as scratch, - // we need to save it first. The pendingAccReg mechanism handles the - // common case, but there's also the case where a param (ClassAcc) lives - // in A and is used AFTER this instruction. In that case, relocate it - // to a scratch register before clobbering A. - if lhs != "A" { - g.saveABeforeOverwrite(inst) - // A will be clobbered (LD A,lhs then ALU). If A holds a value - // that's used later, save it. - g.saveAccIfLive(inst) - } - if lhs != "A" { - g.emitLDA(lhs) - } - g.emit8ALU(mnem, rhs) - g.invalidate("A") // ALU result is a new value — break old lhs alias - if dst != "A" { - g.emitLD8(dst, "A") - g.setCopy(dst, "A") - } else { - g.pendingAccReg = inst.Dst - } - } else { - if lhs == dst && dst != "A" && !isSpill(dst) { - if cv, ok := g.constVals[inst.Src[1]]; ok { - switch mnem { - case "OR": - mask := uint16(cv) - if bits.OnesCount16(mask) == 1 { - totalBit := bits.TrailingZeros16(mask) - target := selectBitReg(dst, totalBit/8) - if g.emitBitRegOp("SET", totalBit%8, target) { - g.invalidate(dst) - g.invalidate(target) - return - } - } - case "AND": - mask := uint16(cv) - if bits.OnesCount16(^mask) == 1 { - totalBit := bits.TrailingZeros16(^mask) - target := selectBitReg(dst, totalBit/8) - if g.emitBitRegOp("RES", totalBit%8, target) { - g.invalidate(dst) - g.invalidate(target) - return - } - } - } - } - } - - // 16-bit peephole: INC/DEC rr when adding/subtracting 1 in-place. - if rhsReg == inst.Src[1] && lhs == dst && !isSpill(dst) && dst != "F" { - if cv, ok := g.constVals[inst.Src[1]]; ok { - if mnem == "ADD" && cv == 1 { - g.emitf(" INC %s", dst) - g.invalidate(dst) // pair + its bytes all change - return - } - if mnem == "SUB" && cv == 1 { - g.emitf(" DEC %s", dst) - g.invalidate(dst) - return - } - } - } - // 16-bit ADD: ADD HL,rr (Z80 only has ADD HL,rr natively) - // SUB/AND/OR/XOR on 16-bit require workarounds; emit comment for now. - switch mnem { - case "ADD": - // Z80 only has ADD HL, rr. If dst is not HL we must route - // through HL. The common case is dst=DE, which can be handled - // cheaply with EX DE,HL: - // EX DE, HL ; HL ← lhs (was in DE), DE ← old HL - // ADD HL, ; rhs' adjusted for any EX swap - // EX DE, HL ; DE ← result, HL ← restored old value - if dst != "HL" { - // After EX DE,HL: the register formerly in DE is now in HL, - // and what was in HL is now in DE. - adjustedRhs := rhs - if lhs == "DE" && dst == "DE" { - g.emit(" EX DE, HL") - if rhs == "HL" { - adjustedRhs = "DE" // what was HL is now in DE after EX - } else if rhs == "DE" { - adjustedRhs = "HL" - } - if adjustedRhs == "IX" || adjustedRhs == "IY" { - g.emit(" PUSH BC") - g.emitMov("BC", adjustedRhs, w) - g.emit(" ADD HL, BC") - g.emit(" POP BC") - } else if isSimpleReg(adjustedRhs) && !isPairReg(adjustedRhs) { - pair := g.promote8toPair(adjustedRhs) - g.emitADDHL(pair) - } else if isSpill(adjustedRhs) { - g.loadSpill16("BC", adjustedRhs) - g.emit(" ADD HL, BC") - } else if adjustedRhs == "IX" || adjustedRhs == "IY" { - g.emit(" PUSH DE") - g.emitMov("DE", adjustedRhs, 16) - g.emit(" ADD HL, DE") - g.emit(" POP DE") - } else { - g.emitADDHL(adjustedRhs) - } - g.emit(" EX DE, HL") - g.invalidate("HL") - g.invalidate("DE") - break - } - // General fallback: move lhs to HL, add rhs, move result to dst. - // Track rhs renaming if lhs is moved from HL/DE. - if rhs == "HL" && lhs == "DE" { - adjustedRhs = "DE" // lhs/dst swap via EX - } - g.emitMov("HL", lhs, w) - if adjustedRhs == "IX" || adjustedRhs == "IY" { - g.emit(" PUSH DE") - g.emitMov("DE", adjustedRhs, w) - g.emit(" ADD HL, DE") - g.emit(" POP DE") - } else if isSimpleReg(adjustedRhs) && !isPairReg(adjustedRhs) { - pair := g.promote8toPair(adjustedRhs) - g.emitADDHL(pair) - } else if isSpill(adjustedRhs) { - g.loadSpill16("BC", adjustedRhs) - g.emit(" ADD HL, BC") - } else { - g.emitADDHL(adjustedRhs) - } - g.emitMov(dst, "HL", w) - g.invalidate(dst) - break - } - // Standard path: dst == HL. - if lhs != dst { - g.emitMov(dst, lhs, w) - } - // ADD HL, rr only accepts BC/DE/HL/SP — not IX/IY or 8-bit regs. - if rhs == "IX" || rhs == "IY" { - g.emit(" PUSH DE") - g.emitMov("DE", rhs, w) - g.emit(" ADD HL, DE") - g.emit(" POP DE") - } else if isSimpleReg(rhs) && !isPairReg(rhs) { - // 8-bit rhs: zero-extend to parent pair, then ADD HL, pair. - pair := g.promote8toPair(rhs) - g.emitADDHL(pair) - } else if isSpill(rhs) { - // LocMem spill: load to BC, then ADD HL, BC. - g.loadSpill16("BC", rhs) - g.emit(" ADD HL, BC") - } else { - // dst must be HL here; ADD HL, rr is the only valid 16-bit ADD. - g.emitADDHL(rhs) - } - g.invalidate(dst) - case "SUB": - // Z80 only has SBC HL, rr for 16-bit subtraction. - // When dst != "HL" we must route through HL; the only legal instruction is - // SBC HL, rr, so the general strategy is: - // EX DE,HL (put lhs in HL, save rhs in DE — only when both are DE/HL) - // OR A - // SBC HL, rhs' - // EX DE,HL (restore HL=rhs, put result in DE=dst) - if dst != "HL" && lhs == dst && rhs == "HL" && dst == "DE" { - // dst=DE, lhs=DE (0 or anything), rhs=HL. - // EX DE,HL swaps: HL=lhs, DE=rhs. SBC HL,DE = lhs-rhs. - // Second EX DE,HL: DE=result, HL=rhs restored. - g.emit(" EX DE, HL") - g.emit(" OR A") - g.emit(" SBC HL, DE") - g.emit(" EX DE, HL") - g.invalidate("HL") - g.invalidate("DE") - return - } - if lhs != dst { - // When lhs and dst are the HL/DE pair AND the rhs is the other member, - // use EX DE,HL (a true swap, 4T) to move lhs→dst while simultaneously - // placing the old dst contents into lhs's old location. - // This keeps rhs accessible in the swapped location. - // Note: emitMov(HL,DE) does byte-by-byte (LD H,D; LD L,E) — it preserves - // DE but does NOT produce the swap that the rhs update below relies on. - if (dst == "HL" && lhs == "DE") || (dst == "DE" && lhs == "HL") { - g.emit(" EX DE, HL") - // After EX DE,HL: whatever was in HL is now in DE, and vice versa. - switch rhs { - case "HL": - rhs = "DE" - case "DE": - rhs = "HL" - } - } else { - g.emitMov(dst, lhs, w) - } - } - g.emit(" OR A") // clear carry (1b/4T; SCF+CCF would be 2b/8T) - if dst == "DE" && (rhs == "HL" || rhs == "BC" || rhs == "DE") { - // Optimal path: EX DE,HL; SBC HL,rhs'; EX DE,HL (23T) - // After EX: HL=old_DE(lhs), DE=old_HL - adjustedRhs := rhs - if rhs == "HL" { - adjustedRhs = "DE" // old HL is now in DE - } else if rhs == "DE" { - adjustedRhs = "HL" // self-subtraction after EX: lhs is now HL - } - g.emit(" EX DE, HL") - g.emitSBCHL(adjustedRhs) - g.emit(" EX DE, HL") // result back to DE, HL restored - g.invalidate("HL") - g.invalidate("DE") - } else if dst != "HL" { - // General non-HL: route through HL via emitMov. - g.emitMov("HL", dst, w) - if rhs == "HL" { - rhs = dst - } - if isSpill(rhs) { - g.loadSpill16("BC", rhs) - rhs = "BC" - } - g.emitSBCHL(rhs) - g.emitMov(dst, "HL", w) - g.invalidate("HL") - } else { - if isSpill(rhs) { - g.loadSpill16("BC", rhs) - rhs = "BC" - } - g.emitSBCHL(rhs) - } - g.invalidate(dst) - case "OR", "AND", "XOR": - if lhs != dst { - g.emitMov(dst, lhs, w) - } - hi, lo := highByte(dst), lowByte(dst) - imm, constant := g.constVals[rhsReg] - for _, part := range []struct { - dst, src string - shift uint - }{{hi, highByte(rhs), 8}, {lo, lowByte(rhs), 0}} { - g.emitLDA(part.dst) - if constant { - g.emit8ALUImm(mnem, imm>>part.shift) - } else { - g.emit8ALU(mnem, part.src) - } - g.invalidate("A") - g.emitLD8(part.dst, "A") - } - g.invalidate(dst) - - default: - g.comment(fmt.Sprintf("TODO: 16-bit %s %s, %s → %s", mnem, lhs, rhs, dst)) - } - } -} - -// genBinOp32 emits 32-bit ADD or SUB using the Z80 EXX / shadow-pair technique. -// -// The 32-bit value is split across main and shadow register banks: -// -// LocDWord{"HL"}: main HL = lo16, shadow H'L' = hi16 (via EXX) -// -// For ADD (dst=HL, rhs=DE): -// -// ADD HL, DE ; lo ← lo(lhs) + lo(rhs), carry set if overflow (11T) -// EXX ; switch to shadow bank: HL←H'L', DE←D'E' (4T) -// ADC HL, DE ; hi ← hi(lhs) + hi(rhs) + carry (15T) -// EXX ; switch back to main bank (4T) -// -// For SUB (dst=HL, rhs=DE): -// -// AND A ; clear carry (4T) -// SBC HL, DE ; lo ← lo(lhs) - lo(rhs), borrow in carry (15T) -// EXX ; switch to shadow bank (4T) -// SBC HL, DE ; hi ← hi(lhs) - hi(rhs) - borrow (15T) -// EXX ; switch back (4T) -// -// EXX simultaneously swaps BC↔BC', DE↔DE', HL↔HL', so after EXX: -// - "HL" refers to the old H'L' (hi of lhs/dst) -// - "DE" refers to the old D'E' (hi of rhs) -// -// This works because both dst and rhs are LocDWord with the same name in both -// banks. The rhs DWord is restored to its original state after the final EXX. -// -// Limitation: Z80 only provides ADD/ADC/SBC with HL as destination. -// When dst≠HL, we temporarily use HL via emitMov32 (TODO: optimise). -func (g *z80cg) genBinOp32(mnem, dst, lhs, rhs string) { - // ADD is commutative: if rhs == dst and lhs != dst, swap so we don't clobber rhs. - if mnem == "ADD" && rhs == dst && lhs != dst { - lhs, rhs = rhs, lhs - } - - // SUB when lhs != dst and rhs == dst: moving lhs into dst would clobber rhs. - // Special case lhs=DE, dst=HL: use EX DE,HL to swap lo/hi words in place, - // then SBC computes original_lhs - original_rhs in HL. - if mnem == "SUB" && lhs != dst && rhs == dst { - if lhs == "DE" && dst == "HL" { - // EX DE,HL swaps main words; EXX + EX DE,HL + EXX swaps shadow words. - // After: HL = old DE = lhs_lo, DE = old HL = rhs_lo (and same in shadow). - g.emit(" EX DE, HL") - g.emit(" EXX") - g.emit(" EX DE, HL") - g.emit(" EXX") - g.emit(" AND A") - g.emit(" SBC HL, DE") // lhs_lo - rhs_lo - g.emit(" EXX") - g.emit(" SBC HL, DE") // lhs_hi - rhs_hi - borrow - g.emit(" EXX") - return - } - // General fallback: save rhs on stack, move lhs to dst, reload rhs to DE. - // Only HL is supported as dst for now. - g.comment(fmt.Sprintf("TODO: 32-bit SUB rhs==dst non-EX case: %s - %s → %s", lhs, rhs, dst)) - return - } - - // Ensure lhs is in dst before the operation. - if lhs != dst { - g.emitMov32(dst, lhs) - } - - switch mnem { - case "ADD": - if dst == "HL" { - // Native 32-bit add: ADD HL,rr / EXX / ADC HL,rr / EXX - g.emitADDHL(rhs) - g.emit(" EXX") - g.emitf(" ADC HL, %s", rhs) - g.emit(" EXX") - } else { - // Non-HL dst: save HL, use it as scratch, then move result. - g.comment(fmt.Sprintf("32-bit ADD via HL scratch: %s += %s", dst, rhs)) - g.emit(" PUSH HL") // save main HL - g.emit(" EXX") - g.emit(" PUSH HL") // save shadow HL - g.emit(" EXX") - // Transfer dst to HL. - g.emitMov32("HL", dst) - // Now add rhs (rhs was LocDWord; after emitMov32 we haven't clobbered it - // because emitMov32 uses PUSH/POP+EXX which preserves all non-HL regs). - g.emitADDHL(rhs) - g.emit(" EXX") - g.emitf(" ADC HL, %s", rhs) - g.emit(" EXX") - // Move result from HL to dst, then restore HL. - g.emitMov32(dst, "HL") - g.emit(" EXX") - g.emit(" POP HL") // restore shadow HL - g.emit(" EXX") - g.emit(" POP HL") // restore main HL - } - case "SUB": - if dst == "HL" { - g.emit(" AND A") // clear carry - g.emitSBCHL(rhs) - g.emit(" EXX") - g.emitSBCHL(rhs) - g.emit(" EXX") - } else { - g.comment(fmt.Sprintf("32-bit SUB via HL scratch: %s -= %s", dst, rhs)) - g.emit(" PUSH HL") - g.emit(" EXX") - g.emit(" PUSH HL") - g.emit(" EXX") - g.emitMov32("HL", dst) - g.emit(" AND A") - g.emitSBCHL(rhs) - g.emit(" EXX") - g.emitSBCHL(rhs) - g.emit(" EXX") - g.emitMov32(dst, "HL") - g.emit(" EXX") - g.emit(" POP HL") - g.emit(" EXX") - g.emit(" POP HL") - } - default: - g.comment(fmt.Sprintf("TODO: 32-bit %s %s, %s → %s", mnem, lhs, rhs, dst)) - } -} - -// saveAccIfLive checks whether A contains a live value from a virtual register -// that will be needed AFTER the current instruction. If so, it relocates -// A's value to a scratch register (via physOverride) so the ALU can use A freely. -func (g *z80cg) saveAccIfLive(inst *Inst) { - // Find which virtual reg currently lives in A. Prefer the lowest ID - // on ties, matching definition order and the allocator tie-break. - var accReg Reg - for _, r := range slices.Sorted(maps.Keys(g.ar.Locs)) { - loc := g.ar.Locs[r] - if loc.Kind == LocReg && loc.Name == "A" { - accReg = r - break - } - } - if accReg == NoReg { - return - } - // Check if physOverride already moved it away. - if p, ok := g.physOverride[accReg]; ok && p != "A" { - return - } - // Skip if this instruction defines or uses accReg (it'll be handled normally). - if accReg == inst.Dst || accReg == inst.Src[0] || accReg == inst.Src[1] { - return - } - // Check if accReg is used after this instruction in the block. - live := g.regsLiveAfterInst(inst) - if !live[accReg] { - return - } - // accReg is live in A and will be clobbered — save to scratch. - // Pick a scratch register that isn't used by this instruction. - scratch := g.pickScratch8(inst) - g.emitf(" LD %s, A", scratch) - g.physOverride[accReg] = scratch -} - -// reorderAccMoves swaps consecutive instruction pairs where a move→A is followed -// by an 8-bit ALU→nonA that will use A as scratch. By putting the ALU first, -// A is free for scratch use and the move loads A last (preserving it for the call). -func reorderAccMoves(insts []*Inst, ar *AllocResult) []*Inst { - result := make([]*Inst, len(insts)) - copy(result, insts) - - for i := 0; i < len(result)-1; i++ { - a, b := result[i], result[i+1] - - // Pattern: a = OpMove, dst in A; b = 8-bit ALU, dst NOT in A. - if a.Op != OpMove { - continue - } - aLoc := ar.Locs[a.Dst] - if aLoc.Name != "A" { - continue - } - - // b must be an 8-bit ALU op that needs A as scratch. - is8bitALU := (b.Op == OpAdd || b.Op == OpSub || b.Op == OpAnd || - b.Op == OpOr || b.Op == OpXor) && b.Ty != nil && b.Ty.Width() <= 8 - if !is8bitALU { - continue - } - bLoc := ar.Locs[b.Dst] - if bLoc.Name == "A" { - continue // b also targets A — no help from swapping - } - - // Safety: b must not READ a.Dst, and a must not READ b.Dst. - bReadsA := b.Src[0] == a.Dst || b.Src[1] == a.Dst - aReadsB := a.Src[0] == b.Dst - if bReadsA || aReadsB { - continue // data dependency — cannot swap - } - - // Swap. - result[i], result[i+1] = b, a - } - return result -} - -// emit8ALU emits an 8-bit ALU instruction with A as the implicit destination. -// MZA requires "ADD A, src" (two operands) but "SUB/AND/OR/XOR src" (one operand). -func (g *z80cg) emit8ALU(mnem, src string) { - // LocMem spill: Z80 ALU ops can't use absolute addresses directly. - // Load spill value into a scratch register first. - if isSpill(src) { - // Load from memory into A first, but A is the implicit LHS for 8-bit ALU. - // Must save A, load spill into scratch, restore A, then ALU. - // Simpler: use (HL) indirect — save HL, point to spill, ALU (HL), restore. - // Simplest: load spill into a scratch 8-bit reg. - // For now, load into A via memory, which works if LHS is already in A. - g.emit(" PUSH HL") - g.emitf(" LD HL, %s", src) - g.emitf(" LD H, (HL)") // H = value at spill address - switch mnem { - case "ADD", "ADC": - g.emitf(" %s A, H", mnem) - default: - g.emitf(" %s H", mnem) - } - g.emit(" POP HL") - g.invalidate("H") - return - } - // Width mismatch: pair reg as 8-bit ALU operand — use low byte. - if isPairReg(src) { - src = lowByte(src) - } - switch mnem { - case "ADD", "ADC": - g.emitf(" %s A, %s", mnem, src) - default: - g.emitf(" %s %s", mnem, src) - } -} - -// emit8ALUImm emits an 8-bit ALU instruction with an immediate constant operand. -func (g *z80cg) emit8ALUImm(mnem string, imm int64) { - imm8 := imm & 0xFF // Z80 8-bit ALU: truncate to byte - switch mnem { - case "ADD", "ADC": - g.emitf(" %s A, %d", mnem, imm8) - default: - g.emitf(" %s %d", mnem, imm8) - } -} - -// ── Shifts ──────────────────────────────────────────────────────────────────── - -func (g *z80cg) genShift(mnem string, inst *Inst) { - if _, constant := g.constVals[inst.Src[1]]; !constant { - g.genVariableShift(mnem, inst) - return - } - - w := inst.Ty.Width() - if w >= 24 { - g.genShift32(mnem, inst) - return - } - dst := g.loc(inst.Dst) - src := g.loc(inst.Src[0]) - if dst != src { - g.emitMov(dst, src, w) - } - // Shift count in Src[1] — Z80 shifts are always by 1 per instruction. - // Constant counts emit N copies; variable counts use genVariableShift. - // IMPORTANT: count=0 must emit zero shifts (identity), not default to 1. - count := int64(1) - if cv, ok := g.constVals[inst.Src[1]]; ok { - count = cv // cv=0 → no shifts emitted (identity) - } - if w == 16 { - // Z80 has no SLA/SRL/SRA on register pairs. - // SHL/SHR/SAR on IX/IY halves are also invalid (DD CB prefix - // always addresses (IX+d), never IXH/IXL directly). - // Route IX/IY shifts through HL. - shiftDst := dst - if isIXY(dst) { - g.emitMov("HL", dst, 16) - shiftDst = "HL" - } - hi := highByte(shiftDst) - lo := lowByte(shiftDst) - if mnem == "SLA" && count == 8 { - g.emitf(" LD %s, %s", hi, lo) - g.emitf(" LD %s, 0", lo) - } else { - for i := int64(0); i < count; i++ { - switch mnem { - case "SLA": - if shiftDst == "HL" { - g.emitf(" ADD HL, HL") - } else { - g.emitf(" SLA %s", lo) - g.emitf(" RL %s", hi) - } - case "SRL": - g.emitf(" SRL %s", hi) - g.emitf(" RR %s", lo) - case "SRA": - g.emitf(" SRA %s", hi) - g.emitf(" RR %s", lo) - } - } - } - if isIXY(dst) { - g.emitMov(dst, "HL", 16) - } - g.invalidate(dst) - if isIXY(dst) { - g.invalidate("HL") - } - return - } - for i := int64(0); i < count; i++ { - g.emitf(" %s %s", mnem, dst) - } - g.invalidate(dst) // shift modifies dst -} - -// genVariableShift uses a saved scratch pair for the counter. Loading it -// before moving the value also handles count registers overlapping the result. -func (g *z80cg) genVariableShift(mnem string, inst *Inst) { - dst, src := g.loc(inst.Dst), g.loc(inst.Src[0]) - w := inst.Ty.Width() - pair, counter := "", "" - for _, p := range []string{"BC", "DE", "HL"} { - overlaps := func(loc string) bool { return loc == p || loc == highByte(p) || loc == lowByte(p) } - if !overlaps(dst) && !overlaps(src) && !(isIXY(dst) && p == "HL") { - pair = p - counter = highByte(p) - break - } - } - if pair == "" { - panic("no scratch pair for variable shift") - } - g.emitf(" PUSH %s", pair) - g.emitLD8(counter, lowByte(g.loc(inst.Src[1]))) - if dst != src { - g.emitMov(dst, src, w) - } - shiftDst := dst - if isIXY(dst) { - g.emit(" PUSH HL") - g.emitMov("HL", dst, 16) - shiftDst = "HL" - } - if isIXYReg(dst) { - g.emit(" PUSH AF") - g.emitLDA(dst) - shiftDst = "A" - } - idx := g.trampIdx - g.trampIdx++ - // INC/DEC test zero without touching the shifted value or accumulator. - g.emitf(" INC %s", counter) - g.emitf(" DEC %s", counter) - g.emitf(" JR Z, .shift_done_%d", idx) - g.emitf(".shift_loop_%d:", idx) - if w >= 24 { - hi, lo := highByte(shiftDst), lowByte(shiftDst) - if mnem == "SLA" { - g.emitf(" SLA %s", lo) - g.emitf(" RL %s", hi) - g.emit(" EXX") - g.emitf(" RL %s", lo) - g.emitf(" RL %s", hi) - g.emit(" EXX") - } else { - g.emit(" EXX") - if w == 24 { - g.emitf(" %s %s", mnem, lo) - } else { - g.emitf(" %s %s", mnem, hi) - g.emitf(" RR %s", lo) - } - g.emit(" EXX") - g.emitf(" RR %s", hi) - g.emitf(" RR %s", lo) - } - } else if w == 16 { - hi, lo := highByte(shiftDst), lowByte(shiftDst) - if mnem == "SLA" { - g.emitf(" SLA %s", lo) - g.emitf(" RL %s", hi) - } else { - g.emitf(" %s %s", mnem, hi) - g.emitf(" RR %s", lo) - } - } else { - g.emitf(" %s %s", mnem, shiftDst) - } - g.emitf(" DEC %s", counter) - g.emitf(" JR NZ, .shift_loop_%d", idx) - g.emitf(".shift_done_%d:", idx) - if isIXY(dst) { - g.emitMov(dst, "HL", 16) - g.emit(" POP HL") - } - if isIXYReg(dst) { - g.emitLD8(dst, "A") - g.emit(" POP AF") - } - g.emitf(" POP %s", pair) - g.invalidate(dst) -} - -// genShift32 emits 24/32-bit shifts via the EXX shadow-pair technique. -// -// SHL by 1 (logical left): -// -// ADD HL, HL ; lo16 <<= 1, carry = old bit15 (11T) -// EXX ; switch to shadow (4T) -// ADC HL, HL ; hi16 <<= 1, carry in from lo (15T) -// EXX ; switch back (4T) = 34T per bit -// -// SHR by 1 (logical right): -// -// EXX ; switch to shadow -// SRL H ; hi16: H >>= 1 (zero-extend), carry = old bit0 (8T) -// RR L ; hi16: L = carry<<7 | L>>1 (8T) -// EXX ; back to main -// RR H ; lo16: H = carry<<7 | H>>1 (8T) -// RR L ; lo16: L = carry<<7 | L>>1 (8T) = 40T per bit -// -// SAR by 1 (arithmetic right, sign-extend): -// -// EXX / SRA H / RR L / EXX / RR H / RR L (same as SHR but SRA preserves sign) -func (g *z80cg) genShift32(mnem string, inst *Inst) { - dst := g.loc(inst.Dst) - src := g.loc(inst.Src[0]) - if dst != src { - g.emitMov32(dst, src) - } - - count := int64(1) - if cv, ok := g.constVals[inst.Src[1]]; ok { - count = cv - } - - hi := highByte(dst) // e.g. "H" for dst="HL" - lo := lowByte(dst) // e.g. "L" - - switch mnem { - case "SLA": - // Left shift: ADD HL,HL / EXX / ADC HL,HL / EXX (× count) - // Z80 only supports ADD HL,HL — not ADD DE,DE or ADD BC,BC. - // Route through HL if needed. - if dst != "HL" { - g.emitMov("HL", dst, 16) - } - for i := int64(0); i < count; i++ { - g.emit(" ADD HL, HL") - g.emit(" EXX") - g.emit(" ADC HL, HL") - g.emit(" EXX") - } - if dst != "HL" { - g.emitMov(dst, "HL", 16) - g.invalidate("HL") - } - case "SRL": - // Logical right shift: EXX / SRL H / RR L / EXX / RR H / RR L (× count) - for i := int64(0); i < count; i++ { - g.emit(" EXX") - if inst.Ty.Width() == 24 { - g.emitf(" SRL %s", lo) - } else { - g.emitf(" SRL %s", hi) - g.emitf(" RR %s", lo) - } - g.emit(" EXX") - g.emitf(" RR %s", hi) - g.emitf(" RR %s", lo) - } - case "SRA": - // Arithmetic right shift: sign bit preserved via SRA on high byte. - for i := int64(0); i < count; i++ { - g.emit(" EXX") - if inst.Ty.Width() == 24 { - g.emitf(" SRA %s", lo) - } else { - g.emitf(" SRA %s", hi) - g.emitf(" RR %s", lo) - } - g.emit(" EXX") - g.emitf(" RR %s", hi) - g.emitf(" RR %s", lo) - } - default: - g.comment(fmt.Sprintf("TODO: 32-bit shift %s", mnem)) - } - g.invalidate(dst) -} - -// ── Multiply ────────────────────────────────────────────────────────────────── - -// genMul emits 8-bit unsigned multiply via shift-and-add. -// Power-of-2 constants use N×(ADD A,A); small constants use LD tmp,A + adds. -// Variable or large constants use the __mul8 runtime. -func (g *z80cg) genMul(inst *Inst) { - dst := g.loc(inst.Dst) - lhs := g.loc(inst.Src[0]) - - if inst.Ty.Width() >= 24 { - g.genMul32(inst) - return - } - if inst.Ty.Width() > 8 { - g.genMul16(inst) - return - } - - // Preserve the accumulator value used after this multiply, including a - // loop block parameter. Relocate before saving runtime scratch pairs. - for r := range g.regsLiveAfterInst(inst) { - if r == inst.Dst || g.loc(r) != "A" { - continue - } - // regsLiveAfterInst includes terminator uses defined later in this - // block; those values are not in A yet. - definedLater := false - for _, later := range g.curBlock.Insts[g.curInstIdx+1:] { - if later.Dst == r { - definedLater = true - break - } - } - if !definedLater { - g.saveAccOperandIfLive(r, inst) - break - } - } - // Every byte multiply path must replace the old accumulator identity. - defer func() { - g.pendingAccReg = NoReg - if dst == "A" { - g.pendingAccReg = inst.Dst - } - }() - lhs = g.loc(inst.Src[0]) - cv, isConst := g.constVals[inst.Src[1]] - - if isConst { - switch cv { - case 0: - g.emit(" XOR A") - g.invalidate("A") - if dst != "A" { - g.emitLD8(dst, "A") - g.setCopy(dst, "A") - } - return - case 1: - if dst != lhs { - g.emitMov(dst, lhs, 8) - } - return - } - // Power-of-2: log2(cv) × (ADD A,A). - if cv > 0 && cv&(cv-1) == 0 { - shifts := 0 - for n := cv; n > 1; n >>= 1 { - shifts++ - } - if !g.holdsValue("A", lhs) { - g.emitLDA(lhs) - } - for i := 0; i < shifts; i++ { - g.emit(" ADD A, A") - } - g.invalidate("A") - if dst != "A" { - g.emitLD8(dst, "A") - g.setCopy(dst, "A") - } - return - } - // x*3 = x + x*2; x*5 = x + x*4; x*6 = x*2 + x*4 - // x*7 = x*8 - x; x*9 = x + x*8; x*10 = x*2 + x*8 - // x*12 = x*4 + x*8; x*15 = x*16 - x - if cv == 3 || cv == 5 || cv == 6 || cv == 7 || cv == 9 || cv == 10 || cv == 12 || cv == 15 { - if !g.holdsValue("A", lhs) { - g.emitLDA(lhs) - } - tmp := g.pickScratch8(inst) - switch cv { - case 3: // x + x*2 - g.emitf(" LD %s, A", tmp) // tmp = x - g.emit(" ADD A, A") // A = x*2 - g.emitf(" ADD A, %s", tmp) // A = x*3 - case 5: // x + x*4 - g.emitf(" LD %s, A", tmp) - g.emit(" ADD A, A") - g.emit(" ADD A, A") // A = x*4 - g.emitf(" ADD A, %s", tmp) // A = x*5 - case 6: // x*2 + x*4 - g.emit(" ADD A, A") // A = x*2 - g.emitf(" LD %s, A", tmp) // tmp= x*2 - g.emit(" ADD A, A") // A = x*4 - g.emitf(" ADD A, %s", tmp) // A = x*6 - case 7: // x*8 - x - g.emitf(" LD %s, A", tmp) // tmp = x - g.emit(" ADD A, A") - g.emit(" ADD A, A") - g.emit(" ADD A, A") // A = x*8 - g.emitf(" SUB %s", tmp) // A = x*7 - case 9: // x + x*8 - g.emitf(" LD %s, A", tmp) - g.emit(" ADD A, A") - g.emit(" ADD A, A") - g.emit(" ADD A, A") // A = x*8 - g.emitf(" ADD A, %s", tmp) // A = x*9 - case 10: // x*2 + x*8 - g.emit(" ADD A, A") // A = x*2 - g.emitf(" LD %s, A", tmp) // tmp= x*2 - g.emit(" ADD A, A") - g.emit(" ADD A, A") // A = x*8 - g.emitf(" ADD A, %s", tmp) // A = x*10 - case 12: // x*4 + x*8 - g.emit(" ADD A, A") - g.emit(" ADD A, A") // A = x*4 - g.emitf(" LD %s, A", tmp) // tmp= x*4 - g.emit(" ADD A, A") // A = x*8 - g.emitf(" ADD A, %s", tmp) // A = x*12 - case 15: // x*16 - x - g.emitf(" LD %s, A", tmp) // tmp = x - g.emit(" ADD A, A") - g.emit(" ADD A, A") - g.emit(" ADD A, A") - g.emit(" ADD A, A") // A = x*16 - g.emitf(" SUB %s", tmp) // A = x*15 - } - g.invalidate("A") - if dst != "A" { - g.emitLD8(dst, "A") - g.setCopy(dst, "A") - } - return - } - } - - // General constant or variable multiply: use runtime __mul8 routine. - // __mul8 clobbers A/B/C/D/E/F. Save pairs before ABI setup so - // live operands (including a multiplier in B) survive the call. - g.emit(" PUSH BC") - g.emit(" PUSH DE") - // Set up both runtime operands as a parallel copy: the multiplier may - // currently be in A, and loading the multiplicand first would destroy it. - if isConst { - g.emitLDA(lhs) - g.emitf(" LD B, %d", cv&0xFF) - } else { - g.emitParallelCopy([]parallelCopy{ - {srcName: lhs, dstName: "A", ty: TyU8}, - {srcName: g.loc(inst.Src[1]), dstName: "B", ty: TyU8}, - }) - } - g.emit(" CALL __mul8") - g.emit(" POP DE") - g.emit(" POP BC") - g.needsMul8 = true - g.invalidate("A") - g.invalidate("F") - if dst != "A" { - g.emitLD8(dst, "A") - g.setCopy(dst, "A") - } -} - -// genMul16 emits 16-bit multiply. -// -// Constants: -// -// 0: LD H,0 / LD L,0 -// 1: no-op -// 2^n: n × ADD HL,HL (7T per shift) -// 3,5,6,9: PUSH HL / POP BC + shift+add sequence -// -// Variable (software shift-and-add, Russian-peasant LSB-first, 16 iterations): -// -// BC = multiplicand (lhs) -// DE = multiplier (rhs) -// HL = result = 0 -// A = 16 (iteration counter — DEC A / JR NZ preserves SRL carry) -// loop: -// SRL D / RR E → DE >>= 1, old bit0 → carry -// JR NC, no_add → skip if bit was 0 -// ADD HL, BC → result += multiplicand (carry from ADD is ignored by next SRL) -// no_add: -// SLA C / RL B → BC <<= 1 (next power-of-2 multiplicand) -// DEC A / JR NZ loop -// -// SRL D is independent of incoming carry, so ADD HL,BC's carry does not corrupt -// the extraction. Total: ~320T worst case. -func (g *z80cg) genMul16(inst *Inst) { - dst := g.loc(inst.Dst) - lhs := g.loc(inst.Src[0]) - - cv, isConst := g.constVals[inst.Src[1]] - regInfo := collectRegInfo(g.fn) - // Widen byte operands according to their source type. Preserve A and its - // flags: either may still hold a live value (including the other operand). - extendByte := func(src Reg, low, high string) { - if IsSigned(regInfo[src].Ty) { - g.emit(" PUSH AF") - g.emitLD8("A", low) - g.emit(" RLA") - g.emit(" SBC A, A") - g.emitLD8(high, "A") - g.emit(" POP AF") - } else { - g.emitf(" LD %s, 0", high) - } - } - - // 16-bit multiply needs HL for ADD HL,rr. - // A live lhs must survive too when the result is allocated elsewhere. - savedHL := false - if dst != "HL" { - // HL may hold a live value (e.g. a constant computed earlier). - // Save it on stack; restore after multiply into dst. - g.emit(" PUSH HL") - savedHL = true - } - - // Move lhs into HL for the multiply. - if lhs != "HL" { - if isIXY(lhs) { - // IX/IY → HL: PUSH/POP (byte-copy invalid due to DD prefix). - g.emitf(" PUSH %s", lhs) - g.emit(" POP HL") - } else if isIXYReg(lhs) { - // IXH/IXL/IYH/IYL: widen the byte to HL. - g.emitMovViaAltA("L", lhs) - extendByte(inst.Src[0], "L", "H") - } else if isSimpleReg(lhs) && !isPairReg(lhs) { - // Widen a byte operand promoted by the expression. - g.emitLD8("L", lhs) - extendByte(inst.Src[0], "L", "H") - } else if isSpill(lhs) { - g.loadSpill16("HL", lhs) - } else { - g.emitf(" LD H, %s", highByte(lhs)) - g.emitLD8("L", lowByte(lhs)) - } - g.invalidate("HL") - } - - // mul16Epilogue moves the result from HL to dst and restores saved HL. - mul16Epilogue := func() { - if dst == "HL" { - // already there - } else if isIXY(dst) { - g.emit(" PUSH HL") - g.emitf(" POP %s", dst) - } else if isSpill(dst) { - g.storeSpill16(dst, "HL") - g.invalidate("HL") - } else { - g.emitLD8(highByte(dst), "H") - g.emitLD8(lowByte(dst), "L") - } - if savedHL { - g.emit(" POP HL") // restore pre-mul HL - } - g.invalidate(dst) - if savedHL { - g.invalidate("HL") - } - } - - if isConst { - switch cv { - case 0: - g.emit(" LD H, 0") - g.emit(" LD L, 0") - mul16Epilogue() - return - case 1: - mul16Epilogue() - return - } - // Fix A: byte-boundary shift — LD H,L / LD L,0 is cheaper than ADD HL,HL chains. - if cv >= 256 && cv&(cv-1) == 0 { - g.emit(" LD H, L") - g.emit(" LD L, 0") - for n := cv >> 8; n > 1; n >>= 1 { - g.emit(" ADD HL, HL") - } - mul16Epilogue() - return - } - if cv > 0 && cv&(cv-1) == 0 { // power-of-2: log2(cv) × ADD HL,HL - for n := cv; n > 1; n >>= 1 { - g.emit(" ADD HL, HL") - } - mul16Epilogue() - return - } - // Small multiples via PUSH/POP BC save + shift sequences. - if cv == 3 || cv == 5 || cv == 6 || cv == 9 { - g.emit(" PUSH BC") // preserve values live across the scratch pair - g.emit(" PUSH HL") // save x → BC - g.emit(" POP BC") // BC = x (17T) - switch cv { - case 3: // x*2 + x - g.emit(" ADD HL, HL") - g.emit(" ADD HL, BC") - case 5: // x*4 + x - g.emit(" ADD HL, HL") - g.emit(" ADD HL, HL") - g.emit(" ADD HL, BC") - case 6: // x*4 + x*2 - g.emit(" ADD HL, HL") - g.emit(" PUSH HL") - g.emit(" POP BC") - g.emit(" ADD HL, HL") - g.emit(" ADD HL, BC") - case 9: // x*8 + x - g.emit(" ADD HL, HL") - g.emit(" ADD HL, HL") - g.emit(" ADD HL, HL") - g.emit(" ADD HL, BC") - } - g.emit(" POP BC") - mul16Epilogue() - return - } - } - - // Variable or unsupported constant: software shift-and-add loop. - // Register layout: - // BC = multiplicand (lhs, copied from HL) - // DE = multiplier (rhs) - // HL = result = 0 - // A = iteration counter (16) - rhs := g.loc(inst.Src[1]) - loopLbl := fmt.Sprintf(".%s_m16_%d_lp", sanitizeIdent(g.fn.Name), int(inst.Dst)) - skipLbl := fmt.Sprintf(".%s_m16_%d_sk", sanitizeIdent(g.fn.Name), int(inst.Dst)) - g.comment(fmt.Sprintf("mul16 %s * %s → HL (~320T, 16-iter shift-and-add)", lhs, rhs)) - // Save AF and DE: the multiply uses A as a loop counter and clobbers DE by - // shifting it 16 times. Any live variable in A, D, or E would be corrupted. - g.emit(" PUSH AF") // preserve A (= possible live variable) and F - g.emit(" PUSH DE") // preserve D and E across multiply - // Load multiplier into DE BEFORE overwriting BC with the multiplicand. - // (rhs may be in C/B/BC which gets clobbered by LD B,H; LD C,L below.) - if isConst { - // OpMul constants have no physical load (deadConstsForFunc). - // Strength reduction consumes cv directly; so must this fallback. - g.emitf(" LD DE, %d", cv&0xFFFF) - g.invalidate("DE") - } else if rhs != "DE" { - if isSpill(rhs) { - g.loadSpill16("DE", rhs) - } else if isIXY(rhs) { - // IX/IY → DE: safe byte-copy (D/E not substituted by DD/FD). - g.emitf(" LD D, %s", highByte(rhs)) - g.emitf(" LD E, %s", lowByte(rhs)) - } else if isPairReg(rhs) { - g.emitf(" LD D, %s", highByte(rhs)) - g.emitf(" LD E, %s", lowByte(rhs)) - } else if isIXYReg(rhs) { - // IXH/IXL half-reg: widen to DE. - g.emitMovViaAltA("E", rhs) - extendByte(inst.Src[1], "E", "D") - } else { - // 8-bit rhs: widen into DE. - g.emitf(" LD E, %s", rhs) - extendByte(inst.Src[1], "E", "D") - } - g.invalidate("DE") - } - g.emit(" PUSH BC") // save BC across multiply - g.emit(" LD B, H") - g.emit(" LD C, L") // BC = multiplicand (lhs) - g.emit(" LD H, 0") - g.emit(" LD L, 0") // HL = result = 0 - g.emit(" LD A, 16") // A = bit counter - g.emitf("%s:", loopLbl) - g.emit(" SRL D") // DE >>= 1 (logical right shift, bit0 → carry) - g.emit(" RR E") - g.emitf(" JRS NC, %s", skipLbl) - g.emit(" ADD HL, BC") // result += multiplicand (current bit weight) - g.emitf("%s:", skipLbl) - g.emit(" SLA C") // BC <<= 1 (multiplicand doubles) - g.emit(" RL B") - g.emit(" DEC A") // counter-- (does not affect carry) - g.emitf(" JRS NZ, %s", loopLbl) - g.emit(" POP BC") // restore BC - g.emit(" POP DE") // restore old DE (live variables in D/E preserved) - g.emit(" POP AF") // restore old A (live variables in A preserved) - mul16Epilogue() - g.invalidate("BC") -} - -// genMul32 emits 24/32-bit multiply by a constant via SHL-and-add. -// -// Power-of-2 constants: log2(cv) × SHL32 (ADD HL,HL / EXX / ADC HL,HL / EXX per bit). -// Small constants (3,5,6,9): LD+saves using emitMov32 for the partial product. -// General constants: decompose into Σ 2^k terms (binary method). -// Variable multiplier: TODO (software loop). -func (g *z80cg) genMul32(inst *Inst) { - dst := g.loc(inst.Dst) - lhs := g.loc(inst.Src[0]) - - // Emit one 32-bit left-shift-by-1 on the register pair p. - // Z80 only has ADD HL,HL — route through HL if p != HL. - shl1 := func(p string) { - if p != "HL" { - g.emitMov("HL", p, 16) - } - g.emit(" ADD HL, HL") - g.emit(" EXX") - g.emit(" ADC HL, HL") - g.emit(" EXX") - if p != "HL" { - g.emitMov(p, "HL", 16) - g.invalidate("HL") - } - } - - cv, isConst := g.constVals[inst.Src[1]] - - if !isConst { - rhs := g.loc(inst.Src[1]) - g.comment(fmt.Sprintf("TODO: 32-bit variable mul %s * %s → %s", lhs, rhs, dst)) - return - } - - // Ensure lhs is in dst before operating. - if dst != lhs { - g.emitMov32(dst, lhs) - } - - switch cv { - case 0: - // dst = 0: load zero constant. - if isSpill(dst) { - g.emit(" LD HL, 0") - g.storeSpill16(dst, "HL") - g.invalidate("HL") - } else { - g.emitf(" LD %s, 0", dst) - g.emit(" EXX") - g.emitf(" LD %s, 0", dst) - g.emit(" EXX") - } - g.invalidate(dst) - return - case 1: - // dst = lhs — already done by move above. - return - } - - // Power-of-2: log2(cv) left shifts. - if cv > 0 && cv&(cv-1) == 0 { - shifts := 0 - for n := cv; n > 1; n >>= 1 { - shifts++ - } - for i := 0; i < shifts; i++ { - shl1(dst) - } - g.invalidate(dst) - return - } - - // Binary decomposition: dst = Σ (dst_orig << k) for each set bit k in cv. - // Uses a saved copy of the original value to accumulate. - // Only valid when dst is HL (has ADD/ADC). For others, fall through to TODO. - if dst == "HL" && cv > 0 { - // Save original in DE (push to stack if DE is occupied). - g.emit(" PUSH DE") // save main DE - g.emit(" EXX") - g.emit(" PUSH DE") // save shadow DE - g.emit(" EXX") - // Copy lhs (already in HL/H'L') to DE/D'E'. - g.emitMov32("DE", "HL") - // Accumulate: acc = 0, then for each set bit k add (orig << k). - // Clear HL for accumulation. - g.emitf(" LD HL, 0") - g.emit(" EXX") - g.emitf(" LD HL, 0") - g.emit(" EXX") - shift := 0 - for bit := int64(0); bit < 32; bit++ { - if cv&(1< 0 && k&(k-1) == 0 && w <= 16 && ((inst.Op != OpSDiv && inst.Op != OpSMod) || k < int64(1)<<(w-1)) { - signed := inst.Op == OpSDiv || inst.Op == OpSMod - if signed && g.nonNegative(inst.Src[0], w, make(map[Reg]bool)) { - signed = false - } - g.genPow2DivMod(inst, k, signed) - return - } - - if w <= 8 { - g.genDivMod8(inst) - } else if w <= 16 { - g.genDivMod16(inst) - } else { - g.comment(fmt.Sprintf("TODO: %d-bit div/mod", w)) - } -} - -// genDivMod8 — 8-bit unsigned division via shift-and-subtract (restoring). -// -// Based on DIVU111 from Dark / X-Trade, Spectrum Expert #01 (1997): -// -// Input: B = dividend, C = divisor -// Output: B = quotient, A = remainder -// Cost: 236–244 T-states -// -// Algorithm: binary long division. Shift dividend left through carry -// into accumulator (A). Try subtracting divisor; if it fits, set the -// quotient bit via INC B (bit 0 is free after SLA B). -func (g *z80cg) genDivMod8(inst *Inst) { - dst := g.loc(inst.Dst) - lhs := g.loc(inst.Src[0]) - rhs := g.loc(inst.Src[1]) - idx := g.trampIdx - g.trampIdx++ - - isSigned := inst.Op == OpSDiv || inst.Op == OpSMod - wantMod := inst.Op == OpMod || inst.Op == OpSMod - - if isSigned { - // Stage both operands before assigning B/C, including swapped allocations. - g.emit(" PUSH AF") - g.emitLDA(lowByte(lhs)) - g.emit(" PUSH AF") - if lowByte(rhs) == "A" { - // Original A is the divisor; loading the dividend must not replace it. - g.emit(" POP BC") // B = staged dividend - g.emit(" POP AF") // A = original divisor - g.emit(" LD C, A") - } else { - g.emitLDA(lowByte(rhs)) - g.emit(" LD C, A") - g.emit(" POP AF") - g.emit(" LD B, A") - g.emit(" POP AF") - } - } else { - // Setup: B = dividend, C = divisor. - // Width guard: lhs/rhs might be a 16-bit pair (const allocated to HL/DE). - if lhs == "B" { - // already there - } else if isPairReg(lhs) { - g.emitf(" LD B, %s", lowByte(lhs)) - } else if isSpill(lhs) { - g.loadSpill8("B", lhs) - } else if lhs == "A" { - g.emit(" LD B, A") - } else { - g.emitf(" LD B, %s", lhs) - } - if rhs == "C" { - // already there - } else if isPairReg(rhs) { - g.emitf(" LD C, %s", lowByte(rhs)) - } else if isSpill(rhs) { - g.loadSpill8("C", rhs) - } else { - g.emitf(" LD C, %s", rhs) - } - - } - if isSigned { - g.emit(" LD A, B") - if !wantMod { - g.emit(" XOR C") - } - g.emit(" PUSH AF") // result sign, separate from division workspace - g.emit(" BIT 7, B") - g.emitf(" JR Z, .div8_abs_l_%d", idx) - g.emit(" LD A, B") - g.emit(" NEG") - g.emit(" LD B, A") - g.emitf(".div8_abs_l_%d:", idx) - g.emit(" BIT 7, C") - g.emitf(" JR Z, .div8_abs_r_%d", idx) - g.emit(" LD A, C") - g.emit(" NEG") - g.emit(" LD C, A") - g.emitf(".div8_abs_r_%d:", idx) - } - - g.emit(" XOR A") // clear accumulator (remainder workspace) - g.emit(" LD D, 8") // 8 bits to process - g.emitf(".div8_%d:", idx) - g.emit(" SLA B") // shift dividend left — MSB → CF - g.emit(" RLA") // shift CF into accumulator - g.emit(" CP C") // try subtract divisor - g.emitf(" JR C, .div8_skip_%d", idx) // A < C → skip - g.emit(" SUB C") // subtract divisor - g.emit(" INC B") // set quotient bit 0 (free after SLA) - g.emitf(".div8_skip_%d:", idx) - g.emit(" DEC D") - g.emitf(" JR NZ, .div8_%d", idx) - // Result: B = quotient, A = remainder. - - if isSigned { - if wantMod { - g.emit(" LD B, A") - } - g.emit(" POP AF") - g.emit(" BIT 7, A") - g.emit(" LD A, B") - g.emitf(" JR Z, .div8_sign_%d", idx) - g.emit(" NEG") - g.emitf(".div8_sign_%d:", idx) - g.emit(" LD B, A") - } - - if wantMod { - if dst != "A" { - g.emitLD8(dst, "A") - g.setCopy(dst, "A") - } - } else { - if dst == "A" { - g.emit(" LD A, B") - } else if dst != "B" { - g.emitf(" LD %s, B", dst) - } - g.setCopy(dst, "B") - } - g.invalidate("A") - g.invalidate("B") - g.invalidate("D") - g.invalidate("F") -} - -// genDivMod16 — 16-bit unsigned division via shift-and-subtract long division. -// -// HL / DE → HL=quotient, BC=remainder -// -// Classic Z80 restoring division (16 iterations): -// -// BC = 0 (remainder), A = 16 (counter) -// loop: -// ADD HL, HL ; shift dividend left, MSB → CF -// RL C; RL B ; shift CF into remainder -// PUSH HL; LD H,B; LD L,C; SBC HL,DE ; trial: remainder - divisor -// if no borrow: BC = HL (accept), POP HL, SET 0,L (quotient bit = 1) -// if borrow: POP HL (quotient bit stays 0, BC unchanged) -// DEC A; JR NZ loop -func (g *z80cg) genDivMod16(inst *Inst) { - dst := g.loc(inst.Dst) - lhs := g.loc(inst.Src[0]) - rhs := g.loc(inst.Src[1]) - idx := g.trampIdx - g.trampIdx++ - - wantMod := inst.Op == OpMod || inst.Op == OpSMod - isSigned := inst.Op == OpSDiv || inst.Op == OpSMod - - // Stage both inputs without changing any source register, including spills - // and index halves. The unsigned path needs the same parallel-move safety. - g.pushWord(lhs) - g.pushWord(rhs) - g.emit(" POP DE") - g.emit(" POP HL") - g.invalidate("DE") - g.invalidate("HL") - - if isSigned { - g.emit(" LD A, H") - if !wantMod { - g.emit(" XOR D") - } - g.emit(" PUSH AF") - for _, pair := range []string{"HL", "DE"} { - hi, lo := highByte(pair), lowByte(pair) - g.emitf(" BIT 7, %s", hi) - g.emitf(" JR Z, .div16_abs_%s_%d", pair, idx) - g.emit(" XOR A") - g.emitf(" SUB %s", lo) - g.emitf(" LD %s, A", lo) - g.emit(" SBC A, A") - g.emitf(" SUB %s", hi) - g.emitf(" LD %s, A", hi) - g.emitf(".div16_abs_%s_%d:", pair, idx) - } - } - - // BC = 0 (remainder accumulator), A = 16 (iteration counter). - g.emit(" LD BC, 0") - g.emit(" LD A, 16") - - // Main loop. - g.emitf(".div16_%d:", idx) - g.emit(" ADD HL, HL") // shift dividend left; MSB → CF - g.emit(" RL C") // shift CF into remainder (BC) - g.emit(" RL B") - g.emit(" PUSH HL") // save quotient-in-progress - g.emit(" LD H, B") // HL = BC (remainder) - g.emit(" LD L, C") - g.emit(" OR A") // clear CF for SBC - g.emit(" SBC HL, DE") // trial subtract: remainder - divisor - g.emitf(" JR C, .div16_skip_%d", idx) // borrow → remainder < divisor - // Accept: remainder = HL (after subtract). - g.emit(" LD B, H") - g.emit(" LD C, L") - g.emit(" POP HL") // restore quotient - g.emit(" SET 0, L") // this quotient bit = 1 - g.emit(" DEC A") - g.emitf(" JR NZ, .div16_%d", idx) - g.emitf(" JR .div16_done_%d", idx) - // Skip: remainder stays, quotient bit stays 0. - g.emitf(".div16_skip_%d:", idx) - g.emit(" POP HL") // restore quotient (bit 0 = 0) - g.emit(" DEC A") - g.emitf(" JR NZ, .div16_%d", idx) - - g.emitf(".div16_done_%d:", idx) - // Result: HL = quotient, BC = remainder. - if isSigned { - hi, lo := "H", "L" - if wantMod { - hi, lo = "B", "C" - } - g.emit(" POP AF") - g.emit(" BIT 7, A") - g.emitf(" JR Z, .div16_sign_%d", idx) - g.emit(" XOR A") - g.emitf(" SUB %s", lo) - g.emitf(" LD %s, A", lo) - g.emit(" SBC A, A") - g.emitf(" SUB %s", hi) - g.emitf(" LD %s, A", hi) - g.emitf(".div16_sign_%d:", idx) - } - - if wantMod { - // Want remainder (BC) → move to dst. - if dst == "HL" { - g.emit(" LD H, B") - g.emit(" LD L, C") - } else if dst == "BC" { - // already there - } else if isSpill(dst) { - g.emit(" LD H, B") - g.emit(" LD L, C") - g.storeSpill16(dst, "HL") - } else if isIXY(dst) { - g.emit(" LD H, B") - g.emit(" LD L, C") - g.emit(" PUSH HL") - g.emitf(" POP %s", dst) - } else { - g.emitLD8(highByte(dst), "B") - g.emitLD8(lowByte(dst), "C") - } - } else { - // Want quotient (HL) → move to dst if needed. - if dst == "HL" { - // already there - } else if isSpill(dst) { - g.storeSpill16(dst, "HL") - } else if isIXY(dst) { - // HL→IX/IY: byte-copy invalid (DD NOP). Use PUSH/POP. - g.emit(" PUSH HL") - g.emitf(" POP %s", dst) - } else { - g.emitLD8(highByte(dst), "H") - g.emitLD8(lowByte(dst), "L") - } - } - g.invalidate("A") - g.invalidate("BC") - g.invalidate("DE") - g.invalidate("HL") - g.invalidate("F") -} - -// ── Type conversions ────────────────────────────────────────────────────────── - -func (g *z80cg) genExt(inst *Inst) { - dst := g.loc(inst.Dst) - src := g.loc(inst.Src[0]) - srcW := inst.SrcTy.Width() - dstW := inst.Ty.Width() - if inst.SrcTy == TyBool && dstW == 8 && src == "F" { - // A comparison in F is a predicate, not a byte. Materialize its - // actual condition as 0/1; carry alone is wrong for GE/LE/EQ. - g.emitFlagPredicateByte(g.condCode(g.fn, inst.Src[0])) - if dst != "A" { - g.emitLD8(dst, "A") - } - g.pendingFlagReg = NoReg - return - } - - if srcW == 8 && dstW == 16 { - // Zero-extend u8 → u16. - // dst should be a 16-bit reg (HL/DE/BC/IX/IY). - // Most natural: put source in low byte, zero high byte. - lo := lowByte(dst) - hi := highByte(dst) - if src != lo { - if (isIXYReg(src) && (lo == "H" || lo == "L")) || - ((src == "H" || src == "L") && isIXYReg(lo)) { - g.emitMovViaAltA(lo, src) - } else { - g.emitLD8(lo, src) - } - } - g.emitf(" LD %s, 0", hi) - return - } - // Fallback. - g.comment(fmt.Sprintf("TODO: ext %s→%s %s→%s", inst.SrcTy, inst.Ty, src, dst)) -} - -// emitFlagPredicateByte writes a canonical 0/1 bool to A from the current -// condition flags, including two-test predicates (C or Z, NC and NZ). -func (g *z80cg) emitFlagPredicateByte(cc string) { - idx := g.trampIdx - g.trampIdx++ - trueLabel := fmt.Sprintf(".bool_true_%d", idx) - falseLabel := fmt.Sprintf(".bool_false_%d", idx) - doneLabel := fmt.Sprintf(".bool_done_%d", idx) - switch cc { - case "CLE": - g.emitf(" JP C, %s", trueLabel) - g.emitf(" JP Z, %s", trueLabel) - case "CGT": - g.emitf(" JP Z, %s", falseLabel) - g.emitf(" JP C, %s", falseLabel) - g.emitf(" JP %s", trueLabel) - default: - g.emitf(" JP %s, %s", cc, trueLabel) - } - g.emitf("%s:", falseLabel) - g.emit(" LD A, 0") - g.emitf(" JP %s", doneLabel) - g.emitf("%s:", trueLabel) - g.emit(" LD A, 1") - g.emitf("%s:", doneLabel) - g.invalidate("A") -} - -func (g *z80cg) genSext(inst *Inst) { - // Sign extension uses A as scratch, but the narrow source may remain live. - g.emit(" PUSH AF") - g.genSextValue(inst) - g.emit(" POP AF") - g.invalidate("A") - g.invalidate(g.loc(inst.Dst)) -} - -func (g *z80cg) genSextValue(inst *Inst) { - dst := g.loc(inst.Dst) - src := g.loc(inst.Src[0]) - srcW := inst.SrcTy.Width() - dstW := inst.Ty.Width() - - if srcW == 24 && dstW == 32 { - if src != dst { - g.emitMov32(dst, src) - } - g.emit(" EXX") - g.emitLDA(lowByte(dst)) - g.emit(" RLCA") - g.emit(" SBC A, A") - g.emitLD8(highByte(dst), "A") - g.emit(" EXX") - g.invalidate("A") - g.invalidate(dst) - return - } - - if dstW >= 24 && (srcW == 8 || srcW == 16) { - hi, lo := highByte(dst), lowByte(dst) - if srcW == 8 { - if src != "A" { - g.emitLDA(src) - } - g.emitLD8(lo, "A") - g.emit(" RLCA") - g.emit(" SBC A, A") - g.emitLD8(hi, "A") - } else { - if src != dst { - g.emitMov(dst, src, 16) - } - g.emitLDA(hi) - g.emit(" RLCA") - g.emit(" SBC A, A") - } - g.emit(" EXX") - g.emitLD8(lo, "A") - if dstW == 24 { - g.emitf(" LD %s, 0", hi) - } else { - g.emitLD8(hi, "A") - } - g.emit(" EXX") - g.invalidate("A") - g.invalidate(dst) - return - } - - if srcW == 8 && dstW == 16 { - // Sign-extend u8 → u16 via A. - lo := lowByte(dst) - hi := highByte(dst) - if src != "A" { - g.emitLDA(src) - } - if lo != "A" { - g.emitLD8(lo, "A") - } - // Propagate sign bit into high byte: RLCA; SBC A,A gives 0x00 or 0xFF. - g.emit(" RLCA") - g.emit(" SBC A, A") - if hi != "A" { - g.emitLD8(hi, "A") - } - return - } - g.comment(fmt.Sprintf("TODO: sext %s→%s %s→%s", inst.SrcTy, inst.Ty, src, dst)) -} - -// ── Compare ─────────────────────────────────────────────────────────────────── - -// flagStillNeeded reports whether pendingFlagReg is used as a source -// in any instruction AFTER upcomingInst in the current block. -func (g *z80cg) flagStillNeeded(upcomingInst *Inst) bool { - if g.curBlock == nil { - return false - } - seen := false - for _, inst := range g.curBlock.Insts { - if inst == upcomingInst { - seen = true - continue - } - if !seen { - continue - } - for _, s := range inst.Src { - if s == g.pendingFlagReg { - return true - } - } - } - return false -} - -// accStillNeeded reports whether pendingAccReg is used as a source -// in any instruction AFTER upcomingInst in the current block, or by -// the block's terminal (e.g. passed as a block arg to a successor). -func (g *z80cg) accStillNeeded(upcomingInst *Inst) bool { - if g.curBlock == nil { - return false - } - seen := false - for _, inst := range g.curBlock.Insts { - if inst == upcomingInst { - seen = true - continue - } - if !seen { - continue - } - for _, s := range inst.Src { - if s == g.pendingAccReg { - return true - } - } - } - // Also check the block terminal — block params are passed via term args - // (e.g. TermBrIf passes acc through to the loop body block). - if g.curBlock.Term != nil { - for _, r := range g.curBlock.Term.termUses() { - if r == g.pendingAccReg { - return true - } - } - } - return false -} - -// pickScratch8 returns an 8-bit scratch register that is not currently -// occupied by an operand, destination or value live after upcomingInst. -// Prefers E, H, L (unlikely to hold params) before D, B, C. -// Always excludes "A" and "F". -func (g *z80cg) pickScratch8(upcomingInst *Inst) string { - used := map[string]bool{"A": true, "F": true} - mark := func(loc string) { - used[loc] = true - if isPairReg(loc) || isIXY(loc) { - used[highByte(loc)] = true - used[lowByte(loc)] = true - } - } - for r := range g.regsLiveAfterInst(upcomingInst) { - mark(g.loc(r)) - } - for _, r := range upcomingInst.Uses() { - mark(g.loc(r)) - } - mark(g.loc(upcomingInst.Dst)) - for _, loc := range g.physOverride { - mark(loc) - } - for _, r := range []string{"E", "H", "L", "D", "B", "C", "IYH", "IYL", "IXH", "IXL"} { - if !used[r] { - return r - } - } - return "D" // existing fallback for register-exhausted blocks -} - -// materializePendingFlag checks whether the most recent ClassFlag result -// (pendingFlagReg) is still needed after upcomingInst. If so, it emits: -// -// SBC A, A ; A = 0xFF if condition-true, 0x00 if condition-false -// LD , A -// -// where is a register that is not currently holding any live value, -// and reroutes pendingFlagReg via physOverride so subsequent genBinOp reads -// it from there rather than from the (now-clobbered) carry flag. -func (g *z80cg) materializePendingFlag(upcomingInst *Inst) { - if g.pendingFlagReg == NoReg { - return - } - if !g.flagStillNeeded(upcomingInst) { - g.pendingFlagReg = NoReg - return - } - scratch := g.pickScratch8(upcomingInst) - // Materialize carry → 0xFF (true) or 0x00 (false) via SBC A, A. - g.emit(" SBC A, A") - g.invalidate("A") - g.emitf(" LD %s, A", scratch) - g.physOverride[g.pendingFlagReg] = scratch - g.pendingFlagReg = NoReg -} - -// materializePendingAcc checks whether pendingAccReg (a ClassAcc value -// currently in A) is still needed after upcomingInst. If so, it saves A -// to a scratch register that is not currently holding any live value, -// and reroutes pendingAccReg via physOverride so the upcoming "LD A, imm" -// in genCmp does not clobber the live value. -func (g *z80cg) materializePendingAcc(upcomingInst *Inst) { - if g.pendingAccReg == NoReg { - return - } - if !g.accStillNeeded(upcomingInst) { - g.pendingAccReg = NoReg - return - } - scratch := g.pickScratch8(upcomingInst) - g.emitf(" LD %s, A ; materialize r%d (pendingAcc)", scratch, g.pendingAccReg) - g.physOverride[g.pendingAccReg] = scratch - g.pendingAccReg = NoReg -} - -// saveAccForCondRet saves A to a scratch register if a CMP is about to load a -// different vreg into A and there is a live vreg in A that the terminator or a -// successor block still needs. -// -// Originally this only covered TermCondRet (abs_val fix). Expanded to also -// cover TermBrIf: gcd(a,b) has CmpEq(b,0) that loads b into A, clobbering a -// which the then-branch returns. -func (g *z80cg) saveAccForCondRet(cmpInst *Inst) { - if g.fn == nil || g.curBlock == nil { - return - } - term := g.curBlock.Term - if term == nil { - return - } - - // Check if CMP will load a different vreg into A (clobbering current A value). - lhs := g.loc(cmpInst.Src[0]) - if lhs == "A" { - return // lhs already in A, no clobber - } - - // Collect all vregs that the terminator and successor blocks need. - // If any of them is currently in A, save A before the CMP clobbers it. - liveInA := g.findVregInA() - if liveInA == NoReg { - return // nothing in A worth saving - } - - needed := false - switch t := term.(type) { - case *TermCondRet: - for _, v := range t.Vals { - if v == liveInA { - needed = true - break - } - } - if !needed { - // Check if the Then successor needs it - needed = g.vregUsedInBlock(liveInA, t.Then) - } - case *TermBrIf: - // Check if either successor uses the vreg in A - needed = g.vregUsedInBlock(liveInA, t.Then) || g.vregUsedInBlock(liveInA, t.Else) - case *TermBrIf2: - needed = g.vregUsedInBlock(liveInA, t.Eq) || - g.vregUsedInBlock(liveInA, t.Lt) || - g.vregUsedInBlock(liveInA, t.Gt) - } - - if needed { - scratch := g.pickScratch8(cmpInst) - g.emitf(" LD %s, A ; save ret val before CMP", scratch) - g.physOverride[liveInA] = scratch - } -} - -// findVregInA returns the vreg currently mapped to A, or NoReg if none. -func (g *z80cg) findVregInA() Reg { - if g.fn == nil { - return NoReg - } - // Check block params first (they're live-through the block) - for _, bp := range g.curBlock.Params { - if g.loc(bp.Dst) == "A" { - return bp.Dst - } - } - // Check function params that might still be in A - for _, cp := range g.fn.Contract.Params { - if g.loc(cp.Reg) == "A" { - return cp.Reg - } - } - return NoReg -} - -// vregUsedInBlock checks if vreg is used in the instructions or terminator -// of the block with the given label. -func (g *z80cg) vregUsedInBlock(vreg Reg, label string) bool { - blk := g.fn.BlockByLabel(label) - if blk == nil { - return false - } - for _, inst := range blk.Insts { - for _, r := range inst.Uses() { - if r == vreg { - return true - } - } - } - if blk.Term != nil { - for _, r := range blk.Term.termUses() { - if r == vreg { - return true - } - } - } - return false -} - -func (g *z80cg) genCmp(inst *Inst) { - // CmpSubCarry / CmpSubCarryNot: carry flag already set by the immediately - // preceding SUB. No instruction needed — carry encodes a < b (C) or a >= b (NC). - if inst.Cond == CmpSubCarry || inst.Cond == CmpSubCarryNot { - return - } - - // Before emitting any flag-clobbering comparison instruction: - // 1. Materialize any live ClassFlag register whose carry would be overwritten. - // 2. Save any live ClassAcc register whose A value would be overwritten by - // the "LD A, imm" used to set up the CP operand. - // 3. Save A if the block ends with TermCondRet and A holds a return value - // that would be clobbered by loading the CMP operand (ADR-0041 fix). - // Both are saved to "D" (scratch) via physOverride; the OR-from-flag handler - // in genBinOp will then read them from "D" instead of from "F"/"A". - g.materializePendingFlag(inst) - g.materializePendingAcc(inst) - g.saveAccForCondRet(inst) - - lhs := g.loc(inst.Src[0]) - rhs := g.loc(inst.Src[1]) - - // 32-bit DWord comparison: must dispatch before isPairReg (DWord locs also - // have pair names like "HL", so they'd otherwise fall into genCmp16). - if g.isDWord(inst.Src[0]) || g.isDWord(inst.Src[1]) { - g.genCmp32(inst) - return - } - - if pat, ok := g.bitCmpPat[inst.Dst]; ok { - if pat.useMem { - ptr := g.loc(pat.ptrReg) - if isSpill(ptr) { - g.emitf(" LD HL, (%s) ; reload spilled ptr", ptr) - g.invalidate("HL") - ptr = "HL" - } - if g.emitBitMemOp("BIT", pat.bit, ptr, pat.byteOffset) { - g.pendingFlagReg = inst.Dst - return - } - } else { - src := g.loc(pat.srcReg) - src = selectBitReg(src, pat.byteOffset) - if isSimpleReg(src) && !isSpill(src) && !isIXYReg(src) { - g.emitf(" BIT %d, %s", pat.bit, src) - g.pendingFlagReg = inst.Dst - return - } - } - } - - // 16-bit comparison: one or both operands are register pairs (HL/DE/BC/…). - // Z80 only supports SBC HL, rr for 16-bit flag-setting subtraction. - cv, immediate := g.constVals[inst.Src[1]] - byteImmediate := immediate && cv >= 0 && cv <= 255 && !isPairReg(lhs) && (isSimpleReg(lhs) || isIXYReg(lhs)) && lhs != "F" && (inst.SrcTy == nil || inst.SrcTy.Width() <= 8) - if isPairReg(lhs) || (isPairReg(rhs) && !byteImmediate) { - g.genCmp16(inst) - return - } - - // CmpGt / CmpUgt and CmpLe / CmpUle are not directly expressible as a - // single Z80 flag after a normal CP (which would need NC+NZ or C+Z). - // Fix: force-swap the operands. - // CmpGt(a,b): a>b ↔ b). SCF/CCF preserve S, Z and P/V and do not change A or HL. -func (g *z80cg) normalizeSignedCmp(inst *Inst) { - if !isSignedOrdering(inst) { - return - } - idx := g.trampIdx - g.trampIdx++ - g.emitf(" JP M, .scmp_neg_%d", idx) - g.emitf(" JP PE, .scmp_true_%d", idx) - g.emitf(" JP .scmp_false_%d", idx) - g.emitf(".scmp_neg_%d:", idx) - g.emitf(" JP PO, .scmp_true_%d", idx) - g.emitf(".scmp_false_%d:", idx) - g.emit(" SCF") - g.emit(" CCF") - g.emitf(" JP .scmp_done_%d", idx) - g.emitf(".scmp_true_%d:", idx) - g.emit(" SCF") - g.emitf(".scmp_done_%d:", idx) -} - -// ── Calls ───────────────────────────────────────────────────────────────────── - -func (g *z80cg) genCall(inst *Inst) { - clear(g.holdsPhys) // calls clobber all volatile registers - - // ── Built-in intrinsics (inlined, no CALL emitted) ──────────────────────── - switch inst.Sym { - case "@mir.io.print.str": - // Emit inline NUL-terminated string output loop via OUT ($23), A. - // The string pointer must be in HL (ClassPointer). - // HL is advanced in-place; A and F are clobbered. - idx := g.trampIdx - g.trampIdx++ - g.emitf(".print_str_%d:", idx) - g.emit(" LD A, (HL)") - g.emit(" AND A") - g.emitf(" JR Z, .print_str_done_%d", idx) - g.emit(" OUT (0x23), A") - g.emit(" INC HL") - g.emitf(" JR .print_str_%d", idx) - g.emitf(".print_str_done_%d:", idx) - // A, F, HL clobbered — invalidate holdsPhys entries for these. - g.invalidate("A") - g.invalidate("HL") - return - - case "@mir.io.print.u8": - // Emit single OUT ($23), A. The value must already be in A. - g.emit(" OUT (0x23), A") - g.invalidate("A") - return - - case "@mir.io.print.char": - // Print ASCII character in A via OUT ($23). - g.emit(" OUT (0x23), A") - g.invalidate("A") - return - - case "@mir.io.print.nl": - // Emit newline (\n = 0x0A) via OUT ($23), A. - g.emit(" LD A, 0x0A") - g.emit(" OUT (0x23), A") - g.invalidate("A") - return - - case "@mir.io.print.dec": - // Print u8 value in A as decimal ASCII digits via OUT ($23). - // Inline: divide by 100, 10 (using repeated subtraction — small and fast - // enough for 0-255). Clobbers A, B, C, D, H, L. - idx := g.trampIdx - g.trampIdx++ - // Hundreds digit - g.emitf(" LD C, A") // C = n - g.emitf(" LD B, 0") // B = hundreds count - g.emitf(".pdec_h%d:", idx) - g.emitf(" LD A, C") - g.emitf(" CP 100") - g.emitf(" JR C, .pdec_ht%d", idx) - g.emitf(" SUB 100") - g.emitf(" LD C, A") - g.emitf(" INC B") - g.emitf(" JR .pdec_h%d", idx) - g.emitf(".pdec_ht%d:", idx) - // Only print hundreds if non-zero - g.emitf(" LD A, B") - g.emitf(" OR A") - g.emitf(" JR Z, .pdec_t%d", idx) - g.emitf(" ADD A, 48") // '0' - g.emitf(" OUT (0x23), A") - // Tens digit - g.emitf(".pdec_t%d:", idx) - g.emitf(" LD D, 0") // D = tens count - g.emitf(".pdec_tl%d:", idx) - g.emitf(" LD A, C") - g.emitf(" CP 10") - g.emitf(" JR C, .pdec_tt%d", idx) - g.emitf(" SUB 10") - g.emitf(" LD C, A") - g.emitf(" INC D") - g.emitf(" JR .pdec_tl%d", idx) - g.emitf(".pdec_tt%d:", idx) - // Only print tens if hundreds or tens non-zero - g.emitf(" LD A, B") - g.emitf(" OR D") - g.emitf(" JR Z, .pdec_u%d", idx) - g.emitf(" LD A, D") - g.emitf(" ADD A, 48") - g.emitf(" OUT (0x23), A") - // Units digit (always print) - g.emitf(".pdec_u%d:", idx) - g.emitf(" LD A, C") - g.emitf(" ADD A, 48") - g.emitf(" OUT (0x23), A") - g.invalidate("A") - g.invalidate("B") - g.invalidate("C") - g.invalidate("D") - return - - case "@mir.io.console.log": - // console_log(n: u8) — OUT ($23), A (mze/mzx stdout port) - g.emit(" OUT (0x23), A") - g.invalidate("A") - return - - case "@mir.io.console.err": - // console_err(n: u8) — OUT ($25), A (mze/mzx stderr port) - g.emit(" OUT (0x25), A") - g.invalidate("A") - return - - case "@error": - // @error(N) — set carry flag, error code in A, return. - // Used in fallible functions (name?). A = argument (error code). - g.emit(" SCF") // CY = 1 (error) - g.emit(" RET") // return to caller with CY set + A = code - g.invalidate("A") - return - - case "@check": - // @check — jump to error handler if carry set. - // Emits: JR NC, .skip / / .skip: - // For now: just emit RET C (propagate). Handler block TBD. - idx := g.trampIdx - g.trampIdx++ - g.emitf(" JR NC, .check_ok_%d", idx) - // The handler code follows in the next statement. - // For simple propagation, emit RET: - g.emit(" RET") // propagate error (CY + A intact) - g.emitf(".check_ok_%d:", idx) - return - - case "@propagate": - // @propagate — conditional return on carry. 1 byte, 5T. - // If CY=1 (error from previous call), return immediately. - g.emit(" RET C") - return - } - - sym := sanitizeIdent(inst.Sym) - - var callee *Func - indirect := inst.Op == OpCallIndirect - if indirect { - callee = &Func{} - for i, arg := range inst.Args { - ty := inferTyFromAlloc(g.ar, arg) - callee.Contract.Params = append(callee.Contract.Params, Param{Ty: ty, Class: standardParamClass(ty, i)}) - } - if inst.Dst != NoReg { - callee.Contract.Returns = []Return{{Ty: inst.Ty, Class: ClassAcc}} - } - } else if g.mod != nil { - callee = g.mod.FuncByName(inst.Sym) - } - - // Save live caller values before argument copies, including reused args. - var callerSavePairs []string - if callee != nil && inst != g.tailCallInst { - callerSavePairs = g.callerSavePairs(inst, callee) - for _, pair := range callerSavePairs { - g.emitf(" PUSH %s", pair) - } - } - if indirect { - // Snapshot the pointer before argument setup. IX keeps the target - // separate from the standard ABI's HL word argument. - g.pushWord(g.loc(inst.Src[0])) - } - if len(inst.Args) > 0 && callee != nil { - g.emitCallArgs(inst.Args, callee.Contract.Params) - } - if indirect { - g.emit(" POP IX") - } - - // Tail call optimisation: if this is the tail call instruction detected by - // genBlock, emit JP instead of CALL so the callee's RET returns to our caller. - if inst == g.tailCallInst { - if callee != nil && callee.Attrs.ExternAddr != 0 { - g.emitf(" JP 0x%04X", callee.Attrs.ExternAddr) - } else { - g.emitf(" JP %s", sym) - } - return // genTerm will skip RET - } - - // Check if callee has a fixed address (ExternAddr) - if indirect { - g.emit(" CALL __call_ix") - g.needsCallIX = true - } else if callee != nil && callee.Attrs.ExternAddr != 0 { - addr := callee.Attrs.ExternAddr - // RST addresses on Z80: 0x00, 0x08, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38 - if addr <= 0x38 && addr%8 == 0 { - g.emitf(" RST 0x%02X", addr) - } else { - g.emitf(" CALL 0x%04X", addr) - } - } else { - g.emitf(" CALL %s", sym) - } - - // Pick up every result without letting caller restores destroy it. If a - // destination shares a saved pair, merge its bytes into the saved stack - // image so POP restores both the live half and the returned half. - var results []parallelCopy - dstPhys := "" - flagResult := false - if inst.Dst != NoReg && callee != nil && len(callee.Contract.Returns) > 0 { - ret := callee.Contract.Returns[0] - if ret.Class == ClassFlag { - flagResult = true - } else { - dstPhys = g.loc(inst.Dst) - results = append(results, parallelCopy{srcName: canonicalReturnLoc(ret.Class, ret.Ty), dstName: dstPhys, ty: ret.Ty}) - } - } - for i, r := range inst.ExtraRets { - if r == NoReg { - continue - } - cls, ty := ClassIndex, Ty(TyU16) - if callee != nil && i+1 < len(callee.Contract.Returns) { - cls, ty = callee.Contract.Returns[i+1].Class, callee.Contract.Returns[i+1].Ty - } - if i < len(inst.ExtraRetClasses) { - cls = inst.ExtraRetClasses[i] - } - if i < len(inst.ExtraRetTys) { - ty = inst.ExtraRetTys[i] - } - results = append(results, parallelCopy{srcName: canonicalReturnLoc(cls, ty), dstName: g.loc(r), ty: ty}) - } - g.pickupCallResults(results, callerSavePairs, flagResult) - - // CALL and argument setup invalidate cached physical contents, including - // registers used only as scratch by the emitted callee/runtime sequences. - g.invalidate("A") - g.invalidate("F") - for _, name := range []string{"B", "C", "D", "E", "H", "L", "IX", "IY"} { - g.invalidate(name) - } - g.pendingAccReg = NoReg - if dstPhys == "A" { - g.pendingAccReg = inst.Dst - } - - // Flag-return ABI: if the callee returns ClassFlag, the result is in the CPU - // flag register (not A). Record which condition means "true" so that a - // subsequent TermBrIf on inst.Dst can emit JP directly without - // AND A / CP 0. - if inst.Dst != NoReg && callee != nil { - if len(callee.Contract.Returns) > 0 { - ret := callee.Contract.Returns[0] - if ret.Class == ClassFlag { - g.callFlags[inst.Dst] = ret.FlagCond - } - } - } - -} - -// pickupCallResults never stores into code. Most calls need only parallel -// moves; overlapping destinations use a short-lived, reentrant stack frame. -func (g *z80cg) pickupCallResults(results []parallelCopy, saved []string, flagResult bool) { - savedPair := map[string]bool{} - for _, p := range saved { - savedPair[p] = true - } - before, after := true, true - for _, r := range results { - before = before && !savedPair[regToPairMap[r.dstName]] - after = after && !savedPair[regToPairMap[r.srcName]] - } - if flagResult && savedPair["AF"] { - before, after = false, false - } - if before || after { - if before { - g.emitParallelCopy(results) - } - for i := len(saved) - 1; i >= 0; i-- { - g.emitf(" POP %s", saved[i]) - } - if !before { - g.emitParallelCopy(results) - } - return - } - - // A byte result can use a pair that is absent from the restore list. - // This scratch lifetime ends at pickup, so later allocation cannot reuse - // it while an override remains live. - if !flagResult && len(results) == 1 && results[0].ty.Width() <= 8 && regToPairMap[results[0].dstName] != "" { - r := results[0] - for _, scratch := range []string{"E", "H", "L", "D", "B", "C"} { - if !savedPair[regToPairMap[scratch]] { - g.emitLD8(scratch, r.srcName) - for i := len(saved) - 1; i >= 0; i-- { - g.emitf(" POP %s", saved[i]) - } - g.emitLD8(r.dstName, scratch) - return - } - } - } - - // IX addresses snapshots and the caller-save image. Its original value - // is restored separately, including when IX itself is a result destination. - g.emit(" PUSH IX") - pairs := []string{"AF"} - for _, r := range results { - p := regToPairMap[r.srcName] - if !slices.Contains(pairs, p) { - pairs = append(pairs, p) - } - } - for _, p := range pairs { - g.emitf(" PUSH %s", p) - } - g.emit(" LD IX, 0") - g.emit(" ADD IX, SP") - snapshot := map[string]int{} - for i, p := range pairs { - snapshot[p] = 2 * (len(pairs) - 1 - i) - } - base := 2 * len(pairs) - restore := map[string]int{} - for i, p := range saved { - restore[p] = base + 2 + 2*(len(saved)-1-i) - } - byteOffset := func(name string) int { - p := regToPairMap[name] - o := snapshot[p] - if name == "A" || name == highByte(p) { - o++ - } - return o - } - storeByte := func(src, dst int) { - g.emitf(" LD A, (IX+%d)", src) - g.emitf(" LD (IX+%d), A", dst) - } - // Patch saved destinations before loading any final register, since A - // is scratch while merging the stack image. - for _, r := range results { - p := regToPairMap[r.dstName] - o, ok := restore[p] - if p == "IX" && !ok { - o, ok = base, true - } - if !ok { - continue - } - if r.ty.Width() > 8 { - src := snapshot[regToPairMap[r.srcName]] - storeByte(src, o) - storeByte(src+1, o+1) - } else { - if r.dstName == "A" || r.dstName == highByte(p) { - o++ - } - storeByte(byteOffset(r.srcName), o) - } - } - if flagResult && savedPair["AF"] { - storeByte(snapshot["AF"], restore["AF"]) - } - // Preserve the result in A in the AF snapshot until all scratch work is done. - for _, r := range results { - if r.dstName == "A" && !savedPair["AF"] { - storeByte(byteOffset(r.srcName), snapshot["AF"]+1) - } - } - for _, r := range results { - p := regToPairMap[r.dstName] - if savedPair[p] || p == "IX" || r.dstName == "A" { - continue - } - if r.ty.Width() <= 8 { - // Indexed memory cannot load IY halves directly; use A as scratch. - if p != "" && p != "IY" { - g.emitf(" LD %s, (IX+%d)", r.dstName, byteOffset(r.srcName)) - } else { - g.emitf(" LD A, (IX+%d)", byteOffset(r.srcName)) - g.emitLD8(r.dstName, "A") - } - } else { - src := snapshot[regToPairMap[r.srcName]] - if p == "IY" || p == "" { - g.emit(" PUSH HL") - g.emitf(" LD L, (IX+%d)", src) - g.emitf(" LD H, (IX+%d)", src+1) - g.emitMov(r.dstName, "HL", r.ty.Width()) - g.emit(" POP HL") - } else { - g.emitf(" LD %s, (IX+%d)", lowByte(p), src) - g.emitf(" LD %s, (IX+%d)", highByte(p), src+1) - } - } - } - // Restore returned flags (ADD IX,SP changed them) and the final A without - // disturbing HL. Subsequent INC SP and POP instructions preserve flags. - g.emit(" PUSH HL") - g.emitf(" LD L, (IX+%d)", snapshot["AF"]) - g.emitf(" LD H, (IX+%d)", snapshot["AF"]+1) - g.emit(" PUSH HL") - g.emit(" POP AF") - g.emit(" POP HL") - for i := 0; i < base; i++ { - g.emit(" INC SP") - } - g.emit(" POP IX") - for i := len(saved) - 1; i >= 0; i-- { - g.emitf(" POP %s", saved[i]) - } -} - -// emitCallArgs emits the parallel-copy argument setup for a CALL instruction. -// It maps the current physical locations of the call arguments to the physical -// locations expected by the callee's parameter contract and uses emitParallelCopy -// to resolve any cycles (e.g. A→B, C→A). -// -// Argument locations are transient. Caller values keep their original locations -// after caller-save restoration, so argument setup does not change physOverride. -func (g *z80cg) emitCallArgs(args []Reg, params []Param) { - var copies []parallelCopy - for i, arg := range args { - if i >= len(params) { - break - } - srcPhys := g.loc(arg) - // Use the callee's actual allocated register for the param if available - // (from PBQP). Fall back to canonical class-based location otherwise. - dstPhys := "" - if loc, ok := g.ar.Locs[params[i].Reg]; ok && loc.Name != "" { - dstPhys = physName(loc) - } - if dstPhys == "" { - dstPhys = canonicalReturnLoc(params[i].Class, params[i].Ty) - } - // Always include in copies — even no-ops (src==dst). This ensures the - // parallel copy scratch picker knows ALL live arg registers and won't - // clobber them when resolving cycles (e.g. B↔C using A as scratch - // when A holds arg0). - copies = append(copies, parallelCopy{srcName: srcPhys, dstName: dstPhys, ty: params[i].Ty}) - } - if len(copies) == 0 { - return - } - g.emitParallelCopy(copies) - -} - -// ── 32-bit DWord helpers ────────────────────────────────────────────────────── - -// isDWord reports whether virtual register r is allocated to a LocDWord. -func (g *z80cg) isDWord(r Reg) bool { - return r != NoReg && g.ar.Loc(r).Kind == LocDWord -} - -// emitMov32 moves a 32-bit DWord value from src pair to dst pair using PUSH/EXX. -// -// Strategy (47T, 5 instructions): -// -// PUSH src ; push main src_lo (11T) -// EXX ; switch to shadow (4T) -// PUSH src ; push shadow src_hi (11T) -// POP dst ; pop into shadow dst_hi (10T) -// EXX ; switch back (4T) -// POP dst ; pop into main dst_lo (10T) -// -// The push of src_lo happens before EXX, so it's the main-bank value. -// After EXX, src refers to the shadow-bank value (src_hi). -func (g *z80cg) emitMov32(dst, src string) { - if dst == src { - return - } - g.emitf(" PUSH %s", src) // save main src_lo - g.emit(" EXX") // switch to shadow - g.emitf(" PUSH %s", src) // save shadow src_hi - g.emitf(" POP %s", dst) // restore shadow dst_hi - g.emit(" EXX") // switch back to main - g.emitf(" POP %s", dst) // restore main dst_lo -} - -// ── Move helper ─────────────────────────────────────────────────────────────── - -func (g *z80cg) emitMov(dst, src string, widthBits int) { - if dst == src { - return - } - - // ── Flag-register special cases ─────────────────────────────────────────── - - if dst == "F" { - // Materialise a register value into the flag register. - // Convention: Z=1 (zero) means false, NZ means true — i.e. AND A / OR A. - // Callers using CmpNe (NZ) as the flag condition get this for free. - if src == "A" { - g.emit(" AND A") // sets Z/NZ and clears C based on A's value - } else { - // Load into A first, then set flags. - g.emitf(" LD A, %s", src) - g.emit(" AND A") - } - g.invalidate("A") - return - } - - if src == "F" { - if g.emitKnownFlagCopy(dst, widthBits) { - return - } - // Materialise a flag result into a register. - // Uses the SBC A,A trick for C-flag convention: A = 0xFF (carry set = true) - // or 0x00 (carry clear = false). Works for CmpLt/CmpUlt/CmpGe/CmpUge. - // - // For Z-flag convention (CmpEq/CmpNe), the sequence is: - // JR NZ, $+3 / SCF / then SBC A,A — converts Z to C, then materialise. - // We use the simpler carry path by default; HIR lowers bool-returning - // functions to carry-based comparisons where possible. - if dst == "A" { - g.emit(" SBC A, A") // A=0xFF if C=1 (true), A=0x00 if C=0 (false) - } else if isPairReg(dst) { - // Flag → 16-bit pair: materialise to A, then zero-extend. - // SBC A,A gives 0xFF (true) or 0x00 (false). - g.emit(" SBC A, A") - g.emitf(" LD %s, A", lowByte(dst)) - g.emitf(" LD %s, 0", highByte(dst)) - } else { - g.emit(" SBC A, A") - g.emitLD8(dst, "A") - } - g.invalidate("A") - return - } - - // ── Normal register moves ───────────────────────────────────────────────── - - if widthBits <= 8 { - // LocMem spill slot: "LD r, $Fxxx" and "LD $Fxxx, r" are not valid Z80. - // Only "LD A, (nn)" and "LD (nn), A" work for absolute 8-bit memory I/O. - // TSMC spill: use self-modifying code when eligible. - switch { - case isSpill(src) && dst == "A": - if pair := g.tsmcPairByAddr(src); pair != nil { - g.emitTSMCReload(g.tsmcReloadLabel(pair.reg, g.blockIdx, g.curInstIdx, 0), "A", 8) - } else { - g.emitf(" LD A, (%s)", src) - } - case isSpill(src): - if pair := g.tsmcPairByAddr(src); pair != nil { - g.emitTSMCReload(g.tsmcReloadLabel(pair.reg, g.blockIdx, g.curInstIdx, 0), dst, 8) - } else { - g.emitf(" LD A, (%s)", src) - g.emitLD8(dst, "A") - g.invalidate("A") - } - case isSpill(dst) && src == "A": - if pair := g.tsmcPairByAddr(dst); pair != nil { - g.emitTSMCSpill(pair, "A") - } else { - g.emitf(" LD (%s), A", dst) - } - case isSpill(dst): - if pair := g.tsmcPairByAddr(dst); pair != nil { - g.emitLDA(src) - g.emitTSMCSpill(pair, "A") - } else { - g.emitLDA(src) - g.emitf(" LD (%s), A", dst) - } - default: - // Width mismatch: 8-bit move but one operand is a register pair. - // DD/FD prefix conflict must be checked BEFORE width mismatch, - // because lowByte(HL)="L" + IXL src = DD NOP. - if (isIXYReg(dst) && (src == "H" || src == "L")) || - (isIXYReg(src) && (dst == "H" || dst == "L")) { - g.emitMovViaAltA(dst, src) - } else if isPairReg(src) && !isPairReg(dst) { - // pair→8bit: truncate (take low byte). - lo := lowByte(src) - if (isIXYReg(lo) && (dst == "H" || dst == "L")) || - ((lo == "H" || lo == "L") && isIXYReg(dst)) { - g.emitMovViaAltA(dst, lo) - } else { - g.emitLD8(dst, lo) - } - } else if !isPairReg(src) && isPairReg(dst) { - // 8bit→pair: zero-extend. - lo := lowByte(dst) - if (isIXYReg(src) && (lo == "H" || lo == "L")) || - ((src == "H" || src == "L") && isIXYReg(lo)) { - g.emitMovViaAltA(lo, src) - } else { - g.emitLD8(lo, src) - } - g.emitf(" LD %s, 0", highByte(dst)) - } else { - g.emitLD8(dst, src) - if isSimpleReg(dst) && isSimpleReg(src) { - g.setCopy(dst, src) - } - } - } - return - } - - // 32-bit DWord move via PUSH/EXX sequence. - if widthBits == 32 { - g.emitMov32(dst, src) - return - } - - // 16-bit register move. - switch { - case dst == "HL" && src == "DE": - // Copy DE→HL: must NOT use EX DE,HL (swap destroys DE). - g.emitf(" LD %s, %s", highByte(dst), highByte(src)) // LD H, D - g.emitf(" LD %s, %s", lowByte(dst), lowByte(src)) // LD L, E - case dst == "DE" && src == "HL": - // Copy HL→DE: must NOT use EX DE,HL (swap destroys HL). - g.emitf(" LD %s, %s", highByte(dst), highByte(src)) // LD D, H - g.emitf(" LD %s, %s", lowByte(dst), lowByte(src)) // LD E, L - case (dst == "IX" || dst == "IY") && (src == "DE" || src == "BC"): - // DE/BC → IX/IY: undocumented byte-copy (DD/FD prefix), 2×8T = 16T. - // Safe because D, E, B, C are NOT substituted by the DD/FD prefix. - // e.g. DE→IX: LD IXH, D (DD 62) / LD IXL, E (DD 6B) - // BC→IX: LD IXH, B (DD 60) / LD IXL, C (DD 69) - g.emitf(" LD %s, %s", highByte(dst), highByte(src)) - g.emitf(" LD %s, %s", lowByte(dst), lowByte(src)) - case (dst == "IX" || dst == "IY") && src == "HL": - // HL→IX: byte-copy is INVALID — DD prefix substitutes H→IXH, L→IXL, - // so LD IXH,H encodes as LD IXH,IXH (NOP). Must use PUSH/POP. - g.emitf(" PUSH HL") - g.emitf(" POP %s", dst) - case (src == "IX" || src == "IY") && (dst == "DE" || dst == "BC"): - // IX/IY → DE/BC: safe byte-copy, D/E/B/C not substituted by DD prefix. - // e.g. IX→DE: LD D, IXH (DD 57? no — DD 7A? no) actually DD prefix on LD D,H: - // opcode 0x54 = LD D,H → DD 54 = LD D,IXH ✓ (D is destination, IXH is source) - g.emitf(" LD %s, %s", highByte(dst), highByte(src)) - g.emitf(" LD %s, %s", lowByte(dst), lowByte(src)) - case (src == "IX" || src == "IY") && dst == "HL": - // IX→HL: byte-copy INVALID (H,L substituted). Must use PUSH/POP. - g.emitf(" PUSH %s", src) - g.emit(" POP HL") - default: - // If src is an 8-bit register being widened to a 16-bit pair, - // PUSH/POP is wrong: PUSH A doesn't exist (only PUSH AF, which - // would corrupt the low byte with the flags register). - // Use zero-extension instead: LD lo, src ; LD hi, 0. - if isSimpleReg(src) && !isPairReg(src) && isPairReg(dst) { - lo := lowByte(dst) - hi := highByte(dst) - if lo != src { - g.emitLD8(lo, src) - } - g.emitf(" LD %s, 0", hi) - break - } - // Spill-to-spill: route through HL. - if isSpill(src) && isSpill(dst) { - g.emitf(" LD HL, (%s)", src) - g.emitf(" LD (%s), HL", dst) - g.invalidate("HL") - break - } - // LocMem spill slot → register pair: use LD rr, (nn). - if isSpill(src) { - g.emitf(" LD %s, (%s)", dst, src) - break - } - // register pair → LocMem spill slot: use LD (nn), rr. - if isSpill(dst) { - g.emitf(" LD (%s), %s", dst, src) - break - } - // 16-bit pair → 8-bit: truncate (take low byte). - if isPairReg(src) && !isPairReg(dst) { - lo := lowByte(src) - if (isIXYReg(lo) && (dst == "H" || dst == "L")) || - ((lo == "H" || lo == "L") && isIXYReg(dst)) { - g.emitMovViaAltA(dst, lo) - } else { - g.emitLD8(dst, lo) - } - break - } - // 8-bit → 16-bit pair: zero-extend. - if !isPairReg(src) && isPairReg(dst) { - lo := lowByte(dst) - if (isIXYReg(src) && (lo == "H" || lo == "L")) || - ((src == "H" || src == "L") && isIXYReg(lo)) { - g.emitMovViaAltA(lo, src) - } else { - g.emitLD8(lo, src) - } - g.emitf(" LD %s, 0", highByte(dst)) - break - } - // General 16-bit move via PUSH/POP. - g.emitf(" PUSH %s", src) - g.emitf(" POP %s", dst) - } - // Track the copy alias so downstream redundant moves can be suppressed. - g.setCopy(dst, src) -} - -// restoreLiveOutOverrides moves every relocated vreg that is live out of the -// current block, or read by its terminator, back to its allocated register -// before the terminator. A -// relocation (physOverride) is block-local: successors, and blocks laid out -// after this one, read the allocator's location. Without this, a value saved -// to a scratch register in one branch (gcd's else arm) was read from that -// scratch on paths that never wrote it. -func (g *z80cg) restoreLiveOutOverrides(f *Func, t Term) { - if g.liveness == nil || len(g.physOverride) == 0 { - return - } - liveOut := g.liveness.LiveOutOf(f, g.curBlock) - if liveOut == nil { - return - } - // Terminator operands count too: jmp @head(%r3, …) passes r3 to the param - // that is r3 itself after coalescing, and the parallel copy skips it as an - // identity move, so r3 must already sit in its allocated register. - usedByTerm := map[Reg]bool{} - for _, r := range t.termUses() { - usedByTerm[r] = true - } - regs := make([]Reg, 0, len(g.physOverride)) - for r := range g.physOverride { - if liveOut.Has(r) || usedByTerm[r] { - regs = append(regs, r) - } - } - sort.Slice(regs, func(i, j int) bool { return regs[i] < regs[j] }) - for _, r := range regs { - scratch := g.physOverride[r] - canonLoc := g.ar.Loc(r) - delete(g.physOverride, r) - if canonLoc.Kind != LocReg && canonLoc.Kind != LocIXY8 { - continue - } - if canon := canonLoc.Name; canon != "" && canon != scratch { - width := 8 - if canon == "HL" || canon == "DE" || canon == "BC" { - width = 16 - } - g.emitMov(canon, scratch, width) - g.comment(fmt.Sprintf("restore live-out r%d from scratch", r)) - } - } -} - -// ── Terminators ─────────────────────────────────────────────────────────────── - -func (g *z80cg) genTerm(f *Func, t Term) { - // If any block param was saved to a scratch register (via materializePendingAcc), - // restore it to its canonical physical location before any terminal jump or ret. - // This ensures live-through values (used in successor blocks without explicit - // block params) are in their allocator-assigned locations when those blocks run. - // physOverride[r] was valid within this block; canonical location = ar.Loc(r). - // - // EXCEPTION: For TermDJNZ, do NOT restore block params that are explicitly - // passed as body-edge args (BodyArgs). Those params receive their new values - // from the parallel copy emitted by the DJNZ handler. Restoring them from - // scratch would overwrite the computed result with the stale initial value. - djnzBodyArgs := map[Reg]bool{} - if td, ok := t.(*TermDJNZ); ok { - bodyBlock := f.BlockByLabel(td.Body) - if bodyBlock != nil { - for i, arg := range td.BodyArgs { - if i+1 < len(bodyBlock.Params) { - djnzBodyArgs[bodyBlock.Params[i+1].Dst] = true - _ = arg // only need the param dst - } - } - } - } - for _, bp := range g.curBlock.Params { - if djnzBodyArgs[bp.Dst] { - delete(g.physOverride, bp.Dst) - continue - } - if scratch, ok := g.physOverride[bp.Dst]; ok { - // Use ar.Loc (static allocation) not g.loc (which returns the override). - canonLoc := g.ar.Loc(bp.Dst) - delete(g.physOverride, bp.Dst) - // Only restore register-backed block params — spilled params - // (LocMem) are already in memory and weren't saved to a scratch GPR. - if canonLoc.Kind == LocReg || canonLoc.Kind == LocIXY8 { - canon := canonLoc.Name - if canon != "" && canon != scratch { - g.emitMov(canon, scratch, bp.Ty.Width()) - g.comment("restore block param from scratch") - } - } - } - } - g.restoreLiveOutOverrides(f, t) - - switch t := t.(type) { - case *TermRet: - // Tail call: JP was already emitted by genCall; skip all moves and RET. - if g.tailCallInst != nil { - g.tailCallInst = nil - return - } - // Move return values to their calling-convention physical registers using - // parallel copy resolution. Sequential moves are incorrect when two return - // values share the same physical register after constant folding/PBQP - // (e.g. both [acc]=A), causing write-after-write clobber. - if len(t.Vals) == 1 && len(f.Contract.Returns) == 1 && - (f.Contract.Returns[0].Ty == TyBool || (f.Contract.Returns[0].Ty.Width() <= 8 && collectRegInfo(f)[t.Vals[0]].Ty == TyBool)) && - f.Contract.Returns[0].Class != ClassFlag && g.loc(t.Vals[0]) == "F" { - g.emitFlagPredicateByte(g.condCode(f, t.Vals[0])) - if dst := canonicalReturnLoc(f.Contract.Returns[0].Class, TyBool); dst != "A" { - g.emitLD8(dst, "A") - } - } else { - g.emitParallelCopy(g.buildReturnCopies(t.Vals)) - } - // main() is the program entry point: use XOR A / DI / HALT instead of - // RET so mze/mzx can detect program termination cleanly. XOR A zeroes - // A for a clean exit code 0. - if f.Name == "main" && len(t.Vals) == 0 { - g.emit(" XOR A") - g.emit(" DI") - g.emit(" HALT") - } else { - g.emit(" RET") - } - - case *TermJmp: - // Resolve block-argument parallel copies before the jump. - copies := g.buildBlockCopies(f, t.Target, t.Args) - g.emitParallelCopy(copies) - // Fall-through elimination: if the target is the very next physical block, - // the JR is redundant — execution falls through naturally. - if !g.isFallThrough(f, t.Target) { - g.emitf(" JRS %s", blockLabel(f.Name, t.Target)) - } - - case *TermBrIf: - cc := g.condCode(f, t.Cond) - - // Build block-arg copies for each edge. - thenCopies := g.buildBlockCopies(f, t.Then, t.ThenArgs) - elseCopies := g.buildBlockCopies(f, t.Else, t.ElseArgs) - - // If a branch has non-trivial copies, it cannot be a fall-through: - // redirect it through a trampoline that performs the copies then JR. - thenLbl := g.branchLabel(f, t.Then, thenCopies) - elseLbl := g.branchLabel(f, t.Else, elseCopies) - - // Fall-through is only possible when there are no copies for that edge. - thenFT := g.isFallThrough(f, t.Then) && len(thenCopies) == 0 - elseFT := g.isFallThrough(f, t.Else) && len(elseCopies) == 0 - - // Two-condition sentinels: CmpGt/CmpLe with lhs in A (no operand swap). - // CGT = NC && NZ (a > b); CLE = C || Z (a ≤ b). - // Note: CLE/CGT need two jumps — first uses JR, second uses JR too. - // MZA auto-promotes any JR to JP when out of range. - if cc == "CLE" { - // a ≤ b: branch to then if C (a b: branch to else if Z (a==b) or C (a). - // Emit: CP rhs (with A=lhs), then JR Z eq, JR C lt, fall-through/JR gt. - lhs := g.loc(t.Lhs) - rhs := g.loc(t.Rhs) - - // Emit the CP (same logic as genCmp). - if cv, ok := g.constVals[t.Rhs]; ok { - if !g.holdsValue("A", lhs) { - g.emitLDA(lhs) - } - g.emitf(" CP %d", cv&0xFF) - } else if rhs == "A" && lhs != "A" { - // A already holds rhs; swap: CP lhs computes rhs-lhs. - // Eq is still Z (rhs-lhs==0 ↔ rhs==lhs), but C now means rhs= len(bodyBlock.Params) { - break - } - param := bodyBlock.Params[i+1] // skip counter param[0] - src := g.loc(arg) - dst := g.loc(param.Dst) - if src != dst { - bodyCopies = append(bodyCopies, parallelCopy{srcName: src, dstName: dst, ty: param.Ty}) - } - } - } - g.emitParallelCopy(bodyCopies) - g.emitf(" DJNZ %s", blockLabel(f.Name, t.Body)) - // Exit path. - exitCopies := g.buildBlockCopies(f, t.Exit, t.ExitArgs) - g.emitParallelCopy(exitCopies) - if !g.isFallThrough(f, t.Exit) || len(exitCopies) != 0 { - g.emitf(" JP %s", blockLabel(f.Name, t.Exit)) - } - - case *TermCondRet: - // Strategy: jump to Then FIRST (before moving return values), - // because moving return values may emit instructions that clobber - // the flag register the condition depends on (e.g. OR A before SBC). - cc := g.condCode(f, t.Cond) - copies := g.buildBlockCopies(f, t.Then, t.ThenArgs) - thenLbl := g.branchLabel(f, t.Then, copies) - retCopies := g.buildReturnCopies(t.Vals) - thenFT := g.isFallThrough(f, t.Then) && len(copies) == 0 - - switch cc { - case "CLE": - // a ≤ b: take Then if C (a b: take Then if NC && NZ. - // Invert: skip Then (jump to ret path) if C or Z. - retLbl := fmt.Sprintf(".%s_cret%d", sanitizeIdent(f.Name), g.trampIdx) - g.trampIdx++ - g.emitf(" JRS Z, %s", retLbl) - g.emitf(" JRS C, %s", retLbl) - if !thenFT { - g.emitf(" JRS %s", thenLbl) - } - // Register ret path as trampoline so it's emitted AFTER the fallthrough - // then-block (otherwise the inline label would block the fallthrough path). - g.trampolines = append(g.trampolines, trampolineBlock{ - label: retLbl, - copies: retCopies, - isRet: true, - }) - default: - if thenFT && len(retCopies) == 0 { - // Then-block is the fallthrough AND return path is empty: - // use RET invertCC (1 byte, optimal). Only safe when retCopies is - // empty — otherwise retCopies would corrupt registers that @then - // reads, since they execute before the branch is resolved. - g.emitf(" RET %s", invertCC(cc)) - } else if thenFT { - // Then-block is the fallthrough but retCopies are non-trivial. - // Jump to @then when condition is true (skipping the return path), - // then fall through to @then naturally. - g.emitf(" JRS %s, %s", cc, thenLbl) - g.emitParallelCopy(retCopies) - g.emit(" RET") - } else { - // General case: emit conditional jump to Then first, then return path. - g.emitf(" JRS %s, %s", cc, thenLbl) - g.emitParallelCopy(copies) - g.emitParallelCopy(retCopies) - g.emit(" RET") - } - } - - case *TermUnreachable: - g.comment("unreachable") - } -} - -// branchLabel returns the label to jump to for a BrIf edge. -// If the edge has parallel copies, a trampoline block is registered and its -// label is returned instead of the real target label. -func (g *z80cg) branchLabel(f *Func, target string, copies []parallelCopy) string { - realLbl := blockLabel(f.Name, target) - if len(copies) == 0 { - return realLbl - } - tramLbl := fmt.Sprintf(".%s_trmp%d", sanitizeIdent(f.Name), g.trampIdx) - g.trampIdx++ - g.trampolines = append(g.trampolines, trampolineBlock{ - label: tramLbl, - copies: copies, - target: realLbl, - }) - return tramLbl -} - -// buildReturnCopies computes the parallel copies needed to move return values -// into their calling-convention physical registers. -// Using parallel copy resolution (instead of sequential emitMov) is essential -// when two return values are allocated to the same physical register after -// constant folding/PBQP — sequential moves would clobber one value. -func (g *z80cg) buildReturnCopies(vals []Reg) []parallelCopy { - var copies []parallelCopy - for i, rv := range vals { - if i >= len(g.fn.Contract.Returns) { - break - } - if rv == NoReg { - continue // void call result; return register already correct - } - ret := g.fn.Contract.Returns[i] - retLoc := canonicalReturnLoc(ret.Class, ret.Ty) - src := g.loc(rv) - if src != retLoc && !g.holdsValue(retLoc, src) { - copies = append(copies, parallelCopy{srcName: src, dstName: retLoc, ty: ret.Ty}) - } - } - return copies -} - -// buildBlockCopies computes the parallel copies needed to pass args to a -// target block's formal parameters. Returns only moves where src ≠ dst. -func (g *z80cg) buildBlockCopies(f *Func, targetName string, args []Reg) []parallelCopy { - if len(args) == 0 { - return nil - } - var target *Block - for _, b := range f.Blocks { - if b.Label == targetName { - target = b - break - } - } - if target == nil { - return nil - } - var copies []parallelCopy - for i, arg := range args { - if i >= len(target.Params) { - break - } - dst := g.loc(target.Params[i].Dst) - ty := target.Params[i].Ty - // Use g.loc (which respects physOverride) so that values saved to scratch - // registers by materializePendingAcc/materializePendingFlag are correctly - // moved to their canonical destination in the target block. - src := g.loc(arg) - if src == dst { - continue // pre-coalescing or same-location allocation — no copy needed - } - // If this argument is a compile-time constant, mark it for deferred - // rematerialisation AFTER all register-to-register copies are resolved. - // Emitting LD dst, imm inline would clobber live source registers that - // other copies still need (BUG-002). - // Guard: only rematerialise if src != dst (handled above) to avoid - // overwriting a live value when a const reg was pre-coalesced with its - // loop param (they share a root; the const check would fire spuriously). - if cv, isConst := g.constVals[arg]; isConst && dst != "" { - copies = append(copies, parallelCopy{dstName: dst, ty: ty, isImm: true, immVal: cv}) - continue - } - if src != "" { - copies = append(copies, parallelCopy{srcName: src, dstName: dst, ty: ty}) - } - } - return copies -} - -// emitParallelCopy resolves and emits a set of parallel register moves, -// correctly handling cycles (e.g. HL↔DE → EX DE,HL). -// Constant rematerialisations (isImm=true) are emitted AFTER all register-to- -// register moves so they don't clobber live source registers. -func (g *z80cg) emitParallelCopy(copies []parallelCopy) { - if len(copies) == 0 { - return - } - type move struct { - src, dst string - ty Ty - done bool - } - // Separate constants from register moves; emit register moves first. - var immCopies []parallelCopy - var regCopies []parallelCopy - for _, c := range copies { - if c.isImm { - immCopies = append(immCopies, c) - } else { - regCopies = append(regCopies, c) - } - } - moves := make([]move, len(regCopies)) - for i, c := range regCopies { - moves[i] = move{src: c.srcName, dst: c.dstName, ty: c.ty} - } - - for { - // Emit any move whose dst is not a source of another pending move. - progress := false - for i := range moves { - if moves[i].done { - continue - } - dstNeeded := false - for j := range moves { - if j != i && !moves[j].done && moves[j].src == moves[i].dst { - dstNeeded = true - break - } - } - if !dstNeeded { - g.emitSingleCopy(moves[i].src, moves[i].dst, moves[i].ty) - moves[i].done = true - progress = true - break - } - } - if progress { - continue - } - - // All remaining moves form cycles. Find first undone. - first := -1 - undone := 0 - for i := range moves { - if !moves[i].done { - undone++ - if first == -1 { - first = i - } - } - } - if first == -1 { - break - } - - // Special case: HL↔DE two-node cycle → EX DE, HL. - if undone == 2 { - a, b := -1, -1 - for i := range moves { - if !moves[i].done { - if a == -1 { - a = i - } else { - b = i - } - } - } - as, ad := moves[a].src, moves[a].dst - bs, bd := moves[b].src, moves[b].dst - if (as == "HL" && ad == "DE" && bs == "DE" && bd == "HL") || - (as == "DE" && ad == "HL" && bs == "HL" && bd == "DE") { - g.emit(" EX DE, HL") - moves[a].done = true - moves[b].done = true - continue - } - } - - // General cycle: break with a temporary. - m := &moves[first] - if m.ty.Width() <= 8 { - // u8 cycle: break by saving the first node's source to a scratch register. - // - // Walk BACKWARD through the cycle so we always read old values. - // Example 3-cycle D→C, C→E, E→D (scratch=A): - // LD A, D (save D into scratch) - // LD D, E (backward: who writes TO D? E→D. E still original.) - // LD E, C (backward: who writes TO E? C→E. C still original.) - // LD C, A (put D_old into C via scratch) - // - // When A is part of the cycle, using A as scratch would overwrite it - // during the walk before the final restore. Pick the first 8-bit - // register not involved in any pending move instead. - cycleRegs := map[string]bool{} - for i := range moves { - if !moves[i].done { - cycleRegs[moves[i].src] = true - cycleRegs[moves[i].dst] = true - } - // Also mark done-move destinations as live (they hold values). - if moves[i].done { - cycleRegs[moves[i].dst] = true - } - } - scratch := "A" - if cycleRegs["A"] { - for _, r := range []string{"C", "E", "H", "L", "D", "B"} { - if !cycleRegs[r] { - scratch = r - break - } - } - } - - if cycleRegs[scratch] { - // All seven byte registers are occupied. Snapshot the pending - // sources on the stack; shadow AF protects the main accumulator. - var pending []int - for i := range moves { - if moves[i].done { - continue - } - pending = append(pending, i) - if moves[i].src != "A" { - g.emit(" EX AF, AF'") - g.emitLD8("A", moves[i].src) - } - g.emit(" PUSH AF") - if moves[i].src != "A" { - g.emit(" EX AF, AF'") - } - } - for j := len(pending) - 1; j >= 0; j-- { - i := pending[j] - if moves[i].dst != "A" { - g.emit(" EX AF, AF'") - } - g.emit(" POP AF") - if moves[i].dst != "A" { - g.emitLD8(moves[i].dst, "A") - g.emit(" EX AF, AF'") - } - moves[i].done = true - } - continue - } - - firstDst := m.dst - if m.src != scratch { - g.emitSingleCopy(m.src, scratch, m.ty) - } - m.done = true - cur := m.src // start at the freed slot (m.src saved to scratch) - for { - found := false - for i := range moves { - if !moves[i].done && moves[i].dst == cur { - g.emitSingleCopy(moves[i].src, moves[i].dst, moves[i].ty) - cur = moves[i].src // the source is the new freed slot - moves[i].done = true - found = true - break - } - } - if !found { - break - } - } - if firstDst != scratch { - g.emitSingleCopy(scratch, firstDst, m.ty) - } - } else { - // Walk backwards around a word cycle, just as for byte cycles. - // Saving src frees src, not dst: BC->HL, HL->DE, DE->BC - // becomes PUSH BC; BC=DE; DE=HL; POP HL. - if isSpill(m.src) { - g.emit(" PUSH HL") - g.loadSpill16("HL", m.src) - g.emit(" EX (SP), HL") - } else { - g.emitf(" PUSH %s", m.src) - } - m.done = true - cur := m.src - for { - found := false - for i := range moves { - if !moves[i].done && moves[i].dst == cur { - g.emitSingleCopy(moves[i].src, moves[i].dst, moves[i].ty) - cur = moves[i].src - moves[i].done = true - found = true - break - } - } - if !found { - break - } - } - if isSpill(m.dst) { - g.emit(" EX (SP), HL") - g.storeSpill16(m.dst, "HL") - g.emit(" POP HL") - } else { - g.emitf(" POP %s", m.dst) - } - } - } - // After all register-to-register moves are resolved, emit constant assignments. - // Done last so they cannot clobber live source registers. - for _, c := range immCopies { - if c.dstName == "F" { - // F register: use AND A (clear=0) or SCF (set=1). - if c.immVal == 0 { - g.emit(" AND A") // clears C, sets Z - } else { - g.emit(" SCF") // sets C - } - } else if isSpill(c.dstName) { - g.emit(" EX AF, AF'") - g.emitf(" LD A, %d", c.immVal&0xFF) - g.emitf(" LD (%s), A", c.dstName) - g.emit(" EX AF, AF'") - } else if c.ty.Width() <= 8 { - g.emitf(" LD %s, %d", c.dstName, c.immVal&0xFF) - } else { - g.emitf(" LD %s, %d", c.dstName, c.immVal&0xFFFF) - } - } -} - -// emitSingleCopy emits a non-destructive register copy (src → dst). -// For 16-bit, this emits two LD instructions (not EX DE,HL which is a swap). -func (g *z80cg) emitSingleCopy(src, dst string, ty Ty) { - if src == dst { - return - } - // F register: cannot be accessed directly. Materialise flag→register or - // register→flag via the same logic as emitMov. - if src == "F" { - if g.emitKnownFlagCopy(dst, ty.Width()) { - return - } - // Flag → register: SBC A,A materialises carry into A (0xFF/0x00). - if dst == "A" { - g.emit(" SBC A, A") - } else if isPairReg(dst) { - // Flag → 16-bit pair: materialise + zero-extend. - g.emit(" SBC A, A") - g.emitf(" LD %s, A", lowByte(dst)) - g.emitf(" LD %s, 0", highByte(dst)) - } else { - g.emit(" SBC A, A") - g.emitLD8(dst, "A") - } - g.invalidate("A") - return - } - if dst == "F" { - // Register → flag: set Z/NZ from value via AND A. - if src != "A" { - g.emitf(" LD A, %s", src) - } - g.emit(" AND A") - g.invalidate("A") - return - } - if ty.Width() <= 8 { - switch { - case isSpill(src) && dst == "A": - g.emitf(" LD A, (%s)", src) - case isSpill(src): - g.emitf(" LD A, (%s)", src) - g.emitLD8(dst, "A") - g.invalidate("A") - case isSpill(dst) && src == "A": - g.emitf(" LD (%s), A", dst) - case isSpill(dst): - g.emitLDA(src) - g.emitf(" LD (%s), A", dst) - case (isIXYReg(src) && (dst == "H" || dst == "L")) || - (isIXYReg(dst) && (src == "H" || src == "L")): - // DD/FD prefix conflict: LD H,IXH encodes as LD IXH,IXH (NOP). - // Route through shadow A to preserve main A. - g.emitMovViaAltA(dst, src) - case isPairReg(src) && !isPairReg(dst): - // Width mismatch: 16-bit source → 8-bit dest. Truncate (take low byte). - lo := lowByte(src) - if (isIXYReg(lo) && (dst == "H" || dst == "L")) || - ((lo == "H" || lo == "L") && isIXYReg(dst)) { - g.emitMovViaAltA(dst, lo) - } else { - g.emitLD8(dst, lo) - } - case !isPairReg(src) && isPairReg(dst): - // Width mismatch: 8-bit source → 16-bit dest. Zero-extend. - lo := lowByte(dst) - if (isIXYReg(src) && (lo == "H" || lo == "L")) || - ((src == "H" || src == "L") && isIXYReg(lo)) { - g.emitMovViaAltA(lo, src) - } else { - g.emitLD8(lo, src) - } - g.emitf(" LD %s, 0", highByte(dst)) - default: - g.emitLD8(dst, src) - } - } else if isSpill(src) && isSpill(dst) { - // Spill-to-spill: route through HL. - g.loadSpill16("HL", src) - g.storeSpill16(dst, "HL") - g.invalidate("HL") - } else if isSpill(src) { - // LocMem spill → register: use loadSpill16 (handles TSMC). - if isPairReg(dst) { - g.loadSpill16(dst, src) - } else { - // Spill → 8-bit: load low byte only - g.loadSpill8(dst, src) - } - } else if isSpill(dst) { - // Register → LocMem spill: use storeSpill16 (handles TSMC). - if isPairReg(src) { - g.storeSpill16(dst, src) - } else { - // 8-bit → spill - g.storeSpill8(dst, src) - } - } else if isPairReg(src) && !isPairReg(dst) { - // Width mismatch in 16-bit context: pair→8bit truncation. - lo := lowByte(src) - if (isIXYReg(lo) && (dst == "H" || dst == "L")) || - ((lo == "H" || lo == "L") && isIXYReg(dst)) { - g.emitMovViaAltA(dst, lo) - } else { - g.emitLD8(dst, lo) - } - } else if !isPairReg(src) && isPairReg(dst) { - // Width mismatch in 16-bit context: 8bit→pair zero-extension. - lo := lowByte(dst) - if (isIXYReg(src) && (lo == "H" || lo == "L")) || - ((src == "H" || src == "L") && isIXYReg(lo)) { - g.emitMovViaAltA(lo, src) - } else { - g.emitLD8(lo, src) - } - g.emitf(" LD %s, 0", highByte(dst)) - } else { - // 16-bit non-destructive copy: two 8-bit LDs. - hi := highByte(dst) - lo := lowByte(dst) - hiS := highByte(src) - loS := lowByte(src) - // DD/FD prefix conflict: LD H,IXH / LD L,IXL encode as NOPs. - // IX↔HL copies must use PUSH/POP or route through A. - if (isIXY(src) && dst == "HL") || (isIXY(dst) && src == "HL") { - g.emitf(" PUSH %s", src) - g.emitf(" POP %s", dst) - } else if (isIXYReg(hiS) && (hi == "H" || hi == "L")) || - (isIXYReg(loS) && (lo == "H" || lo == "L")) || - ((hiS == "H" || hiS == "L") && isIXYReg(hi)) || - ((loS == "H" || loS == "L") && isIXYReg(lo)) { - // Route through shadow A to avoid clobbering main A. - g.emit(" EX AF, AF'") - g.emitf(" LD A, %s", hiS) - g.emitLD8(hi, "A") - g.emitf(" LD A, %s", loS) - g.emitLD8(lo, "A") - g.emit(" EX AF, AF'") - } else { - g.emitLD8(hi, hiS) - g.emitLD8(lo, loS) - } - } -} - -// condCode returns the Z80 condition code for the virtual register holding -// the comparison result. If genCmp swapped the operands, the condition -// is automatically inverted via CmpCond.Swap(). -// -// For ClassFlag returns from CALL instructions, callFlags[cond] holds the -// agreed FlagCond directly — no AND A / CP 0 is needed at the call site. -func (g *z80cg) condCode(f *Func, cond Reg) string { - // Fast path: call result with ClassFlag return — condition already in F. - if fc, ok := g.callFlags[cond]; ok { - return cmpCondCode(fc) - } - - for _, b := range f.Blocks { - for _, inst := range b.Insts { - if inst.Dst == cond && inst.Op == OpCmp { - c := inst.Cond - if g.cmpSwapped[cond] { - c = c.Swap() // operands were swapped → invert condition - } - // When genCmp emitted AND A (rhs=0), carry is always 0. - // Replace NC/C-based conditions with NZ/Z-based ones: - // CmpGt(n, 0) → n != 0 → NZ - // CmpGe(n, 0) → n >= 0 → always true → handled as NC (fine) - // CmpLt(n, 0) → never for unsigned → C (never fires) - // CmpLe(n, 0) → n == 0 → Z - if g.cmpAndZero[cond] { - switch c { - case CmpGt, CmpUgt: - return "NZ" - case CmpLe, CmpUle: - return "Z" - } - } - if g.cmpNeedsTwo[cond] { - switch c { - case CmpGt, CmpUgt: - return "CGT" // NC && NZ: JP Z,else; JP NC,then - case CmpLe, CmpUle: - return "CLE" // C || Z: JP C,then; JP Z,then - } - } - return cmpCondCode(c) - } - } - } - return "NZ" // fallback -} - -// isFallThrough reports whether targetLabel is the next physical block after -// the current one (g.blockIdx), making a JP redundant. -func (g *z80cg) isFallThrough(f *Func, targetLabel string) bool { - next := g.blockIdx + 1 - if next >= len(f.Blocks) { - return false - } - return f.Blocks[next].Label == targetLabel -} - -// invertCC returns the Z80 condition code that is the logical negation of cc. -func invertCC(cc string) string { - switch cc { - case "Z": - return "NZ" - case "NZ": - return "Z" - case "C": - return "NC" - case "NC": - return "C" - case "PE": - return "PO" - case "PO": - return "PE" - case "M": - return "P" - case "P": - return "M" - } - return cc -} - -// cmpCondCode maps CmpCond → Z80 condition string. -func cmpCondCode(c CmpCond) string { - switch c { - case CmpEq: - return "Z" - case CmpNe: - return "NZ" - case CmpLt, CmpUlt, CmpSubCarry: - return "C" - case CmpGe, CmpUge, CmpSubCarryNot: - return "NC" - case CmpLe, CmpUle: - // LE: JP Z / JP C — requires two jumps; emit Z for now (backend TODO). - return "Z" - case CmpGt, CmpUgt: - return "NC" - } - return "NZ" -} - -// canonicalReturnLoc returns the physical register name where a return value of -// the given class and type must reside on function exit (calling convention). -func canonicalReturnLoc(cls RegClass, ty Ty) string { - w := ty.Width() - switch cls { - case ClassFlag: - // Flag-return ABI: result lives in the CPU flag register (F), not in A. - // Callers test with JP directly after the CALL instruction. - // "F" is the sentinel name for LocFlag in Z80PhysLocs. - return "F" - case ClassAcc: - if w <= 8 { - return "A" - } - return "HL" // u16 [acc] → HL - case ClassPointer, ClassPair: - return "HL" - case ClassIndex: - return "DE" - case ClassCounter: - return "B" - case ClassRegC: - return "C" - case ClassRegD: - return "D" - case ClassRegE: - return "E" - case ClassRegH: - return "H" - case ClassRegL: - return "L" - case ClassIX: - return "IX" - case ClassIY: - return "IY" - case ClassGeneral: - if w <= 8 { - return "C" - } - return "HL" - default: - if w <= 8 { - return "A" - } - return "HL" - } -} - -// standardParamClass returns the register class for position i in the -// standard callable ABI (used by function pointers / indirect calls). -// Matches classForParam in hir/lower.go. -func standardParamClass(ty Ty, pos int) RegClass { - if ty == TyPtr { - return ClassPointer - } - if ty.Width() > 8 { - if pos == 0 { - return ClassPointer // HL - } - return ClassIndex // DE - } - switch pos { - case 0: - return ClassAcc // A - case 1: - return ClassGeneral // C - case 2: - return ClassCounter // B - default: - return ClassGeneral - } -} - -// inferTyFromAlloc guesses the type of a virtual register from its allocated -// physical location. 16-bit pair → TyU16; 8-bit reg → TyU8. -func inferTyFromAlloc(ar *AllocResult, r Reg) Ty { - loc := ar.Loc(r) - switch loc.Kind { - case LocReg: - if isPairReg(loc.Name) { - return TyU16 - } - return TyU8 - case LocIXY: - return TyU16 - case LocIXY8, LocShadow: - return TyU8 - default: - return TyU8 - } -} - -// ── Label helpers ───────────────────────────────────────────────────────────── - -func blockLabel(funcName, blockLabel string) string { - if blockLabel == "entry" { - return sanitizeIdent(funcName) - } - return fmt.Sprintf(".%s_%s", sanitizeIdent(funcName), sanitizeIdent(blockLabel)) -} - -// isIntrinsic reports whether sym is a built-in intrinsic handled inline by -// genCall (i.e. the call does NOT lower to a real CALL/JP instruction). -// Intrinsics must NOT be treated as tail calls because genCall returns early -// without consuming g.tailCallInst, which would cause genTerm to skip RET. -func isIntrinsic(sym string) bool { - return strings.HasPrefix(sym, "@mir.") -} - -// z80ReservedNames are Z80 register names that cannot be used as labels. -var z80ReservedNames = map[string]bool{ - "A": true, "B": true, "C": true, "D": true, "E": true, "F": true, - "H": true, "L": true, "I": true, "R": true, - "AF": true, "BC": true, "DE": true, "HL": true, "SP": true, - "IX": true, "IY": true, "IXH": true, "IXL": true, "IYH": true, "IYL": true, -} - -func sanitizeIdent(s string) string { - var b strings.Builder - for _, c := range s { - if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || - (c >= '0' && c <= '9') || c == '_' { - b.WriteRune(c) - } else { - b.WriteByte('_') - } - } - result := b.String() - // Avoid Z80 register name collisions (e.g. Pascal variable "I"). - if z80ReservedNames[strings.ToUpper(result)] { - return "v_" + result - } - return result -} - -// ── 16-bit register half helpers ────────────────────────────────────────────── -// -// These helpers encode the project's "first-class half-register" policy: -// HL/DE/BC/IX/IY all expose stable low/high 8-bit halves for generic backend -// reasoning. IXH/IXL/IYH/IYL must be considered alongside H/L/D/E/B/C in all -// legal direct paths; they are not special-cased only for bit ops. - -// lowByte returns the low-byte name of a 16-bit register. -func lowByte(rr string) string { - switch rr { - case "HL": - return "L" - case "DE": - return "E" - case "BC": - return "C" - case "IX": - return "IXL" - case "IY": - return "IYL" - } - return rr // already 8-bit -} - -// highByte returns the high-byte name of a 16-bit register. -func highByte(rr string) string { - switch rr { - case "HL": - return "H" - case "DE": - return "D" - case "BC": - return "B" - case "IX": - return "IXH" - case "IY": - return "IYH" - } - return rr -} - -// isPairReg reports whether r is a 16-bit register pair name. -func isPairReg(r string) bool { - switch r { - case "HL", "DE", "BC", "IX", "IY": - return true - } - return false -} - -// parentPair returns the 16-bit register pair containing an 8-bit register. -func parentPair(r string) string { - switch r { - case "A": - return "AF" // rarely useful; callers should special-case A - case "B", "C": - return "BC" - case "D", "E": - return "DE" - case "H", "L": - return "HL" - case "IXH", "IXL": - return "IX" - case "IYH", "IYL": - return "IY" - } - return r // already a pair or unknown -} - -// promote8toPair zero-extends an 8-bit register into a 16-bit pair suitable -// for ADD HL,rr or SBC HL,rr. For A, routes through BC (since AF is not a -// valid arithmetic pair). For others, uses parentPair with high byte zeroed. -// Returns the pair name to use. -func (g *z80cg) promote8toPair(r string) string { - if r == "A" { - g.emit(" LD C, A") - g.emit(" LD B, 0") - return "BC" - } - pair := parentPair(r) - g.emitf(" LD %s, 0", highByte(pair)) - return pair -} - -// genCmp16 emits a 16-bit comparison using SBC HL, rr. -// -// Z80 only provides SBC HL, rr for 16-bit arithmetic that sets flags. -// The sequence is: -// -// OR A ; clear carry (preserves A; sets S/Z/H/PV from A) -// SBC HL, rr ; HL = lhs − rhs − 0; sets C (borrow) and Z (equal) -// -// SBC is destructive (clobbers HL). We save/restore HL with PUSH/POP so -// that lhs remains available for subsequent uses (e.g. the "return a" branch -// of min16). -// -// Conditions after SBC HL, rr (unsigned): -// -// C flag set → lhs < rhs (CmpUlt / CmpUge after negation) -// Z flag set → lhs == rhs -// -// Signed comparisons with an explicit signed SrcTy normalize S⊕V to carry -// after restoring HL, so the branch/materialization paths share one contract. -func (g *z80cg) genCmp16(inst *Inst) { - lhs := g.loc(inst.Src[0]) - rhs := g.loc(inst.Src[1]) - defer g.saveWrittenScratch(inst)() - - // CmpGt/CmpLe traditionally swap operands so SBC HL,rr gives the C flag - // directly. However, when lhs=HL and rhs=DE (common case), the swap - // forces EX DE,HL before+after SBC — 2 extra bytes, 16 extra T-states. - // - // Optimisation: when lhs is already in HL, do NOT swap; compute lhs-rhs - // directly and use a two-jump pattern (CGT/CLE) at the branch site. - // Cost: same bytes in branch, but saves the two EX DE,HL around the SBC. - isGtOrLe := inst.Cond == CmpGt || inst.Cond == CmpUgt || - inst.Cond == CmpLe || inst.Cond == CmpUle - if isGtOrLe { - if lhs == "HL" && isPairReg(rhs) { - // lhs already in HL — skip swap, use CGT/CLE two-jump pattern. - g.cmpNeedsTwo[inst.Dst] = true - // Do NOT set cmpSwapped — we keep original operand order. - } else { - // Fallback: swap operands as before. - lhs, rhs = rhs, lhs - g.cmpSwapped[inst.Dst] = true - } - } - - // A byte lhs must not be extended into HL while rhs still lives there. - // Stage both values before changing either register (also covers D/E). - if rhs == "HL" && (isIXYReg(lhs) || (isSimpleReg(lhs) && !isPairReg(lhs) && lhs != "F")) { - g.pushWord(lhs) - g.pushWord(rhs) - g.emit(" POP DE") - g.emit(" POP HL") - g.invalidate("DE") - g.invalidate("HL") - lhs, rhs = "HL", "DE" - } - // Guard: materialise F or 8-bit operands to proper 16-bit regs. - if lhs == "F" { - g.emit(" SBC A, A") // materialise flag to A - g.emit(" LD L, A") - g.emit(" LD H, 0") - g.invalidate("A") - g.invalidate("HL") - lhs = "HL" - } else if isIXYReg(lhs) { - // IXH/IXL/IYH/IYL → zero-extend to HL via shadow A. - g.emitMovViaAltA("L", lhs) - g.emit(" LD H, 0") - g.invalidate("HL") - lhs = "HL" - } else if isSimpleReg(lhs) && !isPairReg(lhs) { - // 8-bit reg → zero-extend to HL - g.emitLD8("L", lhs) - g.emit(" LD H, 0") - g.invalidate("HL") - lhs = "HL" - } - if rhs == "F" || isIXYReg(rhs) || (isSimpleReg(rhs) && !isPairReg(rhs)) { - // Already handled below in the SBC guard, but mark for safety. - } - - // SBC HL, rr requires lhs in HL. If lhs is already HL we proceed. - // If rhs is HL and lhs is DE, EX DE,HL is the cheapest fix. - // Otherwise move lhs byte-by-byte into HL. - origLhs, origRhs := lhs, rhs - if lhs != "HL" { - if lhs == "DE" && rhs == "HL" { - // Swap via EX DE,HL: after EX, HL=old_DE=lhs, DE=old_HL=rhs. - g.emit(" EX DE, HL") - g.invalidate("HL") - g.invalidate("DE") - lhs, rhs = "HL", "DE" - } else if rhs == "HL" { - // rhs is HL, lhs is BC or IX/IY. Swap: save rhs(HL)→DE, load lhs→HL. - g.emitf(" LD D, H") // save rhs high into D - g.emitf(" LD E, L") // save rhs low into E - if isIXY(lhs) { - // IX→HL: byte-copy invalid. Use PUSH/POP. - g.emitf(" PUSH %s", lhs) - g.emit(" POP HL") - } else if isIXYReg(lhs) || (isSimpleReg(lhs) && !isPairReg(lhs)) { - // 8-bit register: zero-extend to HL. - g.emitMovViaAltA("L", lhs) - g.emit(" LD H, 0") - } else if isSpill(lhs) { - g.loadSpill16("HL", lhs) - } else { - g.emitf(" LD H, %s", highByte(lhs)) - g.emitLD8("L", lowByte(lhs)) - } - g.invalidate("HL") - g.invalidate("DE") - lhs, rhs = "HL", "DE" - } else if isIXY(lhs) { - // IX/IY → HL: byte-copy invalid (DD prefix conflict). Use PUSH/POP. - g.emitf(" PUSH %s", lhs) - g.emit(" POP HL") - g.invalidate("HL") - lhs = "HL" - } else if isIXYReg(lhs) || (isSimpleReg(lhs) && !isPairReg(lhs)) { - // 8-bit register (including IXH/IXL): zero-extend into HL. - g.emitMovViaAltA("L", lhs) - g.emit(" LD H, 0") - g.invalidate("HL") - } else if isSpill(lhs) { - g.loadSpill16("HL", lhs) - g.invalidate("HL") - } else { - // Neither operand is HL; move lhs into HL byte-by-byte. - g.emitf(" LD H, %s", highByte(lhs)) - g.emitLD8("L", lowByte(lhs)) - g.invalidate("HL") - lhs = "HL" - } - } - - // Restore the original operand orientation after SBC. The wrapper - // restores any live values overwritten by staging or subtraction. - - swappedDE := origRhs == "HL" && origLhs != "HL" - - // SBC HL, rr requires rhs to be a 16-bit register pair (BC/DE/HL/SP). - // Guard against invalid operands: F, single 8-bit regs, IXY halves. - if rhs == "F" { - // Flag result used as 16-bit comparison operand — materialise to BC. - g.emit(" SBC A, A") // A = 0xFF if carry, 0x00 if not - g.emit(" LD C, A") - g.emit(" LD B, 0") - g.invalidate("A") - rhs = "BC" - } else if isIXYReg(rhs) { - // IXH/IXL/IYH/IYL: load into BC via A. - g.emitf(" LD A, %s", rhs) - g.emit(" LD C, A") - g.emit(" LD B, 0") - g.invalidate("A") - rhs = "BC" - } else if isSimpleReg(rhs) && !isPairReg(rhs) { - // 8-bit register: promote to pair (handles A→BC, others→parentPair). - rhs = g.promote8toPair(rhs) - } else if isIXY(rhs) { - // IX/IY: push to HL would conflict. Push rhs → stack, pop to BC. - g.emitf(" PUSH %s", rhs) - g.emit(" POP BC") - rhs = "BC" - } else if isSpill(rhs) { - // LocMem spill: SBC HL,$F0xx is not valid. Load to BC first. - g.loadSpill16("BC", rhs) - rhs = "BC" - } - - // The selected-sequence wrapper preserves HL only when its original - // value is live. POP restores it without disturbing comparison flags. - g.emit(" OR A") - g.emitSBCHL(rhs) - - if swappedDE { - // EX leaves the comparison flags intact; live originals are restored - // by the wrapper after signed-flag normalization. - - g.emit(" EX DE, HL") - g.invalidate("HL") - g.invalidate("DE") - } else { - g.invalidate("HL") - } - g.normalizeSignedCmp(inst) - g.pendingFlagReg = inst.Dst -} - -// genCmp32 emits a non-destructive 32-bit unsigned comparison (lhs vs rhs) -// via a PUSH/SBC-chain/POP pattern; carry is set if lhs < rhs, NC if lhs >= rhs. -// -// Strategy (lhs=HL+H'L', rhs=DE+D'E'): -// -// PUSH HL ; save a_lo (11T) -// EXX -// PUSH HL ; save a_hi (11T, HL = H'L' after EXX) -// EXX -// AND A ; clear carry (4T) -// SBC HL, DE ; a_lo - b_lo; carry = lo borrow (15T) -// EXX -// SBC HL, DE ; a_hi - b_hi - lo_carry; carry = 32-bit borrow (15T) -// EXX -// EXX ; switch to restore a_hi -// POP HL ; restore H'L' (10T); carry preserved by POP rr -// EXX -// POP HL ; restore HL (10T); carry preserved -// -// Total: 91T. For the typical abs_diff pattern this is followed immediately by -// BrIf which branches on carry — no intermediate store of the flag needed. -func (g *z80cg) genCmp32(inst *Inst) { - lhs := g.loc(inst.Src[0]) - rhs := g.loc(inst.Src[1]) - - // CmpGt/CmpLe: swap operands so the carry encodes the right condition. - // CmpUgt(a,b): a>b ↔ b 2 { - return djnzPeepholeResult{} - } - // Find the OpCmp instruction (last if there are two). - var cmp *Inst - for i := len(check.Insts) - 1; i >= 0; i-- { - if check.Insts[i].Op == OpCmp { - cmp = check.Insts[i] - break - } - } - if cmp == nil || cmp.Cond != CmpNe { - return djnzPeepholeResult{} - } - if g.ar.Loc(cmp.Src[0]).Name != "B" { - return djnzPeepholeResult{} - } - zero, found2 := findConstInFunc(f, cmp.Src[1]) - if !found2 || zero != 0 { - return djnzPeepholeResult{} - } - // Verify all instructions in check block are CmpNe + optional Const(0) only. - for _, inst := range check.Insts { - if inst.Op != OpCmp && inst.Op != OpConst { - return djnzPeepholeResult{} - } - } - brif, ok2 := check.Term.(*TermBrIf) - if !ok2 || brif.Cond != cmp.Dst { - return djnzPeepholeResult{} - } - // Verify all TermJmp args are trivially in-place (excluding counter B→B). - for i, arg := range jmp.Args { - if i >= len(check.Params) { - break - } - srcLoc := g.loc(arg) - dstLoc := g.loc(check.Params[i].Dst) - if srcLoc == "B" && dstLoc == "B" { - continue // counter handled by DJNZ - } - if srcLoc != dstLoc { - return djnzPeepholeResult{} // non-trivial copy, fall back - } - } - // Verify exit-edge copies are trivial. - exitBlock := f.BlockByLabel(brif.Else) - if exitBlock != nil { - for i, arg := range brif.ElseArgs { - if i >= len(exitBlock.Params) { - break - } - if g.ar.Loc(arg).Name != g.ar.Loc(exitBlock.Params[i].Dst).Name { - return djnzPeepholeResult{} // non-trivial exit copy, fall back - } - } - } - return djnzPeepholeResult{ - skipLast: true, - bodyLbl: blockLabel(f.Name, brif.Then), - checkBrif: brif, - } -} - -// ── Function annotation comment ─────────────────────────────────────────────── +// ── Function annotation comment ─────────────────────────────────────────────── // funcAnnotation builds a one-line ABI summary for a function: // @@ -8579,329 +1010,6 @@ func funcAnnotation(f *Func, ar *AllocResult) string { return sb.String() } -// multiReturnLocs returns the physical register name for each return position -// by scanning the function's TermRet instructions and looking up the allocator -// result. This is more accurate than classToRegName(Contract.Returns[i].Class) -// because the contract optimizer may reassign classes after lowering. -// Returns a slice with one entry per return position; entries may be "" if unknown. -func multiReturnLocs(f *Func, ar *AllocResult) []string { - n := len(f.Contract.Returns) - if n == 0 { - return nil - } - for _, b := range f.Blocks { - if ret, ok := b.Term.(*TermRet); ok && len(ret.Vals) == n { - locs := make([]string, n) - for i, v := range ret.Vals { - locs[i] = ar.Loc(v).Name - } - return locs - } - } - return nil -} - -// classToRegName maps a RegClass to the canonical Z80 register name for that class. -// ty is used to disambiguate (e.g. ClassAcc is always A, ClassPointer is HL for 16-bit). -func classToRegName(cls RegClass, ty Ty) string { - switch cls { - case ClassAcc: - return "A" - case ClassPointer: - return "HL" - case ClassIndex: - return "DE" - case ClassCounter: - return "B" - case ClassGeneral: - return "C" - } - return "" -} - -// isRecursive reports whether f contains a direct recursive call to itself. -func isRecursive(f *Func) bool { - for _, b := range f.Blocks { - for _, inst := range b.Insts { - if inst.Op == OpCall && inst.Sym == f.Name { - return true - } - } - } - return false -} - -// callerSavePairs returns the Z80 register pairs that must be PUSH'd/POP'd -// around a CALL to protect live values from the callee's clobbers. -// -// Strategy: compute the callee's clobber set (physical reg names), then check -// which pushable pairs contain registers allocated to virtual regs in the -// caller that remain live after the call, including arguments reused later. -// The call results are excluded because their definitions begin after CALL. -func (g *z80cg) callerSavePairs(inst *Inst, callee *Func) []string { - if callee == nil { - return nil - } - - // The emitter uses implicit scratch registers, runtime helpers and nested - // calls. Preserve every live physical register conservatively until a - // post-emission write-set analysis can prove a smaller clobber contract. - clobberedRegs := map[string]bool{} - for _, name := range computeClobbers(callee, g.ar) { - clobberedRegs[name] = true - } - - // Compute which virtual regs are live AFTER this call instruction. - // A reg is live-across-call if it's used by any instruction after the call - // in the same block, or by the block's terminator, or by a successor block - // using backward CFG liveness and instruction uses/definitions. - liveAfter := g.regsLiveAfterInst(inst) - - // Collect physical regs that are (a) clobbered by callee and (b) hold a - // value that is live after the call. Exclude result definitions only. - excluded := make(map[Reg]bool) - if inst.Dst != NoReg { - excluded[inst.Dst] = true - } - for _, er := range inst.ExtraRets { - excluded[er] = true - } - - livePhys := make(map[string]bool) - for r := range liveAfter { - if excluded[r] { - continue - } - // Use g.loc() which respects physOverride — a vreg may have been - // relocated to a different register by materializePendingAcc or - // save-before-overwrite. The static ar.Locs doesn't reflect these - // runtime relocations. - locName := g.loc(r) - if locName == "" || locName == "?" { - continue - } - if isSpill(locName) { - continue // spilled to memory — not a register - } - if clobberedRegs[locName] { - livePhys[locName] = true - } - } - - // Map individual registers to PUSH-able pairs. - // Z80 can only PUSH/POP pairs: BC, DE, HL (and AF, IX, IY). - pairNeeded := make(map[string]bool) - for reg := range livePhys { - if pair, ok := regToPairMap[reg]; ok { - pairNeeded[pair] = true - } - } - - // Return in stable order. - var result []string - for _, pair := range []string{"AF", "BC", "DE", "HL", "IX", "IY"} { - if pairNeeded[pair] { - result = append(result, pair) - } - } - return result -} - -// regToPairMap maps individual Z80 registers to their PUSH-able pair. -var regToPairMap = map[string]string{ - "A": "AF", "F": "AF", "AF": "AF", - "BC": "BC", "DE": "DE", "HL": "HL", "IX": "IX", "IY": "IY", - "IXH": "IX", "IXL": "IX", "IYH": "IY", "IYL": "IY", - "B": "BC", "C": "BC", - "D": "DE", "E": "DE", - "H": "HL", "L": "HL", -} - -// saveABeforeOverwrite checks if A currently holds a vreg that is still live -// after the current instruction. If so, saves it to a scratch register. -// Used before emitLDA in ALU paths to prevent clobbering live values. -func (g *z80cg) saveABeforeOverwrite(inst *Inst) { - if g.curBlock == nil { - return - } - // Ties between entry parameters prefer the lowest virtual register ID. - // Find the vreg MOST RECENTLY defined in A, but only considering - // definitions BEFORE the current instruction. Only that vreg's - // value is actually in A right now. - instIdx := -1 - for idx, bi := range g.curBlock.Insts { - if bi == inst { - instIdx = idx - break - } - } - - var bestReg Reg - bestIdx := -2 - for r, loc := range g.ar.Locs { - if loc.Kind != LocReg || loc.Name != "A" { - continue - } - if _, ok := g.physOverride[r]; ok { - continue // already saved elsewhere - } - // Find definition index in the current block, only BEFORE inst - defIdx := -2 - for idx, bi := range g.curBlock.Insts { - if idx >= instIdx { - break // stop before current instruction - } - if bi.Dst == r { - defIdx = idx - } - } - // Params are defined before block — use -1 as sentinel - for _, cp := range g.fn.Contract.Params { - if cp.Reg == r && defIdx == -2 { - defIdx = -1 - } - } - if defIdx > bestIdx || (defIdx == bestIdx && defIdx > -2 && r < bestReg) { - bestIdx = defIdx - bestReg = r - } - } - if bestReg == NoReg { - return - } - // Skip if this vreg is not live after the current instruction - if !g.isVregLiveAfter(bestReg, inst) { - return - } - // Skip if this vreg is a source of the current instruction - for _, s := range inst.Src[:] { - if s == bestReg { - return - } - } - scratch := g.pickScratch8(inst) - g.emitf(" LD %s, A ; save r%d before A overwrite", scratch, bestReg) - g.physOverride[bestReg] = scratch -} - -// isVregLiveAfter checks if vreg is used by any instruction after target in the -// current block, or by the block's terminator. -// saveAccOperandIfLive copies the operand vreg held in A to a scratch -// register when an 8-bit ALU result is about to overwrite A and the operand is -// still used after inst. Later reads of the vreg are redirected via physOverride. -func (g *z80cg) saveAccOperandIfLive(src Reg, inst *Inst) { - if src == NoReg || src == inst.Dst || !g.isVregLiveAfter(src, inst) { - return - } - scratch := g.pickScratch8(inst) - g.emitf(" LD %s, A ; save r%d before ALU overwrite", scratch, src) - g.physOverride[src] = scratch - if g.pendingAccReg == src { - g.pendingAccReg = NoReg - } -} - -func (g *z80cg) isVregLiveAfter(vreg Reg, target *Inst) bool { - if g.curBlock == nil || vreg == NoReg { - return false - } - found := false - for _, inst := range g.curBlock.Insts { - if !found { - if inst == target { - found = true - } - continue - } - // Check if vreg is used by this instruction - for _, r := range inst.Uses() { - if r == vreg { - return true - } - } - } - // Check terminator - if g.curBlock.Term != nil { - for _, r := range g.curBlock.Term.termUses() { - if r == vreg { - return true - } - } - } - return false -} - -// regsLiveAfterInst includes successor and loop-backedge uses via CFG liveness. -func (g *z80cg) regsLiveAfterInst(target *Inst) map[Reg]bool { - live := make(map[Reg]bool) - if g.curBlock == nil { - return live - } - lr := ComputeLiveness(g.fn) - if out := lr.LiveOutOf(g.fn, g.curBlock); out != nil { - for _, r := range out.Slice() { - live[r] = true - } - } - if g.curBlock.Term != nil { - for _, r := range g.curBlock.Term.termUses() { - live[r] = true - } - } - for i := len(g.curBlock.Insts) - 1; i >= 0; i-- { - inst := g.curBlock.Insts[i] - if inst == target { - break - } - delete(live, inst.Dst) - for _, r := range inst.ExtraRets { - delete(live, r) - } - for _, r := range inst.Uses() { - if r != NoReg { - live[r] = true - } - } - } - return live -} - -// classPhysRegs returns physical register names associated with a register class. -func classPhysRegs(cls RegClass) []string { - switch cls { - case ClassAcc: - return []string{"A"} - case ClassCounter: - return []string{"B"} - case ClassGeneral: - return []string{"B", "C", "D", "E", "H", "L"} - case ClassPointer: - return []string{"H", "L", "D", "E"} - case ClassIndex: - return []string{"D", "E"} - case ClassPair: - return []string{"H", "L", "D", "E", "B", "C"} - } - return nil -} - -// computeClobbers returns a conservative sorted superset of emitted writes. -func computeClobbers(f *Func, ar *AllocResult) []string { - // Allocation destinations omit implicit writes by ALU lowering, parallel - // copies, caller saves, runtime helpers and transitive calls. ABI-owned - // registers are also clobbered: ownership does not imply preservation. - return []string{"A", "B", "BC", "C", "D", "DE", "E", "F", "H", "HL", "IX", "IXH", "IXL", "IY", "IYH", "IYL", "L"} -} - -func sortStrings(ss []string) { - // Simple insertion sort for small lists. - for i := 1; i < len(ss); i++ { - for j := i; j > 0 && ss[j] < ss[j-1]; j-- { - ss[j], ss[j-1] = ss[j-1], ss[j] - } - } -} - // emitStringLiteral writes bytes as a mix of quoted text and numeric escapes. // Printable ASCII runs are grouped into "...", non-printable bytes are emitted // as individual numeric values. Example: "Hello", 10, "world" @@ -8939,375 +1047,3 @@ func emitStringLiteral(sb *strings.Builder, data []byte) { func isPrintableASCII(b byte) bool { return b >= 0x20 && b <= 0x7E } - -// pushWord stages a value while preserving all register operands and flags. -// EX (SP),HL restores HL and leaves the loaded word on the stack. -func (g *z80cg) pushWord(src string) { - if isPairReg(src) || isIXY(src) { - g.emitf(" PUSH %s", src) - return - } - g.emit(" PUSH HL") - if isSpill(src) { - g.loadSpill16("HL", src) - } else { - g.emit(" PUSH AF") - g.emitLD8("L", src) - g.emit(" LD H, 0") - g.emit(" POP AF") - } - g.emit(" EX (SP), HL") - g.invalidate("HL") -} - -// Only use proofs that survive same-width signed casts: narrower unsigned parameters, -// zero extension from a narrower width, and copies of a proven value. -func (g *z80cg) nonNegative(r Reg, width int, seen map[Reg]bool) bool { - if seen[r] { - return false - } - seen[r] = true - for _, p := range g.fn.Contract.Params { - if p.Reg == r { - return p.Ty.Width() < width && !IsSigned(p.Ty) - } - } - for _, b := range g.fn.Blocks { - for _, i := range b.Insts { - if i.Dst != r { - continue - } - switch i.Op { - case OpExt: - return i.SrcTy != nil && i.Ty != nil && i.SrcTy.Width() < i.Ty.Width() && i.SrcTy.Width() < width - case OpConst: - return i.Imm >= 0 && i.Imm < (int64(1)<<(width-1)) - case OpMove: - return g.nonNegative(i.Src[0], width, seen) - } - } - } - return false -} - -func (g *z80cg) genPow2DivMod(inst *Inst, k int64, signed bool) { - dst, lhs := g.loc(inst.Dst), g.loc(inst.Src[0]) - w := inst.Ty.Width() - wantMod := inst.Op == OpMod || inst.Op == OpSMod - if !signed && !wantMod && w <= 8 && !isSpill(dst) && !isIXY(dst) { - if isPairReg(lhs) { - lhs = lowByte(lhs) - } - g.emitLDA(lhs) - for v := k; v > 1; v >>= 1 { - g.emit(" SRL A") - } - if isPairReg(dst) { - g.emitLD8(lowByte(dst), "A") - g.emitf(" LD %s, 0", highByte(dst)) - } else { - g.emitLD8(dst, "A") - } - g.invalidate("A") - g.invalidate(dst) - return - } - - if !signed && wantMod && k <= 256 && !isIXY(dst) && !isSpill(dst) { - - if isPairReg(lhs) { - lhs = lowByte(lhs) - } - g.emitLDA(lhs) - g.emitf(" AND %d", k-1) - if isPairReg(dst) { - g.emitLD8(lowByte(dst), "A") - g.emitf(" LD %s, 0", highByte(dst)) - } else { - g.emitLD8(dst, "A") - } - - g.invalidate("A") - g.invalidate(dst) - return - } - // HL is temporary unless it is the result. AF holds the original sign. - - if lhs != "HL" { - g.pushWord(lhs) - g.emit(" POP HL") - } - idx := g.trampIdx - g.trampIdx++ - negate := func() { - g.emit(" XOR A") - g.emit(" SUB L") - g.emit(" LD L, A") - if w == 16 { - g.emit(" SBC A, A") - g.emit(" SUB H") - g.emit(" LD H, A") - } - } - if signed { - hi := "H" - if w <= 8 { - hi = "L" - } - g.emitf(" LD A, %s", hi) - g.emit(" PUSH AF") - g.emitf(" BIT 7, %s", hi) - g.emitf(" JR Z, .pow2_abs_%d", idx) - negate() - g.emitf(".pow2_abs_%d:", idx) - } - if wantMod { - mask := k - 1 - g.emit(" LD A, L") - g.emitf(" AND %d", mask&255) - g.emit(" LD L, A") - if w == 16 { - if mask < 256 { - g.emit(" LD H, 0") - } else { - g.emit(" LD A, H") - g.emitf(" AND %d", (mask>>8)&255) - g.emit(" LD H, A") - } - } - } else { - for v := k; v > 1; v >>= 1 { - if w == 16 { - g.emit(" SRL H") - g.emit(" RR L") - } else { - g.emit(" SRL L") - } - } - } - if signed { - g.emit(" POP AF") - g.emit(" OR A") - g.emitf(" JP P, .pow2_done_%d", idx) - negate() - g.emitf(".pow2_done_%d:", idx) - } - if w <= 8 { - g.emitMov(dst, "L", w) - } else { - g.emitMov(dst, "HL", w) - } - - g.invalidate("A") - g.invalidate(dst) - g.invalidate("HL") -} - -// Arithmetic helpers use fixed scratch registers beyond their allocated dst. -// Preserve any unrelated live values there, including byte halves of pairs. -func (g *z80cg) saveLiveScratch(inst *Inst, pairs ...string) func() { - live := g.scratchLiveAfter(inst) - - dst := g.loc(inst.Dst) - accPair := "BC" - if dst == "BC" || dst == "B" || dst == "C" { - accPair = "HL" - } - saved := []string{} - for _, pair := range pairs { - overlaps := func(loc string) bool { - return loc == pair || loc == highByte(pair) || loc == lowByte(pair) || (pair == "AF" && (loc == "A")) - } - if overlaps(dst) { - continue - } - needed := false - for r := range live { - if r != inst.Dst && overlaps(g.loc(r)) { - needed = true - break - } - } - if needed { - if pair == "AF" { - g.emitf(" PUSH %s", accPair) - } - g.emitf(" PUSH %s", pair) - saved = append(saved, pair) - } - } - return func() { - for i := len(saved) - 1; i >= 0; i-- { - if saved[i] == "AF" { - g.emitf(" POP %s", accPair) - g.emitf(" LD A, %s", highByte(accPair)) - g.emitf(" POP %s", accPair) - g.invalidate("A") - } else { - g.emitf(" POP %s", saved[i]) - g.invalidate(saved[i]) - } - } - } -} - -// Comparisons can encode their result in Z, carry, or a two-flag predicate. -// A scalar copy must use that predicate, and must preserve a live A when -// writing another register (e.g. a short-circuit condition's block argument). -func (g *z80cg) emitKnownFlagCopy(dst string, width int) bool { - if g.pendingFlagReg == NoReg { - return false - } - pair := "BC" - if dst == "BC" || dst == "B" || dst == "C" { - pair = "HL" - } - if dst != "A" { - g.emitf(" PUSH %s", pair) - g.emit(" PUSH AF") - } - g.emitFlagPredicateByte(g.condCode(g.fn, g.pendingFlagReg)) - g.emitMov(dst, "A", width) - if dst != "A" { - g.emitf(" POP %s", pair) - g.emitf(" LD A, %s", highByte(pair)) - g.emitf(" POP %s", pair) - g.invalidate(pair) - } - g.invalidate("A") - g.invalidate(dst) - return true -} - -// scratchLiveAfter walks backwards from live-out, killing later definitions. -// A value first defined after inst does not occupy its allocation yet. -func (g *z80cg) scratchLiveAfter(inst *Inst) map[Reg]bool { - live := make(map[Reg]bool) - if g.curBlock == nil { - return live - } - if g.liveness != nil { - out := g.liveness.LiveOutOf(g.fn, g.curBlock) - for r := range g.ar.Locs { - if out != nil && out.Has(r) { - live[r] = true - } - } - } - if g.curBlock.Term != nil { - for _, r := range g.curBlock.Term.termUses() { - live[r] = true - } - } - for i := len(g.curBlock.Insts) - 1; i >= 0; i-- { - next := g.curBlock.Insts[i] - if next == inst { - break - } - delete(live, next.Dst) - for _, r := range next.ExtraRets { - delete(live, r) - } - for _, r := range next.Uses() { - if r != NoReg { - live[r] = true - } - } - } - return live -} - -// Buffer the selected sequence so preservation follows its physical writes, -// rather than a worst-case list for the MIR opcode. Flags are intentionally -// excluded: comparison flags must survive restoration of the accumulator. -func (g *z80cg) saveWrittenScratch(inst *Inst) func() { - outer := g.sb - var body strings.Builder - g.sb = &body - live := g.scratchLiveAfter(inst) - locs := make(map[Reg]string) - for r := range live { - locs[r] = g.loc(r) - } - return func() { - g.sb = outer - writes := scratchSequenceWrites(body.String()) - var pairs []string - for _, pair := range []string{"AF", "HL", "DE", "BC", "IX", "IY"} { - for r, loc := range locs { - if r == inst.Dst { - continue - } - if (loc == pair && (writes[highByte(pair)] || writes[lowByte(pair)])) || - (parentPair(loc) == pair && writes[loc]) { - pairs = append(pairs, pair) - break - } - } - } - restore := g.saveLiveScratch(inst, pairs...) - g.sb.WriteString(body.String()) - restore() - } -} - -// These helpers emit only ordinary scalar Z80 instructions. Unknown operations -// conservatively write all registers, so adding a sequence cannot omit a save. -func scratchSequenceWrites(asm string) map[string]bool { - writes := make(map[string]bool) - mark := func(r string) { - if r == "AF" { - writes["A"] = true - } else if isPairReg(r) { - writes[highByte(r)], writes[lowByte(r)] = true, true - } else if isSimpleReg(r) || isIXYReg(r) { - if r != "F" { - writes[r] = true - } - } - } - for _, line := range strings.Split(asm, "\n") { - line = strings.TrimSpace(strings.SplitN(line, ";", 2)[0]) - fields := strings.Fields(line) - if len(fields) == 0 || strings.HasSuffix(line, ":") { - continue - } - op := fields[0] - args := strings.Split(strings.TrimSpace(strings.TrimPrefix(line, op)), ",") - for i := range args { - args[i] = strings.TrimSpace(args[i]) - } - switch op { - case "LD", "POP", "INC", "DEC", "SLA", "SRA", "SRL", "RL", "RR", "RLC", "RRC": - mark(args[0]) - case "SET", "RES": - if len(args) > 1 { - mark(args[1]) - } - case "ADD", "ADC", "SBC": - if len(args) > 1 { - mark(args[0]) - } else { - mark("A") - } - case "AND", "OR": - if args[0] != "A" { - mark("A") - } - case "SUB", "XOR", "NEG", "RLA", "RRA", "RLCA", "RRCA", "CPL": - mark("A") - case "EX": - for _, r := range args { - mark(r) - } - case "CP", "BIT", "PUSH", "JP", "JR", "SCF", "CCF", "NOP": - case "DJNZ": - mark("B") - default: - for _, r := range []string{"AF", "HL", "DE", "BC", "IX", "IY"} { - mark(r) - } - } - } - return writes -} diff --git a/minzc/pkg/mir2/z80codegen_alu.go b/minzc/pkg/mir2/z80codegen_alu.go new file mode 100644 index 000000000..505200420 --- /dev/null +++ b/minzc/pkg/mir2/z80codegen_alu.go @@ -0,0 +1,1067 @@ +package mir2 + +import ( + "fmt" + "maps" + "math/bits" + "slices" +) + +// emitADDHL emits ADD HL, rr with IX/IY guard. +// Z80 ADD HL only accepts BC/DE/HL/SP — NOT IX/IY. +func (g *z80cg) emitADDHL(rhs string) { + if rhs == "IX" || rhs == "IY" { + g.emit(" PUSH DE") + if rhs == "IX" { + g.emit(" LD D, IXH") + g.emit(" LD E, IXL") + } else { + g.emit(" LD D, IYH") + g.emit(" LD E, IYL") + } + g.emit(" ADD HL, DE") + g.emit(" POP DE") + } else if isSpill(rhs) { + g.emitf(" LD BC, (%s)", rhs) + g.emit(" ADD HL, BC") + } else { + g.emitf(" ADD HL, %s", rhs) + } +} + +// emitSBCHL emits SBC HL, rr with IX/IY/spill/8-bit guard. +// Z80 SBC HL only accepts BC/DE/HL/SP — NOT IX/IY or 8-bit. +func (g *z80cg) emitSBCHL(rhs string) { + if rhs == "IX" || rhs == "IY" { + g.emit(" PUSH DE") + if rhs == "IX" { + g.emit(" LD D, IXH") + g.emit(" LD E, IXL") + } else { + g.emit(" LD D, IYH") + g.emit(" LD E, IYL") + } + g.emit(" SBC HL, DE") + g.emit(" POP DE") + } else if isSpill(rhs) { + g.emitf(" LD BC, (%s)", rhs) + g.emit(" SBC HL, BC") + } else if isSimpleReg(rhs) && !isPairReg(rhs) { + // 8-bit operand: promote to pair via promote8toPair. + pair := g.promote8toPair(rhs) + g.emitf(" SBC HL, %s", pair) + } else { + g.emitf(" SBC HL, %s", rhs) + } +} + +// ── Binary operations ───────────────────────────────────────────────────────── + +func (g *z80cg) genBinOp(mnem string, inst *Inst) { + dst := g.loc(inst.Dst) + lhs := g.loc(inst.Src[0]) + rhs := g.loc(inst.Src[1]) + rhsReg := inst.Src[1] + w := inst.Ty.Width() + + if w == 16 { + defer g.saveWrittenScratch(inst)() + } + + if w >= 24 { + g.genBinOp32(mnem, dst, lhs, rhs) + return + } + + // Word operations also use HL as a working destination. Preserve a rhs + // there before copying lhs, regardless of the allocated result location. + if w == 16 { + rhsValue, rhsConstant := g.constVals[rhsReg] + inPlaceUnit := rhsConstant && rhsValue == 1 && lhs == dst + if (mnem == "ADD" && rhs == "HL" && !inPlaceUnit) || ((mnem == "AND" || mnem == "OR" || mnem == "XOR") && rhs == dst && !rhsConstant) { + lhs, rhs = rhs, lhs + rhsReg = inst.Src[0] + } + if mnem == "SUB" && ((rhs == dst && lhs != dst) || (rhs == "HL" && lhs != "HL")) { + g.pushWord(lhs) + g.pushWord(rhs) + g.emit(" POP DE") + g.emit(" POP HL") + g.emit(" OR A") + g.emit(" SBC HL, DE") + g.emitMov(dst, "HL", 16) + g.invalidate(dst) + return + } + } + + if w <= 8 { + if lhs == dst && dst != "A" && dst != "F" && !isSpill(dst) { + if cv, ok := g.constVals[inst.Src[1]]; ok { + switch mnem { + case "OR": + mask := uint16(cv) + if bits.OnesCount16(mask) == 1 { + bit := bits.TrailingZeros16(mask) + if bit < 8 && g.emitBitRegOp("SET", bit, dst) { + g.invalidate(dst) + return + } + } + case "AND": + mask := uint16(cv) + if bits.OnesCount16(^mask) == 1 { + bit := bits.TrailingZeros16(^mask) + if bit < 8 && g.emitBitRegOp("RES", bit, dst) { + g.invalidate(dst) + return + } + } + } + } + } + + // Special case: boolean OR with a ClassFlag operand — Z80 cannot LD A, F + // or OR F. Materialize the flag condition as 0/1 via a conditional skip: + // (ensure A holds the non-flag operand) + // JR Ncc, skip ; if condition false, skip setting bit 0 + // OR 1 ; A |= 1 (condition true) + // skip: + // Also treat a source as a flag if it's the pending carry-flag result + // (g.loc() may return "A" because PBQP assigned it there, but the + // actual value is still live in the carry flag via pendingFlagReg). + isPendingFlag0 := len(inst.Src) > 0 && inst.Src[0] == g.pendingFlagReg + isPendingFlag1 := len(inst.Src) > 1 && inst.Src[1] == g.pendingFlagReg + if mnem == "OR" && (lhs == "F" || rhs == "F" || isPendingFlag0 || isPendingFlag1) { + var flagSrc Reg + var otherLoc string + if lhs == "F" || isPendingFlag0 { + flagSrc = inst.Src[0] + otherLoc = rhs + } else { + flagSrc = inst.Src[1] + otherLoc = lhs + } + // Consume the pending flag tracking for flagSrc. + if flagSrc == g.pendingFlagReg { + g.pendingFlagReg = NoReg + } + if otherLoc != "A" && !g.holdsValue("A", otherLoc) { + g.emitLDA(otherLoc) + } + g.invalidate("A") + cc := g.condCode(g.fn, flagSrc) + skipLbl := fmt.Sprintf(".%s_orf_%d_sk", sanitizeIdent(g.fn.Name), int(inst.Dst)) + g.emitf(" JR %s, %s", invertCC(cc), skipLbl) + g.emit(" OR 1") + g.emitf("%s:", skipLbl) + if dst != "A" { + g.emitLD8(dst, "A") + g.setCopy(dst, "A") + } + // Result is in A (or dst). Track it as a pending acc value in case + // a subsequent genCmp overwrites A with "LD A, imm". + if dst == "A" { + g.pendingAccReg = inst.Dst + } + return + } + + // If there is a pending ClassAcc value in A that is still live after this + // instruction, save it to a scratch register before we overwrite A. + // This fires when two successive 8-bit ALU ops both need A: the first + // result (r33=acc) must survive while the second (r35=i+1) runs. + // Skip when the peephole below will use INC/DEC (does not touch A). + // INC/DEC doesn't clobber A — only fires when rhs is a small constant. + _, rhsIsConst := g.constVals[inst.Src[1]] + willUseINCDEC := (mnem == "ADD" || mnem == "SUB") && lhs == dst && rhsIsConst + if !willUseINCDEC { + g.materializePendingAcc(inst) + } + + // Peephole: ADD/SUB dst, N where dst == lhs and N ≤ 3 → INC/DEC dst × N. + // N=1: 1B/4T vs 4B/15T; N=2: 2B/8T vs 4B/15T; N=3: 3B/12T vs 4B/15T. + // N=4 would be 16T vs 15T — worse in T-states, skip and fall through to ALU. + if mnem == "ADD" && lhs == dst && !isSpill(dst) && dst != "F" { + if cv, ok := g.constVals[inst.Src[1]]; ok && cv >= 1 && cv <= 3 { + for range cv { + g.emitf(" INC %s", dst) + } + g.invalidate(dst) + return + } + } + if mnem == "SUB" && lhs == dst && !isSpill(dst) && dst != "F" { + if cv, ok := g.constVals[inst.Src[1]]; ok && cv >= 1 && cv <= 3 { + for range cv { + g.emitf(" DEC %s", dst) + } + g.invalidate(dst) + return + } + } + // 8-bit: accumulator must hold lhs. Emit LD A,lhs if needed. + // + // Special case: rhs is already A and lhs is elsewhere. + // Loading lhs into A would destroy the rhs value. + // ADD: commutative → just ADD A, lhs. + // SUB: A = lhs - A → NEG first (A = -A), then ADD A, lhs. + if rhs == "A" && lhs != "A" { + // The result overwrites A, which holds rhs: save rhs if it is + // still live (gcd's b = b - a keeps a live in A for the loop). + g.saveAccOperandIfLive(inst.Src[1], inst) + g.invalidate("A") // A about to hold a new result + switch mnem { + case "ADD": + g.emitf(" ADD A, %s", lhs) + case "AND", "OR", "XOR": + g.emit8ALU(mnem, lhs) + case "SUB": + g.emit(" NEG") + g.emitf(" ADD A, %s", lhs) + default: + g.comment(fmt.Sprintf("TODO: 8-bit %s %s, A → %s with A as rhs", mnem, lhs, dst)) + g.emitLDA(lhs) + g.emit8ALU(mnem, rhs) + } + if dst != "A" { + g.emitLD8(dst, "A") + g.setCopy(dst, "A") + } else { + g.pendingAccReg = inst.Dst + } + return + } + // Coalescing: if A already holds lhs or (commutative) rhs, skip LD A, reg. + isCommutative := mnem == "ADD" || mnem == "AND" || mnem == "OR" || mnem == "XOR" + if g.holdsValue("A", lhs) { + // Save-before-overwrite: if the lhs vreg in A is still live after this + // instruction (used by a later inst or terminator), save it to a scratch + // register. The ALU op will overwrite A with the result. + // Example: fib's r6=sub(r0,1) where r0 is still needed for r9=sub(r0,2). + g.saveAccOperandIfLive(inst.Src[0], inst) + // A == lhs already — skip LD A, lhs. + g.invalidate("A") + // Use immediate form if rhs is a known constant. + if cv, ok := g.constVals[inst.Src[1]]; ok { + g.emit8ALUImm(mnem, cv) + } else { + g.emit8ALU(mnem, rhs) + } + if dst != "A" { + g.emitLD8(dst, "A") + g.setCopy(dst, "A") + } else { + g.pendingAccReg = inst.Dst + } + return + } + if isCommutative && g.holdsValue("A", rhs) { + // A == rhs, commutative swap: emit OP A, lhs instead. + g.saveAccOperandIfLive(inst.Src[1], inst) + g.invalidate("A") + // Use immediate form if lhs is a known constant. + if cv, ok := g.constVals[inst.Src[0]]; ok { + g.emit8ALUImm(mnem, cv) + } else { + g.emit8ALU(mnem, lhs) + } + if dst != "A" { + g.emitLD8(dst, "A") + g.setCopy(dst, "A") + } else { + g.pendingAccReg = inst.Dst + } + return + } + + // Immediate peephole: AND/OR/XOR/ADD/SUB n — no register needed for rhs. + if cv, ok := g.constVals[inst.Src[1]]; ok { + if lhs != "A" { + // Before loading lhs into A, save any live vreg currently in A. + // Example: fib — after first CALL, A=fib(n-1) is needed later but + // computing n-2 requires loading n (from scratch) into A. + g.saveABeforeOverwrite(inst) + g.emitLDA(lhs) + } + switch mnem { + case "AND", "OR", "XOR", "SUB": + g.emitf(" %s %d", mnem, cv) + case "ADD": + g.emitf(" ADD A, %d", cv) + default: + g.emit8ALU(mnem, rhs) + } + g.invalidate("A") // ALU result is a new value — break old lhs alias + if dst != "A" { + g.emitLD8(dst, "A") + g.setCopy(dst, "A") + } else { + g.pendingAccReg = inst.Dst + } + return + } + // If A holds a live value from a different virtual reg (e.g. a function + // param allocated to A), and this ALU op will clobber A as scratch, + // we need to save it first. The pendingAccReg mechanism handles the + // common case, but there's also the case where a param (ClassAcc) lives + // in A and is used AFTER this instruction. In that case, relocate it + // to a scratch register before clobbering A. + if lhs != "A" { + g.saveABeforeOverwrite(inst) + // A will be clobbered (LD A,lhs then ALU). If A holds a value + // that's used later, save it. + g.saveAccIfLive(inst) + } + if lhs != "A" { + g.emitLDA(lhs) + } + g.emit8ALU(mnem, rhs) + g.invalidate("A") // ALU result is a new value — break old lhs alias + if dst != "A" { + g.emitLD8(dst, "A") + g.setCopy(dst, "A") + } else { + g.pendingAccReg = inst.Dst + } + } else { + if lhs == dst && dst != "A" && !isSpill(dst) { + if cv, ok := g.constVals[inst.Src[1]]; ok { + switch mnem { + case "OR": + mask := uint16(cv) + if bits.OnesCount16(mask) == 1 { + totalBit := bits.TrailingZeros16(mask) + target := selectBitReg(dst, totalBit/8) + if g.emitBitRegOp("SET", totalBit%8, target) { + g.invalidate(dst) + g.invalidate(target) + return + } + } + case "AND": + mask := uint16(cv) + if bits.OnesCount16(^mask) == 1 { + totalBit := bits.TrailingZeros16(^mask) + target := selectBitReg(dst, totalBit/8) + if g.emitBitRegOp("RES", totalBit%8, target) { + g.invalidate(dst) + g.invalidate(target) + return + } + } + } + } + } + + // 16-bit peephole: INC/DEC rr when adding/subtracting 1 in-place. + if rhsReg == inst.Src[1] && lhs == dst && !isSpill(dst) && dst != "F" { + if cv, ok := g.constVals[inst.Src[1]]; ok { + if mnem == "ADD" && cv == 1 { + g.emitf(" INC %s", dst) + g.invalidate(dst) // pair + its bytes all change + return + } + if mnem == "SUB" && cv == 1 { + g.emitf(" DEC %s", dst) + g.invalidate(dst) + return + } + } + } + // 16-bit ADD: ADD HL,rr (Z80 only has ADD HL,rr natively) + // SUB/AND/OR/XOR on 16-bit require workarounds; emit comment for now. + switch mnem { + case "ADD": + // Z80 only has ADD HL, rr. If dst is not HL we must route + // through HL. The common case is dst=DE, which can be handled + // cheaply with EX DE,HL: + // EX DE, HL ; HL ← lhs (was in DE), DE ← old HL + // ADD HL, ; rhs' adjusted for any EX swap + // EX DE, HL ; DE ← result, HL ← restored old value + if dst != "HL" { + // After EX DE,HL: the register formerly in DE is now in HL, + // and what was in HL is now in DE. + adjustedRhs := rhs + if lhs == "DE" && dst == "DE" { + g.emit(" EX DE, HL") + if rhs == "HL" { + adjustedRhs = "DE" // what was HL is now in DE after EX + } else if rhs == "DE" { + adjustedRhs = "HL" + } + if adjustedRhs == "IX" || adjustedRhs == "IY" { + g.emit(" PUSH BC") + g.emitMov("BC", adjustedRhs, w) + g.emit(" ADD HL, BC") + g.emit(" POP BC") + } else if isSimpleReg(adjustedRhs) && !isPairReg(adjustedRhs) { + pair := g.promote8toPair(adjustedRhs) + g.emitADDHL(pair) + } else if isSpill(adjustedRhs) { + g.loadSpill16("BC", adjustedRhs) + g.emit(" ADD HL, BC") + } else if adjustedRhs == "IX" || adjustedRhs == "IY" { + g.emit(" PUSH DE") + g.emitMov("DE", adjustedRhs, 16) + g.emit(" ADD HL, DE") + g.emit(" POP DE") + } else { + g.emitADDHL(adjustedRhs) + } + g.emit(" EX DE, HL") + g.invalidate("HL") + g.invalidate("DE") + break + } + // General fallback: move lhs to HL, add rhs, move result to dst. + // Track rhs renaming if lhs is moved from HL/DE. + if rhs == "HL" && lhs == "DE" { + adjustedRhs = "DE" // lhs/dst swap via EX + } + g.emitMov("HL", lhs, w) + if adjustedRhs == "IX" || adjustedRhs == "IY" { + g.emit(" PUSH DE") + g.emitMov("DE", adjustedRhs, w) + g.emit(" ADD HL, DE") + g.emit(" POP DE") + } else if isSimpleReg(adjustedRhs) && !isPairReg(adjustedRhs) { + pair := g.promote8toPair(adjustedRhs) + g.emitADDHL(pair) + } else if isSpill(adjustedRhs) { + g.loadSpill16("BC", adjustedRhs) + g.emit(" ADD HL, BC") + } else { + g.emitADDHL(adjustedRhs) + } + g.emitMov(dst, "HL", w) + g.invalidate(dst) + break + } + // Standard path: dst == HL. + if lhs != dst { + g.emitMov(dst, lhs, w) + } + // ADD HL, rr only accepts BC/DE/HL/SP — not IX/IY or 8-bit regs. + if rhs == "IX" || rhs == "IY" { + g.emit(" PUSH DE") + g.emitMov("DE", rhs, w) + g.emit(" ADD HL, DE") + g.emit(" POP DE") + } else if isSimpleReg(rhs) && !isPairReg(rhs) { + // 8-bit rhs: zero-extend to parent pair, then ADD HL, pair. + pair := g.promote8toPair(rhs) + g.emitADDHL(pair) + } else if isSpill(rhs) { + // LocMem spill: load to BC, then ADD HL, BC. + g.loadSpill16("BC", rhs) + g.emit(" ADD HL, BC") + } else { + // dst must be HL here; ADD HL, rr is the only valid 16-bit ADD. + g.emitADDHL(rhs) + } + g.invalidate(dst) + case "SUB": + // Z80 only has SBC HL, rr for 16-bit subtraction. + // When dst != "HL" we must route through HL; the only legal instruction is + // SBC HL, rr, so the general strategy is: + // EX DE,HL (put lhs in HL, save rhs in DE — only when both are DE/HL) + // OR A + // SBC HL, rhs' + // EX DE,HL (restore HL=rhs, put result in DE=dst) + if dst != "HL" && lhs == dst && rhs == "HL" && dst == "DE" { + // dst=DE, lhs=DE (0 or anything), rhs=HL. + // EX DE,HL swaps: HL=lhs, DE=rhs. SBC HL,DE = lhs-rhs. + // Second EX DE,HL: DE=result, HL=rhs restored. + g.emit(" EX DE, HL") + g.emit(" OR A") + g.emit(" SBC HL, DE") + g.emit(" EX DE, HL") + g.invalidate("HL") + g.invalidate("DE") + return + } + if lhs != dst { + // When lhs and dst are the HL/DE pair AND the rhs is the other member, + // use EX DE,HL (a true swap, 4T) to move lhs→dst while simultaneously + // placing the old dst contents into lhs's old location. + // This keeps rhs accessible in the swapped location. + // Note: emitMov(HL,DE) does byte-by-byte (LD H,D; LD L,E) — it preserves + // DE but does NOT produce the swap that the rhs update below relies on. + if (dst == "HL" && lhs == "DE") || (dst == "DE" && lhs == "HL") { + g.emit(" EX DE, HL") + // After EX DE,HL: whatever was in HL is now in DE, and vice versa. + switch rhs { + case "HL": + rhs = "DE" + case "DE": + rhs = "HL" + } + } else { + g.emitMov(dst, lhs, w) + } + } + g.emit(" OR A") // clear carry (1b/4T; SCF+CCF would be 2b/8T) + if dst == "DE" && (rhs == "HL" || rhs == "BC" || rhs == "DE") { + // Optimal path: EX DE,HL; SBC HL,rhs'; EX DE,HL (23T) + // After EX: HL=old_DE(lhs), DE=old_HL + adjustedRhs := rhs + if rhs == "HL" { + adjustedRhs = "DE" // old HL is now in DE + } else if rhs == "DE" { + adjustedRhs = "HL" // self-subtraction after EX: lhs is now HL + } + g.emit(" EX DE, HL") + g.emitSBCHL(adjustedRhs) + g.emit(" EX DE, HL") // result back to DE, HL restored + g.invalidate("HL") + g.invalidate("DE") + } else if dst != "HL" { + // General non-HL: route through HL via emitMov. + g.emitMov("HL", dst, w) + if rhs == "HL" { + rhs = dst + } + if isSpill(rhs) { + g.loadSpill16("BC", rhs) + rhs = "BC" + } + g.emitSBCHL(rhs) + g.emitMov(dst, "HL", w) + g.invalidate("HL") + } else { + if isSpill(rhs) { + g.loadSpill16("BC", rhs) + rhs = "BC" + } + g.emitSBCHL(rhs) + } + g.invalidate(dst) + case "OR", "AND", "XOR": + if lhs != dst { + g.emitMov(dst, lhs, w) + } + hi, lo := highByte(dst), lowByte(dst) + imm, constant := g.constVals[rhsReg] + for _, part := range []struct { + dst, src string + shift uint + }{{hi, highByte(rhs), 8}, {lo, lowByte(rhs), 0}} { + g.emitLDA(part.dst) + if constant { + g.emit8ALUImm(mnem, imm>>part.shift) + } else { + g.emit8ALU(mnem, part.src) + } + g.invalidate("A") + g.emitLD8(part.dst, "A") + } + g.invalidate(dst) + + default: + g.comment(fmt.Sprintf("TODO: 16-bit %s %s, %s → %s", mnem, lhs, rhs, dst)) + } + } +} + +// genBinOp32 emits 32-bit ADD or SUB using the Z80 EXX / shadow-pair technique. +// +// The 32-bit value is split across main and shadow register banks: +// +// LocDWord{"HL"}: main HL = lo16, shadow H'L' = hi16 (via EXX) +// +// For ADD (dst=HL, rhs=DE): +// +// ADD HL, DE ; lo ← lo(lhs) + lo(rhs), carry set if overflow (11T) +// EXX ; switch to shadow bank: HL←H'L', DE←D'E' (4T) +// ADC HL, DE ; hi ← hi(lhs) + hi(rhs) + carry (15T) +// EXX ; switch back to main bank (4T) +// +// For SUB (dst=HL, rhs=DE): +// +// AND A ; clear carry (4T) +// SBC HL, DE ; lo ← lo(lhs) - lo(rhs), borrow in carry (15T) +// EXX ; switch to shadow bank (4T) +// SBC HL, DE ; hi ← hi(lhs) - hi(rhs) - borrow (15T) +// EXX ; switch back (4T) +// +// EXX simultaneously swaps BC↔BC', DE↔DE', HL↔HL', so after EXX: +// - "HL" refers to the old H'L' (hi of lhs/dst) +// - "DE" refers to the old D'E' (hi of rhs) +// +// This works because both dst and rhs are LocDWord with the same name in both +// banks. The rhs DWord is restored to its original state after the final EXX. +// +// Limitation: Z80 only provides ADD/ADC/SBC with HL as destination. +// When dst≠HL, we temporarily use HL via emitMov32 (TODO: optimise). +func (g *z80cg) genBinOp32(mnem, dst, lhs, rhs string) { + // ADD is commutative: if rhs == dst and lhs != dst, swap so we don't clobber rhs. + if mnem == "ADD" && rhs == dst && lhs != dst { + lhs, rhs = rhs, lhs + } + + // SUB when lhs != dst and rhs == dst: moving lhs into dst would clobber rhs. + // Special case lhs=DE, dst=HL: use EX DE,HL to swap lo/hi words in place, + // then SBC computes original_lhs - original_rhs in HL. + if mnem == "SUB" && lhs != dst && rhs == dst { + if lhs == "DE" && dst == "HL" { + // EX DE,HL swaps main words; EXX + EX DE,HL + EXX swaps shadow words. + // After: HL = old DE = lhs_lo, DE = old HL = rhs_lo (and same in shadow). + g.emit(" EX DE, HL") + g.emit(" EXX") + g.emit(" EX DE, HL") + g.emit(" EXX") + g.emit(" AND A") + g.emit(" SBC HL, DE") // lhs_lo - rhs_lo + g.emit(" EXX") + g.emit(" SBC HL, DE") // lhs_hi - rhs_hi - borrow + g.emit(" EXX") + return + } + // General fallback: save rhs on stack, move lhs to dst, reload rhs to DE. + // Only HL is supported as dst for now. + g.comment(fmt.Sprintf("TODO: 32-bit SUB rhs==dst non-EX case: %s - %s → %s", lhs, rhs, dst)) + return + } + + // Ensure lhs is in dst before the operation. + if lhs != dst { + g.emitMov32(dst, lhs) + } + + switch mnem { + case "ADD": + if dst == "HL" { + // Native 32-bit add: ADD HL,rr / EXX / ADC HL,rr / EXX + g.emitADDHL(rhs) + g.emit(" EXX") + g.emitf(" ADC HL, %s", rhs) + g.emit(" EXX") + } else { + // Non-HL dst: save HL, use it as scratch, then move result. + g.comment(fmt.Sprintf("32-bit ADD via HL scratch: %s += %s", dst, rhs)) + g.emit(" PUSH HL") // save main HL + g.emit(" EXX") + g.emit(" PUSH HL") // save shadow HL + g.emit(" EXX") + // Transfer dst to HL. + g.emitMov32("HL", dst) + // Now add rhs (rhs was LocDWord; after emitMov32 we haven't clobbered it + // because emitMov32 uses PUSH/POP+EXX which preserves all non-HL regs). + g.emitADDHL(rhs) + g.emit(" EXX") + g.emitf(" ADC HL, %s", rhs) + g.emit(" EXX") + // Move result from HL to dst, then restore HL. + g.emitMov32(dst, "HL") + g.emit(" EXX") + g.emit(" POP HL") // restore shadow HL + g.emit(" EXX") + g.emit(" POP HL") // restore main HL + } + case "SUB": + if dst == "HL" { + g.emit(" AND A") // clear carry + g.emitSBCHL(rhs) + g.emit(" EXX") + g.emitSBCHL(rhs) + g.emit(" EXX") + } else { + g.comment(fmt.Sprintf("32-bit SUB via HL scratch: %s -= %s", dst, rhs)) + g.emit(" PUSH HL") + g.emit(" EXX") + g.emit(" PUSH HL") + g.emit(" EXX") + g.emitMov32("HL", dst) + g.emit(" AND A") + g.emitSBCHL(rhs) + g.emit(" EXX") + g.emitSBCHL(rhs) + g.emit(" EXX") + g.emitMov32(dst, "HL") + g.emit(" EXX") + g.emit(" POP HL") + g.emit(" EXX") + g.emit(" POP HL") + } + default: + g.comment(fmt.Sprintf("TODO: 32-bit %s %s, %s → %s", mnem, lhs, rhs, dst)) + } +} + +// saveAccIfLive checks whether A contains a live value from a virtual register +// that will be needed AFTER the current instruction. If so, it relocates +// A's value to a scratch register (via physOverride) so the ALU can use A freely. +func (g *z80cg) saveAccIfLive(inst *Inst) { + // Find which virtual reg currently lives in A. Prefer the lowest ID + // on ties, matching definition order and the allocator tie-break. + var accReg Reg + for _, r := range slices.Sorted(maps.Keys(g.ar.Locs)) { + loc := g.ar.Locs[r] + if loc.Kind == LocReg && loc.Name == "A" { + accReg = r + break + } + } + if accReg == NoReg { + return + } + // Check if physOverride already moved it away. + if p, ok := g.physOverride[accReg]; ok && p != "A" { + return + } + // Skip if this instruction defines or uses accReg (it'll be handled normally). + if accReg == inst.Dst || accReg == inst.Src[0] || accReg == inst.Src[1] { + return + } + // Check if accReg is used after this instruction in the block. + live := g.regsLiveAfterInst(inst) + if !live[accReg] { + return + } + // accReg is live in A and will be clobbered — save to scratch. + // Pick a scratch register that isn't used by this instruction. + scratch := g.pickScratch8(inst) + g.emitf(" LD %s, A", scratch) + g.physOverride[accReg] = scratch +} + +// reorderAccMoves swaps consecutive instruction pairs where a move→A is followed +// by an 8-bit ALU→nonA that will use A as scratch. By putting the ALU first, +// A is free for scratch use and the move loads A last (preserving it for the call). +func reorderAccMoves(insts []*Inst, ar *AllocResult) []*Inst { + result := make([]*Inst, len(insts)) + copy(result, insts) + + for i := 0; i < len(result)-1; i++ { + a, b := result[i], result[i+1] + + // Pattern: a = OpMove, dst in A; b = 8-bit ALU, dst NOT in A. + if a.Op != OpMove { + continue + } + aLoc := ar.Locs[a.Dst] + if aLoc.Name != "A" { + continue + } + + // b must be an 8-bit ALU op that needs A as scratch. + is8bitALU := (b.Op == OpAdd || b.Op == OpSub || b.Op == OpAnd || + b.Op == OpOr || b.Op == OpXor) && b.Ty != nil && b.Ty.Width() <= 8 + if !is8bitALU { + continue + } + bLoc := ar.Locs[b.Dst] + if bLoc.Name == "A" { + continue // b also targets A — no help from swapping + } + + // Safety: b must not READ a.Dst, and a must not READ b.Dst. + bReadsA := b.Src[0] == a.Dst || b.Src[1] == a.Dst + aReadsB := a.Src[0] == b.Dst + if bReadsA || aReadsB { + continue // data dependency — cannot swap + } + + // Swap. + result[i], result[i+1] = b, a + } + return result +} + +// emit8ALU emits an 8-bit ALU instruction with A as the implicit destination. +// MZA requires "ADD A, src" (two operands) but "SUB/AND/OR/XOR src" (one operand). +func (g *z80cg) emit8ALU(mnem, src string) { + // LocMem spill: Z80 ALU ops can't use absolute addresses directly. + // Load spill value into a scratch register first. + if isSpill(src) { + // Load from memory into A first, but A is the implicit LHS for 8-bit ALU. + // Must save A, load spill into scratch, restore A, then ALU. + // Simpler: use (HL) indirect — save HL, point to spill, ALU (HL), restore. + // Simplest: load spill into a scratch 8-bit reg. + // For now, load into A via memory, which works if LHS is already in A. + g.emit(" PUSH HL") + g.emitf(" LD HL, %s", src) + g.emitf(" LD H, (HL)") // H = value at spill address + switch mnem { + case "ADD", "ADC": + g.emitf(" %s A, H", mnem) + default: + g.emitf(" %s H", mnem) + } + g.emit(" POP HL") + g.invalidate("H") + return + } + // Width mismatch: pair reg as 8-bit ALU operand — use low byte. + if isPairReg(src) { + src = lowByte(src) + } + switch mnem { + case "ADD", "ADC": + g.emitf(" %s A, %s", mnem, src) + default: + g.emitf(" %s %s", mnem, src) + } +} + +// emit8ALUImm emits an 8-bit ALU instruction with an immediate constant operand. +func (g *z80cg) emit8ALUImm(mnem string, imm int64) { + imm8 := imm & 0xFF // Z80 8-bit ALU: truncate to byte + switch mnem { + case "ADD", "ADC": + g.emitf(" %s A, %d", mnem, imm8) + default: + g.emitf(" %s %d", mnem, imm8) + } +} + +// ── Shifts ──────────────────────────────────────────────────────────────────── + +func (g *z80cg) genShift(mnem string, inst *Inst) { + if _, constant := g.constVals[inst.Src[1]]; !constant { + g.genVariableShift(mnem, inst) + return + } + + w := inst.Ty.Width() + if w >= 24 { + g.genShift32(mnem, inst) + return + } + dst := g.loc(inst.Dst) + src := g.loc(inst.Src[0]) + if dst != src { + g.emitMov(dst, src, w) + } + // Shift count in Src[1] — Z80 shifts are always by 1 per instruction. + // Constant counts emit N copies; variable counts use genVariableShift. + // IMPORTANT: count=0 must emit zero shifts (identity), not default to 1. + count := int64(1) + if cv, ok := g.constVals[inst.Src[1]]; ok { + count = cv // cv=0 → no shifts emitted (identity) + } + if w == 16 { + // Z80 has no SLA/SRL/SRA on register pairs. + // SHL/SHR/SAR on IX/IY halves are also invalid (DD CB prefix + // always addresses (IX+d), never IXH/IXL directly). + // Route IX/IY shifts through HL. + shiftDst := dst + if isIXY(dst) { + g.emitMov("HL", dst, 16) + shiftDst = "HL" + } + hi := highByte(shiftDst) + lo := lowByte(shiftDst) + if mnem == "SLA" && count == 8 { + g.emitf(" LD %s, %s", hi, lo) + g.emitf(" LD %s, 0", lo) + } else { + for i := int64(0); i < count; i++ { + switch mnem { + case "SLA": + if shiftDst == "HL" { + g.emitf(" ADD HL, HL") + } else { + g.emitf(" SLA %s", lo) + g.emitf(" RL %s", hi) + } + case "SRL": + g.emitf(" SRL %s", hi) + g.emitf(" RR %s", lo) + case "SRA": + g.emitf(" SRA %s", hi) + g.emitf(" RR %s", lo) + } + } + } + if isIXY(dst) { + g.emitMov(dst, "HL", 16) + } + g.invalidate(dst) + if isIXY(dst) { + g.invalidate("HL") + } + return + } + for i := int64(0); i < count; i++ { + g.emitf(" %s %s", mnem, dst) + } + g.invalidate(dst) // shift modifies dst +} + +// genVariableShift uses a saved scratch pair for the counter. Loading it +// before moving the value also handles count registers overlapping the result. +func (g *z80cg) genVariableShift(mnem string, inst *Inst) { + dst, src := g.loc(inst.Dst), g.loc(inst.Src[0]) + w := inst.Ty.Width() + pair, counter := "", "" + for _, p := range []string{"BC", "DE", "HL"} { + overlaps := func(loc string) bool { return loc == p || loc == highByte(p) || loc == lowByte(p) } + if !overlaps(dst) && !overlaps(src) && !(isIXY(dst) && p == "HL") { + pair = p + counter = highByte(p) + break + } + } + if pair == "" { + panic("no scratch pair for variable shift") + } + g.emitf(" PUSH %s", pair) + g.emitLD8(counter, lowByte(g.loc(inst.Src[1]))) + if dst != src { + g.emitMov(dst, src, w) + } + shiftDst := dst + if isIXY(dst) { + g.emit(" PUSH HL") + g.emitMov("HL", dst, 16) + shiftDst = "HL" + } + if isIXYReg(dst) { + g.emit(" PUSH AF") + g.emitLDA(dst) + shiftDst = "A" + } + idx := g.trampIdx + g.trampIdx++ + // INC/DEC test zero without touching the shifted value or accumulator. + g.emitf(" INC %s", counter) + g.emitf(" DEC %s", counter) + g.emitf(" JR Z, .shift_done_%d", idx) + g.emitf(".shift_loop_%d:", idx) + if w >= 24 { + hi, lo := highByte(shiftDst), lowByte(shiftDst) + if mnem == "SLA" { + g.emitf(" SLA %s", lo) + g.emitf(" RL %s", hi) + g.emit(" EXX") + g.emitf(" RL %s", lo) + g.emitf(" RL %s", hi) + g.emit(" EXX") + } else { + g.emit(" EXX") + if w == 24 { + g.emitf(" %s %s", mnem, lo) + } else { + g.emitf(" %s %s", mnem, hi) + g.emitf(" RR %s", lo) + } + g.emit(" EXX") + g.emitf(" RR %s", hi) + g.emitf(" RR %s", lo) + } + } else if w == 16 { + hi, lo := highByte(shiftDst), lowByte(shiftDst) + if mnem == "SLA" { + g.emitf(" SLA %s", lo) + g.emitf(" RL %s", hi) + } else { + g.emitf(" %s %s", mnem, hi) + g.emitf(" RR %s", lo) + } + } else { + g.emitf(" %s %s", mnem, shiftDst) + } + g.emitf(" DEC %s", counter) + g.emitf(" JR NZ, .shift_loop_%d", idx) + g.emitf(".shift_done_%d:", idx) + if isIXY(dst) { + g.emitMov(dst, "HL", 16) + g.emit(" POP HL") + } + if isIXYReg(dst) { + g.emitLD8(dst, "A") + g.emit(" POP AF") + } + g.emitf(" POP %s", pair) + g.invalidate(dst) +} + +// genShift32 emits 24/32-bit shifts via the EXX shadow-pair technique. +// +// SHL by 1 (logical left): +// +// ADD HL, HL ; lo16 <<= 1, carry = old bit15 (11T) +// EXX ; switch to shadow (4T) +// ADC HL, HL ; hi16 <<= 1, carry in from lo (15T) +// EXX ; switch back (4T) = 34T per bit +// +// SHR by 1 (logical right): +// +// EXX ; switch to shadow +// SRL H ; hi16: H >>= 1 (zero-extend), carry = old bit0 (8T) +// RR L ; hi16: L = carry<<7 | L>>1 (8T) +// EXX ; back to main +// RR H ; lo16: H = carry<<7 | H>>1 (8T) +// RR L ; lo16: L = carry<<7 | L>>1 (8T) = 40T per bit +// +// SAR by 1 (arithmetic right, sign-extend): +// +// EXX / SRA H / RR L / EXX / RR H / RR L (same as SHR but SRA preserves sign) +func (g *z80cg) genShift32(mnem string, inst *Inst) { + dst := g.loc(inst.Dst) + src := g.loc(inst.Src[0]) + if dst != src { + g.emitMov32(dst, src) + } + + count := int64(1) + if cv, ok := g.constVals[inst.Src[1]]; ok { + count = cv + } + + hi := highByte(dst) // e.g. "H" for dst="HL" + lo := lowByte(dst) // e.g. "L" + + switch mnem { + case "SLA": + // Left shift: ADD HL,HL / EXX / ADC HL,HL / EXX (× count) + // Z80 only supports ADD HL,HL — not ADD DE,DE or ADD BC,BC. + // Route through HL if needed. + if dst != "HL" { + g.emitMov("HL", dst, 16) + } + for i := int64(0); i < count; i++ { + g.emit(" ADD HL, HL") + g.emit(" EXX") + g.emit(" ADC HL, HL") + g.emit(" EXX") + } + if dst != "HL" { + g.emitMov(dst, "HL", 16) + g.invalidate("HL") + } + case "SRL": + // Logical right shift: EXX / SRL H / RR L / EXX / RR H / RR L (× count) + for i := int64(0); i < count; i++ { + g.emit(" EXX") + if inst.Ty.Width() == 24 { + g.emitf(" SRL %s", lo) + } else { + g.emitf(" SRL %s", hi) + g.emitf(" RR %s", lo) + } + g.emit(" EXX") + g.emitf(" RR %s", hi) + g.emitf(" RR %s", lo) + } + case "SRA": + // Arithmetic right shift: sign bit preserved via SRA on high byte. + for i := int64(0); i < count; i++ { + g.emit(" EXX") + if inst.Ty.Width() == 24 { + g.emitf(" SRA %s", lo) + } else { + g.emitf(" SRA %s", hi) + g.emitf(" RR %s", lo) + } + g.emit(" EXX") + g.emitf(" RR %s", hi) + g.emitf(" RR %s", lo) + } + default: + g.comment(fmt.Sprintf("TODO: 32-bit shift %s", mnem)) + } + g.invalidate(dst) +} diff --git a/minzc/pkg/mir2/z80codegen_call.go b/minzc/pkg/mir2/z80codegen_call.go new file mode 100644 index 000000000..e21e8a417 --- /dev/null +++ b/minzc/pkg/mir2/z80codegen_call.go @@ -0,0 +1,885 @@ +package mir2 + +import ( + "slices" + "strings" +) + +// ── Calls ───────────────────────────────────────────────────────────────────── + +func (g *z80cg) genCall(inst *Inst) { + clear(g.holdsPhys) // calls clobber all volatile registers + + // ── Built-in intrinsics (inlined, no CALL emitted) ──────────────────────── + switch inst.Sym { + case "@mir.io.print.str": + // Emit inline NUL-terminated string output loop via OUT ($23), A. + // The string pointer must be in HL (ClassPointer). + // HL is advanced in-place; A and F are clobbered. + idx := g.trampIdx + g.trampIdx++ + g.emitf(".print_str_%d:", idx) + g.emit(" LD A, (HL)") + g.emit(" AND A") + g.emitf(" JR Z, .print_str_done_%d", idx) + g.emit(" OUT (0x23), A") + g.emit(" INC HL") + g.emitf(" JR .print_str_%d", idx) + g.emitf(".print_str_done_%d:", idx) + // A, F, HL clobbered — invalidate holdsPhys entries for these. + g.invalidate("A") + g.invalidate("HL") + return + + case "@mir.io.print.u8": + // Emit single OUT ($23), A. The value must already be in A. + g.emit(" OUT (0x23), A") + g.invalidate("A") + return + + case "@mir.io.print.char": + // Print ASCII character in A via OUT ($23). + g.emit(" OUT (0x23), A") + g.invalidate("A") + return + + case "@mir.io.print.nl": + // Emit newline (\n = 0x0A) via OUT ($23), A. + g.emit(" LD A, 0x0A") + g.emit(" OUT (0x23), A") + g.invalidate("A") + return + + case "@mir.io.print.dec": + // Print u8 value in A as decimal ASCII digits via OUT ($23). + // Inline: divide by 100, 10 (using repeated subtraction — small and fast + // enough for 0-255). Clobbers A, B, C, D, H, L. + idx := g.trampIdx + g.trampIdx++ + // Hundreds digit + g.emitf(" LD C, A") // C = n + g.emitf(" LD B, 0") // B = hundreds count + g.emitf(".pdec_h%d:", idx) + g.emitf(" LD A, C") + g.emitf(" CP 100") + g.emitf(" JR C, .pdec_ht%d", idx) + g.emitf(" SUB 100") + g.emitf(" LD C, A") + g.emitf(" INC B") + g.emitf(" JR .pdec_h%d", idx) + g.emitf(".pdec_ht%d:", idx) + // Only print hundreds if non-zero + g.emitf(" LD A, B") + g.emitf(" OR A") + g.emitf(" JR Z, .pdec_t%d", idx) + g.emitf(" ADD A, 48") // '0' + g.emitf(" OUT (0x23), A") + // Tens digit + g.emitf(".pdec_t%d:", idx) + g.emitf(" LD D, 0") // D = tens count + g.emitf(".pdec_tl%d:", idx) + g.emitf(" LD A, C") + g.emitf(" CP 10") + g.emitf(" JR C, .pdec_tt%d", idx) + g.emitf(" SUB 10") + g.emitf(" LD C, A") + g.emitf(" INC D") + g.emitf(" JR .pdec_tl%d", idx) + g.emitf(".pdec_tt%d:", idx) + // Only print tens if hundreds or tens non-zero + g.emitf(" LD A, B") + g.emitf(" OR D") + g.emitf(" JR Z, .pdec_u%d", idx) + g.emitf(" LD A, D") + g.emitf(" ADD A, 48") + g.emitf(" OUT (0x23), A") + // Units digit (always print) + g.emitf(".pdec_u%d:", idx) + g.emitf(" LD A, C") + g.emitf(" ADD A, 48") + g.emitf(" OUT (0x23), A") + g.invalidate("A") + g.invalidate("B") + g.invalidate("C") + g.invalidate("D") + return + + case "@mir.io.console.log": + // console_log(n: u8) — OUT ($23), A (mze/mzx stdout port) + g.emit(" OUT (0x23), A") + g.invalidate("A") + return + + case "@mir.io.console.err": + // console_err(n: u8) — OUT ($25), A (mze/mzx stderr port) + g.emit(" OUT (0x25), A") + g.invalidate("A") + return + + case "@error": + // @error(N) — set carry flag, error code in A, return. + // Used in fallible functions (name?). A = argument (error code). + g.emit(" SCF") // CY = 1 (error) + g.emit(" RET") // return to caller with CY set + A = code + g.invalidate("A") + return + + case "@check": + // @check — jump to error handler if carry set. + // Emits: JR NC, .skip / / .skip: + // For now: just emit RET C (propagate). Handler block TBD. + idx := g.trampIdx + g.trampIdx++ + g.emitf(" JR NC, .check_ok_%d", idx) + // The handler code follows in the next statement. + // For simple propagation, emit RET: + g.emit(" RET") // propagate error (CY + A intact) + g.emitf(".check_ok_%d:", idx) + return + + case "@propagate": + // @propagate — conditional return on carry. 1 byte, 5T. + // If CY=1 (error from previous call), return immediately. + g.emit(" RET C") + return + } + + sym := sanitizeIdent(inst.Sym) + + var callee *Func + indirect := inst.Op == OpCallIndirect + if indirect { + callee = &Func{} + for i, arg := range inst.Args { + ty := inferTyFromAlloc(g.ar, arg) + callee.Contract.Params = append(callee.Contract.Params, Param{Ty: ty, Class: standardParamClass(ty, i)}) + } + if inst.Dst != NoReg { + callee.Contract.Returns = []Return{{Ty: inst.Ty, Class: ClassAcc}} + } + } else if g.mod != nil { + callee = g.mod.FuncByName(inst.Sym) + } + + // Save live caller values before argument copies, including reused args. + var callerSavePairs []string + if callee != nil && inst != g.tailCallInst { + callerSavePairs = g.callerSavePairs(inst, callee) + for _, pair := range callerSavePairs { + g.emitf(" PUSH %s", pair) + } + } + if indirect { + // Snapshot the pointer before argument setup. IX keeps the target + // separate from the standard ABI's HL word argument. + g.pushWord(g.loc(inst.Src[0])) + } + if len(inst.Args) > 0 && callee != nil { + g.emitCallArgs(inst.Args, callee.Contract.Params) + } + if indirect { + g.emit(" POP IX") + } + + // Tail call optimisation: if this is the tail call instruction detected by + // genBlock, emit JP instead of CALL so the callee's RET returns to our caller. + if inst == g.tailCallInst { + if callee != nil && callee.Attrs.ExternAddr != 0 { + g.emitf(" JP 0x%04X", callee.Attrs.ExternAddr) + } else { + g.emitf(" JP %s", sym) + } + return // genTerm will skip RET + } + + // Check if callee has a fixed address (ExternAddr) + if indirect { + g.emit(" CALL __call_ix") + g.needsCallIX = true + } else if callee != nil && callee.Attrs.ExternAddr != 0 { + addr := callee.Attrs.ExternAddr + // RST addresses on Z80: 0x00, 0x08, 0x10, 0x18, 0x20, 0x28, 0x30, 0x38 + if addr <= 0x38 && addr%8 == 0 { + g.emitf(" RST 0x%02X", addr) + } else { + g.emitf(" CALL 0x%04X", addr) + } + } else { + g.emitf(" CALL %s", sym) + } + + // Pick up every result without letting caller restores destroy it. If a + // destination shares a saved pair, merge its bytes into the saved stack + // image so POP restores both the live half and the returned half. + var results []parallelCopy + dstPhys := "" + flagResult := false + if inst.Dst != NoReg && callee != nil && len(callee.Contract.Returns) > 0 { + ret := callee.Contract.Returns[0] + if ret.Class == ClassFlag { + flagResult = true + } else { + dstPhys = g.loc(inst.Dst) + results = append(results, parallelCopy{srcName: canonicalReturnLoc(ret.Class, ret.Ty), dstName: dstPhys, ty: ret.Ty}) + } + } + for i, r := range inst.ExtraRets { + if r == NoReg { + continue + } + cls, ty := ClassIndex, Ty(TyU16) + if callee != nil && i+1 < len(callee.Contract.Returns) { + cls, ty = callee.Contract.Returns[i+1].Class, callee.Contract.Returns[i+1].Ty + } + if i < len(inst.ExtraRetClasses) { + cls = inst.ExtraRetClasses[i] + } + if i < len(inst.ExtraRetTys) { + ty = inst.ExtraRetTys[i] + } + results = append(results, parallelCopy{srcName: canonicalReturnLoc(cls, ty), dstName: g.loc(r), ty: ty}) + } + g.pickupCallResults(results, callerSavePairs, flagResult) + + // CALL and argument setup invalidate cached physical contents, including + // registers used only as scratch by the emitted callee/runtime sequences. + g.invalidate("A") + g.invalidate("F") + for _, name := range []string{"B", "C", "D", "E", "H", "L", "IX", "IY"} { + g.invalidate(name) + } + g.pendingAccReg = NoReg + if dstPhys == "A" { + g.pendingAccReg = inst.Dst + } + + // Flag-return ABI: if the callee returns ClassFlag, the result is in the CPU + // flag register (not A). Record which condition means "true" so that a + // subsequent TermBrIf on inst.Dst can emit JP directly without + // AND A / CP 0. + if inst.Dst != NoReg && callee != nil { + if len(callee.Contract.Returns) > 0 { + ret := callee.Contract.Returns[0] + if ret.Class == ClassFlag { + g.callFlags[inst.Dst] = ret.FlagCond + } + } + } + +} + +// pickupCallResults never stores into code. Most calls need only parallel +// moves; overlapping destinations use a short-lived, reentrant stack frame. +func (g *z80cg) pickupCallResults(results []parallelCopy, saved []string, flagResult bool) { + savedPair := map[string]bool{} + for _, p := range saved { + savedPair[p] = true + } + before, after := true, true + for _, r := range results { + before = before && !savedPair[regToPairMap[r.dstName]] + after = after && !savedPair[regToPairMap[r.srcName]] + } + if flagResult && savedPair["AF"] { + before, after = false, false + } + if before || after { + if before { + g.emitParallelCopy(results) + } + for i := len(saved) - 1; i >= 0; i-- { + g.emitf(" POP %s", saved[i]) + } + if !before { + g.emitParallelCopy(results) + } + return + } + + // A byte result can use a pair that is absent from the restore list. + // This scratch lifetime ends at pickup, so later allocation cannot reuse + // it while an override remains live. + if !flagResult && len(results) == 1 && results[0].ty.Width() <= 8 && regToPairMap[results[0].dstName] != "" { + r := results[0] + for _, scratch := range []string{"E", "H", "L", "D", "B", "C"} { + if !savedPair[regToPairMap[scratch]] { + g.emitLD8(scratch, r.srcName) + for i := len(saved) - 1; i >= 0; i-- { + g.emitf(" POP %s", saved[i]) + } + g.emitLD8(r.dstName, scratch) + return + } + } + } + + // IX addresses snapshots and the caller-save image. Its original value + // is restored separately, including when IX itself is a result destination. + g.emit(" PUSH IX") + pairs := []string{"AF"} + for _, r := range results { + p := regToPairMap[r.srcName] + if !slices.Contains(pairs, p) { + pairs = append(pairs, p) + } + } + for _, p := range pairs { + g.emitf(" PUSH %s", p) + } + g.emit(" LD IX, 0") + g.emit(" ADD IX, SP") + snapshot := map[string]int{} + for i, p := range pairs { + snapshot[p] = 2 * (len(pairs) - 1 - i) + } + base := 2 * len(pairs) + restore := map[string]int{} + for i, p := range saved { + restore[p] = base + 2 + 2*(len(saved)-1-i) + } + byteOffset := func(name string) int { + p := regToPairMap[name] + o := snapshot[p] + if name == "A" || name == highByte(p) { + o++ + } + return o + } + storeByte := func(src, dst int) { + g.emitf(" LD A, (IX+%d)", src) + g.emitf(" LD (IX+%d), A", dst) + } + // Patch saved destinations before loading any final register, since A + // is scratch while merging the stack image. + for _, r := range results { + p := regToPairMap[r.dstName] + o, ok := restore[p] + if p == "IX" && !ok { + o, ok = base, true + } + if !ok { + continue + } + if r.ty.Width() > 8 { + src := snapshot[regToPairMap[r.srcName]] + storeByte(src, o) + storeByte(src+1, o+1) + } else { + if r.dstName == "A" || r.dstName == highByte(p) { + o++ + } + storeByte(byteOffset(r.srcName), o) + } + } + if flagResult && savedPair["AF"] { + storeByte(snapshot["AF"], restore["AF"]) + } + // Preserve the result in A in the AF snapshot until all scratch work is done. + for _, r := range results { + if r.dstName == "A" && !savedPair["AF"] { + storeByte(byteOffset(r.srcName), snapshot["AF"]+1) + } + } + for _, r := range results { + p := regToPairMap[r.dstName] + if savedPair[p] || p == "IX" || r.dstName == "A" { + continue + } + if r.ty.Width() <= 8 { + // Indexed memory cannot load IY halves directly; use A as scratch. + if p != "" && p != "IY" { + g.emitf(" LD %s, (IX+%d)", r.dstName, byteOffset(r.srcName)) + } else { + g.emitf(" LD A, (IX+%d)", byteOffset(r.srcName)) + g.emitLD8(r.dstName, "A") + } + } else { + src := snapshot[regToPairMap[r.srcName]] + if p == "IY" || p == "" { + g.emit(" PUSH HL") + g.emitf(" LD L, (IX+%d)", src) + g.emitf(" LD H, (IX+%d)", src+1) + g.emitMov(r.dstName, "HL", r.ty.Width()) + g.emit(" POP HL") + } else { + g.emitf(" LD %s, (IX+%d)", lowByte(p), src) + g.emitf(" LD %s, (IX+%d)", highByte(p), src+1) + } + } + } + // Restore returned flags (ADD IX,SP changed them) and the final A without + // disturbing HL. Subsequent INC SP and POP instructions preserve flags. + g.emit(" PUSH HL") + g.emitf(" LD L, (IX+%d)", snapshot["AF"]) + g.emitf(" LD H, (IX+%d)", snapshot["AF"]+1) + g.emit(" PUSH HL") + g.emit(" POP AF") + g.emit(" POP HL") + for i := 0; i < base; i++ { + g.emit(" INC SP") + } + g.emit(" POP IX") + for i := len(saved) - 1; i >= 0; i-- { + g.emitf(" POP %s", saved[i]) + } +} + +// emitCallArgs emits the parallel-copy argument setup for a CALL instruction. +// It maps the current physical locations of the call arguments to the physical +// locations expected by the callee's parameter contract and uses emitParallelCopy +// to resolve any cycles (e.g. A→B, C→A). +// +// Argument locations are transient. Caller values keep their original locations +// after caller-save restoration, so argument setup does not change physOverride. +func (g *z80cg) emitCallArgs(args []Reg, params []Param) { + var copies []parallelCopy + for i, arg := range args { + if i >= len(params) { + break + } + srcPhys := g.loc(arg) + // Use the callee's actual allocated register for the param if available + // (from PBQP). Fall back to canonical class-based location otherwise. + dstPhys := "" + if loc, ok := g.ar.Locs[params[i].Reg]; ok && loc.Name != "" { + dstPhys = physName(loc) + } + if dstPhys == "" { + dstPhys = canonicalReturnLoc(params[i].Class, params[i].Ty) + } + // Always include in copies — even no-ops (src==dst). This ensures the + // parallel copy scratch picker knows ALL live arg registers and won't + // clobber them when resolving cycles (e.g. B↔C using A as scratch + // when A holds arg0). + copies = append(copies, parallelCopy{srcName: srcPhys, dstName: dstPhys, ty: params[i].Ty}) + } + if len(copies) == 0 { + return + } + g.emitParallelCopy(copies) + +} + +// canonicalReturnLoc returns the physical register name where a return value of +// the given class and type must reside on function exit (calling convention). +func canonicalReturnLoc(cls RegClass, ty Ty) string { + w := ty.Width() + switch cls { + case ClassFlag: + // Flag-return ABI: result lives in the CPU flag register (F), not in A. + // Callers test with JP directly after the CALL instruction. + // "F" is the sentinel name for LocFlag in Z80PhysLocs. + return "F" + case ClassAcc: + if w <= 8 { + return "A" + } + return "HL" // u16 [acc] → HL + case ClassPointer, ClassPair: + return "HL" + case ClassIndex: + return "DE" + case ClassCounter: + return "B" + case ClassRegC: + return "C" + case ClassRegD: + return "D" + case ClassRegE: + return "E" + case ClassRegH: + return "H" + case ClassRegL: + return "L" + case ClassIX: + return "IX" + case ClassIY: + return "IY" + case ClassGeneral: + if w <= 8 { + return "C" + } + return "HL" + default: + if w <= 8 { + return "A" + } + return "HL" + } +} + +// standardParamClass returns the register class for position i in the +// standard callable ABI (used by function pointers / indirect calls). +// Matches classForParam in hir/lower.go. +func standardParamClass(ty Ty, pos int) RegClass { + if ty == TyPtr { + return ClassPointer + } + if ty.Width() > 8 { + if pos == 0 { + return ClassPointer // HL + } + return ClassIndex // DE + } + switch pos { + case 0: + return ClassAcc // A + case 1: + return ClassGeneral // C + case 2: + return ClassCounter // B + default: + return ClassGeneral + } +} + +// isIntrinsic reports whether sym is a built-in intrinsic handled inline by +// genCall (i.e. the call does NOT lower to a real CALL/JP instruction). +// Intrinsics must NOT be treated as tail calls because genCall returns early +// without consuming g.tailCallInst, which would cause genTerm to skip RET. +func isIntrinsic(sym string) bool { + return strings.HasPrefix(sym, "@mir.") +} + +// multiReturnLocs returns the physical register name for each return position +// by scanning the function's TermRet instructions and looking up the allocator +// result. This is more accurate than classToRegName(Contract.Returns[i].Class) +// because the contract optimizer may reassign classes after lowering. +// Returns a slice with one entry per return position; entries may be "" if unknown. +func multiReturnLocs(f *Func, ar *AllocResult) []string { + n := len(f.Contract.Returns) + if n == 0 { + return nil + } + for _, b := range f.Blocks { + if ret, ok := b.Term.(*TermRet); ok && len(ret.Vals) == n { + locs := make([]string, n) + for i, v := range ret.Vals { + locs[i] = ar.Loc(v).Name + } + return locs + } + } + return nil +} + +// classToRegName maps a RegClass to the canonical Z80 register name for that class. +// ty is used to disambiguate (e.g. ClassAcc is always A, ClassPointer is HL for 16-bit). +func classToRegName(cls RegClass, ty Ty) string { + switch cls { + case ClassAcc: + return "A" + case ClassPointer: + return "HL" + case ClassIndex: + return "DE" + case ClassCounter: + return "B" + case ClassGeneral: + return "C" + } + return "" +} + +// isRecursive reports whether f contains a direct recursive call to itself. +func isRecursive(f *Func) bool { + for _, b := range f.Blocks { + for _, inst := range b.Insts { + if inst.Op == OpCall && inst.Sym == f.Name { + return true + } + } + } + return false +} + +// callerSavePairs returns the Z80 register pairs that must be PUSH'd/POP'd +// around a CALL to protect live values from the callee's clobbers. +// +// Strategy: compute the callee's clobber set (physical reg names), then check +// which pushable pairs contain registers allocated to virtual regs in the +// caller that remain live after the call, including arguments reused later. +// The call results are excluded because their definitions begin after CALL. +func (g *z80cg) callerSavePairs(inst *Inst, callee *Func) []string { + if callee == nil { + return nil + } + + // The emitter uses implicit scratch registers, runtime helpers and nested + // calls. Preserve every live physical register conservatively until a + // post-emission write-set analysis can prove a smaller clobber contract. + clobberedRegs := map[string]bool{} + for _, name := range computeClobbers(callee, g.ar) { + clobberedRegs[name] = true + } + + // Compute which virtual regs are live AFTER this call instruction. + // A reg is live-across-call if it's used by any instruction after the call + // in the same block, or by the block's terminator, or by a successor block + // using backward CFG liveness and instruction uses/definitions. + liveAfter := g.regsLiveAfterInst(inst) + + // Collect physical regs that are (a) clobbered by callee and (b) hold a + // value that is live after the call. Exclude result definitions only. + excluded := make(map[Reg]bool) + if inst.Dst != NoReg { + excluded[inst.Dst] = true + } + for _, er := range inst.ExtraRets { + excluded[er] = true + } + + livePhys := make(map[string]bool) + for r := range liveAfter { + if excluded[r] { + continue + } + // Use g.loc() which respects physOverride — a vreg may have been + // relocated to a different register by materializePendingAcc or + // save-before-overwrite. The static ar.Locs doesn't reflect these + // runtime relocations. + locName := g.loc(r) + if locName == "" || locName == "?" { + continue + } + if isSpill(locName) { + continue // spilled to memory — not a register + } + if clobberedRegs[locName] { + livePhys[locName] = true + } + } + + // Map individual registers to PUSH-able pairs. + // Z80 can only PUSH/POP pairs: BC, DE, HL (and AF, IX, IY). + pairNeeded := make(map[string]bool) + for reg := range livePhys { + if pair, ok := regToPairMap[reg]; ok { + pairNeeded[pair] = true + } + } + + // Return in stable order. + var result []string + for _, pair := range []string{"AF", "BC", "DE", "HL", "IX", "IY"} { + if pairNeeded[pair] { + result = append(result, pair) + } + } + return result +} + +// regToPairMap maps individual Z80 registers to their PUSH-able pair. +var regToPairMap = map[string]string{ + "A": "AF", "F": "AF", "AF": "AF", + "BC": "BC", "DE": "DE", "HL": "HL", "IX": "IX", "IY": "IY", + "IXH": "IX", "IXL": "IX", "IYH": "IY", "IYL": "IY", + "B": "BC", "C": "BC", + "D": "DE", "E": "DE", + "H": "HL", "L": "HL", +} + +// saveABeforeOverwrite checks if A currently holds a vreg that is still live +// after the current instruction. If so, saves it to a scratch register. +// Used before emitLDA in ALU paths to prevent clobbering live values. +func (g *z80cg) saveABeforeOverwrite(inst *Inst) { + if g.curBlock == nil { + return + } + // Ties between entry parameters prefer the lowest virtual register ID. + // Find the vreg MOST RECENTLY defined in A, but only considering + // definitions BEFORE the current instruction. Only that vreg's + // value is actually in A right now. + instIdx := -1 + for idx, bi := range g.curBlock.Insts { + if bi == inst { + instIdx = idx + break + } + } + + var bestReg Reg + bestIdx := -2 + for r, loc := range g.ar.Locs { + if loc.Kind != LocReg || loc.Name != "A" { + continue + } + if _, ok := g.physOverride[r]; ok { + continue // already saved elsewhere + } + // Find definition index in the current block, only BEFORE inst + defIdx := -2 + for idx, bi := range g.curBlock.Insts { + if idx >= instIdx { + break // stop before current instruction + } + if bi.Dst == r { + defIdx = idx + } + } + // Params are defined before block — use -1 as sentinel + for _, cp := range g.fn.Contract.Params { + if cp.Reg == r && defIdx == -2 { + defIdx = -1 + } + } + if defIdx > bestIdx || (defIdx == bestIdx && defIdx > -2 && r < bestReg) { + bestIdx = defIdx + bestReg = r + } + } + if bestReg == NoReg { + return + } + // Skip if this vreg is not live after the current instruction + if !g.isVregLiveAfter(bestReg, inst) { + return + } + // Skip if this vreg is a source of the current instruction + for _, s := range inst.Src[:] { + if s == bestReg { + return + } + } + scratch := g.pickScratch8(inst) + g.emitf(" LD %s, A ; save r%d before A overwrite", scratch, bestReg) + g.physOverride[bestReg] = scratch +} + +// isVregLiveAfter checks if vreg is used by any instruction after target in the +// current block, or by the block's terminator. +// saveAccOperandIfLive copies the operand vreg held in A to a scratch +// register when an 8-bit ALU result is about to overwrite A and the operand is +// still used after inst. Later reads of the vreg are redirected via physOverride. +func (g *z80cg) saveAccOperandIfLive(src Reg, inst *Inst) { + if src == NoReg || src == inst.Dst || !g.isVregLiveAfter(src, inst) { + return + } + scratch := g.pickScratch8(inst) + g.emitf(" LD %s, A ; save r%d before ALU overwrite", scratch, src) + g.physOverride[src] = scratch + if g.pendingAccReg == src { + g.pendingAccReg = NoReg + } +} + +func (g *z80cg) isVregLiveAfter(vreg Reg, target *Inst) bool { + if g.curBlock == nil || vreg == NoReg { + return false + } + found := false + for _, inst := range g.curBlock.Insts { + if !found { + if inst == target { + found = true + } + continue + } + // Check if vreg is used by this instruction + for _, r := range inst.Uses() { + if r == vreg { + return true + } + } + } + // Check terminator + if g.curBlock.Term != nil { + for _, r := range g.curBlock.Term.termUses() { + if r == vreg { + return true + } + } + } + return false +} + +// regsLiveAfterInst includes successor and loop-backedge uses via CFG liveness. +func (g *z80cg) regsLiveAfterInst(target *Inst) map[Reg]bool { + live := make(map[Reg]bool) + if g.curBlock == nil { + return live + } + lr := ComputeLiveness(g.fn) + if out := lr.LiveOutOf(g.fn, g.curBlock); out != nil { + for _, r := range out.Slice() { + live[r] = true + } + } + if g.curBlock.Term != nil { + for _, r := range g.curBlock.Term.termUses() { + live[r] = true + } + } + for i := len(g.curBlock.Insts) - 1; i >= 0; i-- { + inst := g.curBlock.Insts[i] + if inst == target { + break + } + delete(live, inst.Dst) + for _, r := range inst.ExtraRets { + delete(live, r) + } + for _, r := range inst.Uses() { + if r != NoReg { + live[r] = true + } + } + } + return live +} + +// classPhysRegs returns physical register names associated with a register class. +func classPhysRegs(cls RegClass) []string { + switch cls { + case ClassAcc: + return []string{"A"} + case ClassCounter: + return []string{"B"} + case ClassGeneral: + return []string{"B", "C", "D", "E", "H", "L"} + case ClassPointer: + return []string{"H", "L", "D", "E"} + case ClassIndex: + return []string{"D", "E"} + case ClassPair: + return []string{"H", "L", "D", "E", "B", "C"} + } + return nil +} + +// computeClobbers returns a conservative sorted superset of emitted writes. +func computeClobbers(f *Func, ar *AllocResult) []string { + // Allocation destinations omit implicit writes by ALU lowering, parallel + // copies, caller saves, runtime helpers and transitive calls. ABI-owned + // registers are also clobbered: ownership does not imply preservation. + return []string{"A", "B", "BC", "C", "D", "DE", "E", "F", "H", "HL", "IX", "IXH", "IXL", "IY", "IYH", "IYL", "L"} +} + +func sortStrings(ss []string) { + // Simple insertion sort for small lists. + for i := 1; i < len(ss); i++ { + for j := i; j > 0 && ss[j] < ss[j-1]; j-- { + ss[j], ss[j-1] = ss[j-1], ss[j] + } + } +} + +// pushWord stages a value while preserving all register operands and flags. +// EX (SP),HL restores HL and leaves the loaded word on the stack. +func (g *z80cg) pushWord(src string) { + if isPairReg(src) || isIXY(src) { + g.emitf(" PUSH %s", src) + return + } + g.emit(" PUSH HL") + if isSpill(src) { + g.loadSpill16("HL", src) + } else { + g.emit(" PUSH AF") + g.emitLD8("L", src) + g.emit(" LD H, 0") + g.emit(" POP AF") + } + g.emit(" EX (SP), HL") + g.invalidate("HL") +} diff --git a/minzc/pkg/mir2/z80codegen_cmp.go b/minzc/pkg/mir2/z80codegen_cmp.go new file mode 100644 index 000000000..52e7ff756 --- /dev/null +++ b/minzc/pkg/mir2/z80codegen_cmp.go @@ -0,0 +1,867 @@ +package mir2 + +import ( + "fmt" +) + +// emitFlagPredicateByte writes a canonical 0/1 bool to A from the current +// condition flags, including two-test predicates (C or Z, NC and NZ). +func (g *z80cg) emitFlagPredicateByte(cc string) { + idx := g.trampIdx + g.trampIdx++ + trueLabel := fmt.Sprintf(".bool_true_%d", idx) + falseLabel := fmt.Sprintf(".bool_false_%d", idx) + doneLabel := fmt.Sprintf(".bool_done_%d", idx) + switch cc { + case "CLE": + g.emitf(" JP C, %s", trueLabel) + g.emitf(" JP Z, %s", trueLabel) + case "CGT": + g.emitf(" JP Z, %s", falseLabel) + g.emitf(" JP C, %s", falseLabel) + g.emitf(" JP %s", trueLabel) + default: + g.emitf(" JP %s, %s", cc, trueLabel) + } + g.emitf("%s:", falseLabel) + g.emit(" LD A, 0") + g.emitf(" JP %s", doneLabel) + g.emitf("%s:", trueLabel) + g.emit(" LD A, 1") + g.emitf("%s:", doneLabel) + g.invalidate("A") +} + +// ── Compare ─────────────────────────────────────────────────────────────────── + +// flagStillNeeded reports whether pendingFlagReg is used as a source +// in any instruction AFTER upcomingInst in the current block. +func (g *z80cg) flagStillNeeded(upcomingInst *Inst) bool { + if g.curBlock == nil { + return false + } + seen := false + for _, inst := range g.curBlock.Insts { + if inst == upcomingInst { + seen = true + continue + } + if !seen { + continue + } + for _, s := range inst.Src { + if s == g.pendingFlagReg { + return true + } + } + } + return false +} + +// accStillNeeded reports whether pendingAccReg is used as a source +// in any instruction AFTER upcomingInst in the current block, or by +// the block's terminal (e.g. passed as a block arg to a successor). +func (g *z80cg) accStillNeeded(upcomingInst *Inst) bool { + if g.curBlock == nil { + return false + } + seen := false + for _, inst := range g.curBlock.Insts { + if inst == upcomingInst { + seen = true + continue + } + if !seen { + continue + } + for _, s := range inst.Src { + if s == g.pendingAccReg { + return true + } + } + } + // Also check the block terminal — block params are passed via term args + // (e.g. TermBrIf passes acc through to the loop body block). + if g.curBlock.Term != nil { + for _, r := range g.curBlock.Term.termUses() { + if r == g.pendingAccReg { + return true + } + } + } + return false +} + +// pickScratch8 returns an 8-bit scratch register that is not currently +// occupied by an operand, destination or value live after upcomingInst. +// Prefers E, H, L (unlikely to hold params) before D, B, C. +// Always excludes "A" and "F". +func (g *z80cg) pickScratch8(upcomingInst *Inst) string { + used := map[string]bool{"A": true, "F": true} + mark := func(loc string) { + used[loc] = true + if isPairReg(loc) || isIXY(loc) { + used[highByte(loc)] = true + used[lowByte(loc)] = true + } + } + for r := range g.regsLiveAfterInst(upcomingInst) { + mark(g.loc(r)) + } + for _, r := range upcomingInst.Uses() { + mark(g.loc(r)) + } + mark(g.loc(upcomingInst.Dst)) + for _, loc := range g.physOverride { + mark(loc) + } + for _, r := range []string{"E", "H", "L", "D", "B", "C", "IYH", "IYL", "IXH", "IXL"} { + if !used[r] { + return r + } + } + return "D" // existing fallback for register-exhausted blocks +} + +// materializePendingFlag checks whether the most recent ClassFlag result +// (pendingFlagReg) is still needed after upcomingInst. If so, it emits: +// +// SBC A, A ; A = 0xFF if condition-true, 0x00 if condition-false +// LD , A +// +// where is a register that is not currently holding any live value, +// and reroutes pendingFlagReg via physOverride so subsequent genBinOp reads +// it from there rather than from the (now-clobbered) carry flag. +func (g *z80cg) materializePendingFlag(upcomingInst *Inst) { + if g.pendingFlagReg == NoReg { + return + } + if !g.flagStillNeeded(upcomingInst) { + g.pendingFlagReg = NoReg + return + } + scratch := g.pickScratch8(upcomingInst) + // Materialize carry → 0xFF (true) or 0x00 (false) via SBC A, A. + g.emit(" SBC A, A") + g.invalidate("A") + g.emitf(" LD %s, A", scratch) + g.physOverride[g.pendingFlagReg] = scratch + g.pendingFlagReg = NoReg +} + +// materializePendingAcc checks whether pendingAccReg (a ClassAcc value +// currently in A) is still needed after upcomingInst. If so, it saves A +// to a scratch register that is not currently holding any live value, +// and reroutes pendingAccReg via physOverride so the upcoming "LD A, imm" +// in genCmp does not clobber the live value. +func (g *z80cg) materializePendingAcc(upcomingInst *Inst) { + if g.pendingAccReg == NoReg { + return + } + if !g.accStillNeeded(upcomingInst) { + g.pendingAccReg = NoReg + return + } + scratch := g.pickScratch8(upcomingInst) + g.emitf(" LD %s, A ; materialize r%d (pendingAcc)", scratch, g.pendingAccReg) + g.physOverride[g.pendingAccReg] = scratch + g.pendingAccReg = NoReg +} + +// saveAccForCondRet saves A to a scratch register if a CMP is about to load a +// different vreg into A and there is a live vreg in A that the terminator or a +// successor block still needs. +// +// Originally this only covered TermCondRet (abs_val fix). Expanded to also +// cover TermBrIf: gcd(a,b) has CmpEq(b,0) that loads b into A, clobbering a +// which the then-branch returns. +func (g *z80cg) saveAccForCondRet(cmpInst *Inst) { + if g.fn == nil || g.curBlock == nil { + return + } + term := g.curBlock.Term + if term == nil { + return + } + + // Check if CMP will load a different vreg into A (clobbering current A value). + lhs := g.loc(cmpInst.Src[0]) + if lhs == "A" { + return // lhs already in A, no clobber + } + + // Collect all vregs that the terminator and successor blocks need. + // If any of them is currently in A, save A before the CMP clobbers it. + liveInA := g.findVregInA() + if liveInA == NoReg { + return // nothing in A worth saving + } + + needed := false + switch t := term.(type) { + case *TermCondRet: + for _, v := range t.Vals { + if v == liveInA { + needed = true + break + } + } + if !needed { + // Check if the Then successor needs it + needed = g.vregUsedInBlock(liveInA, t.Then) + } + case *TermBrIf: + // Check if either successor uses the vreg in A + needed = g.vregUsedInBlock(liveInA, t.Then) || g.vregUsedInBlock(liveInA, t.Else) + case *TermBrIf2: + needed = g.vregUsedInBlock(liveInA, t.Eq) || + g.vregUsedInBlock(liveInA, t.Lt) || + g.vregUsedInBlock(liveInA, t.Gt) + } + + if needed { + scratch := g.pickScratch8(cmpInst) + g.emitf(" LD %s, A ; save ret val before CMP", scratch) + g.physOverride[liveInA] = scratch + } +} + +// findVregInA returns the vreg currently mapped to A, or NoReg if none. +func (g *z80cg) findVregInA() Reg { + if g.fn == nil { + return NoReg + } + // Check block params first (they're live-through the block) + for _, bp := range g.curBlock.Params { + if g.loc(bp.Dst) == "A" { + return bp.Dst + } + } + // Check function params that might still be in A + for _, cp := range g.fn.Contract.Params { + if g.loc(cp.Reg) == "A" { + return cp.Reg + } + } + return NoReg +} + +// vregUsedInBlock checks if vreg is used in the instructions or terminator +// of the block with the given label. +func (g *z80cg) vregUsedInBlock(vreg Reg, label string) bool { + blk := g.fn.BlockByLabel(label) + if blk == nil { + return false + } + for _, inst := range blk.Insts { + for _, r := range inst.Uses() { + if r == vreg { + return true + } + } + } + if blk.Term != nil { + for _, r := range blk.Term.termUses() { + if r == vreg { + return true + } + } + } + return false +} + +func (g *z80cg) genCmp(inst *Inst) { + // CmpSubCarry / CmpSubCarryNot: carry flag already set by the immediately + // preceding SUB. No instruction needed — carry encodes a < b (C) or a >= b (NC). + if inst.Cond == CmpSubCarry || inst.Cond == CmpSubCarryNot { + return + } + + // Before emitting any flag-clobbering comparison instruction: + // 1. Materialize any live ClassFlag register whose carry would be overwritten. + // 2. Save any live ClassAcc register whose A value would be overwritten by + // the "LD A, imm" used to set up the CP operand. + // 3. Save A if the block ends with TermCondRet and A holds a return value + // that would be clobbered by loading the CMP operand (ADR-0041 fix). + // Both are saved to "D" (scratch) via physOverride; the OR-from-flag handler + // in genBinOp will then read them from "D" instead of from "F"/"A". + g.materializePendingFlag(inst) + g.materializePendingAcc(inst) + g.saveAccForCondRet(inst) + + lhs := g.loc(inst.Src[0]) + rhs := g.loc(inst.Src[1]) + + // 32-bit DWord comparison: must dispatch before isPairReg (DWord locs also + // have pair names like "HL", so they'd otherwise fall into genCmp16). + if g.isDWord(inst.Src[0]) || g.isDWord(inst.Src[1]) { + g.genCmp32(inst) + return + } + + if pat, ok := g.bitCmpPat[inst.Dst]; ok { + if pat.useMem { + ptr := g.loc(pat.ptrReg) + if isSpill(ptr) { + g.emitf(" LD HL, (%s) ; reload spilled ptr", ptr) + g.invalidate("HL") + ptr = "HL" + } + if g.emitBitMemOp("BIT", pat.bit, ptr, pat.byteOffset) { + g.pendingFlagReg = inst.Dst + return + } + } else { + src := g.loc(pat.srcReg) + src = selectBitReg(src, pat.byteOffset) + if isSimpleReg(src) && !isSpill(src) && !isIXYReg(src) { + g.emitf(" BIT %d, %s", pat.bit, src) + g.pendingFlagReg = inst.Dst + return + } + } + } + + // 16-bit comparison: one or both operands are register pairs (HL/DE/BC/…). + // Z80 only supports SBC HL, rr for 16-bit flag-setting subtraction. + cv, immediate := g.constVals[inst.Src[1]] + byteImmediate := immediate && cv >= 0 && cv <= 255 && !isPairReg(lhs) && (isSimpleReg(lhs) || isIXYReg(lhs)) && lhs != "F" && (inst.SrcTy == nil || inst.SrcTy.Width() <= 8) + if isPairReg(lhs) || (isPairReg(rhs) && !byteImmediate) { + g.genCmp16(inst) + return + } + + // CmpGt / CmpUgt and CmpLe / CmpUle are not directly expressible as a + // single Z80 flag after a normal CP (which would need NC+NZ or C+Z). + // Fix: force-swap the operands. + // CmpGt(a,b): a>b ↔ b). SCF/CCF preserve S, Z and P/V and do not change A or HL. +func (g *z80cg) normalizeSignedCmp(inst *Inst) { + if !isSignedOrdering(inst) { + return + } + idx := g.trampIdx + g.trampIdx++ + g.emitf(" JP M, .scmp_neg_%d", idx) + g.emitf(" JP PE, .scmp_true_%d", idx) + g.emitf(" JP .scmp_false_%d", idx) + g.emitf(".scmp_neg_%d:", idx) + g.emitf(" JP PO, .scmp_true_%d", idx) + g.emitf(".scmp_false_%d:", idx) + g.emit(" SCF") + g.emit(" CCF") + g.emitf(" JP .scmp_done_%d", idx) + g.emitf(".scmp_true_%d:", idx) + g.emit(" SCF") + g.emitf(".scmp_done_%d:", idx) +} + +// condCode returns the Z80 condition code for the virtual register holding +// the comparison result. If genCmp swapped the operands, the condition +// is automatically inverted via CmpCond.Swap(). +// +// For ClassFlag returns from CALL instructions, callFlags[cond] holds the +// agreed FlagCond directly — no AND A / CP 0 is needed at the call site. +func (g *z80cg) condCode(f *Func, cond Reg) string { + // Fast path: call result with ClassFlag return — condition already in F. + if fc, ok := g.callFlags[cond]; ok { + return cmpCondCode(fc) + } + + for _, b := range f.Blocks { + for _, inst := range b.Insts { + if inst.Dst == cond && inst.Op == OpCmp { + c := inst.Cond + if g.cmpSwapped[cond] { + c = c.Swap() // operands were swapped → invert condition + } + // When genCmp emitted AND A (rhs=0), carry is always 0. + // Replace NC/C-based conditions with NZ/Z-based ones: + // CmpGt(n, 0) → n != 0 → NZ + // CmpGe(n, 0) → n >= 0 → always true → handled as NC (fine) + // CmpLt(n, 0) → never for unsigned → C (never fires) + // CmpLe(n, 0) → n == 0 → Z + if g.cmpAndZero[cond] { + switch c { + case CmpGt, CmpUgt: + return "NZ" + case CmpLe, CmpUle: + return "Z" + } + } + if g.cmpNeedsTwo[cond] { + switch c { + case CmpGt, CmpUgt: + return "CGT" // NC && NZ: JP Z,else; JP NC,then + case CmpLe, CmpUle: + return "CLE" // C || Z: JP C,then; JP Z,then + } + } + return cmpCondCode(c) + } + } + } + return "NZ" // fallback +} + +// invertCC returns the Z80 condition code that is the logical negation of cc. +func invertCC(cc string) string { + switch cc { + case "Z": + return "NZ" + case "NZ": + return "Z" + case "C": + return "NC" + case "NC": + return "C" + case "PE": + return "PO" + case "PO": + return "PE" + case "M": + return "P" + case "P": + return "M" + } + return cc +} + +// cmpCondCode maps CmpCond → Z80 condition string. +func cmpCondCode(c CmpCond) string { + switch c { + case CmpEq: + return "Z" + case CmpNe: + return "NZ" + case CmpLt, CmpUlt, CmpSubCarry: + return "C" + case CmpGe, CmpUge, CmpSubCarryNot: + return "NC" + case CmpLe, CmpUle: + // LE: JP Z / JP C — requires two jumps; emit Z for now (backend TODO). + return "Z" + case CmpGt, CmpUgt: + return "NC" + } + return "NZ" +} + +// genCmp16 emits a 16-bit comparison using SBC HL, rr. +// +// Z80 only provides SBC HL, rr for 16-bit arithmetic that sets flags. +// The sequence is: +// +// OR A ; clear carry (preserves A; sets S/Z/H/PV from A) +// SBC HL, rr ; HL = lhs − rhs − 0; sets C (borrow) and Z (equal) +// +// SBC is destructive (clobbers HL). We save/restore HL with PUSH/POP so +// that lhs remains available for subsequent uses (e.g. the "return a" branch +// of min16). +// +// Conditions after SBC HL, rr (unsigned): +// +// C flag set → lhs < rhs (CmpUlt / CmpUge after negation) +// Z flag set → lhs == rhs +// +// Signed comparisons with an explicit signed SrcTy normalize S⊕V to carry +// after restoring HL, so the branch/materialization paths share one contract. +func (g *z80cg) genCmp16(inst *Inst) { + lhs := g.loc(inst.Src[0]) + rhs := g.loc(inst.Src[1]) + defer g.saveWrittenScratch(inst)() + + // CmpGt/CmpLe traditionally swap operands so SBC HL,rr gives the C flag + // directly. However, when lhs=HL and rhs=DE (common case), the swap + // forces EX DE,HL before+after SBC — 2 extra bytes, 16 extra T-states. + // + // Optimisation: when lhs is already in HL, do NOT swap; compute lhs-rhs + // directly and use a two-jump pattern (CGT/CLE) at the branch site. + // Cost: same bytes in branch, but saves the two EX DE,HL around the SBC. + isGtOrLe := inst.Cond == CmpGt || inst.Cond == CmpUgt || + inst.Cond == CmpLe || inst.Cond == CmpUle + if isGtOrLe { + if lhs == "HL" && isPairReg(rhs) { + // lhs already in HL — skip swap, use CGT/CLE two-jump pattern. + g.cmpNeedsTwo[inst.Dst] = true + // Do NOT set cmpSwapped — we keep original operand order. + } else { + // Fallback: swap operands as before. + lhs, rhs = rhs, lhs + g.cmpSwapped[inst.Dst] = true + } + } + + // A byte lhs must not be extended into HL while rhs still lives there. + // Stage both values before changing either register (also covers D/E). + if rhs == "HL" && (isIXYReg(lhs) || (isSimpleReg(lhs) && !isPairReg(lhs) && lhs != "F")) { + g.pushWord(lhs) + g.pushWord(rhs) + g.emit(" POP DE") + g.emit(" POP HL") + g.invalidate("DE") + g.invalidate("HL") + lhs, rhs = "HL", "DE" + } + // Guard: materialise F or 8-bit operands to proper 16-bit regs. + if lhs == "F" { + g.emit(" SBC A, A") // materialise flag to A + g.emit(" LD L, A") + g.emit(" LD H, 0") + g.invalidate("A") + g.invalidate("HL") + lhs = "HL" + } else if isIXYReg(lhs) { + // IXH/IXL/IYH/IYL → zero-extend to HL via shadow A. + g.emitMovViaAltA("L", lhs) + g.emit(" LD H, 0") + g.invalidate("HL") + lhs = "HL" + } else if isSimpleReg(lhs) && !isPairReg(lhs) { + // 8-bit reg → zero-extend to HL + g.emitLD8("L", lhs) + g.emit(" LD H, 0") + g.invalidate("HL") + lhs = "HL" + } + if rhs == "F" || isIXYReg(rhs) || (isSimpleReg(rhs) && !isPairReg(rhs)) { + // Already handled below in the SBC guard, but mark for safety. + } + + // SBC HL, rr requires lhs in HL. If lhs is already HL we proceed. + // If rhs is HL and lhs is DE, EX DE,HL is the cheapest fix. + // Otherwise move lhs byte-by-byte into HL. + origLhs, origRhs := lhs, rhs + if lhs != "HL" { + if lhs == "DE" && rhs == "HL" { + // Swap via EX DE,HL: after EX, HL=old_DE=lhs, DE=old_HL=rhs. + g.emit(" EX DE, HL") + g.invalidate("HL") + g.invalidate("DE") + lhs, rhs = "HL", "DE" + } else if rhs == "HL" { + // rhs is HL, lhs is BC or IX/IY. Swap: save rhs(HL)→DE, load lhs→HL. + g.emitf(" LD D, H") // save rhs high into D + g.emitf(" LD E, L") // save rhs low into E + if isIXY(lhs) { + // IX→HL: byte-copy invalid. Use PUSH/POP. + g.emitf(" PUSH %s", lhs) + g.emit(" POP HL") + } else if isIXYReg(lhs) || (isSimpleReg(lhs) && !isPairReg(lhs)) { + // 8-bit register: zero-extend to HL. + g.emitMovViaAltA("L", lhs) + g.emit(" LD H, 0") + } else if isSpill(lhs) { + g.loadSpill16("HL", lhs) + } else { + g.emitf(" LD H, %s", highByte(lhs)) + g.emitLD8("L", lowByte(lhs)) + } + g.invalidate("HL") + g.invalidate("DE") + lhs, rhs = "HL", "DE" + } else if isIXY(lhs) { + // IX/IY → HL: byte-copy invalid (DD prefix conflict). Use PUSH/POP. + g.emitf(" PUSH %s", lhs) + g.emit(" POP HL") + g.invalidate("HL") + lhs = "HL" + } else if isIXYReg(lhs) || (isSimpleReg(lhs) && !isPairReg(lhs)) { + // 8-bit register (including IXH/IXL): zero-extend into HL. + g.emitMovViaAltA("L", lhs) + g.emit(" LD H, 0") + g.invalidate("HL") + } else if isSpill(lhs) { + g.loadSpill16("HL", lhs) + g.invalidate("HL") + } else { + // Neither operand is HL; move lhs into HL byte-by-byte. + g.emitf(" LD H, %s", highByte(lhs)) + g.emitLD8("L", lowByte(lhs)) + g.invalidate("HL") + lhs = "HL" + } + } + + // Restore the original operand orientation after SBC. The wrapper + // restores any live values overwritten by staging or subtraction. + + swappedDE := origRhs == "HL" && origLhs != "HL" + + // SBC HL, rr requires rhs to be a 16-bit register pair (BC/DE/HL/SP). + // Guard against invalid operands: F, single 8-bit regs, IXY halves. + if rhs == "F" { + // Flag result used as 16-bit comparison operand — materialise to BC. + g.emit(" SBC A, A") // A = 0xFF if carry, 0x00 if not + g.emit(" LD C, A") + g.emit(" LD B, 0") + g.invalidate("A") + rhs = "BC" + } else if isIXYReg(rhs) { + // IXH/IXL/IYH/IYL: load into BC via A. + g.emitf(" LD A, %s", rhs) + g.emit(" LD C, A") + g.emit(" LD B, 0") + g.invalidate("A") + rhs = "BC" + } else if isSimpleReg(rhs) && !isPairReg(rhs) { + // 8-bit register: promote to pair (handles A→BC, others→parentPair). + rhs = g.promote8toPair(rhs) + } else if isIXY(rhs) { + // IX/IY: push to HL would conflict. Push rhs → stack, pop to BC. + g.emitf(" PUSH %s", rhs) + g.emit(" POP BC") + rhs = "BC" + } else if isSpill(rhs) { + // LocMem spill: SBC HL,$F0xx is not valid. Load to BC first. + g.loadSpill16("BC", rhs) + rhs = "BC" + } + + // The selected-sequence wrapper preserves HL only when its original + // value is live. POP restores it without disturbing comparison flags. + g.emit(" OR A") + g.emitSBCHL(rhs) + + if swappedDE { + // EX leaves the comparison flags intact; live originals are restored + // by the wrapper after signed-flag normalization. + + g.emit(" EX DE, HL") + g.invalidate("HL") + g.invalidate("DE") + } else { + g.invalidate("HL") + } + g.normalizeSignedCmp(inst) + g.pendingFlagReg = inst.Dst +} + +// genCmp32 emits a non-destructive 32-bit unsigned comparison (lhs vs rhs) +// via a PUSH/SBC-chain/POP pattern; carry is set if lhs < rhs, NC if lhs >= rhs. +// +// Strategy (lhs=HL+H'L', rhs=DE+D'E'): +// +// PUSH HL ; save a_lo (11T) +// EXX +// PUSH HL ; save a_hi (11T, HL = H'L' after EXX) +// EXX +// AND A ; clear carry (4T) +// SBC HL, DE ; a_lo - b_lo; carry = lo borrow (15T) +// EXX +// SBC HL, DE ; a_hi - b_hi - lo_carry; carry = 32-bit borrow (15T) +// EXX +// EXX ; switch to restore a_hi +// POP HL ; restore H'L' (10T); carry preserved by POP rr +// EXX +// POP HL ; restore HL (10T); carry preserved +// +// Total: 91T. For the typical abs_diff pattern this is followed immediately by +// BrIf which branches on carry — no intermediate store of the flag needed. +func (g *z80cg) genCmp32(inst *Inst) { + lhs := g.loc(inst.Src[0]) + rhs := g.loc(inst.Src[1]) + + // CmpGt/CmpLe: swap operands so the carry encodes the right condition. + // CmpUgt(a,b): a>b ↔ b= len(g.fn.Contract.Returns) { + break + } + if rv == NoReg { + continue // void call result; return register already correct + } + ret := g.fn.Contract.Returns[i] + retLoc := canonicalReturnLoc(ret.Class, ret.Ty) + src := g.loc(rv) + if src != retLoc && !g.holdsValue(retLoc, src) { + copies = append(copies, parallelCopy{srcName: src, dstName: retLoc, ty: ret.Ty}) + } + } + return copies +} + +// buildBlockCopies computes the parallel copies needed to pass args to a +// target block's formal parameters. Returns only moves where src ≠ dst. +func (g *z80cg) buildBlockCopies(f *Func, targetName string, args []Reg) []parallelCopy { + if len(args) == 0 { + return nil + } + var target *Block + for _, b := range f.Blocks { + if b.Label == targetName { + target = b + break + } + } + if target == nil { + return nil + } + var copies []parallelCopy + for i, arg := range args { + if i >= len(target.Params) { + break + } + dst := g.loc(target.Params[i].Dst) + ty := target.Params[i].Ty + // Use g.loc (which respects physOverride) so that values saved to scratch + // registers by materializePendingAcc/materializePendingFlag are correctly + // moved to their canonical destination in the target block. + src := g.loc(arg) + if src == dst { + continue // pre-coalescing or same-location allocation — no copy needed + } + // If this argument is a compile-time constant, mark it for deferred + // rematerialisation AFTER all register-to-register copies are resolved. + // Emitting LD dst, imm inline would clobber live source registers that + // other copies still need (BUG-002). + // Guard: only rematerialise if src != dst (handled above) to avoid + // overwriting a live value when a const reg was pre-coalesced with its + // loop param (they share a root; the const check would fire spuriously). + if cv, isConst := g.constVals[arg]; isConst && dst != "" { + copies = append(copies, parallelCopy{dstName: dst, ty: ty, isImm: true, immVal: cv}) + continue + } + if src != "" { + copies = append(copies, parallelCopy{srcName: src, dstName: dst, ty: ty}) + } + } + return copies +} + +// emitParallelCopy resolves and emits a set of parallel register moves, +// correctly handling cycles (e.g. HL↔DE → EX DE,HL). +// Constant rematerialisations (isImm=true) are emitted AFTER all register-to- +// register moves so they don't clobber live source registers. +func (g *z80cg) emitParallelCopy(copies []parallelCopy) { + if len(copies) == 0 { + return + } + type move struct { + src, dst string + ty Ty + done bool + } + // Separate constants from register moves; emit register moves first. + var immCopies []parallelCopy + var regCopies []parallelCopy + for _, c := range copies { + if c.isImm { + immCopies = append(immCopies, c) + } else { + regCopies = append(regCopies, c) + } + } + moves := make([]move, len(regCopies)) + for i, c := range regCopies { + moves[i] = move{src: c.srcName, dst: c.dstName, ty: c.ty} + } + + for { + // Emit any move whose dst is not a source of another pending move. + progress := false + for i := range moves { + if moves[i].done { + continue + } + dstNeeded := false + for j := range moves { + if j != i && !moves[j].done && moves[j].src == moves[i].dst { + dstNeeded = true + break + } + } + if !dstNeeded { + g.emitSingleCopy(moves[i].src, moves[i].dst, moves[i].ty) + moves[i].done = true + progress = true + break + } + } + if progress { + continue + } + + // All remaining moves form cycles. Find first undone. + first := -1 + undone := 0 + for i := range moves { + if !moves[i].done { + undone++ + if first == -1 { + first = i + } + } + } + if first == -1 { + break + } + + // Special case: HL↔DE two-node cycle → EX DE, HL. + if undone == 2 { + a, b := -1, -1 + for i := range moves { + if !moves[i].done { + if a == -1 { + a = i + } else { + b = i + } + } + } + as, ad := moves[a].src, moves[a].dst + bs, bd := moves[b].src, moves[b].dst + if (as == "HL" && ad == "DE" && bs == "DE" && bd == "HL") || + (as == "DE" && ad == "HL" && bs == "HL" && bd == "DE") { + g.emit(" EX DE, HL") + moves[a].done = true + moves[b].done = true + continue + } + } + + // General cycle: break with a temporary. + m := &moves[first] + if m.ty.Width() <= 8 { + // u8 cycle: break by saving the first node's source to a scratch register. + // + // Walk BACKWARD through the cycle so we always read old values. + // Example 3-cycle D→C, C→E, E→D (scratch=A): + // LD A, D (save D into scratch) + // LD D, E (backward: who writes TO D? E→D. E still original.) + // LD E, C (backward: who writes TO E? C→E. C still original.) + // LD C, A (put D_old into C via scratch) + // + // When A is part of the cycle, using A as scratch would overwrite it + // during the walk before the final restore. Pick the first 8-bit + // register not involved in any pending move instead. + cycleRegs := map[string]bool{} + for i := range moves { + if !moves[i].done { + cycleRegs[moves[i].src] = true + cycleRegs[moves[i].dst] = true + } + // Also mark done-move destinations as live (they hold values). + if moves[i].done { + cycleRegs[moves[i].dst] = true + } + } + scratch := "A" + if cycleRegs["A"] { + for _, r := range []string{"C", "E", "H", "L", "D", "B"} { + if !cycleRegs[r] { + scratch = r + break + } + } + } + + if cycleRegs[scratch] { + // All seven byte registers are occupied. Snapshot the pending + // sources on the stack; shadow AF protects the main accumulator. + var pending []int + for i := range moves { + if moves[i].done { + continue + } + pending = append(pending, i) + if moves[i].src != "A" { + g.emit(" EX AF, AF'") + g.emitLD8("A", moves[i].src) + } + g.emit(" PUSH AF") + if moves[i].src != "A" { + g.emit(" EX AF, AF'") + } + } + for j := len(pending) - 1; j >= 0; j-- { + i := pending[j] + if moves[i].dst != "A" { + g.emit(" EX AF, AF'") + } + g.emit(" POP AF") + if moves[i].dst != "A" { + g.emitLD8(moves[i].dst, "A") + g.emit(" EX AF, AF'") + } + moves[i].done = true + } + continue + } + + firstDst := m.dst + if m.src != scratch { + g.emitSingleCopy(m.src, scratch, m.ty) + } + m.done = true + cur := m.src // start at the freed slot (m.src saved to scratch) + for { + found := false + for i := range moves { + if !moves[i].done && moves[i].dst == cur { + g.emitSingleCopy(moves[i].src, moves[i].dst, moves[i].ty) + cur = moves[i].src // the source is the new freed slot + moves[i].done = true + found = true + break + } + } + if !found { + break + } + } + if firstDst != scratch { + g.emitSingleCopy(scratch, firstDst, m.ty) + } + } else { + // Walk backwards around a word cycle, just as for byte cycles. + // Saving src frees src, not dst: BC->HL, HL->DE, DE->BC + // becomes PUSH BC; BC=DE; DE=HL; POP HL. + if isSpill(m.src) { + g.emit(" PUSH HL") + g.loadSpill16("HL", m.src) + g.emit(" EX (SP), HL") + } else { + g.emitf(" PUSH %s", m.src) + } + m.done = true + cur := m.src + for { + found := false + for i := range moves { + if !moves[i].done && moves[i].dst == cur { + g.emitSingleCopy(moves[i].src, moves[i].dst, moves[i].ty) + cur = moves[i].src + moves[i].done = true + found = true + break + } + } + if !found { + break + } + } + if isSpill(m.dst) { + g.emit(" EX (SP), HL") + g.storeSpill16(m.dst, "HL") + g.emit(" POP HL") + } else { + g.emitf(" POP %s", m.dst) + } + } + } + // After all register-to-register moves are resolved, emit constant assignments. + // Done last so they cannot clobber live source registers. + for _, c := range immCopies { + if c.dstName == "F" { + // F register: use AND A (clear=0) or SCF (set=1). + if c.immVal == 0 { + g.emit(" AND A") // clears C, sets Z + } else { + g.emit(" SCF") // sets C + } + } else if isSpill(c.dstName) { + g.emit(" EX AF, AF'") + g.emitf(" LD A, %d", c.immVal&0xFF) + g.emitf(" LD (%s), A", c.dstName) + g.emit(" EX AF, AF'") + } else if c.ty.Width() <= 8 { + g.emitf(" LD %s, %d", c.dstName, c.immVal&0xFF) + } else { + g.emitf(" LD %s, %d", c.dstName, c.immVal&0xFFFF) + } + } +} + +// emitSingleCopy emits a non-destructive register copy (src → dst). +// For 16-bit, this emits two LD instructions (not EX DE,HL which is a swap). +func (g *z80cg) emitSingleCopy(src, dst string, ty Ty) { + if src == dst { + return + } + // F register: cannot be accessed directly. Materialise flag→register or + // register→flag via the same logic as emitMov. + if src == "F" { + if g.emitKnownFlagCopy(dst, ty.Width()) { + return + } + // Flag → register: SBC A,A materialises carry into A (0xFF/0x00). + if dst == "A" { + g.emit(" SBC A, A") + } else if isPairReg(dst) { + // Flag → 16-bit pair: materialise + zero-extend. + g.emit(" SBC A, A") + g.emitf(" LD %s, A", lowByte(dst)) + g.emitf(" LD %s, 0", highByte(dst)) + } else { + g.emit(" SBC A, A") + g.emitLD8(dst, "A") + } + g.invalidate("A") + return + } + if dst == "F" { + // Register → flag: set Z/NZ from value via AND A. + if src != "A" { + g.emitf(" LD A, %s", src) + } + g.emit(" AND A") + g.invalidate("A") + return + } + if ty.Width() <= 8 { + switch { + case isSpill(src) && dst == "A": + g.emitf(" LD A, (%s)", src) + case isSpill(src): + g.emitf(" LD A, (%s)", src) + g.emitLD8(dst, "A") + g.invalidate("A") + case isSpill(dst) && src == "A": + g.emitf(" LD (%s), A", dst) + case isSpill(dst): + g.emitLDA(src) + g.emitf(" LD (%s), A", dst) + case (isIXYReg(src) && (dst == "H" || dst == "L")) || + (isIXYReg(dst) && (src == "H" || src == "L")): + // DD/FD prefix conflict: LD H,IXH encodes as LD IXH,IXH (NOP). + // Route through shadow A to preserve main A. + g.emitMovViaAltA(dst, src) + case isPairReg(src) && !isPairReg(dst): + // Width mismatch: 16-bit source → 8-bit dest. Truncate (take low byte). + lo := lowByte(src) + if (isIXYReg(lo) && (dst == "H" || dst == "L")) || + ((lo == "H" || lo == "L") && isIXYReg(dst)) { + g.emitMovViaAltA(dst, lo) + } else { + g.emitLD8(dst, lo) + } + case !isPairReg(src) && isPairReg(dst): + // Width mismatch: 8-bit source → 16-bit dest. Zero-extend. + lo := lowByte(dst) + if (isIXYReg(src) && (lo == "H" || lo == "L")) || + ((src == "H" || src == "L") && isIXYReg(lo)) { + g.emitMovViaAltA(lo, src) + } else { + g.emitLD8(lo, src) + } + g.emitf(" LD %s, 0", highByte(dst)) + default: + g.emitLD8(dst, src) + } + } else if isSpill(src) && isSpill(dst) { + // Spill-to-spill: route through HL. + g.loadSpill16("HL", src) + g.storeSpill16(dst, "HL") + g.invalidate("HL") + } else if isSpill(src) { + // LocMem spill → register: use loadSpill16 (handles TSMC). + if isPairReg(dst) { + g.loadSpill16(dst, src) + } else { + // Spill → 8-bit: load low byte only + g.loadSpill8(dst, src) + } + } else if isSpill(dst) { + // Register → LocMem spill: use storeSpill16 (handles TSMC). + if isPairReg(src) { + g.storeSpill16(dst, src) + } else { + // 8-bit → spill + g.storeSpill8(dst, src) + } + } else if isPairReg(src) && !isPairReg(dst) { + // Width mismatch in 16-bit context: pair→8bit truncation. + lo := lowByte(src) + if (isIXYReg(lo) && (dst == "H" || dst == "L")) || + ((lo == "H" || lo == "L") && isIXYReg(dst)) { + g.emitMovViaAltA(dst, lo) + } else { + g.emitLD8(dst, lo) + } + } else if !isPairReg(src) && isPairReg(dst) { + // Width mismatch in 16-bit context: 8bit→pair zero-extension. + lo := lowByte(dst) + if (isIXYReg(src) && (lo == "H" || lo == "L")) || + ((src == "H" || src == "L") && isIXYReg(lo)) { + g.emitMovViaAltA(lo, src) + } else { + g.emitLD8(lo, src) + } + g.emitf(" LD %s, 0", highByte(dst)) + } else { + // 16-bit non-destructive copy: two 8-bit LDs. + hi := highByte(dst) + lo := lowByte(dst) + hiS := highByte(src) + loS := lowByte(src) + // DD/FD prefix conflict: LD H,IXH / LD L,IXL encode as NOPs. + // IX↔HL copies must use PUSH/POP or route through A. + if (isIXY(src) && dst == "HL") || (isIXY(dst) && src == "HL") { + g.emitf(" PUSH %s", src) + g.emitf(" POP %s", dst) + } else if (isIXYReg(hiS) && (hi == "H" || hi == "L")) || + (isIXYReg(loS) && (lo == "H" || lo == "L")) || + ((hiS == "H" || hiS == "L") && isIXYReg(hi)) || + ((loS == "H" || loS == "L") && isIXYReg(lo)) { + // Route through shadow A to avoid clobbering main A. + g.emit(" EX AF, AF'") + g.emitf(" LD A, %s", hiS) + g.emitLD8(hi, "A") + g.emitf(" LD A, %s", loS) + g.emitLD8(lo, "A") + g.emit(" EX AF, AF'") + } else { + g.emitLD8(hi, hiS) + g.emitLD8(lo, loS) + } + } +} diff --git a/minzc/pkg/mir2/z80codegen_inst.go b/minzc/pkg/mir2/z80codegen_inst.go new file mode 100644 index 000000000..1f3d59410 --- /dev/null +++ b/minzc/pkg/mir2/z80codegen_inst.go @@ -0,0 +1,1166 @@ +package mir2 + +import ( + "fmt" + "maps" + "slices" +) + +// ── Instructions ────────────────────────────────────────────────────────────── + +func (g *z80cg) genInst(inst *Inst) { + // Instructions merged into a page-aligned LUT load are skipped here; + // their code is emitted by the Load case below. + if g.lutSkip[inst.Dst] { + return + } + + if inst.Op != OpStore && g.bitStoreSkip[inst.Dst] { + return + } + + if inst.Op != OpCmp && g.bitCmpSkip[inst.Dst] { + return + } + + // Instructions merged into a global-field direct-addressing sequence are + // skipped here; their code is emitted by the Load/Store case below. + // (Stores have Dst==NoReg; check the addr-reg via the instruction's Src[0].) + if inst.Op != OpStore && g.globalFieldSkip[inst.Dst] { + return + } + + // Scratch relocations of A are instruction-local. Return them to their + // allocated home before another instruction can allocate that scratch. + for _, r := range slices.Sorted(maps.Keys(g.physOverride)) { + loc := g.physOverride[r] + if g.ar.Loc(r).Name == "A" && loc != "A" && loc != "F" { + g.emitLD8("A", loc) + delete(g.physOverride, r) + g.invalidate("A") + } + } + + // Flag-clobbering ops clear lastFlagsLhs/Rhs explicitly in their cases. + // Flag-preserving ops (LD, PUSH, POP, …) leave the tracker intact so that + // the Sub+CmpLt flag-fusion peephole can fire across an intervening LD. + + dst := g.loc(inst.Dst) + + switch inst.Op { + case OpConst: + // Only record as constant if the Dst is NOT a block parameter. + // After PreallocCoalesce, an OpConst may share a Reg with a block param + // (loop counter). The initial value is constant but the param changes + // each iteration — treating it as constant produces wrong code. + if !g.blockParamRegs[inst.Dst] { + g.constVals[inst.Dst] = inst.Imm + } + if dst == "F" { + // Bool constant into flag register: SCF (true) or AND A (false, clears C+sets Z). + g.lastFlagsLhs = "" + g.lastFlagsRhs = "" + if inst.Imm != 0 { + g.emit(" SCF") + } else { + g.emit(" AND A") // A=A (no change), clears C, sets Z based on A + } + return + } + if !g.deadConsts[inst.Dst] { + w := inst.Ty.Width() + if w >= 24 { + // 24/32-bit constant via shadow pair. + // LD rr, lo16 (10T) + // EXX (4T) + // LD rr, hi (10T) — hi8 for u24, hi16 for u32 + // EXX (4T) = 28T total + // For u24: hi is at most 0x00FF, so shadow H' is always 0. + lo := uint32(inst.Imm) & 0xFFFF + hi := uint32(inst.Imm) >> 16 + g.emitf(" LD %s, %d", dst, lo) + g.emit(" EXX") + g.emitf(" LD %s, %d", dst, hi) + g.emit(" EXX") + } else if isSpill(dst) { + // LocMem spill destination. + // TSMC: if eligible, patch reload sites instead of memory store. + if pair := g.tsmcSpillPairFor(inst.Dst); pair != nil { + if w <= 8 { + g.emitf(" LD A, %d", inst.Imm&0xFF) + g.emitTSMCSpill(pair, "A") + g.invalidate("A") + } else { + g.emitf(" LD HL, %d", inst.Imm&0xFFFF) + g.emitTSMCSpill(pair, "HL") + g.invalidate("HL") + } + } else { + // Traditional spill + if w <= 8 { + g.emitf(" LD A, %d", inst.Imm&0xFF) + g.emitf(" LD (%s), A", dst) + g.invalidate("A") + } else { + g.emitf(" LD HL, %d", inst.Imm&0xFFFF) + g.emitf(" LD (%s), HL", dst) + g.invalidate("HL") + } + } + } else if isSpill(dst) { + g.emit(" EX AF, AF'") + g.emitf(" LD A, %d", inst.Imm&0xFF) + g.emitf(" LD (%s), A", dst) + g.emit(" EX AF, AF'") + } else { + g.emitf(" LD %s, %d", dst, inst.Imm) + } + } + + case OpMove: + src := g.loc(inst.Src[0]) + if dst == src { + return // no-op + } + g.emitMov(dst, src, inst.Ty.Width()) + + case OpAdd: + g.lastFlagsLhs = "" + g.lastFlagsRhs = "" + g.genBinOp("ADD", inst) + + case OpSub: + g.lastFlagsLhs = "" // clear stale tracking before this sub + g.lastFlagsRhs = "" + g.genBinOp("SUB", inst) + // Record that flags were set by SUB(lhs, rhs) for the Sub+CmpLt fusion. + // Only track 8-bit subs; 16-bit uses SBC which sets different flags. + if inst.Ty.Width() <= 8 { + rhsP := g.loc(inst.Src[1]) + if cv, ok := g.constVals[inst.Src[1]]; ok { + rhsP = fmt.Sprintf("%d", cv) + } + if rhsP != "A" { // skip NEG-trick case (rhs was in A) + g.lastFlagsLhs = g.loc(inst.Src[0]) // logical lhs + g.lastFlagsRhs = rhsP + } + } + + case OpMul: + g.lastFlagsLhs = "" + g.lastFlagsRhs = "" + g.genMul(inst) + + case OpDiv, OpSDiv, OpSMod, OpMod: + g.lastFlagsLhs = "" + g.lastFlagsRhs = "" + g.genDivMod(inst) + + case OpAnd: + g.lastFlagsLhs = "" + g.lastFlagsRhs = "" + g.genBinOp("AND", inst) + + case OpOr: + g.lastFlagsLhs = "" + g.lastFlagsRhs = "" + g.genBinOp("OR", inst) + + case OpXor: + g.lastFlagsLhs = "" + g.lastFlagsRhs = "" + g.genBinOp("XOR", inst) + + case OpBitGet: + g.lastFlagsLhs = "" + g.lastFlagsRhs = "" + src := g.loc(inst.Src[0]) + bit := int(inst.Imm) + byteOffset := bit / 8 + bitInByte := bit % 8 + target := selectBitReg(src, byteOffset) + if isSpill(src) { + if inst.SrcTy != nil && inst.SrcTy.Width() > 8 { + g.loadSpill16("HL", src) + target = selectBitReg("HL", byteOffset) + } else { + g.emitLD8("A", src) + target = "A" + } + } + if target != "A" { + g.emitLDA(target) + } + for i := 0; i < bitInByte; i++ { + g.emit(" SRL A") + } + g.emit(" AND 1") + g.invalidate("A") + if dst != "A" { + g.emitLD8(dst, "A") + } else { + g.pendingAccReg = inst.Dst + } + + case OpBitSet, OpBitReset: + g.lastFlagsLhs = "" + g.lastFlagsRhs = "" + src := g.loc(inst.Src[0]) + bit := int(inst.Imm) + byteOffset := bit / 8 + bitInByte := bit % 8 + op := "SET" + if inst.Op == OpBitReset { + op = "RES" + } + if dst != src { + g.emitMov(dst, src, inst.Ty.Width()) + } + target := selectBitReg(dst, byteOffset) + if g.emitBitRegOp(op, bitInByte, target) { + g.invalidate(dst) + g.invalidate(target) + break + } + if isSpill(dst) && inst.Ty.Width() > 8 { + g.loadSpill16("HL", dst) + target = selectBitReg("HL", byteOffset) + g.emitLDA(target) + if op == "SET" { + g.emitf(" OR %d", 1<= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { + return false + } + } + return true +} + +// ── Register topology ───────────────────────────────────────────────────────── +// +// Writing to a 16-bit pair register (e.g. HL) implicitly changes its bytes +// (H, L), and vice versa: writing to H invalidates HL. These tables encode +// that topology so invalidate() can clear all affected aliases at once. + +// regBytes maps a 16-bit register to its 8-bit byte components. +var regBytes = map[string][]string{ + "BC": {"B", "C"}, + "DE": {"D", "E"}, + "HL": {"H", "L"}, + "IX": {"IXH", "IXL"}, + "IY": {"IYH", "IYL"}, +} + +// regParent maps an 8-bit component to its 16-bit parent register. +var regParent = map[string]string{ + "B": "BC", "C": "BC", + "D": "DE", "E": "DE", + "H": "HL", "L": "HL", + "IXH": "IX", "IXL": "IX", + "IYH": "IY", "IYL": "IY", +} + +// killOne removes a single register and its bidirectional partner from holdsPhys. +func (g *z80cg) killOne(r string) { + if partner, ok := g.holdsPhys[r]; ok { + delete(g.holdsPhys, partner) + delete(g.holdsPhys, r) + } +} + +// invalidate removes all tracking for r: kills r itself, its byte components +// (if r is a 16-bit pair), and its parent pair (if r is an 8-bit component). +func (g *z80cg) invalidate(r string) { + g.killOne(r) + if bytes, ok := regBytes[r]; ok { + for _, b := range bytes { + g.killOne(b) + } + } + if parent, ok := regParent[r]; ok { + g.killOne(parent) + } +} + +// setCopy records that dst was loaded from src (they now hold the same value). +// Invalidates dst's old aliases and clears src's previous bidirectional link +// before establishing the new one, preventing stale reverse entries. +func (g *z80cg) setCopy(dst, src string) { + g.invalidate(dst) + g.killOne(src) // break src's old reverse-link (could be left by a prior setCopy) + if isSimpleReg(dst) && isSimpleReg(src) { + g.holdsPhys[dst] = src + g.holdsPhys[src] = dst + } +} + +// holdsValue reports whether physical register a currently holds the same +// value as b (either they are the same register, or a copy alias exists). +func (g *z80cg) holdsValue(a, b string) bool { + return a == b || g.holdsPhys[a] == b +} + +// emitLDA emits "LD A, src" and records the copy alias A ≡ src. +// When src is a LocMem address ($Fxxx), emits "LD A, ($Fxxx)" — the only +// valid Z80 form for loading an 8-bit value from an absolute 16-bit address. +func (g *z80cg) emitLDA(src string) { + if src == "F" { + // F register cannot be loaded directly. Materialise carry flag. + g.emit(" SBC A, A") // A=0xFF if C=1, A=0x00 if C=0 + g.invalidate("A") + return + } + if isSpill(src) { + g.emitf(" LD A, (%s)", src) + } else if isPairReg(src) { + // Width mismatch: pair→8bit. Take low byte. + g.emitf(" LD A, %s", lowByte(src)) + } else { + g.emitf(" LD A, %s", src) + } + g.setCopy("A", src) +} + +// loc returns the physical location name for virtual register r. +// For LocMem registers the allocator stores the actual byte address in Offset; +// return it as a hex literal so the assembler resolves it correctly. +// e.g. Offset=0xF001 → "$F001", which assembles as LD D, ($F001) / LD ($F001), A etc. +func (g *z80cg) loc(r Reg) string { + if r == NoReg { + return "?" + } + if phys, ok := g.physOverride[r]; ok { + return phys + } + pl := g.ar.Loc(r) + if pl.Kind == LocMem { + return g.spillLabel(r) + } + return pl.Name +} + +// spillLabel returns a named label for a spilled virtual register. +// Format: _spill_{funcName}_r{regNum} +// The label is emitted in the data section at end of function. +func (g *z80cg) spillLabel(r Reg) string { + return fmt.Sprintf("_spill_%s_r%d", sanitizeIdent(g.fn.Name), int(r)) +} + +// spillWidth returns the byte width of a spilled register (1, 2, or 3 for eZ80). +func (g *z80cg) spillWidth(r Reg) int { + if info, ok := collectRegInfo(g.fn)[r]; ok { + return (info.Ty.Width() + 7) / 8 + } + return 1 +} + +// physName returns the assembly-level name for a PhysLoc. +// For LocMem, returns the named spill label; for everything else, returns pl.Name. +func physName(pl PhysLoc) string { + // Note: physName is used outside genFunc context (e.g. buildBlockCopies) + // where we don't have access to g.fn. Fall back to $F0xx for those cases. + if pl.Kind == LocMem { + return fmt.Sprintf("$%04X", pl.Offset) + } + return pl.Name +} + +// physNameFor returns the assembly-level name for a PhysLoc within function context. +func (g *z80cg) physNameFor(pl PhysLoc, r Reg) string { + if pl.Kind == LocMem { + return g.spillLabel(r) + } + return pl.Name +} + +// emitLD8 emits a safe 8-bit LD, handling spill labels. +// +// Backend policy: IXH/IXL/IYH/IYL are treated as first-class 8-bit registers, +// not as exotic fallbacks reserved only for bit operations. Direct half-reg +// paths should be preferred whenever they are encoding-correct; alternative +// routes through A / pair copies / memory are justified only by real DD/FD +// prefix conflicts or by proven size/timing wins elsewhere. +// +// Z80 only supports LD A,(nn) and LD (nn),A for absolute memory. +// Routes through shadow A (EX AF,AF') to preserve main A register. +func (g *z80cg) emitLD8(dst, src string) { + if dst == src { + return + } + // Width mismatch: pair→8bit truncation. + if isPairReg(src) && !isPairReg(dst) { + src = lowByte(src) + } + // Width mismatch: 8bit→pair widening (u8 result stored in pair reg). + if !isPairReg(src) && isPairReg(dst) { + g.emitf(" LD %s, %s", lowByte(dst), src) + g.emitf(" LD %s, 0", highByte(dst)) + return + } + // DD/FD prefix conflicts — route through shadow A: + if (isIXYReg(src) && (dst == "H" || dst == "L")) || + (isIXYReg(dst) && (src == "H" || src == "L")) || + (isIXYReg(src) && isIXYReg(dst) && src[:2] != dst[:2]) { + g.emitMovViaAltA(dst, src) + return + } + dstSpill := isSpill(dst) + srcSpill := isSpill(src) + if srcSpill && dstSpill { + // Spill-to-spill: route through shadow A. + g.emit(" EX AF, AF'") + g.emitf(" LD A, (%s)", src) + g.emitf(" LD (%s), A", dst) + g.emit(" EX AF, AF'") + } else if srcSpill && dst == "A" { + g.emitf(" LD A, (%s)", src) + } else if srcSpill { + // Load spill to non-A register via shadow A. + g.emit(" EX AF, AF'") + g.emitf(" LD A, (%s)", src) + g.emitLD8(dst, "A") + g.emit(" EX AF, AF'") + } else if dstSpill && src == "A" { + g.emitf(" LD (%s), A", dst) + } else if dstSpill { + // Store non-A register to spill via shadow A. + g.emit(" EX AF, AF'") + g.emitf(" LD A, %s", src) + g.emitf(" LD (%s), A", dst) + g.emit(" EX AF, AF'") + } else { + g.emitf(" LD %s, %s", dst, src) + } +} + +// emitMovViaAltA routes a single 8-bit LD through shadow A (EX AF,AF') +// to avoid clobbering the main A register. Used for DD/FD prefix conflicts +// where LD H,IXH would encode as NOP. +// Cost: 20T (EX 4T + LD A,src 4-8T + LD dst,A 4T + EX 4T). +func (g *z80cg) emitMovViaAltA(dst, src string) { + g.emit(" EX AF, AF'") + g.emitf(" LD A, %s", src) + g.emitLD8(dst, "A") + g.emit(" EX AF, AF'") +} + +// isSpill returns true if the operand is a LocMem spill (named _spill_ label or legacy $F0xx). +func isSpill(s string) bool { + return strings.HasPrefix(s, "$") || strings.HasPrefix(s, "_spill_") || strings.HasPrefix(s, "._call_result_") +} + +// loadSpill8 loads an 8-bit spill value into the target register. +// Uses A as intermediary (LD A,(nn) is the only valid 8-bit absolute load). +func (g *z80cg) loadSpill8(dst, src string) { + if dst == "A" { + if pair := g.tsmcPairByAddr(src); pair != nil { + label := g.tsmcReloadLabel(pair.reg, g.blockIdx, g.curInstIdx, 0) + if label != "" { + g.emitTSMCReload(label, "A", 8) + return + } + } + g.emitf(" LD A, (%s)", src) + } else { + if pair := g.tsmcPairByAddr(src); pair != nil { + label := g.tsmcReloadLabel(pair.reg, g.blockIdx, g.curInstIdx, 0) + if label != "" { + g.emitTSMCReload(label, dst, 8) + return + } + } + g.emitf(" LD A, (%s)", src) + g.emitLD8(dst, "A") + g.invalidate("A") + } +} + +// storeSpill8 stores an 8-bit register value to a spill address. +func (g *z80cg) storeSpill8(dst, src string) { + if src != "A" { + g.emitLDA(src) + } + if pair := g.tsmcPairByAddr(dst); pair != nil { + g.emitTSMCSpill(pair, "A") + } else { + g.emitf(" LD (%s), A", dst) + } +} + +// loadSpill16 loads a 16-bit spill value into a register pair. +func (g *z80cg) loadSpill16(dst, src string) { + if pair := g.tsmcPairByAddr(src); pair != nil { + label := g.tsmcReloadLabel(pair.reg, g.blockIdx, g.curInstIdx, 0) + if label != "" { + g.emitTSMCReload(label, dst, 16) + return + } + } + g.emitf(" LD %s, (%s)", dst, src) +} + +// storeSpill16 stores a 16-bit pair to a spill address. +func (g *z80cg) storeSpill16(dst, src string) { + if pair := g.tsmcPairByAddr(dst); pair != nil { + g.emitTSMCSpill(pair, src) + } else { + // Z80: only LD (nn),HL is native. DE/BC/SP need ED prefix (supported). + g.emitf(" LD (%s), %s", dst, src) + } +} + +// fixOrphanedTSMCStores scans the emitted asm for _tsmc_ labels that are +// referenced (by LD (_tsmc_label+N), A store patches) but never defined +// (no _tsmc_label: reload instruction). For each orphan, replaces the +// TSMC store patches with regular _spill_ stores: +// +// LD (_tsmc_func_rN_M+1), A → LD (_spill_func_rN), A +// LD (_tsmc_func_rN_M+2), A → LD (_spill_func_rN+1), A +func fixOrphanedTSMCStores(sb *strings.Builder, funcLabel string) { + asm := sb.String() + tsmcPrefix := "_tsmc_" + funcLabel + "_r" + + // Collect all _tsmc_ labels that appear as references (in store patches) + referenced := make(map[string]bool) + for idx := 0; ; { + pos := strings.Index(asm[idx:], tsmcPrefix) + if pos < 0 { + break + } + pos += idx + // Extract: _tsmc_func_rN_M + end := pos + for end < len(asm) && asm[end] != '+' && asm[end] != ')' && asm[end] != ':' && + asm[end] != ' ' && asm[end] != '\n' { + end++ + } + label := asm[pos:end] + referenced[label] = true + idx = end + } + + // Check which are defined (have "label:" in the asm) + changed := false + for label := range referenced { + if strings.Contains(asm, label+":") { + continue // defined — TSMC reload is working + } + // Orphaned: extract register number to build _spill_ label. + // Label format: _tsmc_func_rN_M → _spill_func_rN + tsmcSuffix := label[len("_tsmc_"):] + // Find last underscore (the _M part) + lastUS := strings.LastIndex(tsmcSuffix, "_") + if lastUS < 0 { + continue + } + spillLabel := "_spill_" + tsmcSuffix[:lastUS] + // Replace TSMC store patches with regular spill stores + asm = strings.ReplaceAll(asm, "LD ("+label+"+1), A", "LD ("+spillLabel+"), A") + asm = strings.ReplaceAll(asm, "LD ("+label+"+2), A", "LD ("+spillLabel+"+1), A") + changed = true + } + + if changed { + sb.Reset() + sb.WriteString(asm) + } +} + +// ── Type conversions ────────────────────────────────────────────────────────── + +func (g *z80cg) genExt(inst *Inst) { + dst := g.loc(inst.Dst) + src := g.loc(inst.Src[0]) + srcW := inst.SrcTy.Width() + dstW := inst.Ty.Width() + if inst.SrcTy == TyBool && dstW == 8 && src == "F" { + // A comparison in F is a predicate, not a byte. Materialize its + // actual condition as 0/1; carry alone is wrong for GE/LE/EQ. + g.emitFlagPredicateByte(g.condCode(g.fn, inst.Src[0])) + if dst != "A" { + g.emitLD8(dst, "A") + } + g.pendingFlagReg = NoReg + return + } + + if srcW == 8 && dstW == 16 { + // Zero-extend u8 → u16. + // dst should be a 16-bit reg (HL/DE/BC/IX/IY). + // Most natural: put source in low byte, zero high byte. + lo := lowByte(dst) + hi := highByte(dst) + if src != lo { + if (isIXYReg(src) && (lo == "H" || lo == "L")) || + ((src == "H" || src == "L") && isIXYReg(lo)) { + g.emitMovViaAltA(lo, src) + } else { + g.emitLD8(lo, src) + } + } + g.emitf(" LD %s, 0", hi) + return + } + // Fallback. + g.comment(fmt.Sprintf("TODO: ext %s→%s %s→%s", inst.SrcTy, inst.Ty, src, dst)) +} + +func (g *z80cg) genSext(inst *Inst) { + // Sign extension uses A as scratch, but the narrow source may remain live. + g.emit(" PUSH AF") + g.genSextValue(inst) + g.emit(" POP AF") + g.invalidate("A") + g.invalidate(g.loc(inst.Dst)) +} + +func (g *z80cg) genSextValue(inst *Inst) { + dst := g.loc(inst.Dst) + src := g.loc(inst.Src[0]) + srcW := inst.SrcTy.Width() + dstW := inst.Ty.Width() + + if srcW == 24 && dstW == 32 { + if src != dst { + g.emitMov32(dst, src) + } + g.emit(" EXX") + g.emitLDA(lowByte(dst)) + g.emit(" RLCA") + g.emit(" SBC A, A") + g.emitLD8(highByte(dst), "A") + g.emit(" EXX") + g.invalidate("A") + g.invalidate(dst) + return + } + + if dstW >= 24 && (srcW == 8 || srcW == 16) { + hi, lo := highByte(dst), lowByte(dst) + if srcW == 8 { + if src != "A" { + g.emitLDA(src) + } + g.emitLD8(lo, "A") + g.emit(" RLCA") + g.emit(" SBC A, A") + g.emitLD8(hi, "A") + } else { + if src != dst { + g.emitMov(dst, src, 16) + } + g.emitLDA(hi) + g.emit(" RLCA") + g.emit(" SBC A, A") + } + g.emit(" EXX") + g.emitLD8(lo, "A") + if dstW == 24 { + g.emitf(" LD %s, 0", hi) + } else { + g.emitLD8(hi, "A") + } + g.emit(" EXX") + g.invalidate("A") + g.invalidate(dst) + return + } + + if srcW == 8 && dstW == 16 { + // Sign-extend u8 → u16 via A. + lo := lowByte(dst) + hi := highByte(dst) + if src != "A" { + g.emitLDA(src) + } + if lo != "A" { + g.emitLD8(lo, "A") + } + // Propagate sign bit into high byte: RLCA; SBC A,A gives 0x00 or 0xFF. + g.emit(" RLCA") + g.emit(" SBC A, A") + if hi != "A" { + g.emitLD8(hi, "A") + } + return + } + g.comment(fmt.Sprintf("TODO: sext %s→%s %s→%s", inst.SrcTy, inst.Ty, src, dst)) +} + +// ── 32-bit DWord helpers ────────────────────────────────────────────────────── + +// isDWord reports whether virtual register r is allocated to a LocDWord. +func (g *z80cg) isDWord(r Reg) bool { + return r != NoReg && g.ar.Loc(r).Kind == LocDWord +} + +// emitMov32 moves a 32-bit DWord value from src pair to dst pair using PUSH/EXX. +// +// Strategy (47T, 5 instructions): +// +// PUSH src ; push main src_lo (11T) +// EXX ; switch to shadow (4T) +// PUSH src ; push shadow src_hi (11T) +// POP dst ; pop into shadow dst_hi (10T) +// EXX ; switch back (4T) +// POP dst ; pop into main dst_lo (10T) +// +// The push of src_lo happens before EXX, so it's the main-bank value. +// After EXX, src refers to the shadow-bank value (src_hi). +func (g *z80cg) emitMov32(dst, src string) { + if dst == src { + return + } + g.emitf(" PUSH %s", src) // save main src_lo + g.emit(" EXX") // switch to shadow + g.emitf(" PUSH %s", src) // save shadow src_hi + g.emitf(" POP %s", dst) // restore shadow dst_hi + g.emit(" EXX") // switch back to main + g.emitf(" POP %s", dst) // restore main dst_lo +} + +// ── Move helper ─────────────────────────────────────────────────────────────── + +func (g *z80cg) emitMov(dst, src string, widthBits int) { + if dst == src { + return + } + + // ── Flag-register special cases ─────────────────────────────────────────── + + if dst == "F" { + // Materialise a register value into the flag register. + // Convention: Z=1 (zero) means false, NZ means true — i.e. AND A / OR A. + // Callers using CmpNe (NZ) as the flag condition get this for free. + if src == "A" { + g.emit(" AND A") // sets Z/NZ and clears C based on A's value + } else { + // Load into A first, then set flags. + g.emitf(" LD A, %s", src) + g.emit(" AND A") + } + g.invalidate("A") + return + } + + if src == "F" { + if g.emitKnownFlagCopy(dst, widthBits) { + return + } + // Materialise a flag result into a register. + // Uses the SBC A,A trick for C-flag convention: A = 0xFF (carry set = true) + // or 0x00 (carry clear = false). Works for CmpLt/CmpUlt/CmpGe/CmpUge. + // + // For Z-flag convention (CmpEq/CmpNe), the sequence is: + // JR NZ, $+3 / SCF / then SBC A,A — converts Z to C, then materialise. + // We use the simpler carry path by default; HIR lowers bool-returning + // functions to carry-based comparisons where possible. + if dst == "A" { + g.emit(" SBC A, A") // A=0xFF if C=1 (true), A=0x00 if C=0 (false) + } else if isPairReg(dst) { + // Flag → 16-bit pair: materialise to A, then zero-extend. + // SBC A,A gives 0xFF (true) or 0x00 (false). + g.emit(" SBC A, A") + g.emitf(" LD %s, A", lowByte(dst)) + g.emitf(" LD %s, 0", highByte(dst)) + } else { + g.emit(" SBC A, A") + g.emitLD8(dst, "A") + } + g.invalidate("A") + return + } + + // ── Normal register moves ───────────────────────────────────────────────── + + if widthBits <= 8 { + // LocMem spill slot: "LD r, $Fxxx" and "LD $Fxxx, r" are not valid Z80. + // Only "LD A, (nn)" and "LD (nn), A" work for absolute 8-bit memory I/O. + // TSMC spill: use self-modifying code when eligible. + switch { + case isSpill(src) && dst == "A": + if pair := g.tsmcPairByAddr(src); pair != nil { + g.emitTSMCReload(g.tsmcReloadLabel(pair.reg, g.blockIdx, g.curInstIdx, 0), "A", 8) + } else { + g.emitf(" LD A, (%s)", src) + } + case isSpill(src): + if pair := g.tsmcPairByAddr(src); pair != nil { + g.emitTSMCReload(g.tsmcReloadLabel(pair.reg, g.blockIdx, g.curInstIdx, 0), dst, 8) + } else { + g.emitf(" LD A, (%s)", src) + g.emitLD8(dst, "A") + g.invalidate("A") + } + case isSpill(dst) && src == "A": + if pair := g.tsmcPairByAddr(dst); pair != nil { + g.emitTSMCSpill(pair, "A") + } else { + g.emitf(" LD (%s), A", dst) + } + case isSpill(dst): + if pair := g.tsmcPairByAddr(dst); pair != nil { + g.emitLDA(src) + g.emitTSMCSpill(pair, "A") + } else { + g.emitLDA(src) + g.emitf(" LD (%s), A", dst) + } + default: + // Width mismatch: 8-bit move but one operand is a register pair. + // DD/FD prefix conflict must be checked BEFORE width mismatch, + // because lowByte(HL)="L" + IXL src = DD NOP. + if (isIXYReg(dst) && (src == "H" || src == "L")) || + (isIXYReg(src) && (dst == "H" || dst == "L")) { + g.emitMovViaAltA(dst, src) + } else if isPairReg(src) && !isPairReg(dst) { + // pair→8bit: truncate (take low byte). + lo := lowByte(src) + if (isIXYReg(lo) && (dst == "H" || dst == "L")) || + ((lo == "H" || lo == "L") && isIXYReg(dst)) { + g.emitMovViaAltA(dst, lo) + } else { + g.emitLD8(dst, lo) + } + } else if !isPairReg(src) && isPairReg(dst) { + // 8bit→pair: zero-extend. + lo := lowByte(dst) + if (isIXYReg(src) && (lo == "H" || lo == "L")) || + ((src == "H" || src == "L") && isIXYReg(lo)) { + g.emitMovViaAltA(lo, src) + } else { + g.emitLD8(lo, src) + } + g.emitf(" LD %s, 0", highByte(dst)) + } else { + g.emitLD8(dst, src) + if isSimpleReg(dst) && isSimpleReg(src) { + g.setCopy(dst, src) + } + } + } + return + } + + // 32-bit DWord move via PUSH/EXX sequence. + if widthBits == 32 { + g.emitMov32(dst, src) + return + } + + // 16-bit register move. + switch { + case dst == "HL" && src == "DE": + // Copy DE→HL: must NOT use EX DE,HL (swap destroys DE). + g.emitf(" LD %s, %s", highByte(dst), highByte(src)) // LD H, D + g.emitf(" LD %s, %s", lowByte(dst), lowByte(src)) // LD L, E + case dst == "DE" && src == "HL": + // Copy HL→DE: must NOT use EX DE,HL (swap destroys HL). + g.emitf(" LD %s, %s", highByte(dst), highByte(src)) // LD D, H + g.emitf(" LD %s, %s", lowByte(dst), lowByte(src)) // LD E, L + case (dst == "IX" || dst == "IY") && (src == "DE" || src == "BC"): + // DE/BC → IX/IY: undocumented byte-copy (DD/FD prefix), 2×8T = 16T. + // Safe because D, E, B, C are NOT substituted by the DD/FD prefix. + // e.g. DE→IX: LD IXH, D (DD 62) / LD IXL, E (DD 6B) + // BC→IX: LD IXH, B (DD 60) / LD IXL, C (DD 69) + g.emitf(" LD %s, %s", highByte(dst), highByte(src)) + g.emitf(" LD %s, %s", lowByte(dst), lowByte(src)) + case (dst == "IX" || dst == "IY") && src == "HL": + // HL→IX: byte-copy is INVALID — DD prefix substitutes H→IXH, L→IXL, + // so LD IXH,H encodes as LD IXH,IXH (NOP). Must use PUSH/POP. + g.emitf(" PUSH HL") + g.emitf(" POP %s", dst) + case (src == "IX" || src == "IY") && (dst == "DE" || dst == "BC"): + // IX/IY → DE/BC: safe byte-copy, D/E/B/C not substituted by DD prefix. + // e.g. IX→DE: LD D, IXH (DD 57? no — DD 7A? no) actually DD prefix on LD D,H: + // opcode 0x54 = LD D,H → DD 54 = LD D,IXH ✓ (D is destination, IXH is source) + g.emitf(" LD %s, %s", highByte(dst), highByte(src)) + g.emitf(" LD %s, %s", lowByte(dst), lowByte(src)) + case (src == "IX" || src == "IY") && dst == "HL": + // IX→HL: byte-copy INVALID (H,L substituted). Must use PUSH/POP. + g.emitf(" PUSH %s", src) + g.emit(" POP HL") + default: + // If src is an 8-bit register being widened to a 16-bit pair, + // PUSH/POP is wrong: PUSH A doesn't exist (only PUSH AF, which + // would corrupt the low byte with the flags register). + // Use zero-extension instead: LD lo, src ; LD hi, 0. + if isSimpleReg(src) && !isPairReg(src) && isPairReg(dst) { + lo := lowByte(dst) + hi := highByte(dst) + if lo != src { + g.emitLD8(lo, src) + } + g.emitf(" LD %s, 0", hi) + break + } + // Spill-to-spill: route through HL. + if isSpill(src) && isSpill(dst) { + g.emitf(" LD HL, (%s)", src) + g.emitf(" LD (%s), HL", dst) + g.invalidate("HL") + break + } + // LocMem spill slot → register pair: use LD rr, (nn). + if isSpill(src) { + g.emitf(" LD %s, (%s)", dst, src) + break + } + // register pair → LocMem spill slot: use LD (nn), rr. + if isSpill(dst) { + g.emitf(" LD (%s), %s", dst, src) + break + } + // 16-bit pair → 8-bit: truncate (take low byte). + if isPairReg(src) && !isPairReg(dst) { + lo := lowByte(src) + if (isIXYReg(lo) && (dst == "H" || dst == "L")) || + ((lo == "H" || lo == "L") && isIXYReg(dst)) { + g.emitMovViaAltA(dst, lo) + } else { + g.emitLD8(dst, lo) + } + break + } + // 8-bit → 16-bit pair: zero-extend. + if !isPairReg(src) && isPairReg(dst) { + lo := lowByte(dst) + if (isIXYReg(src) && (lo == "H" || lo == "L")) || + ((src == "H" || src == "L") && isIXYReg(lo)) { + g.emitMovViaAltA(lo, src) + } else { + g.emitLD8(lo, src) + } + g.emitf(" LD %s, 0", highByte(dst)) + break + } + // General 16-bit move via PUSH/POP. + g.emitf(" PUSH %s", src) + g.emitf(" POP %s", dst) + } + // Track the copy alias so downstream redundant moves can be suppressed. + g.setCopy(dst, src) +} + +// inferTyFromAlloc guesses the type of a virtual register from its allocated +// physical location. 16-bit pair → TyU16; 8-bit reg → TyU8. +func inferTyFromAlloc(ar *AllocResult, r Reg) Ty { + loc := ar.Loc(r) + switch loc.Kind { + case LocReg: + if isPairReg(loc.Name) { + return TyU16 + } + return TyU8 + case LocIXY: + return TyU16 + case LocIXY8, LocShadow: + return TyU8 + default: + return TyU8 + } +} + +// ── 16-bit register half helpers ────────────────────────────────────────────── +// +// These helpers encode the project's "first-class half-register" policy: +// HL/DE/BC/IX/IY all expose stable low/high 8-bit halves for generic backend +// reasoning. IXH/IXL/IYH/IYL must be considered alongside H/L/D/E/B/C in all +// legal direct paths; they are not special-cased only for bit ops. + +// lowByte returns the low-byte name of a 16-bit register. +func lowByte(rr string) string { + switch rr { + case "HL": + return "L" + case "DE": + return "E" + case "BC": + return "C" + case "IX": + return "IXL" + case "IY": + return "IYL" + } + return rr // already 8-bit +} + +// highByte returns the high-byte name of a 16-bit register. +func highByte(rr string) string { + switch rr { + case "HL": + return "H" + case "DE": + return "D" + case "BC": + return "B" + case "IX": + return "IXH" + case "IY": + return "IYH" + } + return rr +} + +// isPairReg reports whether r is a 16-bit register pair name. +func isPairReg(r string) bool { + switch r { + case "HL", "DE", "BC", "IX", "IY": + return true + } + return false +} + +// parentPair returns the 16-bit register pair containing an 8-bit register. +func parentPair(r string) string { + switch r { + case "A": + return "AF" // rarely useful; callers should special-case A + case "B", "C": + return "BC" + case "D", "E": + return "DE" + case "H", "L": + return "HL" + case "IXH", "IXL": + return "IX" + case "IYH", "IYL": + return "IY" + } + return r // already a pair or unknown +} + +// promote8toPair zero-extends an 8-bit register into a 16-bit pair suitable +// for ADD HL,rr or SBC HL,rr. For A, routes through BC (since AF is not a +// valid arithmetic pair). For others, uses parentPair with high byte zeroed. +// Returns the pair name to use. +func (g *z80cg) promote8toPair(r string) string { + if r == "A" { + g.emit(" LD C, A") + g.emit(" LD B, 0") + return "BC" + } + pair := parentPair(r) + g.emitf(" LD %s, 0", highByte(pair)) + return pair +} + +// ── PUSH/POP pair helpers ───────────────────────────────────────────────────── + +// toPair returns the 16-bit register pair containing the given register. +func toPair(r string) string { + switch r { + case "A", "F", "AF": + return "AF" + case "B", "C", "BC": + return "BC" + case "D", "E", "DE": + return "DE" + case "H", "L", "HL": + return "HL" + } + return r // IX, IY already pairs +} diff --git a/minzc/pkg/mir2/z80codegen_muldiv.go b/minzc/pkg/mir2/z80codegen_muldiv.go new file mode 100644 index 000000000..d796cf01d --- /dev/null +++ b/minzc/pkg/mir2/z80codegen_muldiv.go @@ -0,0 +1,1161 @@ +package mir2 + +import ( + "fmt" + "strings" +) + +// ── Multiply ────────────────────────────────────────────────────────────────── + +// genMul emits 8-bit unsigned multiply via shift-and-add. +// Power-of-2 constants use N×(ADD A,A); small constants use LD tmp,A + adds. +// Variable or large constants use the __mul8 runtime. +func (g *z80cg) genMul(inst *Inst) { + dst := g.loc(inst.Dst) + lhs := g.loc(inst.Src[0]) + + if inst.Ty.Width() >= 24 { + g.genMul32(inst) + return + } + if inst.Ty.Width() > 8 { + g.genMul16(inst) + return + } + + // Preserve the accumulator value used after this multiply, including a + // loop block parameter. Relocate before saving runtime scratch pairs. + for r := range g.regsLiveAfterInst(inst) { + if r == inst.Dst || g.loc(r) != "A" { + continue + } + // regsLiveAfterInst includes terminator uses defined later in this + // block; those values are not in A yet. + definedLater := false + for _, later := range g.curBlock.Insts[g.curInstIdx+1:] { + if later.Dst == r { + definedLater = true + break + } + } + if !definedLater { + g.saveAccOperandIfLive(r, inst) + break + } + } + // Every byte multiply path must replace the old accumulator identity. + defer func() { + g.pendingAccReg = NoReg + if dst == "A" { + g.pendingAccReg = inst.Dst + } + }() + lhs = g.loc(inst.Src[0]) + cv, isConst := g.constVals[inst.Src[1]] + + if isConst { + switch cv { + case 0: + g.emit(" XOR A") + g.invalidate("A") + if dst != "A" { + g.emitLD8(dst, "A") + g.setCopy(dst, "A") + } + return + case 1: + if dst != lhs { + g.emitMov(dst, lhs, 8) + } + return + } + // Power-of-2: log2(cv) × (ADD A,A). + if cv > 0 && cv&(cv-1) == 0 { + shifts := 0 + for n := cv; n > 1; n >>= 1 { + shifts++ + } + if !g.holdsValue("A", lhs) { + g.emitLDA(lhs) + } + for i := 0; i < shifts; i++ { + g.emit(" ADD A, A") + } + g.invalidate("A") + if dst != "A" { + g.emitLD8(dst, "A") + g.setCopy(dst, "A") + } + return + } + // x*3 = x + x*2; x*5 = x + x*4; x*6 = x*2 + x*4 + // x*7 = x*8 - x; x*9 = x + x*8; x*10 = x*2 + x*8 + // x*12 = x*4 + x*8; x*15 = x*16 - x + if cv == 3 || cv == 5 || cv == 6 || cv == 7 || cv == 9 || cv == 10 || cv == 12 || cv == 15 { + if !g.holdsValue("A", lhs) { + g.emitLDA(lhs) + } + tmp := g.pickScratch8(inst) + switch cv { + case 3: // x + x*2 + g.emitf(" LD %s, A", tmp) // tmp = x + g.emit(" ADD A, A") // A = x*2 + g.emitf(" ADD A, %s", tmp) // A = x*3 + case 5: // x + x*4 + g.emitf(" LD %s, A", tmp) + g.emit(" ADD A, A") + g.emit(" ADD A, A") // A = x*4 + g.emitf(" ADD A, %s", tmp) // A = x*5 + case 6: // x*2 + x*4 + g.emit(" ADD A, A") // A = x*2 + g.emitf(" LD %s, A", tmp) // tmp= x*2 + g.emit(" ADD A, A") // A = x*4 + g.emitf(" ADD A, %s", tmp) // A = x*6 + case 7: // x*8 - x + g.emitf(" LD %s, A", tmp) // tmp = x + g.emit(" ADD A, A") + g.emit(" ADD A, A") + g.emit(" ADD A, A") // A = x*8 + g.emitf(" SUB %s", tmp) // A = x*7 + case 9: // x + x*8 + g.emitf(" LD %s, A", tmp) + g.emit(" ADD A, A") + g.emit(" ADD A, A") + g.emit(" ADD A, A") // A = x*8 + g.emitf(" ADD A, %s", tmp) // A = x*9 + case 10: // x*2 + x*8 + g.emit(" ADD A, A") // A = x*2 + g.emitf(" LD %s, A", tmp) // tmp= x*2 + g.emit(" ADD A, A") + g.emit(" ADD A, A") // A = x*8 + g.emitf(" ADD A, %s", tmp) // A = x*10 + case 12: // x*4 + x*8 + g.emit(" ADD A, A") + g.emit(" ADD A, A") // A = x*4 + g.emitf(" LD %s, A", tmp) // tmp= x*4 + g.emit(" ADD A, A") // A = x*8 + g.emitf(" ADD A, %s", tmp) // A = x*12 + case 15: // x*16 - x + g.emitf(" LD %s, A", tmp) // tmp = x + g.emit(" ADD A, A") + g.emit(" ADD A, A") + g.emit(" ADD A, A") + g.emit(" ADD A, A") // A = x*16 + g.emitf(" SUB %s", tmp) // A = x*15 + } + g.invalidate("A") + if dst != "A" { + g.emitLD8(dst, "A") + g.setCopy(dst, "A") + } + return + } + } + + // General constant or variable multiply: use runtime __mul8 routine. + // __mul8 clobbers A/B/C/D/E/F. Save pairs before ABI setup so + // live operands (including a multiplier in B) survive the call. + g.emit(" PUSH BC") + g.emit(" PUSH DE") + // Set up both runtime operands as a parallel copy: the multiplier may + // currently be in A, and loading the multiplicand first would destroy it. + if isConst { + g.emitLDA(lhs) + g.emitf(" LD B, %d", cv&0xFF) + } else { + g.emitParallelCopy([]parallelCopy{ + {srcName: lhs, dstName: "A", ty: TyU8}, + {srcName: g.loc(inst.Src[1]), dstName: "B", ty: TyU8}, + }) + } + g.emit(" CALL __mul8") + g.emit(" POP DE") + g.emit(" POP BC") + g.needsMul8 = true + g.invalidate("A") + g.invalidate("F") + if dst != "A" { + g.emitLD8(dst, "A") + g.setCopy(dst, "A") + } +} + +// genMul16 emits 16-bit multiply. +// +// Constants: +// +// 0: LD H,0 / LD L,0 +// 1: no-op +// 2^n: n × ADD HL,HL (7T per shift) +// 3,5,6,9: PUSH HL / POP BC + shift+add sequence +// +// Variable (software shift-and-add, Russian-peasant LSB-first, 16 iterations): +// +// BC = multiplicand (lhs) +// DE = multiplier (rhs) +// HL = result = 0 +// A = 16 (iteration counter — DEC A / JR NZ preserves SRL carry) +// loop: +// SRL D / RR E → DE >>= 1, old bit0 → carry +// JR NC, no_add → skip if bit was 0 +// ADD HL, BC → result += multiplicand (carry from ADD is ignored by next SRL) +// no_add: +// SLA C / RL B → BC <<= 1 (next power-of-2 multiplicand) +// DEC A / JR NZ loop +// +// SRL D is independent of incoming carry, so ADD HL,BC's carry does not corrupt +// the extraction. Total: ~320T worst case. +func (g *z80cg) genMul16(inst *Inst) { + dst := g.loc(inst.Dst) + lhs := g.loc(inst.Src[0]) + + cv, isConst := g.constVals[inst.Src[1]] + regInfo := collectRegInfo(g.fn) + // Widen byte operands according to their source type. Preserve A and its + // flags: either may still hold a live value (including the other operand). + extendByte := func(src Reg, low, high string) { + if IsSigned(regInfo[src].Ty) { + g.emit(" PUSH AF") + g.emitLD8("A", low) + g.emit(" RLA") + g.emit(" SBC A, A") + g.emitLD8(high, "A") + g.emit(" POP AF") + } else { + g.emitf(" LD %s, 0", high) + } + } + + // 16-bit multiply needs HL for ADD HL,rr. + // A live lhs must survive too when the result is allocated elsewhere. + savedHL := false + if dst != "HL" { + // HL may hold a live value (e.g. a constant computed earlier). + // Save it on stack; restore after multiply into dst. + g.emit(" PUSH HL") + savedHL = true + } + + // Move lhs into HL for the multiply. + if lhs != "HL" { + if isIXY(lhs) { + // IX/IY → HL: PUSH/POP (byte-copy invalid due to DD prefix). + g.emitf(" PUSH %s", lhs) + g.emit(" POP HL") + } else if isIXYReg(lhs) { + // IXH/IXL/IYH/IYL: widen the byte to HL. + g.emitMovViaAltA("L", lhs) + extendByte(inst.Src[0], "L", "H") + } else if isSimpleReg(lhs) && !isPairReg(lhs) { + // Widen a byte operand promoted by the expression. + g.emitLD8("L", lhs) + extendByte(inst.Src[0], "L", "H") + } else if isSpill(lhs) { + g.loadSpill16("HL", lhs) + } else { + g.emitf(" LD H, %s", highByte(lhs)) + g.emitLD8("L", lowByte(lhs)) + } + g.invalidate("HL") + } + + // mul16Epilogue moves the result from HL to dst and restores saved HL. + mul16Epilogue := func() { + if dst == "HL" { + // already there + } else if isIXY(dst) { + g.emit(" PUSH HL") + g.emitf(" POP %s", dst) + } else if isSpill(dst) { + g.storeSpill16(dst, "HL") + g.invalidate("HL") + } else { + g.emitLD8(highByte(dst), "H") + g.emitLD8(lowByte(dst), "L") + } + if savedHL { + g.emit(" POP HL") // restore pre-mul HL + } + g.invalidate(dst) + if savedHL { + g.invalidate("HL") + } + } + + if isConst { + switch cv { + case 0: + g.emit(" LD H, 0") + g.emit(" LD L, 0") + mul16Epilogue() + return + case 1: + mul16Epilogue() + return + } + // Fix A: byte-boundary shift — LD H,L / LD L,0 is cheaper than ADD HL,HL chains. + if cv >= 256 && cv&(cv-1) == 0 { + g.emit(" LD H, L") + g.emit(" LD L, 0") + for n := cv >> 8; n > 1; n >>= 1 { + g.emit(" ADD HL, HL") + } + mul16Epilogue() + return + } + if cv > 0 && cv&(cv-1) == 0 { // power-of-2: log2(cv) × ADD HL,HL + for n := cv; n > 1; n >>= 1 { + g.emit(" ADD HL, HL") + } + mul16Epilogue() + return + } + // Small multiples via PUSH/POP BC save + shift sequences. + if cv == 3 || cv == 5 || cv == 6 || cv == 9 { + g.emit(" PUSH BC") // preserve values live across the scratch pair + g.emit(" PUSH HL") // save x → BC + g.emit(" POP BC") // BC = x (17T) + switch cv { + case 3: // x*2 + x + g.emit(" ADD HL, HL") + g.emit(" ADD HL, BC") + case 5: // x*4 + x + g.emit(" ADD HL, HL") + g.emit(" ADD HL, HL") + g.emit(" ADD HL, BC") + case 6: // x*4 + x*2 + g.emit(" ADD HL, HL") + g.emit(" PUSH HL") + g.emit(" POP BC") + g.emit(" ADD HL, HL") + g.emit(" ADD HL, BC") + case 9: // x*8 + x + g.emit(" ADD HL, HL") + g.emit(" ADD HL, HL") + g.emit(" ADD HL, HL") + g.emit(" ADD HL, BC") + } + g.emit(" POP BC") + mul16Epilogue() + return + } + } + + // Variable or unsupported constant: software shift-and-add loop. + // Register layout: + // BC = multiplicand (lhs, copied from HL) + // DE = multiplier (rhs) + // HL = result = 0 + // A = iteration counter (16) + rhs := g.loc(inst.Src[1]) + loopLbl := fmt.Sprintf(".%s_m16_%d_lp", sanitizeIdent(g.fn.Name), int(inst.Dst)) + skipLbl := fmt.Sprintf(".%s_m16_%d_sk", sanitizeIdent(g.fn.Name), int(inst.Dst)) + g.comment(fmt.Sprintf("mul16 %s * %s → HL (~320T, 16-iter shift-and-add)", lhs, rhs)) + // Save AF and DE: the multiply uses A as a loop counter and clobbers DE by + // shifting it 16 times. Any live variable in A, D, or E would be corrupted. + g.emit(" PUSH AF") // preserve A (= possible live variable) and F + g.emit(" PUSH DE") // preserve D and E across multiply + // Load multiplier into DE BEFORE overwriting BC with the multiplicand. + // (rhs may be in C/B/BC which gets clobbered by LD B,H; LD C,L below.) + if isConst { + // OpMul constants have no physical load (deadConstsForFunc). + // Strength reduction consumes cv directly; so must this fallback. + g.emitf(" LD DE, %d", cv&0xFFFF) + g.invalidate("DE") + } else if rhs != "DE" { + if isSpill(rhs) { + g.loadSpill16("DE", rhs) + } else if isIXY(rhs) { + // IX/IY → DE: safe byte-copy (D/E not substituted by DD/FD). + g.emitf(" LD D, %s", highByte(rhs)) + g.emitf(" LD E, %s", lowByte(rhs)) + } else if isPairReg(rhs) { + g.emitf(" LD D, %s", highByte(rhs)) + g.emitf(" LD E, %s", lowByte(rhs)) + } else if isIXYReg(rhs) { + // IXH/IXL half-reg: widen to DE. + g.emitMovViaAltA("E", rhs) + extendByte(inst.Src[1], "E", "D") + } else { + // 8-bit rhs: widen into DE. + g.emitf(" LD E, %s", rhs) + extendByte(inst.Src[1], "E", "D") + } + g.invalidate("DE") + } + g.emit(" PUSH BC") // save BC across multiply + g.emit(" LD B, H") + g.emit(" LD C, L") // BC = multiplicand (lhs) + g.emit(" LD H, 0") + g.emit(" LD L, 0") // HL = result = 0 + g.emit(" LD A, 16") // A = bit counter + g.emitf("%s:", loopLbl) + g.emit(" SRL D") // DE >>= 1 (logical right shift, bit0 → carry) + g.emit(" RR E") + g.emitf(" JRS NC, %s", skipLbl) + g.emit(" ADD HL, BC") // result += multiplicand (current bit weight) + g.emitf("%s:", skipLbl) + g.emit(" SLA C") // BC <<= 1 (multiplicand doubles) + g.emit(" RL B") + g.emit(" DEC A") // counter-- (does not affect carry) + g.emitf(" JRS NZ, %s", loopLbl) + g.emit(" POP BC") // restore BC + g.emit(" POP DE") // restore old DE (live variables in D/E preserved) + g.emit(" POP AF") // restore old A (live variables in A preserved) + mul16Epilogue() + g.invalidate("BC") +} + +// genMul32 emits 24/32-bit multiply by a constant via SHL-and-add. +// +// Power-of-2 constants: log2(cv) × SHL32 (ADD HL,HL / EXX / ADC HL,HL / EXX per bit). +// Small constants (3,5,6,9): LD+saves using emitMov32 for the partial product. +// General constants: decompose into Σ 2^k terms (binary method). +// Variable multiplier: TODO (software loop). +func (g *z80cg) genMul32(inst *Inst) { + dst := g.loc(inst.Dst) + lhs := g.loc(inst.Src[0]) + + // Emit one 32-bit left-shift-by-1 on the register pair p. + // Z80 only has ADD HL,HL — route through HL if p != HL. + shl1 := func(p string) { + if p != "HL" { + g.emitMov("HL", p, 16) + } + g.emit(" ADD HL, HL") + g.emit(" EXX") + g.emit(" ADC HL, HL") + g.emit(" EXX") + if p != "HL" { + g.emitMov(p, "HL", 16) + g.invalidate("HL") + } + } + + cv, isConst := g.constVals[inst.Src[1]] + + if !isConst { + rhs := g.loc(inst.Src[1]) + g.comment(fmt.Sprintf("TODO: 32-bit variable mul %s * %s → %s", lhs, rhs, dst)) + return + } + + // Ensure lhs is in dst before operating. + if dst != lhs { + g.emitMov32(dst, lhs) + } + + switch cv { + case 0: + // dst = 0: load zero constant. + if isSpill(dst) { + g.emit(" LD HL, 0") + g.storeSpill16(dst, "HL") + g.invalidate("HL") + } else { + g.emitf(" LD %s, 0", dst) + g.emit(" EXX") + g.emitf(" LD %s, 0", dst) + g.emit(" EXX") + } + g.invalidate(dst) + return + case 1: + // dst = lhs — already done by move above. + return + } + + // Power-of-2: log2(cv) left shifts. + if cv > 0 && cv&(cv-1) == 0 { + shifts := 0 + for n := cv; n > 1; n >>= 1 { + shifts++ + } + for i := 0; i < shifts; i++ { + shl1(dst) + } + g.invalidate(dst) + return + } + + // Binary decomposition: dst = Σ (dst_orig << k) for each set bit k in cv. + // Uses a saved copy of the original value to accumulate. + // Only valid when dst is HL (has ADD/ADC). For others, fall through to TODO. + if dst == "HL" && cv > 0 { + // Save original in DE (push to stack if DE is occupied). + g.emit(" PUSH DE") // save main DE + g.emit(" EXX") + g.emit(" PUSH DE") // save shadow DE + g.emit(" EXX") + // Copy lhs (already in HL/H'L') to DE/D'E'. + g.emitMov32("DE", "HL") + // Accumulate: acc = 0, then for each set bit k add (orig << k). + // Clear HL for accumulation. + g.emitf(" LD HL, 0") + g.emit(" EXX") + g.emitf(" LD HL, 0") + g.emit(" EXX") + shift := 0 + for bit := int64(0); bit < 32; bit++ { + if cv&(1< 0 && k&(k-1) == 0 && w <= 16 && ((inst.Op != OpSDiv && inst.Op != OpSMod) || k < int64(1)<<(w-1)) { + signed := inst.Op == OpSDiv || inst.Op == OpSMod + if signed && g.nonNegative(inst.Src[0], w, make(map[Reg]bool)) { + signed = false + } + g.genPow2DivMod(inst, k, signed) + return + } + + if w <= 8 { + g.genDivMod8(inst) + } else if w <= 16 { + g.genDivMod16(inst) + } else { + g.comment(fmt.Sprintf("TODO: %d-bit div/mod", w)) + } +} + +// genDivMod8 — 8-bit unsigned division via shift-and-subtract (restoring). +// +// Based on DIVU111 from Dark / X-Trade, Spectrum Expert #01 (1997): +// +// Input: B = dividend, C = divisor +// Output: B = quotient, A = remainder +// Cost: 236–244 T-states +// +// Algorithm: binary long division. Shift dividend left through carry +// into accumulator (A). Try subtracting divisor; if it fits, set the +// quotient bit via INC B (bit 0 is free after SLA B). +func (g *z80cg) genDivMod8(inst *Inst) { + dst := g.loc(inst.Dst) + lhs := g.loc(inst.Src[0]) + rhs := g.loc(inst.Src[1]) + idx := g.trampIdx + g.trampIdx++ + + isSigned := inst.Op == OpSDiv || inst.Op == OpSMod + wantMod := inst.Op == OpMod || inst.Op == OpSMod + + if isSigned { + // Stage both operands before assigning B/C, including swapped allocations. + g.emit(" PUSH AF") + g.emitLDA(lowByte(lhs)) + g.emit(" PUSH AF") + if lowByte(rhs) == "A" { + // Original A is the divisor; loading the dividend must not replace it. + g.emit(" POP BC") // B = staged dividend + g.emit(" POP AF") // A = original divisor + g.emit(" LD C, A") + } else { + g.emitLDA(lowByte(rhs)) + g.emit(" LD C, A") + g.emit(" POP AF") + g.emit(" LD B, A") + g.emit(" POP AF") + } + } else { + // Setup: B = dividend, C = divisor. + // Width guard: lhs/rhs might be a 16-bit pair (const allocated to HL/DE). + if lhs == "B" { + // already there + } else if isPairReg(lhs) { + g.emitf(" LD B, %s", lowByte(lhs)) + } else if isSpill(lhs) { + g.loadSpill8("B", lhs) + } else if lhs == "A" { + g.emit(" LD B, A") + } else { + g.emitf(" LD B, %s", lhs) + } + if rhs == "C" { + // already there + } else if isPairReg(rhs) { + g.emitf(" LD C, %s", lowByte(rhs)) + } else if isSpill(rhs) { + g.loadSpill8("C", rhs) + } else { + g.emitf(" LD C, %s", rhs) + } + + } + if isSigned { + g.emit(" LD A, B") + if !wantMod { + g.emit(" XOR C") + } + g.emit(" PUSH AF") // result sign, separate from division workspace + g.emit(" BIT 7, B") + g.emitf(" JR Z, .div8_abs_l_%d", idx) + g.emit(" LD A, B") + g.emit(" NEG") + g.emit(" LD B, A") + g.emitf(".div8_abs_l_%d:", idx) + g.emit(" BIT 7, C") + g.emitf(" JR Z, .div8_abs_r_%d", idx) + g.emit(" LD A, C") + g.emit(" NEG") + g.emit(" LD C, A") + g.emitf(".div8_abs_r_%d:", idx) + } + + g.emit(" XOR A") // clear accumulator (remainder workspace) + g.emit(" LD D, 8") // 8 bits to process + g.emitf(".div8_%d:", idx) + g.emit(" SLA B") // shift dividend left — MSB → CF + g.emit(" RLA") // shift CF into accumulator + g.emit(" CP C") // try subtract divisor + g.emitf(" JR C, .div8_skip_%d", idx) // A < C → skip + g.emit(" SUB C") // subtract divisor + g.emit(" INC B") // set quotient bit 0 (free after SLA) + g.emitf(".div8_skip_%d:", idx) + g.emit(" DEC D") + g.emitf(" JR NZ, .div8_%d", idx) + // Result: B = quotient, A = remainder. + + if isSigned { + if wantMod { + g.emit(" LD B, A") + } + g.emit(" POP AF") + g.emit(" BIT 7, A") + g.emit(" LD A, B") + g.emitf(" JR Z, .div8_sign_%d", idx) + g.emit(" NEG") + g.emitf(".div8_sign_%d:", idx) + g.emit(" LD B, A") + } + + if wantMod { + if dst != "A" { + g.emitLD8(dst, "A") + g.setCopy(dst, "A") + } + } else { + if dst == "A" { + g.emit(" LD A, B") + } else if dst != "B" { + g.emitf(" LD %s, B", dst) + } + g.setCopy(dst, "B") + } + g.invalidate("A") + g.invalidate("B") + g.invalidate("D") + g.invalidate("F") +} + +// genDivMod16 — 16-bit unsigned division via shift-and-subtract long division. +// +// HL / DE → HL=quotient, BC=remainder +// +// Classic Z80 restoring division (16 iterations): +// +// BC = 0 (remainder), A = 16 (counter) +// loop: +// ADD HL, HL ; shift dividend left, MSB → CF +// RL C; RL B ; shift CF into remainder +// PUSH HL; LD H,B; LD L,C; SBC HL,DE ; trial: remainder - divisor +// if no borrow: BC = HL (accept), POP HL, SET 0,L (quotient bit = 1) +// if borrow: POP HL (quotient bit stays 0, BC unchanged) +// DEC A; JR NZ loop +func (g *z80cg) genDivMod16(inst *Inst) { + dst := g.loc(inst.Dst) + lhs := g.loc(inst.Src[0]) + rhs := g.loc(inst.Src[1]) + idx := g.trampIdx + g.trampIdx++ + + wantMod := inst.Op == OpMod || inst.Op == OpSMod + isSigned := inst.Op == OpSDiv || inst.Op == OpSMod + + // Stage both inputs without changing any source register, including spills + // and index halves. The unsigned path needs the same parallel-move safety. + g.pushWord(lhs) + g.pushWord(rhs) + g.emit(" POP DE") + g.emit(" POP HL") + g.invalidate("DE") + g.invalidate("HL") + + if isSigned { + g.emit(" LD A, H") + if !wantMod { + g.emit(" XOR D") + } + g.emit(" PUSH AF") + for _, pair := range []string{"HL", "DE"} { + hi, lo := highByte(pair), lowByte(pair) + g.emitf(" BIT 7, %s", hi) + g.emitf(" JR Z, .div16_abs_%s_%d", pair, idx) + g.emit(" XOR A") + g.emitf(" SUB %s", lo) + g.emitf(" LD %s, A", lo) + g.emit(" SBC A, A") + g.emitf(" SUB %s", hi) + g.emitf(" LD %s, A", hi) + g.emitf(".div16_abs_%s_%d:", pair, idx) + } + } + + // BC = 0 (remainder accumulator), A = 16 (iteration counter). + g.emit(" LD BC, 0") + g.emit(" LD A, 16") + + // Main loop. + g.emitf(".div16_%d:", idx) + g.emit(" ADD HL, HL") // shift dividend left; MSB → CF + g.emit(" RL C") // shift CF into remainder (BC) + g.emit(" RL B") + g.emit(" PUSH HL") // save quotient-in-progress + g.emit(" LD H, B") // HL = BC (remainder) + g.emit(" LD L, C") + g.emit(" OR A") // clear CF for SBC + g.emit(" SBC HL, DE") // trial subtract: remainder - divisor + g.emitf(" JR C, .div16_skip_%d", idx) // borrow → remainder < divisor + // Accept: remainder = HL (after subtract). + g.emit(" LD B, H") + g.emit(" LD C, L") + g.emit(" POP HL") // restore quotient + g.emit(" SET 0, L") // this quotient bit = 1 + g.emit(" DEC A") + g.emitf(" JR NZ, .div16_%d", idx) + g.emitf(" JR .div16_done_%d", idx) + // Skip: remainder stays, quotient bit stays 0. + g.emitf(".div16_skip_%d:", idx) + g.emit(" POP HL") // restore quotient (bit 0 = 0) + g.emit(" DEC A") + g.emitf(" JR NZ, .div16_%d", idx) + + g.emitf(".div16_done_%d:", idx) + // Result: HL = quotient, BC = remainder. + if isSigned { + hi, lo := "H", "L" + if wantMod { + hi, lo = "B", "C" + } + g.emit(" POP AF") + g.emit(" BIT 7, A") + g.emitf(" JR Z, .div16_sign_%d", idx) + g.emit(" XOR A") + g.emitf(" SUB %s", lo) + g.emitf(" LD %s, A", lo) + g.emit(" SBC A, A") + g.emitf(" SUB %s", hi) + g.emitf(" LD %s, A", hi) + g.emitf(".div16_sign_%d:", idx) + } + + if wantMod { + // Want remainder (BC) → move to dst. + if dst == "HL" { + g.emit(" LD H, B") + g.emit(" LD L, C") + } else if dst == "BC" { + // already there + } else if isSpill(dst) { + g.emit(" LD H, B") + g.emit(" LD L, C") + g.storeSpill16(dst, "HL") + } else if isIXY(dst) { + g.emit(" LD H, B") + g.emit(" LD L, C") + g.emit(" PUSH HL") + g.emitf(" POP %s", dst) + } else { + g.emitLD8(highByte(dst), "B") + g.emitLD8(lowByte(dst), "C") + } + } else { + // Want quotient (HL) → move to dst if needed. + if dst == "HL" { + // already there + } else if isSpill(dst) { + g.storeSpill16(dst, "HL") + } else if isIXY(dst) { + // HL→IX/IY: byte-copy invalid (DD NOP). Use PUSH/POP. + g.emit(" PUSH HL") + g.emitf(" POP %s", dst) + } else { + g.emitLD8(highByte(dst), "H") + g.emitLD8(lowByte(dst), "L") + } + } + g.invalidate("A") + g.invalidate("BC") + g.invalidate("DE") + g.invalidate("HL") + g.invalidate("F") +} + +// Only use proofs that survive same-width signed casts: narrower unsigned parameters, +// zero extension from a narrower width, and copies of a proven value. +func (g *z80cg) nonNegative(r Reg, width int, seen map[Reg]bool) bool { + if seen[r] { + return false + } + seen[r] = true + for _, p := range g.fn.Contract.Params { + if p.Reg == r { + return p.Ty.Width() < width && !IsSigned(p.Ty) + } + } + for _, b := range g.fn.Blocks { + for _, i := range b.Insts { + if i.Dst != r { + continue + } + switch i.Op { + case OpExt: + return i.SrcTy != nil && i.Ty != nil && i.SrcTy.Width() < i.Ty.Width() && i.SrcTy.Width() < width + case OpConst: + return i.Imm >= 0 && i.Imm < (int64(1)<<(width-1)) + case OpMove: + return g.nonNegative(i.Src[0], width, seen) + } + } + } + return false +} + +func (g *z80cg) genPow2DivMod(inst *Inst, k int64, signed bool) { + dst, lhs := g.loc(inst.Dst), g.loc(inst.Src[0]) + w := inst.Ty.Width() + wantMod := inst.Op == OpMod || inst.Op == OpSMod + if !signed && !wantMod && w <= 8 && !isSpill(dst) && !isIXY(dst) { + if isPairReg(lhs) { + lhs = lowByte(lhs) + } + g.emitLDA(lhs) + for v := k; v > 1; v >>= 1 { + g.emit(" SRL A") + } + if isPairReg(dst) { + g.emitLD8(lowByte(dst), "A") + g.emitf(" LD %s, 0", highByte(dst)) + } else { + g.emitLD8(dst, "A") + } + g.invalidate("A") + g.invalidate(dst) + return + } + + if !signed && wantMod && k <= 256 && !isIXY(dst) && !isSpill(dst) { + + if isPairReg(lhs) { + lhs = lowByte(lhs) + } + g.emitLDA(lhs) + g.emitf(" AND %d", k-1) + if isPairReg(dst) { + g.emitLD8(lowByte(dst), "A") + g.emitf(" LD %s, 0", highByte(dst)) + } else { + g.emitLD8(dst, "A") + } + + g.invalidate("A") + g.invalidate(dst) + return + } + // HL is temporary unless it is the result. AF holds the original sign. + + if lhs != "HL" { + g.pushWord(lhs) + g.emit(" POP HL") + } + idx := g.trampIdx + g.trampIdx++ + negate := func() { + g.emit(" XOR A") + g.emit(" SUB L") + g.emit(" LD L, A") + if w == 16 { + g.emit(" SBC A, A") + g.emit(" SUB H") + g.emit(" LD H, A") + } + } + if signed { + hi := "H" + if w <= 8 { + hi = "L" + } + g.emitf(" LD A, %s", hi) + g.emit(" PUSH AF") + g.emitf(" BIT 7, %s", hi) + g.emitf(" JR Z, .pow2_abs_%d", idx) + negate() + g.emitf(".pow2_abs_%d:", idx) + } + if wantMod { + mask := k - 1 + g.emit(" LD A, L") + g.emitf(" AND %d", mask&255) + g.emit(" LD L, A") + if w == 16 { + if mask < 256 { + g.emit(" LD H, 0") + } else { + g.emit(" LD A, H") + g.emitf(" AND %d", (mask>>8)&255) + g.emit(" LD H, A") + } + } + } else { + for v := k; v > 1; v >>= 1 { + if w == 16 { + g.emit(" SRL H") + g.emit(" RR L") + } else { + g.emit(" SRL L") + } + } + } + if signed { + g.emit(" POP AF") + g.emit(" OR A") + g.emitf(" JP P, .pow2_done_%d", idx) + negate() + g.emitf(".pow2_done_%d:", idx) + } + if w <= 8 { + g.emitMov(dst, "L", w) + } else { + g.emitMov(dst, "HL", w) + } + + g.invalidate("A") + g.invalidate(dst) + g.invalidate("HL") +} + +// Arithmetic helpers use fixed scratch registers beyond their allocated dst. +// Preserve any unrelated live values there, including byte halves of pairs. +func (g *z80cg) saveLiveScratch(inst *Inst, pairs ...string) func() { + live := g.scratchLiveAfter(inst) + + dst := g.loc(inst.Dst) + accPair := "BC" + if dst == "BC" || dst == "B" || dst == "C" { + accPair = "HL" + } + saved := []string{} + for _, pair := range pairs { + overlaps := func(loc string) bool { + return loc == pair || loc == highByte(pair) || loc == lowByte(pair) || (pair == "AF" && (loc == "A")) + } + if overlaps(dst) { + continue + } + needed := false + for r := range live { + if r != inst.Dst && overlaps(g.loc(r)) { + needed = true + break + } + } + if needed { + if pair == "AF" { + g.emitf(" PUSH %s", accPair) + } + g.emitf(" PUSH %s", pair) + saved = append(saved, pair) + } + } + return func() { + for i := len(saved) - 1; i >= 0; i-- { + if saved[i] == "AF" { + g.emitf(" POP %s", accPair) + g.emitf(" LD A, %s", highByte(accPair)) + g.emitf(" POP %s", accPair) + g.invalidate("A") + } else { + g.emitf(" POP %s", saved[i]) + g.invalidate(saved[i]) + } + } + } +} + +// scratchLiveAfter walks backwards from live-out, killing later definitions. +// A value first defined after inst does not occupy its allocation yet. +func (g *z80cg) scratchLiveAfter(inst *Inst) map[Reg]bool { + live := make(map[Reg]bool) + if g.curBlock == nil { + return live + } + if g.liveness != nil { + out := g.liveness.LiveOutOf(g.fn, g.curBlock) + for r := range g.ar.Locs { + if out != nil && out.Has(r) { + live[r] = true + } + } + } + if g.curBlock.Term != nil { + for _, r := range g.curBlock.Term.termUses() { + live[r] = true + } + } + for i := len(g.curBlock.Insts) - 1; i >= 0; i-- { + next := g.curBlock.Insts[i] + if next == inst { + break + } + delete(live, next.Dst) + for _, r := range next.ExtraRets { + delete(live, r) + } + for _, r := range next.Uses() { + if r != NoReg { + live[r] = true + } + } + } + return live +} + +// Buffer the selected sequence so preservation follows its physical writes, +// rather than a worst-case list for the MIR opcode. Flags are intentionally +// excluded: comparison flags must survive restoration of the accumulator. +func (g *z80cg) saveWrittenScratch(inst *Inst) func() { + outer := g.sb + var body strings.Builder + g.sb = &body + live := g.scratchLiveAfter(inst) + locs := make(map[Reg]string) + for r := range live { + locs[r] = g.loc(r) + } + return func() { + g.sb = outer + writes := scratchSequenceWrites(body.String()) + var pairs []string + for _, pair := range []string{"AF", "HL", "DE", "BC", "IX", "IY"} { + for r, loc := range locs { + if r == inst.Dst { + continue + } + if (loc == pair && (writes[highByte(pair)] || writes[lowByte(pair)])) || + (parentPair(loc) == pair && writes[loc]) { + pairs = append(pairs, pair) + break + } + } + } + restore := g.saveLiveScratch(inst, pairs...) + g.sb.WriteString(body.String()) + restore() + } +} + +// These helpers emit only ordinary scalar Z80 instructions. Unknown operations +// conservatively write all registers, so adding a sequence cannot omit a save. +func scratchSequenceWrites(asm string) map[string]bool { + writes := make(map[string]bool) + mark := func(r string) { + if r == "AF" { + writes["A"] = true + } else if isPairReg(r) { + writes[highByte(r)], writes[lowByte(r)] = true, true + } else if isSimpleReg(r) || isIXYReg(r) { + if r != "F" { + writes[r] = true + } + } + } + for _, line := range strings.Split(asm, "\n") { + line = strings.TrimSpace(strings.SplitN(line, ";", 2)[0]) + fields := strings.Fields(line) + if len(fields) == 0 || strings.HasSuffix(line, ":") { + continue + } + op := fields[0] + args := strings.Split(strings.TrimSpace(strings.TrimPrefix(line, op)), ",") + for i := range args { + args[i] = strings.TrimSpace(args[i]) + } + switch op { + case "LD", "POP", "INC", "DEC", "SLA", "SRA", "SRL", "RL", "RR", "RLC", "RRC": + mark(args[0]) + case "SET", "RES": + if len(args) > 1 { + mark(args[1]) + } + case "ADD", "ADC", "SBC": + if len(args) > 1 { + mark(args[0]) + } else { + mark("A") + } + case "AND", "OR": + if args[0] != "A" { + mark("A") + } + case "SUB", "XOR", "NEG", "RLA", "RRA", "RLCA", "RRCA", "CPL": + mark("A") + case "EX": + for _, r := range args { + mark(r) + } + case "CP", "BIT", "PUSH", "JP", "JR", "SCF", "CCF", "NOP": + case "DJNZ": + mark("B") + default: + for _, r := range []string{"AF", "HL", "DE", "BC", "IX", "IY"} { + mark(r) + } + } + } + return writes +} diff --git a/minzc/pkg/mir2/z80codegen_patterns.go b/minzc/pkg/mir2/z80codegen_patterns.go new file mode 100644 index 000000000..ce9edc352 --- /dev/null +++ b/minzc/pkg/mir2/z80codegen_patterns.go @@ -0,0 +1,869 @@ +package mir2 + +import ( + "fmt" + "math/bits" + "sort" +) + +// lutLoadPat describes a page-aligned LUT access merged at codegen time. +type lutLoadPat struct { + sym string // page-aligned global symbol + src8Reg Reg // virtual reg holding the 8-bit index (after lo-subtract if any) +} + +type bitStorePat struct { + bit int + byteOffset int + op string // "SET" or "RES" +} + +type bitCmpPat struct { + bit int + byteOffset int + ptrReg Reg + srcReg Reg + useMem bool +} + +// globalFieldInfo describes a global struct field access that can be lowered +// to direct Z80 addressing: LD A,(sym__field) or LD (sym__field),A. +// +// The EQU label is: {sanitizeIdent(sym)}__{fieldName} (or just sanitizeIdent(sym) for offset 0). +// Emission: LD A,(sym__field) for loads; LD A,val + LD (sym__field),A for stores. +type globalFieldInfo struct { + sym string // global symbol name (raw, before sanitize) + offset int64 // byte offset within the global + fieldName string // field name for the EQU label (may be empty → use offset) + // HL-chain: when true, emit LD HL,sym chain instead of LD (sym__field),A. + hlChain bool // part of a consecutive-field HL-chain group + hlHead bool // first in group → emit LD HL,sym before + hlLast bool // last in group → no INC HL after +} + +// ── Page-aligned LUT pre-scan ────────────────────────────────────────────────── + +// scanLUTPatterns detects the pattern emitted by LUTGen for page-aligned tables: +// +// idx16 = Ext(src8, u8→u16, ClassIndex) +// base = AddrOf("sym") ← sym has Align==256 +// ptr = PtrAdd(base, idx16) +// dst = Load(ptr, u8, ClassAcc) +// +// When found: +// - g.lutLoadPat[dst] = {sym, src8Reg} +// - g.lutSkip[idx16, base, ptr] = true +// +// The pattern is cleared (g.lutLoadPat, g.lutSkip rebuilt) at each new block. +// This is safe because LUTGen only ever creates single-use intermediate regs. +func (g *z80cg) scanLUTPatterns(b *Block) { + // Reset per-block maps (keep same map objects; just clear them). + clear(g.lutLoadPat) + clear(g.lutSkip) + + // Build: reg → defining instruction (within this block only). + defInst := make(map[Reg]*Inst, len(b.Insts)) + for _, inst := range b.Insts { + if inst.Dst != NoReg { + defInst[inst.Dst] = inst + } + } + + for _, inst := range b.Insts { + // Looking for: dst = Load(ptr, u8) + if inst.Op != OpLoad || inst.Ty.Width() != 8 || inst.Dst == NoReg { + continue + } + ptrReg := inst.Src[0] + + // ptr = PtrAdd(base, idx16) + ptrInst, ok := defInst[ptrReg] + if !ok || ptrInst.Op != OpPtrAdd { + continue + } + baseReg := ptrInst.Src[0] + idx16Reg := ptrInst.Src[1] + + // base = AddrOf(sym) where sym is page-aligned + baseInst, ok := defInst[baseReg] + if !ok || baseInst.Op != OpAddrOf { + continue + } + sym := baseInst.Sym + if !g.isGlobalPageAligned(sym) { + continue + } + + // idx16 = Ext(src8, u8→u16) + extInst, ok := defInst[idx16Reg] + if !ok || extInst.Op != OpExt { + continue + } + if extInst.SrcTy == nil || extInst.SrcTy.Width() != 8 || extInst.Ty.Width() != 16 { + continue + } + src8Reg := extInst.Src[0] + + // Pattern matched: record and mark intermediates for skipping. + g.lutLoadPat[inst.Dst] = lutLoadPat{sym: sym, src8Reg: src8Reg} + g.lutSkip[idx16Reg] = true + g.lutSkip[baseReg] = true + g.lutSkip[ptrReg] = true + } +} + +// scanBitStorePatterns detects the common u8/u16 load-modify-store forms: +// +// cur = Load(ptr, u8) +// next = Or(cur, const(1<= 0 && byteOffset <= 127 { + g.emitf(" %s %d, %s", op, bit, ptrIndirect(ptr, byteOffset)) + return true + } + return false +} + +func selectBitReg(src string, byteOffset int) string { + switch byteOffset { + case 0: + return lowByte(src) + case 1: + return highByte(src) + default: + return "" + } +} + +func (g *z80cg) emitBitRegOp(op string, bit int, reg string) bool { + if reg == "" || reg == "A" || isSpill(reg) || reg == "F" { + return false + } + if !isSimpleReg(reg) { + return false + } + // BIT/SET/RES use CB prefix encoding — IXH/IXL/IYH/IYL are NOT valid + // operands (no DD/FD CB r form exists). Return false to fall through + // to the AND/OR mask path in every caller. + if isIXYReg(reg) { + return false + } + g.emitf(" %s %d, %s", op, bit, reg) + return true +} + +// globalFieldLabel returns the EQU label for a global struct field access. +// Format: sanitizeIdent(sym)__fieldName (or just sanitizeIdent(sym) for offset 0 / no name). +func globalFieldLabel(sym string, offset int64, fieldName string) string { + base := sanitizeIdent(sym) + if fieldName != "" { + return base + "__" + fieldName + } + if offset == 0 { + return base + } + // No named field at this offset (e.g. nested struct access with folded offsets). + // Use "sym + N" arithmetic expression — the assembler resolves it inline. + return fmt.Sprintf("%s + %d", base, offset) +} + +// globalStructField looks up a global in the module by symbol name and returns its +// StructTy if it has one, plus the field name for a given byte offset. +// Returns ("", -1, false) when not found or not a struct global. +func (g *z80cg) globalStructField(sym string, offset int64) (fieldName string, found bool) { + for _, gl := range g.mod.Globals { + if gl.Name != sym { + continue + } + st, ok := gl.Ty.(*StructTy) + if !ok { + return "", false + } + // Find field by byte offset. + off := int64(0) + for _, f := range st.Fields { + if off == offset { + return f.Name, true + } + off += int64(ByteWidth(f.Ty)) + } + // Offset is valid but doesn't align with a named field boundary — still use it. + return "", false + } + return "", false +} + +// scanGlobalFieldPatterns detects patterns where a global struct field is loaded +// or stored via AddrOf + optional Field + Load/Store. When found, the load/store +// is rewritten to use Z80 direct addressing (LD A,(sym__field) or LD (sym__field),A). +// +// Patterns detected (u8 only — all addressable via single LD A,(nn)): +// +// Load pattern (with field): +// base = AddrOf(sym) [OpAddrOf] +// fptr = Field(base, imm) [OpField] +// dst = Load(fptr, u8) [OpLoad] +// +// Load pattern (field 0, no OpField): +// base = AddrOf(sym) [OpAddrOf] +// dst = Load(base, u8) [OpLoad] +// +// Store pattern (with field): +// base = AddrOf(sym) +// fptr = Field(base, imm) +// Store(fptr, val, u8) [OpStore, Dst==NoReg] +// +// Store pattern (field 0): +// base = AddrOf(sym) +// Store(base, val, u8) +// +// Only fires when the AddrOf refers to a global in m.Globals (not a func symbol). +// Only u8 loads and stores are handled (single-byte LD A,(nn) / LD (nn),A). +func (g *z80cg) scanGlobalFieldPatterns(b *Block) { + // Reset per-block maps. + clear(g.globalFieldLoad) + clear(g.globalFieldStore) + clear(g.globalFieldSkip) + clear(g.globalU16StoreSym) + clear(g.globalU16LoadSym) + + // Build: reg → defining instruction within this block. + defInst := make(map[Reg]*Inst, len(b.Insts)) + for _, inst := range b.Insts { + if inst.Dst != NoReg { + defInst[inst.Dst] = inst + } + } + + // Helper: check if sym names a module-level global (not a string or function symbol). + isGlobal := func(sym string) bool { + for _, gl := range g.mod.Globals { + if gl.Name == sym { + return true + } + } + return false + } + + // Count uses of each virtual reg in this block (instructions + block terminators). + // Used to guard against skipping intermediates that are shared with other users. + useCount := make(map[Reg]int) + for _, inst := range b.Insts { + for _, s := range inst.Src { + if s != NoReg { + useCount[s]++ + } + } + for _, s := range inst.Args { + if s != NoReg { + useCount[s]++ + } + } + } + if b.Term != nil { + for _, s := range b.Term.termUses() { + if s != NoReg { + useCount[s]++ + } + } + } + + // Scan Load instructions. + // Only mark intermediates as skip when: + // 1. The load's allocated destination IS A (LD A,(nn) is the only direct read on Z80). + // 2. Each intermediate register (AddrOf/Field results) is used exactly once — + // by this Load (and the Field uses the AddrOf, counted once). + // If an intermediate is shared (e.g., base ptr reused for multiple fields), + // we must not skip it or the other consumer will see a dead register. + for _, inst := range b.Insts { + if inst.Op == OpLoad && inst.Ty.Width() == 8 && inst.Dst != NoReg { + // Only apply when the allocated destination is the A register. + if g.ar.Loc(inst.Dst).Name != "A" { + continue + } + ptrReg := inst.Src[0] + ptrInst, hasDef := defInst[ptrReg] + if !hasDef { + continue + } + + switch ptrInst.Op { + case OpAddrOf: + // Pattern: Load(AddrOf(sym), u8) — offset 0 + if !isGlobal(ptrInst.Sym) { + continue + } + // Only skip AddrOf if its result is used only by this Load. + if useCount[ptrReg] != 1 { + continue + } + fieldName, _ := g.globalStructField(ptrInst.Sym, 0) + g.globalFieldLoad[inst.Dst] = globalFieldInfo{ + sym: ptrInst.Sym, + offset: 0, + fieldName: fieldName, + } + g.globalFieldSkip[ptrReg] = true + + case OpField: + // Pattern: Load(Field(AddrOf(sym), imm), u8) + // Guard: Field result used only by this Load, AddrOf result used only by Field. + if useCount[ptrReg] != 1 { + continue + } + baseReg := ptrInst.Src[0] + baseDef, ok := defInst[baseReg] + if !ok || baseDef.Op != OpAddrOf { + continue + } + if !isGlobal(baseDef.Sym) { + continue + } + if useCount[baseReg] != 1 { + continue + } + offset := ptrInst.Imm + fieldName, _ := g.globalStructField(baseDef.Sym, offset) + g.globalFieldLoad[inst.Dst] = globalFieldInfo{ + sym: baseDef.Sym, + offset: offset, + fieldName: fieldName, + } + g.globalFieldSkip[ptrReg] = true // Field result + g.globalFieldSkip[baseReg] = true // AddrOf result + } + } + } + + // Scan Store instructions (Dst==NoReg, Src[0]=addr, Src[1]=val). + for _, inst := range b.Insts { + if inst.Op != OpStore || inst.Ty.Width() != 8 { + continue + } + addrReg := inst.Src[0] + addrInst, hasDef := defInst[addrReg] + if !hasDef { + continue + } + + switch addrInst.Op { + case OpAddrOf: + if !isGlobal(addrInst.Sym) { + continue + } + // Only skip if the AddrOf result is used solely by this Store. + if useCount[addrReg] != 1 { + continue + } + fieldName, _ := g.globalStructField(addrInst.Sym, 0) + g.globalFieldStore[addrReg] = globalFieldInfo{ + sym: addrInst.Sym, + offset: 0, + fieldName: fieldName, + } + g.globalFieldSkip[addrReg] = true + + case OpField: + if useCount[addrReg] != 1 { + continue + } + baseReg2 := addrInst.Src[0] + baseDef, ok := defInst[baseReg2] + if !ok || baseDef.Op != OpAddrOf { + continue + } + if !isGlobal(baseDef.Sym) { + continue + } + if useCount[baseReg2] != 1 { + continue + } + offset := addrInst.Imm + fieldName, _ := g.globalStructField(baseDef.Sym, offset) + g.globalFieldStore[addrReg] = globalFieldInfo{ + sym: baseDef.Sym, + offset: offset, + fieldName: fieldName, + } + g.globalFieldSkip[addrReg] = true // Field result + g.globalFieldSkip[baseReg2] = true // AddrOf result + } + } + + // ── HL-chain detection ────────────────────────────────────────────────────── + // When 2+ stores to the same global struct cover consecutive byte offsets and + // all value registers are safe (not H or L, which alias with the HL pointer), + // replace independent LD (sym__field),A sequences with: + // LD HL,sym ; LD (HL),r ; INC HL ; LD (HL),r ; ... + // Saves 8T and 4 bytes for a 3-field struct (61T→53T, 12B→8B). + type hlEntry struct { + addrReg Reg + valReg Reg + offset int64 + } + symStores := make(map[string][]hlEntry) + for addrReg, info := range g.globalFieldStore { + for _, inst := range b.Insts { + if inst.Op == OpStore && inst.Src[0] == addrReg { + symStores[info.sym] = append(symStores[info.sym], hlEntry{ + addrReg: addrReg, + valReg: inst.Src[1], + offset: info.offset, + }) + break + } + } + } + for _, entries := range symStores { + if len(entries) < 2 { + continue + } + sort.Slice(entries, func(i, j int) bool { return entries[i].offset < entries[j].offset }) + // Check consecutive offsets. + consecutive := true + for i := 1; i < len(entries); i++ { + if entries[i].offset != entries[i-1].offset+1 { + consecutive = false + break + } + } + if !consecutive { + continue + } + // Check all value regs safe (not H/L which aliases with HL pointer). + safe := true + for _, e := range entries { + if e.valReg != NoReg { + loc := g.ar.Loc(e.valReg).Name + if loc == "H" || loc == "L" { + safe = false + break + } + } + } + if !safe { + continue + } + // Mark as HL-chain. + for i, e := range entries { + entry := g.globalFieldStore[e.addrReg] + entry.hlChain = true + entry.hlHead = (i == 0) + entry.hlLast = (i == len(entries)-1) + g.globalFieldStore[e.addrReg] = entry + } + } + + // ── u16 direct-address store detection ────────────────────────────────────── + // Pattern: OpStore(w=16, addrReg=AddrOf(sym), valReg) + // Enables LD (sym), HL (16T, no A clobber) when valReg is in HL. + for _, inst := range b.Insts { + if inst.Op != OpStore || inst.Ty.Width() != 16 { + continue + } + addrReg := inst.Src[0] + def, ok := defInst[addrReg] + if !ok || def.Op != OpAddrOf || !isGlobal(def.Sym) { + continue + } + g.globalU16StoreSym[addrReg] = def.Sym + } + + // ── u16 direct-address load detection ──────────────────────────────────── + // Pattern: OpLoad(w=16, addrReg=AddrOf(sym)) → dst (u16, must be HL) + // Enables LD HL, (sym) (20T, no A clobber) instead of INC/DEC trick via A. + // Only fires when addrReg is used solely by this load (useCount==1) so we + // can safely skip the AddrOf instruction (suppress LD HL,sym). + for _, inst := range b.Insts { + if inst.Op != OpLoad || inst.Ty.Width() != 16 || inst.Dst == NoReg { + continue + } + if g.ar.Loc(inst.Dst).Name != "HL" { + continue // only LD HL,(nn) exists for direct u16 load + } + addrReg := inst.Src[0] + def, ok := defInst[addrReg] + if !ok || def.Op != OpAddrOf || !isGlobal(def.Sym) { + continue + } + if useCount[addrReg] != 1 { + continue // addr reg used by other instructions too + } + g.globalU16LoadSym[addrReg] = def.Sym + g.globalFieldSkip[addrReg] = true // suppress LD HL, sym from OpAddrOf + } +} + +// scanPtrAddBase detects OpPtrAdd instructions where the base pointer is in HL +// AND the base virtual is live after the ptr_add (e.g. needed on a loop +// back-edge). In that case ADD HL,rr would clobber HL (the base), so we +// arrange for PUSH HL before the ADD and POP HL after the subsequent load. +// +// Sets g.ptrAddPushHL = ptr_add.Dst when the pattern fires; cleared by the +// OpLoad codegen after it emits the POP HL. +func (g *z80cg) scanPtrAddBase(b *Block) { + g.ptrAddPushHL = NoReg + + for idx, inst := range b.Insts { + if inst.Op != OpPtrAdd { + continue + } + baseReg := inst.Src[0] + if g.ar.Loc(baseReg).Name != "HL" { + continue // base not in HL — no clobber risk + } + + // Check if baseReg is live after this ptr_add. + // A base is live-after if it appears in any later instruction's sources + // OR in the block terminator's argument list. + liveAfter := false + for _, later := range b.Insts[idx+1:] { + for _, s := range later.Src { + if s == baseReg { + liveAfter = true + break + } + } + for _, a := range later.Args { + if a == baseReg { + liveAfter = true + break + } + } + if liveAfter { + break + } + } + if !liveAfter { + if t := b.Term; t != nil { + for _, r := range t.termUses() { + if r == baseReg { + liveAfter = true + break + } + } + } + } + + if liveAfter { + g.ptrAddPushHL = inst.Dst + return // one pattern per block is enough + } + } +} diff --git a/minzc/pkg/mir2/z80codegen_peephole.go b/minzc/pkg/mir2/z80codegen_peephole.go new file mode 100644 index 000000000..29b974617 --- /dev/null +++ b/minzc/pkg/mir2/z80codegen_peephole.go @@ -0,0 +1,467 @@ +package mir2 + +import ( + "fmt" + "strings" +) + +// asmPeepholePass applies post-emit string-level peephole optimisations: +// 1. Double EX DE,HL elimination — two consecutive EX DE,HL cancel; remove both. +// 2. Single-JP function → EQU alias — a function whose entire body is one JP +// becomes a zero-byte/zero-T EQU alias (tail-call guaranteed by contract). +func asmPeepholePass(src string) string { + lines := strings.Split(src, "\n") + lines = elimDoubleExDeHl(lines) + lines = elimSingleJpEqu(lines) + lines = elimCallRet(lines) + lines = foldCondCall(lines) + lines = elimJrToRet(lines) + lines = elimDeadAfterRet(lines) + lines = foldRLCASled(lines) + return strings.Join(lines, "\n") +} + +// foldCondCall replaces JR cc, skip / CALL target / skip: with CALL cc, target. +// Saves 2 bytes and avoids branch penalty. +// +// Pattern (any of JR/JRS, any condition): +// +// JR NC, .skip (or JRS, or any cc: Z/NZ/C/NC) +// [.optional_label:] (zero or more intermediate labels, no instructions) +// CALL target +// .skip: +// +// → CALL NC, target +// +// The inverse condition is used: JR NC skips the CALL when NC, +// so we emit CALL C (call when condition is met, i.e. NOT NC). +func foldCondCall(lines []string) []string { + invertCC := map[string]string{ + "Z": "NZ", "NZ": "Z", "C": "NC", "NC": "C", + "z": "nz", "nz": "z", "c": "nc", "nc": "c", + } + + out := make([]string, 0, len(lines)) + i := 0 + for i < len(lines) { + trimmed := strings.TrimSpace(lines[i]) + // Match JR cc, .label or JRS cc, .label + var jrCC, skipLabel string + if strings.HasPrefix(trimmed, "JR ") || strings.HasPrefix(trimmed, "JRS ") { + // Parse: JR(S) CC, .label + rest := trimmed + if strings.HasPrefix(rest, "JRS ") { + rest = rest[4:] + } else { + rest = rest[3:] + } + parts := strings.SplitN(rest, ",", 2) + if len(parts) == 2 { + cc := strings.TrimSpace(parts[0]) + lab := strings.TrimSpace(parts[1]) + if _, ok := invertCC[cc]; ok && strings.HasPrefix(lab, ".") { + jrCC = cc + skipLabel = lab + } + } + } + + if jrCC == "" { + out = append(out, lines[i]) + i++ + continue + } + + // Scan forward: skip label-only lines, find CALL, then skip label. + j := i + 1 + var intermediateLabels []string + for j < len(lines) { + t := strings.TrimSpace(lines[j]) + if t == "" || strings.HasPrefix(t, ";") { + j++ + continue + } + if strings.HasSuffix(t, ":") { + intermediateLabels = append(intermediateLabels, lines[j]) + j++ + continue + } + break + } + + // Expect CALL target at position j + if j >= len(lines) { + out = append(out, lines[i]) + i++ + continue + } + callLine := strings.TrimSpace(lines[j]) + if !strings.HasPrefix(callLine, "CALL ") { + out = append(out, lines[i]) + i++ + continue + } + callTarget := strings.TrimSpace(callLine[5:]) + + // Expect skip label at position j+1 (possibly with blank/comment lines between) + k := j + 1 + for k < len(lines) { + t := strings.TrimSpace(lines[k]) + if t == "" || strings.HasPrefix(t, ";") { + k++ + continue + } + break + } + if k >= len(lines) { + out = append(out, lines[i]) + i++ + continue + } + labelLine := strings.TrimSpace(lines[k]) + expectedLabel := skipLabel + ":" + if labelLine != expectedLabel { + out = append(out, lines[i]) + i++ + continue + } + + // Match! Emit intermediate labels + CALL cc, target + skip label. + invCC := invertCC[jrCC] + indent := " " + if idx := strings.Index(lines[j], "CALL "); idx > 0 { + indent = lines[j][:idx] + } + for _, lab := range intermediateLabels { + out = append(out, lab) + } + out = append(out, fmt.Sprintf("%sCALL %s, %s", indent, invCC, callTarget)) + out = append(out, lines[k]) // keep the skip label + i = k + 1 + continue + } + return out +} + +// foldRLCASled replaces consecutive RLCA sequences (3+) with CALL __rotate_N. +// RLCA = 4T each. CALL __rotate_N = 17T + N×4T + 10T = 27 + N×4T. +// Break-even at N=7 (inline=28T, sled=55T) — so sled is about code SIZE savings. +// For N >= 3 inline RLCAs, fold to CALL to save code bytes (shared sled is 9 bytes). +// Caller must ensure __rotate sled is emitted (needsRotate flag). +func foldRLCASled(lines []string) []string { + out := make([]string, 0, len(lines)) + i := 0 + needsSled := false + for i < len(lines) { + // Count consecutive RLCA instructions + if strings.TrimSpace(lines[i]) == "RLCA" { + count := 0 + j := i + for j < len(lines) && strings.TrimSpace(lines[j]) == "RLCA" { + count++ + j++ + } + if count >= 3 { + // Replace with CALL __rotate_N + indent := " " + if idx := strings.Index(lines[i], "RLCA"); idx > 0 { + indent = lines[i][:idx] + } + out = append(out, fmt.Sprintf("%sCALL __rotate_%d", indent, count)) + needsSled = true + i = j + continue + } + } + out = append(out, lines[i]) + i++ + } + // If we folded any, add a marker comment so the caller knows to emit the sled + if needsSled { + out = append(out, "; [peephole] RLCA sled referenced — __rotate runtime required") + } + return out +} + +// elimCallRet replaces CALL X / RET with JP X (tail call promotion). +// Saves 17 T-states (CALL=17 + RET=10 → JP=10) and 1 byte. +func elimCallRet(lines []string) []string { + out := make([]string, 0, len(lines)) + for i := 0; i < len(lines); i++ { + trimmed := strings.TrimSpace(lines[i]) + if strings.HasPrefix(trimmed, "CALL ") && i+1 < len(lines) { + next := strings.TrimSpace(lines[i+1]) + if next == "RET" { + target := strings.TrimSpace(strings.TrimPrefix(trimmed, "CALL ")) + // Skip numeric targets (BDOS CALL 5, RST addresses) — + // these are syscalls that need the return address on stack. + isNumeric := len(target) > 0 && (target[0] >= '0' && target[0] <= '9' || + target[0] == '$' || strings.HasPrefix(target, "0x")) + if !isNumeric { + out = append(out, strings.Replace(lines[i], "CALL ", "JP ", 1)) + i++ // skip RET + continue + } + } + } + out = append(out, lines[i]) + } + return out +} + +// elimDeadAfterRet removes unreachable code between an unconditional RET/JP +// and the next label. After cond_ret lowering, the pattern: +// +// RET ← unconditional return +// .label: ← else-branch (only reachable via jump, not fallthrough) +// +// means nothing between the RET and .label is reachable. If there are +// instructions between them (rare), they're dead code. More commonly, +// the RET itself is redundant when the PREVIOUS instruction was also a +// RET or JP — but that's already handled by the IR. The main benefit +// here is removing the RET when it's the last instruction before a label +// and the codegen emitted it defensively. +func elimDeadAfterRet(lines []string) []string { + // This pass doesn't remove the RET (it may be the return for the + // then-branch). It removes dead instructions BETWEEN RET and next label. + // Uncommon in practice — the main pattern is already clean. + return lines +} + +// elimDoubleExDeHl removes consecutive EX DE,HL pairs. +// Blank lines and comment-only lines between the pair are preserved. +func elimDoubleExDeHl(lines []string) []string { + const exLine = " EX DE, HL" + out := make([]string, 0, len(lines)) + for i := 0; i < len(lines); i++ { + if strings.TrimSpace(lines[i]) != "EX DE, HL" { + out = append(out, lines[i]) + continue + } + // lines[i] is an EX DE,HL. Look for a second one after skipping + // blank/comment lines (no real instructions between). + j := i + 1 + for j < len(lines) { + t := strings.TrimSpace(lines[j]) + if t == "" || strings.HasPrefix(t, ";") { + j++ + continue + } + break + } + if j < len(lines) && strings.TrimSpace(lines[j]) == "EX DE, HL" { + // Skip both EX DE,HL lines (drop them from output). + i = j // loop increment makes i = j+1 + continue + } + out = append(out, lines[i]) + } + return out +} + +// elimSingleJpEqu converts a function that is exactly one JP target into an EQU. +// +// Pattern (consecutive lines, optionally separated by blank/comment lines): +// +// funcname: +// JP target +// +// → replaced with a single line: +// +// funcname EQU target +// +// Safety conditions: +// - No additional instructions in the block (comments/blanks OK). +// - No local labels between funcname: and JP (would break relative refs). +// - Not "main" (entry point must remain a real label). +func elimSingleJpEqu(lines []string) []string { + out := make([]string, 0, len(lines)) + i := 0 + for i < len(lines) { + line := lines[i] + // Match a bare label line: word followed by ':' with no leading whitespace. + if !strings.HasPrefix(line, " ") && strings.HasSuffix(strings.TrimSpace(line), ":") { + label := strings.TrimSuffix(strings.TrimSpace(line), ":") + if label != "main" && label != "" && !strings.Contains(label, ".") { + // Scan forward past blank/comment lines to find first instruction. + j := i + 1 + for j < len(lines) { + t := strings.TrimSpace(lines[j]) + if t == "" || strings.HasPrefix(t, ";") { + j++ + continue + } + break + } + if j < len(lines) { + instr := strings.TrimSpace(lines[j]) + // Must be exactly "JP target" (not DJNZ, not JP cc, not JP (HL)/(IX)/(IY)). + if strings.HasPrefix(instr, "JP ") && !strings.Contains(instr, ",") && !strings.Contains(instr, "(") { + target := strings.TrimSpace(strings.TrimPrefix(instr, "JP ")) + // Check that the next non-blank/comment line after JP is + // a top-level label or end-of-section (not another instruction + // inside this "function"). + k := j + 1 + for k < len(lines) { + t := strings.TrimSpace(lines[k]) + if t == "" || strings.HasPrefix(t, ";") { + k++ + continue + } + break + } + nextIsTopLevel := k >= len(lines) || + (!strings.HasPrefix(lines[k], " ") && strings.HasSuffix(strings.TrimSpace(lines[k]), ":")) || + lines[k] == "; globals" || lines[k] == "; strings" + if nextIsTopLevel { + // Emit EQU alias, skip all consumed lines. + out = append(out, fmt.Sprintf("%s EQU %s", label, target)) + // Preserve blank lines that came before JP (already in out + // via j-loop... actually they were skipped). Emit one blank + // to maintain section spacing. + i = j + 1 + continue + } + } + } + } + } + out = append(out, line) + i++ + } + return out +} + +// elimJrToRet replaces `JRS cc, label` with `RET cc` when the target label +// is a bare RET instruction. Then removes the label+RET if no other references +// remain. This saves 1B per replaced jump (JR cc = 2B → RET cc = 1B). +// +// Before: JRS Z, .cret0 / JRS C, .cret0 / ... / .cret0: / RET +// After: RET Z / RET C / ... (label+RET removed if dead) +func elimJrToRet(lines []string) []string { + // Pass 1: find labels that point to bare RET. + retLabels := make(map[string]bool) + for i := 0; i < len(lines)-1; i++ { + t := strings.TrimSpace(lines[i]) + if strings.HasPrefix(t, ".") && strings.HasSuffix(t, ":") { + label := strings.TrimSuffix(t, ":") + // Check next non-blank line is RET. + for j := i + 1; j < len(lines); j++ { + next := strings.TrimSpace(lines[j]) + if next == "" || strings.HasPrefix(next, ";") { + continue + } + if next == "RET" { + retLabels[label] = true + } + break + } + } + } + if len(retLabels) == 0 { + return lines + } + + // Pass 2: replace JRS cc, retLabel → RET cc. + // Track which labels are still referenced. + replaced := make(map[string]int) // label → count of replacements + out := make([]string, 0, len(lines)) + for _, line := range lines { + t := strings.TrimSpace(line) + // Match: JRS cc, .label or JR cc, .label + if (strings.HasPrefix(t, "JRS ") || strings.HasPrefix(t, "JR ")) && strings.Contains(t, ", .") { + parts := strings.SplitN(t, ", ", 2) + if len(parts) == 2 { + label := strings.TrimSpace(parts[1]) + if retLabels[label] { + // Extract condition code. + prefix := parts[0] // "JRS Z" or "JR C" + cc := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(prefix, "JRS"), "JR")) + if cc != "" { + out = append(out, " RET "+cc) + replaced[label]++ + continue + } + } + } + } + out = append(out, line) + } + + if len(replaced) == 0 { + return lines // nothing changed + } + + // Pass 3: count remaining references to each replaced label. + // If a label has zero remaining references, remove the label+RET. + labelRefs := make(map[string]int) + for _, line := range out { + t := strings.TrimSpace(line) + for label := range replaced { + if strings.Contains(t, label) && !strings.HasSuffix(t, ":") { + labelRefs[label]++ + } + } + } + + // Pass 3b: keep RET when the preceding instruction can fall through. + // Arithmetic sign correction, DJNZ and conditional branches all need it. + for i := 0; i < len(out); i++ { + t := strings.TrimSpace(out[i]) + if strings.HasSuffix(t, ":") { + label := strings.TrimSuffix(t, ":") + if _, ok := replaced[label]; ok { + // Walk backward to find the preceding instruction. + for j := i - 1; j >= 0; j-- { + prev := strings.TrimSpace(out[j]) + if prev == "" || strings.HasPrefix(prev, ";") { + continue + } + if prev != "RET" && + !(strings.HasPrefix(prev, "JP ") && !strings.Contains(prev, ",")) && + !(strings.HasPrefix(prev, "JR ") && !strings.Contains(prev, ",")) && + !(strings.HasPrefix(prev, "JRS ") && !strings.Contains(prev, ",")) { + labelRefs[label]++ // implicit fall-through reference + } + break + } + } + } + } + + // Pass 4: remove dead label+RET pairs. + deadLabels := make(map[string]bool) + for label := range replaced { + if labelRefs[label] == 0 { + deadLabels[label] = true + } + } + if len(deadLabels) == 0 { + return out + } + + var final []string + for i := 0; i < len(out); i++ { + t := strings.TrimSpace(out[i]) + if strings.HasSuffix(t, ":") { + label := strings.TrimSuffix(t, ":") + if deadLabels[label] { + // Skip label line and the following RET. + for j := i + 1; j < len(out); j++ { + next := strings.TrimSpace(out[j]) + if next == "" || strings.HasPrefix(next, ";") { + i = j + continue + } + if next == "RET" { + i = j // skip the RET too + } + break + } + continue + } + } + final = append(final, out[i]) + } + return final +} diff --git a/minzc/pkg/mir2/z80codegen_term.go b/minzc/pkg/mir2/z80codegen_term.go new file mode 100644 index 000000000..2dce870bb --- /dev/null +++ b/minzc/pkg/mir2/z80codegen_term.go @@ -0,0 +1,481 @@ +package mir2 + +import ( + "fmt" + "sort" +) + +// trampolineBlock is emitted after all regular blocks of a function. +// It resolves block-argument copies for one outgoing BrIf edge. +// If isRet is true, the block ends with RET instead of JRS target. +type trampolineBlock struct { + label string // local label for this trampoline + copies []parallelCopy + target string // real destination label (already formatted) + isRet bool // true → emit RET instead of JRS target +} + +// restoreLiveOutOverrides moves every relocated vreg that is live out of the +// current block, or read by its terminator, back to its allocated register +// before the terminator. A +// relocation (physOverride) is block-local: successors, and blocks laid out +// after this one, read the allocator's location. Without this, a value saved +// to a scratch register in one branch (gcd's else arm) was read from that +// scratch on paths that never wrote it. +func (g *z80cg) restoreLiveOutOverrides(f *Func, t Term) { + if g.liveness == nil || len(g.physOverride) == 0 { + return + } + liveOut := g.liveness.LiveOutOf(f, g.curBlock) + if liveOut == nil { + return + } + // Terminator operands count too: jmp @head(%r3, …) passes r3 to the param + // that is r3 itself after coalescing, and the parallel copy skips it as an + // identity move, so r3 must already sit in its allocated register. + usedByTerm := map[Reg]bool{} + for _, r := range t.termUses() { + usedByTerm[r] = true + } + regs := make([]Reg, 0, len(g.physOverride)) + for r := range g.physOverride { + if liveOut.Has(r) || usedByTerm[r] { + regs = append(regs, r) + } + } + sort.Slice(regs, func(i, j int) bool { return regs[i] < regs[j] }) + for _, r := range regs { + scratch := g.physOverride[r] + canonLoc := g.ar.Loc(r) + delete(g.physOverride, r) + if canonLoc.Kind != LocReg && canonLoc.Kind != LocIXY8 { + continue + } + if canon := canonLoc.Name; canon != "" && canon != scratch { + width := 8 + if canon == "HL" || canon == "DE" || canon == "BC" { + width = 16 + } + g.emitMov(canon, scratch, width) + g.comment(fmt.Sprintf("restore live-out r%d from scratch", r)) + } + } +} + +// ── Terminators ─────────────────────────────────────────────────────────────── + +func (g *z80cg) genTerm(f *Func, t Term) { + // If any block param was saved to a scratch register (via materializePendingAcc), + // restore it to its canonical physical location before any terminal jump or ret. + // This ensures live-through values (used in successor blocks without explicit + // block params) are in their allocator-assigned locations when those blocks run. + // physOverride[r] was valid within this block; canonical location = ar.Loc(r). + // + // EXCEPTION: For TermDJNZ, do NOT restore block params that are explicitly + // passed as body-edge args (BodyArgs). Those params receive their new values + // from the parallel copy emitted by the DJNZ handler. Restoring them from + // scratch would overwrite the computed result with the stale initial value. + djnzBodyArgs := map[Reg]bool{} + if td, ok := t.(*TermDJNZ); ok { + bodyBlock := f.BlockByLabel(td.Body) + if bodyBlock != nil { + for i, arg := range td.BodyArgs { + if i+1 < len(bodyBlock.Params) { + djnzBodyArgs[bodyBlock.Params[i+1].Dst] = true + _ = arg // only need the param dst + } + } + } + } + for _, bp := range g.curBlock.Params { + if djnzBodyArgs[bp.Dst] { + delete(g.physOverride, bp.Dst) + continue + } + if scratch, ok := g.physOverride[bp.Dst]; ok { + // Use ar.Loc (static allocation) not g.loc (which returns the override). + canonLoc := g.ar.Loc(bp.Dst) + delete(g.physOverride, bp.Dst) + // Only restore register-backed block params — spilled params + // (LocMem) are already in memory and weren't saved to a scratch GPR. + if canonLoc.Kind == LocReg || canonLoc.Kind == LocIXY8 { + canon := canonLoc.Name + if canon != "" && canon != scratch { + g.emitMov(canon, scratch, bp.Ty.Width()) + g.comment("restore block param from scratch") + } + } + } + } + g.restoreLiveOutOverrides(f, t) + + switch t := t.(type) { + case *TermRet: + // Tail call: JP was already emitted by genCall; skip all moves and RET. + if g.tailCallInst != nil { + g.tailCallInst = nil + return + } + // Move return values to their calling-convention physical registers using + // parallel copy resolution. Sequential moves are incorrect when two return + // values share the same physical register after constant folding/PBQP + // (e.g. both [acc]=A), causing write-after-write clobber. + if len(t.Vals) == 1 && len(f.Contract.Returns) == 1 && + (f.Contract.Returns[0].Ty == TyBool || (f.Contract.Returns[0].Ty.Width() <= 8 && collectRegInfo(f)[t.Vals[0]].Ty == TyBool)) && + f.Contract.Returns[0].Class != ClassFlag && g.loc(t.Vals[0]) == "F" { + g.emitFlagPredicateByte(g.condCode(f, t.Vals[0])) + if dst := canonicalReturnLoc(f.Contract.Returns[0].Class, TyBool); dst != "A" { + g.emitLD8(dst, "A") + } + } else { + g.emitParallelCopy(g.buildReturnCopies(t.Vals)) + } + // main() is the program entry point: use XOR A / DI / HALT instead of + // RET so mze/mzx can detect program termination cleanly. XOR A zeroes + // A for a clean exit code 0. + if f.Name == "main" && len(t.Vals) == 0 { + g.emit(" XOR A") + g.emit(" DI") + g.emit(" HALT") + } else { + g.emit(" RET") + } + + case *TermJmp: + // Resolve block-argument parallel copies before the jump. + copies := g.buildBlockCopies(f, t.Target, t.Args) + g.emitParallelCopy(copies) + // Fall-through elimination: if the target is the very next physical block, + // the JR is redundant — execution falls through naturally. + if !g.isFallThrough(f, t.Target) { + g.emitf(" JRS %s", blockLabel(f.Name, t.Target)) + } + + case *TermBrIf: + cc := g.condCode(f, t.Cond) + + // Build block-arg copies for each edge. + thenCopies := g.buildBlockCopies(f, t.Then, t.ThenArgs) + elseCopies := g.buildBlockCopies(f, t.Else, t.ElseArgs) + + // If a branch has non-trivial copies, it cannot be a fall-through: + // redirect it through a trampoline that performs the copies then JR. + thenLbl := g.branchLabel(f, t.Then, thenCopies) + elseLbl := g.branchLabel(f, t.Else, elseCopies) + + // Fall-through is only possible when there are no copies for that edge. + thenFT := g.isFallThrough(f, t.Then) && len(thenCopies) == 0 + elseFT := g.isFallThrough(f, t.Else) && len(elseCopies) == 0 + + // Two-condition sentinels: CmpGt/CmpLe with lhs in A (no operand swap). + // CGT = NC && NZ (a > b); CLE = C || Z (a ≤ b). + // Note: CLE/CGT need two jumps — first uses JR, second uses JR too. + // MZA auto-promotes any JR to JP when out of range. + if cc == "CLE" { + // a ≤ b: branch to then if C (a b: branch to else if Z (a==b) or C (a). + // Emit: CP rhs (with A=lhs), then JR Z eq, JR C lt, fall-through/JR gt. + lhs := g.loc(t.Lhs) + rhs := g.loc(t.Rhs) + + // Emit the CP (same logic as genCmp). + if cv, ok := g.constVals[t.Rhs]; ok { + if !g.holdsValue("A", lhs) { + g.emitLDA(lhs) + } + g.emitf(" CP %d", cv&0xFF) + } else if rhs == "A" && lhs != "A" { + // A already holds rhs; swap: CP lhs computes rhs-lhs. + // Eq is still Z (rhs-lhs==0 ↔ rhs==lhs), but C now means rhs= len(bodyBlock.Params) { + break + } + param := bodyBlock.Params[i+1] // skip counter param[0] + src := g.loc(arg) + dst := g.loc(param.Dst) + if src != dst { + bodyCopies = append(bodyCopies, parallelCopy{srcName: src, dstName: dst, ty: param.Ty}) + } + } + } + g.emitParallelCopy(bodyCopies) + g.emitf(" DJNZ %s", blockLabel(f.Name, t.Body)) + // Exit path. + exitCopies := g.buildBlockCopies(f, t.Exit, t.ExitArgs) + g.emitParallelCopy(exitCopies) + if !g.isFallThrough(f, t.Exit) || len(exitCopies) != 0 { + g.emitf(" JP %s", blockLabel(f.Name, t.Exit)) + } + + case *TermCondRet: + // Strategy: jump to Then FIRST (before moving return values), + // because moving return values may emit instructions that clobber + // the flag register the condition depends on (e.g. OR A before SBC). + cc := g.condCode(f, t.Cond) + copies := g.buildBlockCopies(f, t.Then, t.ThenArgs) + thenLbl := g.branchLabel(f, t.Then, copies) + retCopies := g.buildReturnCopies(t.Vals) + thenFT := g.isFallThrough(f, t.Then) && len(copies) == 0 + + switch cc { + case "CLE": + // a ≤ b: take Then if C (a b: take Then if NC && NZ. + // Invert: skip Then (jump to ret path) if C or Z. + retLbl := fmt.Sprintf(".%s_cret%d", sanitizeIdent(f.Name), g.trampIdx) + g.trampIdx++ + g.emitf(" JRS Z, %s", retLbl) + g.emitf(" JRS C, %s", retLbl) + if !thenFT { + g.emitf(" JRS %s", thenLbl) + } + // Register ret path as trampoline so it's emitted AFTER the fallthrough + // then-block (otherwise the inline label would block the fallthrough path). + g.trampolines = append(g.trampolines, trampolineBlock{ + label: retLbl, + copies: retCopies, + isRet: true, + }) + default: + if thenFT && len(retCopies) == 0 { + // Then-block is the fallthrough AND return path is empty: + // use RET invertCC (1 byte, optimal). Only safe when retCopies is + // empty — otherwise retCopies would corrupt registers that @then + // reads, since they execute before the branch is resolved. + g.emitf(" RET %s", invertCC(cc)) + } else if thenFT { + // Then-block is the fallthrough but retCopies are non-trivial. + // Jump to @then when condition is true (skipping the return path), + // then fall through to @then naturally. + g.emitf(" JRS %s, %s", cc, thenLbl) + g.emitParallelCopy(retCopies) + g.emit(" RET") + } else { + // General case: emit conditional jump to Then first, then return path. + g.emitf(" JRS %s, %s", cc, thenLbl) + g.emitParallelCopy(copies) + g.emitParallelCopy(retCopies) + g.emit(" RET") + } + } + + case *TermUnreachable: + g.comment("unreachable") + } +} + +// branchLabel returns the label to jump to for a BrIf edge. +// If the edge has parallel copies, a trampoline block is registered and its +// label is returned instead of the real target label. +func (g *z80cg) branchLabel(f *Func, target string, copies []parallelCopy) string { + realLbl := blockLabel(f.Name, target) + if len(copies) == 0 { + return realLbl + } + tramLbl := fmt.Sprintf(".%s_trmp%d", sanitizeIdent(f.Name), g.trampIdx) + g.trampIdx++ + g.trampolines = append(g.trampolines, trampolineBlock{ + label: tramLbl, + copies: copies, + target: realLbl, + }) + return tramLbl +} + +// isFallThrough reports whether targetLabel is the next physical block after +// the current one (g.blockIdx), making a JP redundant. +func (g *z80cg) isFallThrough(f *Func, targetLabel string) bool { + next := g.blockIdx + 1 + if next >= len(f.Blocks) { + return false + } + return f.Blocks[next].Label == targetLabel +} + +// ── DJNZ peephole ───────────────────────────────────────────────────────────── + +// findConstInFunc finds the Imm value of an OpConst instruction defining r. +func findConstInFunc(f *Func, r Reg) (int64, bool) { + for _, b := range f.Blocks { + for _, inst := range b.Insts { + if inst.Op == OpConst && inst.Dst == r { + return inst.Imm, true + } + } + } + return 0, false +} + +// djnzPeepholeResult holds the result of DJNZ peephole detection. +type djnzPeepholeResult struct { + skipLast bool // skip the last OpSub instruction (DJNZ handles DEC B) + bodyLbl string // DJNZ target: the loop body label (brif.Then) + checkBrif *TermBrIf // the check block's brif (for exit-edge copies) +} + +// detectDJNZPeephole checks if block b ends with the DJNZ-optimizable pattern: +// +// last_inst: OpSub(counter→B, 1) +// term: TermJmp to check_block +// check_block: [OpCmp(CmpNe, B, 0)] + TermBrIf(Then=body, Else=exit) +// all args trivially in-place (no non-trivial copies needed) +func (g *z80cg) detectDJNZPeephole(f *Func, b *Block) djnzPeepholeResult { + if len(b.Insts) == 0 { + return djnzPeepholeResult{} + } + jmp, ok := b.Term.(*TermJmp) + if !ok { + return djnzPeepholeResult{} + } + // Last instruction must be OpSub(counter, 1) with counter→B. + last := b.Insts[len(b.Insts)-1] + if last.Op != OpSub { + return djnzPeepholeResult{} + } + if g.ar.Loc(last.Src[0]).Name != "B" || g.ar.Loc(last.Dst).Name != "B" { + return djnzPeepholeResult{} + } + // RHS must be constant 1. + rhs, found := findConstInFunc(f, last.Src[1]) + if !found || rhs != 1 { + return djnzPeepholeResult{} + } + // Target must be the check block: only CmpNe(B, 0) + optional Const(0) + TermBrIf. + check := f.BlockByLabel(jmp.Target) + if check == nil || len(check.Insts) == 0 || len(check.Insts) > 2 { + return djnzPeepholeResult{} + } + // Find the OpCmp instruction (last if there are two). + var cmp *Inst + for i := len(check.Insts) - 1; i >= 0; i-- { + if check.Insts[i].Op == OpCmp { + cmp = check.Insts[i] + break + } + } + if cmp == nil || cmp.Cond != CmpNe { + return djnzPeepholeResult{} + } + if g.ar.Loc(cmp.Src[0]).Name != "B" { + return djnzPeepholeResult{} + } + zero, found2 := findConstInFunc(f, cmp.Src[1]) + if !found2 || zero != 0 { + return djnzPeepholeResult{} + } + // Verify all instructions in check block are CmpNe + optional Const(0) only. + for _, inst := range check.Insts { + if inst.Op != OpCmp && inst.Op != OpConst { + return djnzPeepholeResult{} + } + } + brif, ok2 := check.Term.(*TermBrIf) + if !ok2 || brif.Cond != cmp.Dst { + return djnzPeepholeResult{} + } + // Verify all TermJmp args are trivially in-place (excluding counter B→B). + for i, arg := range jmp.Args { + if i >= len(check.Params) { + break + } + srcLoc := g.loc(arg) + dstLoc := g.loc(check.Params[i].Dst) + if srcLoc == "B" && dstLoc == "B" { + continue // counter handled by DJNZ + } + if srcLoc != dstLoc { + return djnzPeepholeResult{} // non-trivial copy, fall back + } + } + // Verify exit-edge copies are trivial. + exitBlock := f.BlockByLabel(brif.Else) + if exitBlock != nil { + for i, arg := range brif.ElseArgs { + if i >= len(exitBlock.Params) { + break + } + if g.ar.Loc(arg).Name != g.ar.Loc(exitBlock.Params[i].Dst).Name { + return djnzPeepholeResult{} // non-trivial exit copy, fall back + } + } + } + return djnzPeepholeResult{ + skipLast: true, + bodyLbl: blockLabel(f.Name, brif.Then), + checkBrif: brif, + } +}