From f4bd723d929e53475f7990374dca7b7dfcbbdc42 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Sun, 1 Nov 2026 10:33:55 -0800 Subject: Add Chapter 29 for tech debt cleanup with the parser split Keep Chapter 28b intact and carry the grammar-oriented parser package, Var move, IterPeeps move, and parser regression tests into Chapter 29. Defer privacy and exclude its experimental tests and fixture changes. Register the chapter in the indexes, IntelliJ modules and run configurations, Maven reactor, roadmap, and linear-history publisher. Windows and Linux each pass 573 tests, release builds, 199 allocation replays, and 29 native checks. --- .../simple/codegen/FlagSpillTestSupport.java | 141 +++++++++ .../simple/codegen/ParseAllTestSupport.java | 16 + .../simple/codegen/PrintRegTestSupport.java | 76 +++++ .../simple/codegen/RegAllocTestSupport.java | 352 +++++++++++++++++++++ 4 files changed, 585 insertions(+) create mode 100644 chapter29/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java create mode 100644 chapter29/src/test/java/com/seaofnodes/simple/codegen/ParseAllTestSupport.java create mode 100644 chapter29/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java create mode 100644 chapter29/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java (limited to 'chapter29/src/test/java/com/seaofnodes/simple/codegen') diff --git a/chapter29/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java b/chapter29/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java new file mode 100644 index 00000000..445acb61 --- /dev/null +++ b/chapter29/src/test/java/com/seaofnodes/simple/codegen/FlagSpillTestSupport.java @@ -0,0 +1,141 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.isa.X86; +import com.seaofnodes.isa.eval.EvalArm64; +import com.seaofnodes.simple.TestC; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.cpus.arm.arm; +import com.seaofnodes.simple.node.cpus.x86_64_v2.x86_64_v2; +import com.seaofnodes.simple.type.TypeInteger; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import static org.junit.Assert.*; + +// Force overlapping flags, then force every GPR unavailable across the gap. +public class FlagSpillTestSupport { + private static class Op extends MachConcreteNode { + final RegMask _def, _use, _kill; + Op(RegMask def,RegMask use,RegMask kill,Node... ins) { + super(ins); _def=def; _use=use; _kill=kill; _type=TypeInteger.BOT; + } + @Override public String op() { return "flagsProbe"; } + @Override public RegMask outregmap() { return _def; } + @Override public RegMask regmap(int i) { return _use; } + @Override public RegMask killmap() { return _kill; } + @Override public void encoding(Encoding enc) { throw new AssertionError(); } + } + + public static void run() throws IOException { + StringBuilder asm = new StringBuilder(".text\n"); + for( boolean isArm : new boolean[]{false,true} ) + for( boolean stack : new boolean[]{false,true} ) { + CodeGen code = new CodeGen(""); + code._callingConv="SystemV"; + code._mach=isArm ? new arm(code) : new x86_64_v2(code); + RegionNode block = new RegionNode(null,code._start,code._start); + code._cfg.add(block); + int flagsReg=isArm ? 64 : 32, firstStack=flagsReg+1; + RegMask flags = new RegMask(flagsReg); + Op def = new Op(flags,null,null,block); + RegMask killMask = stack ? (isArm ? new RegMask(0x7fffffffL,1L) : new RegMask(0x1ffffffefL)) : flags; + Op kill = new Op(null,null,killMask,block); + new Op(null,flags,null,block,def); + RegAlloc alloc = new RegAlloc(code); + alloc.regAlloc(); + + int splits=0, slots=0; + for( Node n : block._outputs ) if( n instanceof SplitNode split ) { + int dst=alloc.regnum(n), src=alloc.regnum(n.in(1)); + assertTrue(split.outregmap().test(dst)); + assertTrue(split.regmap(1).test(src)); + for( int fp=isArm ? 32 : 16; fp=firstStack || src>=firstStack ) slots++; + splits++; + } + assertTrue(splits>=2); + assertEquals(stack,slots!=0); + + // Keep the allocated registers, supplying a frame for encoding stack offsets. + FunNode frame = new FunNode(null,null,"flags",null,(Node)null); + code._regAlloc=new RegAlloc(code); + for( Node n : block._outputs ) + if( alloc.regnum(n)>=0 ) code._regAlloc.newLRG(n,frame)._reg=alloc.regnum(n); + Encoding save = new Encoding(code), restore = new Encoding(code); + Encoding enc = save; + for( Node n : block._outputs ) { + if( n==kill ) enc=restore; + if( n instanceof SplitNode split ) split.encoding(enc); + } + if( isArm ) { + for( int bits=0; bits<16; bits++ ) { + EvalArm64 cpu = new EvalArm64(new byte[1024],512); + System.arraycopy(save._bits.buf(),0,cpu._buf,0,save._bits.size()); + System.arraycopy(restore._bits.buf(),0,cpu._buf,save._bits.size(),restore._bits.size()); + cpu.N=(bits&8)!=0; cpu.Z=(bits&4)!=0; cpu.C=(bits&2)!=0; cpu.V=(bits&1)!=0; + assertEquals(0,cpu.step(save._bits.size()/4)); + cpu.N=cpu.Z=cpu.C=cpu.V=false; + if( stack ) for( int r=0; r<31; r++ ) cpu.regs[r]=0; + assertEquals(0,cpu.step(restore._bits.size()/4)); + assertEquals((bits&8)!=0,cpu.N); assertEquals((bits&4)!=0,cpu.Z); + assertEquals((bits&2)!=0,cpu.C); assertEquals((bits&1)!=0,cpu.V); + assertEquals(512,cpu.regs[31]); + } + } else { + Encoding body = new Encoding(code); + int[] preserved={3,5,6,7,12,13,14,15}; + for( int r : preserved ) pushPop(body,r,false); + X86.imm(body,0x81,5,4,16); // Reserve spill storage. + X86.flags(body,TestC.OS.startsWith("Windows") ? 1 : 7,true); + bytes(body,save); + if( stack ) { + for( int r=0; r<16; r++ ) if( r!=4 ) X86.constant(body,r,0); + } else X86.reg(body,0x39,0,0); // CMP rax,rax clobbers only flags. + bytes(body,restore); + X86.flags(body,0,false); // Return the restored flags. + X86.imm(body,0x81,0,4,16); + for( int i=preserved.length-1; i>=0; i-- ) pushPop(body,preserved[i],true); + X86.ret(body); + String name=stack ? "flags_stack" : "flags_reg"; + asm.append(".globl ").append(name).append('\n').append(name).append(":\n.byte "); + for( int i=0; i=8 ) enc.add1(0x41); + enc.add1((pop ? 0x58 : 0x50)+(reg&7)); + } +} diff --git a/chapter29/src/test/java/com/seaofnodes/simple/codegen/ParseAllTestSupport.java b/chapter29/src/test/java/com/seaofnodes/simple/codegen/ParseAllTestSupport.java new file mode 100644 index 00000000..2ce97a8e --- /dev/null +++ b/chapter29/src/test/java/com/seaofnodes/simple/codegen/ParseAllTestSupport.java @@ -0,0 +1,16 @@ +package com.seaofnodes.simple.codegen; + +import static org.junit.Assert.*; + +public class ParseAllTestSupport { + public static void checkRequiredTypes(CodeGen code) { + CompUnit a = code._compunits.get("App/A"); + CompUnit b = code._compunits.get("App/B"); + CompUnit c = code._compunits.get("App/C"); + assertNotNull(b); + assertTrue(a._deps.find(b)!=-1); + // C was parsed after A, while B's layout was still unresolved. + // Only A used that layout; C must not inherit A's build dependency. + assertNull(c._deps); + } +} diff --git a/chapter29/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java b/chapter29/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java new file mode 100644 index 00000000..b255bcf7 --- /dev/null +++ b/chapter29/src/test/java/com/seaofnodes/simple/codegen/PrintRegTestSupport.java @@ -0,0 +1,76 @@ +package com.seaofnodes.simple.codegen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import static org.junit.Assert.*; +public class PrintRegTestSupport { + public static void check() throws Exception { + var code = new CodeGen("return 0;").parse(); + var alloc = new RegAlloc(code); + var n = new ConstantNode(TypeInteger.ZERO); + var a = new LRG((short)1,null); + var b = new LRG((short)2,null); + var c = new LRG((short)3,null); + a._reg = -1; + c._leader = b; + b._leader = a; + var field = RegAlloc.class.getDeclaredField("_lrgs"); + field.setAccessible(true); + var map = (java.util.Map)field.get(alloc); + map.put(n,c); + assertEquals("V1",alloc.reg(n)); + assertEquals(-1,alloc.regnum(n)); + assertSame(c,map.get(n)); + assertSame(b,c._leader); + assertSame(a,b._leader); + } + public static void checkConstantPool() throws Exception { + var code = new CodeGen("return 0;").parse(); + code._phase = CodeGen.Phase.Encoding; + var enc = new Encoding(code); + var t = com.seaofnodes.simple.type.TypeStruct.make("PrintPool",false, + com.seaofnodes.simple.type.Field.make("x",TypeInteger.ZERO,2,true)); + var n = new ConstantNode(TypeInteger.ZERO); + enc.largeConstant(n,t,0,2); + int size = t.size(); // Encoding lays out data before printing the pool. + for( int i=0; i)field.get(null); + var marker = new Object(); + visit.put(marker,t); + var method = com.seaofnodes.simple.print.ASMPrinter.class.getDeclaredMethod( + "printConstantPool",int.class,com.seaofnodes.simple.util.SB.class, + com.seaofnodes.simple.util.BAOS.class,java.util.HashMap.class,boolean.class,String.class); + method.setAccessible(true); + try { + var sb = new com.seaofnodes.simple.util.SB(); + method.invoke(null,0,sb,enc._cpool,enc._bigCons,true,"Constant Pool"); + assertTrue(sb.toString().contains("PrintPool")); + assertTrue(sb.toString().contains("LayoutProbe")); + assertNull(offsets.get(t)); + assertSame(t,visit.get(marker)); + assertEquals(1,visit.size()); + } finally { visit.clear(); } + } +} diff --git a/chapter29/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java b/chapter29/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java new file mode 100644 index 00000000..292f1ae7 --- /dev/null +++ b/chapter29/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java @@ -0,0 +1,352 @@ +package com.seaofnodes.simple.codegen; + +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.cpus.x86_64_v2.x86_64_v2; +import com.seaofnodes.simple.type.TypeInteger; +import static org.junit.Assert.*; + +// Small scheduled machine graphs isolate register constraints from peepholes. +public class RegAllocTestSupport { + private static final RegMask A = new RegMask(0), B = new RegMask(1); + private static class Op extends MachConcreteNode { + final RegMask _def, _use, _kill; + final boolean _clone; + Op(RegMask def, RegMask use, RegMask kill, boolean clone, Node... ins) { + super(ins); _def=def; _use=use; _kill=kill; _clone=clone; _type=TypeInteger.BOT; + } + @Override public String op() { return "testOp"; } + @Override public RegMask outregmap() { return _def; } + @Override public RegMask regmap(int i) { return _use; } + @Override public RegMask killmap() { return _kill; } + @Override public boolean isClone() { return _clone; } + @Override public Node copy() { return new Op(_def,_use,_kill,_clone,(Node)null); } + @Override public void encoding(Encoding enc) { throw new AssertionError(); } + } + private static CodeGen graph() { + CodeGen code = new CodeGen(""); + code._mach = new x86_64_v2(code); + code._cfg.add(code._start); + return code; + } + + private static SplitNode testSplit(CodeGen code, MachNode def) { + LRG lrg = new LRG((short)0,null); + lrg._machDef = def; + return code._mach.split(lrg,"test",(byte)0); + } + + public static void uncoloredFixedNeighbor() throws Exception { + CodeGen code = graph(); + RegAlloc alloc = new RegAlloc(code); + Op def = new Op(new RegMask(3L),null,null,false,code._start); + LRG lrg = alloc.newLRG(def,null); + LRG neighbor = new LRG((short)99,null); + neighbor._mask = A; + lrg.addNeighbor(neighbor); // Uncolored, but its only possible color is A. + var bias = IFG.class.getDeclaredMethod("biasColorNeighbors",RegAlloc.class,Node.class,RegMask.class); + bias.setAccessible(true); + assertEquals((short)1,bias.invoke(null,alloc,def,new RegMaskRW(3L,0L))); + } + + public static void masks() { + for( int reg : new int[]{0,63,64,65,127} ) { + RegMask mask = new RegMask(reg); + assertEquals(reg,mask.firstReg()); + assertEquals(1,mask.size()); + } + assertEquals(-1,new RegMask(0L).firstReg()); + assertFalse(new RegMask(0L).size1()); + RegMask boundary = new RegMask(Long.MIN_VALUE,Long.MIN_VALUE|1); + assertEquals(64,boundary.nextReg((short)63)); + assertEquals(127,boundary.nextReg((short)64)); + assertEquals(-1,boundary.nextReg((short)127)); + } + + public static void union() { + CodeGen code = graph(); + Op use = new Op(null,B,null,false,code._start); + LRG a = new LRG((short)1,null), b = new LRG((short)2,null); + a._mask = new RegMask(3L); + b._mask = new RegMask(6L); + b.machUse(use,(short)2,true); + LRG both = a.union(b); + assertNotNull(both._mask); + assertEquals(1,both._mask.firstReg()); + assertEquals(1,both._mask.size()); + assertSame(use,both._machUse); + assertEquals(2,both._uidx); + } + + public static void killWithoutResult() { + CodeGen code = graph(); + Op def = new Op(A,null,null,false,code._start); + new Op(null,null,A,false,code._start); // Clobbers A, produces no value. + new Op(null,A,null,false,code._start,def); + RegAlloc alloc = new RegAlloc(code); + assertTrue(BuildLRG.run(0,alloc)); + assertFalse("A live value cannot survive a register kill",IFG.build(0,alloc)); + } + + public static void nullUseMask() { + CodeGen code = graph(); + Op def = new Op(A,null,null,false,code._start); + new Op(null,null,null,false,code._start,def); // Scheduling dependency only. + RegAlloc alloc = new RegAlloc(code); + assertTrue(BuildLRG.run(0,alloc)); + assertTrue(IFG.build(0,alloc)); + } + + public static void cloneRegisterClass() { + for( RegMask required : new RegMask[]{B,new RegMask(6L)} ) { + CodeGen code = graph(); + Op def = new Op(A,null,null,true,code._start); + Op use = new Op(null,required,null,false,code._start,def); + Op use2 = new Op(null,required,null,false,code._start,def); + RegAlloc alloc = new RegAlloc(code); + alloc.regAlloc(); // Re-cloning an A-only value cannot satisfy B/C uses. + assertEquals(0,alloc.regnum(def)); + assertTrue(required.test(alloc.regnum(use.in(1)))); + assertTrue(required.test(alloc.regnum(use2.in(1)))); + assertTrue(use.in(1) instanceof SplitNode); + } + } + + public static void commutativePhi() { + CodeGen code = graph(); + Op a = new Op(A,null,null,false,code._start); + Op b = new Op(A,null,null,false,code._start); + Op add = new Op(A,A,null,false,code._start,a,b) { + @Override public boolean commutes() { return true; } + }; + RegionNode region = new RegionNode(null,code._start,code._start); + new PhiNode("sum",region,add,a); + code._cfg.add(region); + assertTrue(BuildLRG.run(0,new RegAlloc(code))); + } + + private static class CallOp extends CallNode implements MachNode { + CallOp(Node ctrl, Node def) { super(null,ctrl,null,def,def); } + @Override public RegMask regmap(int i) { return i==2 ? A : i==3 ? B : null; } + @Override public RegMask outregmap() { return null; } + @Override public void encoding(Encoding enc) { throw new AssertionError(); } + } + + public static void popularUses() { + for( int calls=0; calls<=2; calls++ ) { + CodeGen code = graph(); + RegAlloc alloc = new RegAlloc(code); + RegionNode block = new RegionNode(null,code._start,code._start); + Op def = new Op(new RegMask(7L),null,null,false,block); + LRG lrg = alloc.newLRG(def,null); + lrg._machDef = def; + lrg._mask = new RegMask(0L); + Op ab = new Op(null,new RegMask(3L),null,false,block,def); + Op bc = new Op(null,new RegMask(6L),null,false,block,def); + Op a = new Op(null,A,null,false,block,def,def); + Op b = new Op(null,B,null,false,block,def); + Op dep = new Op(null,null,null,false,block,def); + CallOp call = calls>0 ? new CallOp(block,def) : null; + if( calls==2 ) new CallOp(block,def); + boolean split = alloc.splitEmptyMaskByUse((byte)0,lrg); + assertEquals(calls<2,split); + if( !split ) { assertSame(def,a.in(1)); assertSame(def,b.in(1)); continue; } + // Two shared copies, even with overlapping masks and repeated inputs. + assertTrue(a.in(1) instanceof SplitNode); + assertTrue(b.in(1) instanceof SplitNode); + assertNotSame(a.in(1),b.in(1)); + assertSame(a.in(1),a.in(2)); + assertSame(b.in(1),ab.in(1)); + assertSame(b.in(1),bc.in(1)); + assertSame(def,dep.in(1)); // A scheduling dependency has no register. + assertEquals(3,def.nOuts()); // Two copies and the dependency. + if( call!=null ) { + assertSame(a.in(1),call.in(2)); + assertSame(b.in(1),call.in(3)); + } + } + } + + // Check the scheduled graph before encoding adds untyped branches or rewrites tail calls. + public static class CheckedCodeGen extends CodeGen { + public CheckedCodeGen(String src) { super(src); } + public CheckedCodeGen(String modDir, String buildDir, com.seaofnodes.simple.util.Ary externPaths, + String srcName, String src, long seed, boolean reset, TypeInteger arg) { + super(modDir,buildDir,externPaths,srcName,src,seed,reset,arg); + } + @Override public CodeGen regAlloc() { + super.regAlloc(); + checkRegisters(this); + com.seaofnodes.simple.SpillStats.record25(this); + return this; + } + } + + public static void topPhi() { + // Either arm can be the synthetic exit. Both predecessors share a + // block here so making TOP live would conflict with the real value. + for( int slot=1; slot<=2; slot++ ) { + CodeGen code = graph(); + RegAlloc alloc = new RegAlloc(code); + Op def = new Op(A,null,null,false,code._start); + Node top = new ConstantNode(com.seaofnodes.simple.type.Type.TOP); + RegionNode region = new RegionNode(null,code._start,code._start); + PhiNode phi = new PhiNode("value",region,slot==1 ? top : def,slot==2 ? top : def); + phi._type = TypeInteger.BOT; + new Op(null,A,null,false,region,phi); + code._cfg.add(region); + assertTrue(BuildLRG.run(0,alloc)); + assertNull(alloc.lrg(top)); + assertSame(alloc.lrg(def),alloc.lrg(phi)); + assertTrue(IFG.build(0,alloc)); + alloc.insertBefore(phi,slot,"test",(byte)0,alloc.lrg(phi)); + assertSame(top,phi.in(slot)); + } + } + + public static void checkRegisters(CodeGen code) { + assertTrue(code._regAlloc.verifyFunctionLocalEdges()); + for( CFGNode bb : code._cfg ) + for( Node n : bb._outputs ) { + if( n instanceof MachNode mach ) { + if( code._regAlloc.regnum(n)>=0 && mach.outregmap()!=null ) + assertTrue(n.getClass().getSimpleName()+"#"+n._nid,code._regAlloc.regnum(n)>=0 && mach.outregmap().test(code._regAlloc.regnum(n))); + for( int i=n instanceof ParmNode ? 2 : 1; i=0 && mach.regmap(i)!=null ) + assertTrue(n.getClass().getSimpleName()+"#"+n._nid+" input "+i,code._regAlloc.regnum(n.in(i))>=0 && mach.regmap(i).test(code._regAlloc.regnum(n.in(i)))); + if( mach.twoAddress()!=0 ) + assertEquals(code._regAlloc.regnum(n),code._regAlloc.regnum(n.in(mach.twoAddress()))); + } + if( n instanceof PhiNode && code._regAlloc.regnum(n)>=0 ) + for( int i=n instanceof ParmNode ? 2 : 1; i=3 ) { + from.addNeighbor(left); left.addNeighbor(from); + if( kind==3 ) { to.addNeighbor(right); right.addNeighbor(to); } + else { to.addNeighbor(left); left.addNeighbor(to); } + } + int fcnt=from.nadj(), tcnt=to.nadj(); + assertTrue(Coalesce.coalesce(0,alloc)); + if( kind==0 || kind==4 ) { + assertSame(def,use.in(1)); + assertSame(alloc.lrg(def),alloc.lrg(copy)); + assertSame(use,alloc.lrg(def)._machUse); + assertEquals(1,alloc.lrg(def)._uidx); + if( kind==4 ) { + assertEquals(1,left.nadj()); + assertSame(alloc.lrg(def),left._adj.at(0)); + assertSame(left,alloc.lrg(def)._adj.at(0)); + } + } else { + assertSame(copy,use.in(1)); + assertNotSame(alloc.lrg(def),alloc.lrg(copy)); + assertEquals(fcnt,from.nadj()); + assertEquals(tcnt,to.nadj()); + } + } + } + +} -- cgit v1.2.3