Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 56 additions & 19 deletions minzc/pkg/hir/exhaustive_judge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,10 @@ func TestExhaustiveJudgeLIRSingleBlock(t *testing.T) {
}

// Word fallback judges assemble once and exhaust the u16 domain for >>3;
// add32 checks fallback provenance and MIR2 semantics: the current PBQP u32
// ABI spills to memory and produces unassemblable code, so it cannot yet be
// judged on Z80. Keep that limitation explicit instead of altering the ABI.
// This judge writes memory arguments directly to callee spill slots; it does
// not test the caller/callee memory-parameter ABI (which currently mismatches).
// add32 checks 65,636 sums on MIR2 and Z80 arithmetic, including
// carries between main and shadow register banks.
func TestExhaustiveJudgeLIRWideFallback(t *testing.T) {
for _, name := range []string{"shr16", "add32"} {
t.Run(name, func(t *testing.T) {
Expand Down Expand Up @@ -368,27 +369,63 @@ func TestExhaustiveJudgeLIRWideFallback(t *testing.T) {
}
}
}
// PBQP cannot assemble this production u32 ABI yet. A future
// repair must replace this source-VM check with a Z80 judge.
res, err := z80asm.NewAssembler().AssembleString(steps.Assembly)
// The production ABI now assembles; exercise both register banks
// and named memory parameters against the MIR2 boundary oracle.
var boot strings.Builder
fmt.Fprintf(&boot, "ORG 0x%04X\nLD SP,0xFF00\n", testLoadAddr)
var inputs [2]string
for i, p := range mf.Contract.Params {
loc := steps.Allocation.Locs[p.Reg]
if loc.Kind == mir2.LocMem {
inputs[i] = fmt.Sprintf("_spill_%s_r%d", name, p.Reg)
} else {
inputs[i] = fmt.Sprintf("judge_arg%d", i)
fmt.Fprintf(&boot, "LD %s,(%s)\nEXX\nLD %s,(%s+2)\nEXX\n", loc.Name, inputs[i], loc.Name, inputs[i])
}
}
fmt.Fprintf(&boot, "CALL %s\nPUSH HL\nEXX\nPUSH HL\nEXX\nPOP BC\nPOP HL\nDI\nHALT\njudge_arg0: DB 0,0,0,0\njudge_arg1: DB 0,0,0,0\n", name)
res, err := z80asm.NewAssembler().AssembleString(boot.String() + steps.Assembly)
if err != nil || len(res.Errors) > 0 {
plainOpts := pipeline.DefaultOptions()
plain, plainErr := pipeline.CompileHIRSteps(&hir.Module{Name: "judge_wide", Funcs: []*hir.Func{f}}, plainOpts)
if plainErr != nil {
t.Fatal(plainErr)
t.Fatalf("assemble: %v %v", err, res.Errors)
}
z := emulator.NewRemogattoZ80()
check := func(a, b int64) {
t.Helper()
z.Reset()
z.LoadMemory(testLoadAddr, res.Binary)
for i, v := range []int64{a, b} {
addr, ok := res.Symbols[inputs[i]]
if !ok {
t.Fatalf("undefined parameter slot %s", inputs[i])
}
for j := 0; j < 4; j++ {
z.SetMemory(uint16(addr+j), byte(uint32(v)>>uint(j*8)))
}
}
z.SetRegisters(emulator.Registers{SP: 0xFF00, PC: testLoadAddr})
for n := 0; !z.IsHalted(); n++ {
if n >= judgeStepBudget {
t.Fatal("no HALT")
}
z.Step()
}
r := z.GetRegisters()
got := uint32(r.HL) | uint32(r.BC)<<16
if got != uint32(a+b) {
t.Fatalf("Z80 add32(%x,%x)=%x want %x", a, b, got, uint32(a+b))
}
plainRes, plainErr := z80asm.NewAssembler().AssembleString(plain.Assembly)
if len(plainRes.Errors) == 0 || len(plainRes.Errors) != len(res.Errors) {
t.Fatalf("known PBQP u32 record requires equal nonzero assembly error counts: --lir %d, plain %d (%v)", len(res.Errors), len(plainRes.Errors), plainErr)
}
for a := int64(0); a < 256; a++ {
for b := int64(0); b < 256; b++ {
check(a, b)
}
// Re-measured on deterministic origin/main ed55c1c7.
const knownAssemblyErrors = 18 // 2026-10-02, both modes
if len(plainRes.Errors) != knownAssemblyErrors {
t.Fatalf("known PBQP u32 assembly error count changed: got %d, recorded %d; re-measure both modes", len(plainRes.Errors), knownAssemblyErrors)
}
for _, a := range boundaries {
for _, b := range boundaries {
check(a, b)
}
t.Skipf("2026-10-02: 65,636 MIR2 sums checked; known PBQP u32 Z80 assembly errors: --lir %d, plain %d: %v %v", len(res.Errors), len(plainRes.Errors), plainErr, plainRes.Errors)
}
t.Fatal("production u32 now assembles: replace this skip with a real judge")
return
}
boot := fmt.Sprintf(" ORG 0x%04X\n CALL %s\n DI\n HALT\n", testLoadAddr, name)
res, err := z80asm.NewAssembler().AssembleString(boot + steps.Assembly)
Expand Down
103 changes: 103 additions & 0 deletions minzc/pkg/hir/p8_split_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package hir

import (
"fmt"
"github.com/minz/minzc/pkg/emulator"
"github.com/minz/minzc/pkg/mir2"
"github.com/minz/minzc/pkg/z80asm"
"testing"
)

func TestP8SplitReturnDependency(t *testing.T) {
f := &Func{Name: "compute", RetTy: mir2.TyU16, Params: []Param{{Name: "a", Ty: mir2.TyU16}}, Body: &Block{Body: []Stmt{
&VarDeclStmt{Name: "x", Ty: mir2.TyU16, Init: &VarRefExpr{Name: "a", Ty: mir2.TyU16}},
&VarDeclStmt{Name: "y", Ty: mir2.TyU16, Init: &IntLitExpr{Val: 7, Ty: mir2.TyU16}},
&VarDeclStmt{Name: "z", Ty: mir2.TyU16, Init: &IntLitExpr{Val: 8, Ty: mir2.TyU16}},
&ReturnStmt{Val: &VarRefExpr{Name: "x", Ty: mir2.TyU16}},
}}}
m := &Module{Name: "p8", Funcs: []*Func{f}}
candidates := FindSplitPoints(f, []int{9, 9, 9, 9})
if len(candidates) == 0 {
t.Fatal("no split")
}
sub := ApplySplit(m, f, candidates[0])
m.Funcs = append(m.Funcs, sub)
mm := LowerModule(m)
if mm.FuncByName(sub.Name) == nil {
t.Fatal("split callee omitted by free-variable detection")
}
result, err := mir2.NewVM(mm).Call("compute", []mir2.Value{{I: 1234}})
if err != nil || len(result) != 1 || result[0].I != 1234 {
t.Fatalf("return lost across split: %v %v", result, err)
}
mm.RenumberRegs()
ar := &mir2.AllocResult{Locs: map[mir2.Reg]mir2.PhysLoc{}}
for _, mf := range mm.Funcs {
a := mir2.PBQPAllocate(mf, mir2.ComputeLiveness(mf), mir2.Z80CostTable{})
for r, l := range a.Locs {
ar.Locs[r] = l
}
ar.Spilled = append(ar.Spilled, a.Spilled...)
}
param := mm.FuncByName("compute").Contract.Params[0].Reg
// Use distinct parameter locations so this tests the forwarding caller
// rather than the codegen's identity-function EQU alias optimization.
ar.Locs[param] = mir2.PhysLoc{Kind: mir2.LocReg, Name: "BC"}
asm := fmt.Sprintf("ORG 0x8000\nLD SP,0xFF00\nLD %s,1234\nCALL compute\nDI\nHALT\n", ar.Loc(param).Name) + mir2.Z80Codegen(mm, ar)
res, e := z80asm.NewAssembler().AssembleString(asm)
if e != nil || len(res.Errors) > 0 {
t.Fatalf("assemble %v %v\n%s", e, res.Errors, asm)
}
z := emulator.NewRemogattoZ80()
z.Reset()
z.LoadMemory(0x8000, res.Binary)
z.SetRegisters(emulator.Registers{PC: 0x8000, SP: 0xff00})
for n := 0; !z.IsHalted(); n++ {
if n > 10000 {
t.Fatalf("no HALT\n%s", asm)
}
z.Step()
}
if got := z.GetRegisters().HL; got != uint16(result[0].I) {
t.Fatalf("Z80 got %d MIR2 got %d\n%s", got, result[0].I, asm)
}
}

func TestP8SplitConditionalDependency(t *testing.T) {
f := &Func{Name: "compute", RetTy: mir2.TyU16, Params: []Param{{Name: "a", Ty: mir2.TyU16}}, Body: &Block{Body: []Stmt{
&VarDeclStmt{Name: "x", Ty: mir2.TyU16, Init: &VarRefExpr{Name: "a", Ty: mir2.TyU16}},
&VarDeclStmt{Name: "y", Ty: mir2.TyU16, Init: &IntLitExpr{Val: 7, Ty: mir2.TyU16}},
&VarDeclStmt{Name: "z", Ty: mir2.TyU16, Init: &CondExpr{Cond: &BoolLitExpr{Val: true}, Then: &VarRefExpr{Name: "x", Ty: mir2.TyU16}, Else: &IntLitExpr{Val: 0, Ty: mir2.TyU16}, Ty: mir2.TyU16}},
&ReturnStmt{Val: &VarRefExpr{Name: "z", Ty: mir2.TyU16}},
}}}
m := &Module{Name: "p8", Funcs: []*Func{f}}
c := FindSplitPoints(f, []int{9, 9, 9, 9})
sub := ApplySplit(m, f, c[0])
m.Funcs = append(m.Funcs, sub)
mm := LowerModule(m)
if mm.FuncByName(sub.Name) == nil {
t.Fatal("conditional dependencies omitted from split interface")
}
got, err := mir2.NewVM(mm).Call("compute", []mir2.Value{{I: 1234}})
if err != nil || len(got) != 1 || got[0].I != 1234 {
t.Fatalf("got %v %v", got, err)
}
}

func TestP8ReturnedSplitIsNotSplitAgain(t *testing.T) {
f := &Func{Name: "compute", RetTy: mir2.TyU16, Body: &Block{}}
var args []Expr
for _, name := range []string{"a", "b", "c", "d", "e", "f", "x", "y", "z"} {
f.Params = append(f.Params, Param{Name: name, Ty: mir2.TyU16})
args = append(args, &VarRefExpr{Name: name, Ty: mir2.TyU16})
}
for i := 0; i < 4; i++ {
f.Body.Body = append(f.Body.Body, &ExprStmt{Expr: &CallExpr{Fn: "side", Args: args, Ty: mir2.TyVoid}})
}
f.Body.Body = append(f.Body.Body, &ReturnStmt{Val: &CallExpr{Fn: "compute$split_1", Args: args[:3], Ty: mir2.TyU16}})
m := &Module{Name: "p8", Funcs: []*Func{f}}
var results []SplitResult
if subs := splitRecursive(m, f, &results, 0); len(subs) != 0 {
t.Fatalf("returned split call was split again: %v", results)
}
}
79 changes: 77 additions & 2 deletions minzc/pkg/hir/split.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ func splitRecursive(m *Module, f *Func, results *[]SplitResult, depth int) []*Fu
}
// Guard: don't re-split if last stmt is already a split call.
if len(f.Body.Body) > 0 {
if rs, ok := f.Body.Body[len(f.Body.Body)-1].(*ReturnStmt); ok {
if ce, ok := rs.Val.(*CallExpr); ok && strings.Contains(ce.Fn, "$split_") {
return nil
}
}
if es, ok := f.Body.Body[len(f.Body.Body)-1].(*ExprStmt); ok {
if ce, ok := es.Expr.(*CallExpr); ok {
if strings.Contains(ce.Fn, "$split_") {
Expand Down Expand Up @@ -285,6 +290,10 @@ func (s splitCandidate) interfaceWidth() int {

// FindSplitPoints returns viable split candidates for a function.
func FindSplitPoints(f *Func, pressure []int) []splitCandidate {
// Multi-result calls need tuple forwarding; leave those functions intact.
if len(f.RetTys) > 1 {
return nil
}
stmts := f.Body.Body
if len(stmts) < 4 {
return nil
Expand Down Expand Up @@ -400,7 +409,8 @@ func ApplySplit(m *Module, f *Func, c splitCandidate) *Func {
sub := &Func{
Name: subName,
Params: params,
RetTy: mir2.TyVoid,
RetTy: f.RetTy,
RetTys: append([]mir2.Ty(nil), f.RetTys...),
Body: subBody,
}

Expand All @@ -417,13 +427,19 @@ func ApplySplit(m *Module, f *Func, c splitCandidate) *Func {
callExpr := &CallExpr{
Fn: subName,
Args: args,
Ty: mir2.TyVoid,
Ty: f.RetTy,
}
if len(f.RetTys) == 1 {
callExpr.Ty = f.RetTys[0]
}

// New body = top half + call statement.
newBody := make([]Stmt, c.splitAt+2)
copy(newBody, stmts[:c.splitAt+1])
newBody[c.splitAt+1] = &ExprStmt{Expr: callExpr}
if countReturns(f) != 0 {
newBody[c.splitAt+1] = &ReturnStmt{Val: callExpr}
}
f.Body.Body = newBody

return sub
Expand All @@ -434,6 +450,36 @@ func ApplySplit(m *Module, f *Func, c splitCandidate) *Func {
// collectVarRefs walks a statement and collects variable references and definitions.
func collectVarRefs(s Stmt, refs, defs map[string]bool) {
switch s := s.(type) {
case *Block:
for _, inner := range s.Body {
collectVarRefs(inner, refs, defs)
}
case *StoreStmt:
collectExprRefs(s.Ptr, refs)
collectExprRefs(s.Val, refs)
case *SwitchStmt:
collectExprRefs(s.Val, refs)
for _, c := range s.Cases {
collectVarRefs(c.Body, refs, defs)
}
if s.Default != nil {
collectVarRefs(s.Default, refs, defs)
}
case *ForEachStmt:
defs[s.Var] = true
collectExprRefs(s.Ptr, refs)
collectExprRefs(s.Start, refs)
collectExprRefs(s.Len, refs)
if s.Body != nil {
collectVarRefs(s.Body, refs, defs)
}
case *AsmStmt:
for _, in := range s.Ins {
refs[in.Name] = true
}
for _, out := range s.Outs {
defs[out.Name] = true
}
case *VarDeclStmt:
defs[s.Name] = true
if s.Init != nil {
Expand All @@ -446,6 +492,7 @@ func collectVarRefs(s Stmt, refs, defs map[string]bool) {
collectExprRefs(s.Target, refs)
collectExprRefs(s.Val, refs)
case *ReturnStmt:
collectExprRefs(s.Val, refs)
for _, v := range s.Vals {
collectExprRefs(v, refs)
}
Expand Down Expand Up @@ -496,6 +543,34 @@ func collectExprRefs(e Expr, refs map[string]bool) {
return
}
switch e := e.(type) {
case *CondExpr:
collectExprRefs(e.Cond, refs)
collectExprRefs(e.Then, refs)
collectExprRefs(e.Else, refs)
case *LoadExpr:
collectExprRefs(e.Ptr, refs)
case *BitExpr:
collectExprRefs(e.X, refs)
case *CallIndirectExpr:
collectExprRefs(e.FnPtr, refs)
for _, a := range e.Args {
collectExprRefs(a, refs)
}
case *StructLitExpr:
for _, f := range e.Fields {
collectExprRefs(f.Val, refs)
}
case *RangeSourceExpr:
collectExprRefs(e.Lo, refs)
collectExprRefs(e.Hi, refs)
case *LetInExpr:
collectExprRefs(e.Init, refs)
bodyRefs := make(map[string]bool)
collectExprRefs(e.Body, bodyRefs)
delete(bodyRefs, e.Name)
for v := range bodyRefs {
refs[v] = true
}
case *VarRefExpr:
refs[e.Name] = true
case *CallExpr:
Expand Down
17 changes: 17 additions & 0 deletions minzc/pkg/mir2/p8_fix1_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package mir2

import (
"fmt"
"strings"
"testing"
)

func TestP8WideSpillExhaustionFails(t *testing.T) {
defer func() {
err := recover()
if err == nil || !strings.Contains(fmt.Sprint(err), "needs 1 wide spill staging pairs, only 0 available") {
t.Fatalf("expected explicit staging failure, got %v", err)
}
}()
wideSpillPairs("full", OpAdd, map[string]bool{"HL": true, "DE": true, "BC": true}, 1)
}
Loading
Loading