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, + } +}