diff options
| author | Cliff Click <cliffc@acm.org> | 2026-10-26 18:42:42 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-10-26 18:42:42 -0700 |
| commit | b3ec548422f6ec63f40e17cdca72f86927aa4f4e (patch) | |
| tree | 4ef9023f79d31f435a8058b3d52e3944924a8033 /chapter28b/src/test/java/com/seaofnodes | |
| parent | 0e7c50b22d7f900b9c3c8db3765f3b7af41e5dd1 (diff) | |
| download | Simple-b3ec548422f6ec63f40e17cdca72f86927aa4f4e.tar.gz Simple-b3ec548422f6ec63f40e17cdca72f86927aa4f4e.zip | |
More incremental loading
and compilation.
Diffstat (limited to 'chapter28b/src/test/java/com/seaofnodes')
9 files changed, 270 insertions, 134 deletions
diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter25Test.java index 31a616b5..dbb2a5ca 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter25Test.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter25Test.java @@ -198,7 +198,8 @@ public class Chapter25Test { new CodeGen(root.toString(),root.toString(),imported ? paths : null,"Main",src,126,true,TypeInteger.BOT) .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); assertEquals("",TestC.gcc(root+"/Main.o","",TestC.C_DRIVERS_DIR+"extern_data_import.c", - (String)null,new Ary<>(new String[]{dir+"/Globals.o"}),root+"/program"+(TestC.OS.startsWith("Windows") ? ".exe" : ""))); + (String)null,new Ary<>(new String[]{imported ? dir+"/Globals.o" : root+"/Main/Globals.o"}), + root+"/program"+(TestC.OS.startsWith("Windows") ? ".exe" : ""))); } } @@ -389,8 +390,9 @@ public class Chapter25Test { // Elf files are sane // Sys depends on io, libc, char, collections, and array utilities. - assertArrayEquals(new String[]{"sys", "sys/adt", "sys/adt/bitset", "sys/ary", + assertArrayEquals(new String[]{"sys/adt", "sys/ary", "sys/aryi64", "sys/aryu8", "sys/char", "sys/io", "sys/libc", "sys/scan"},sys_elf._deps); + assertArrayEquals(new String[]{"sys"},sys_elf._units); assertSame("class:sys" ,sys_elf._clz._name); } @@ -422,7 +424,7 @@ public class Chapter25Test { PrintStream saved = System.out; try( PrintStream capture = new PrintStream(output,true,StandardCharsets.UTF_8) ) { System.setOut(capture); - Simple.main(new String[]{"-L",SYS_FILE.toString(),"-o","build/objs/helloWorldDriverLibFile/", + Simple.main(new String[]{"-L",SYS_BLDDIR,"-o","build/objs/helloWorldDriverLibFile/", "src/test/smp/examples/A_helloWorld.smp"}); } finally { System.setOut(saved); @@ -444,8 +446,7 @@ public class Chapter25Test { String syms = TestC.exec(TestC.TEST_TIMEOUT_SECONDS,"nm",obj); assertTrue(syms, syms.contains(" U sys.io.p_noInline")); - TestC.linkExe(obj,null,null,new Ary<>(new String[]{SYS_FILE.toString()}),exe); - String rez = TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe); + String rez = TestC.gcc(code,null,null,null,exe); assertEquals(expected,rez); } diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter28bTest.java b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter28bTest.java index fb418b9a..e0424573 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/Chapter28bTest.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/Chapter28bTest.java @@ -1,23 +1,5 @@ package com.seaofnodes.simple; -// -//import com.seaofnodes.simple.codegen.CodeGen; -//import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; -//import com.seaofnodes.simple.codegen.ElfReader; -//import com.seaofnodes.simple.codegen.ParseAll; -//import com.seaofnodes.simple.node.CallNode; -//import com.seaofnodes.simple.node.FunNode; -//import com.seaofnodes.simple.node.LoopNode; -//import com.seaofnodes.simple.type.TypeInteger; -//import com.seaofnodes.simple.util.Ary; -//import java.io.File; -//import java.io.FileWriter; -//import java.io.IOException; -//import java.io.ByteArrayOutputStream; -//import java.io.PrintStream; -//import java.nio.charset.StandardCharsets; -//import java.nio.file.Files; -//import java.nio.file.Path; import com.seaofnodes.simple.codegen.CodeGen; import org.junit.Ignore; import org.junit.Test; @@ -39,9 +21,9 @@ struct C { }; return new C(arg).x; """; - CodeGen code = new CodeGen(src).driver(CodeGen.Phase.Encoding,"riscv","SystemV"); - assertEquals("return (arg+3);", code.print()); - assertEquals("3",Eval2.eval(code,0)); + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("Stop[ return Phi(Loop,0,Phi(Region,1,Phi_x)); return MEM[ 2:___ 3:.x=Phi(Loop,0,Phi(Region,1,Phi_x));]; return MEM[ 2:___ 3:.x=0;]; return Test.C; ]", code.print()); + assertEquals("1",Eval2.eval(code,3)); } } diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java b/chapter28b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java index 47e10f97..7f5fcd38 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java @@ -443,7 +443,7 @@ public class ClassInitDependenciesTest { "Main",src,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); assertNotNull(code._stop.walk(n -> n instanceof CLInitNode ? n : null)); assertEquals("0",Eval2.eval(code,0)); - assertNull(code._stop.walk(n -> n instanceof CLInitNode ? n : null)); + assertNotNull("Eval executes CLInit without lowering it",code._stop.walk(n -> n instanceof CLInitNode ? n : null)); assertEquals("134",Eval2.eval(code,1)); } @@ -468,8 +468,13 @@ public class ClassInitDependenciesTest { @Test public void testParentInitializationExecution() throws Exception { Path dir=tempDir("execution-"); object(dir,dir,"State","int count=0;"); - module(dir,dir,"P",Map.of("P","val C=C; State.count=State.count*10+1;", - "P/C","State.count=State.count*10+2; int value=arg;")); + CodeGen library = module(dir,dir,"P",Map.of("P","val C=C; State.count=State.count*10+1;", + "P/C","State.count=State.count*10+2; int value=arg; return State.count;")); + // Direct entry into the child has no surrounding CLInit call/check. + // Its body must establish the parent itself, including an imported parent. + CodeGen child = new CodeGen(library._modDir,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "P/C",null,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertEquals("12",Eval2.eval(child,99)); for( long seed : new long[]{1,123,987654321} ) { CodeGen code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), "Main","return P.C.value+State.count;",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); @@ -484,7 +489,8 @@ public class ClassInitDependenciesTest { """); String exe=dir.resolve(TestC.OS.startsWith("Windows") ? "check.exe" : "check").toString(); TestC.linkExe(dir.resolve("Main.o").toString(),TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", - cfile.toString(),new Ary<>(new String[]{dir.resolve("P.o").toString(),dir.resolve("State.o").toString()}),exe); + cfile.toString(),new Ary<>(new String[]{dir.resolve("P.o").toString(),dir.resolve("P/C.o").toString(), + dir.resolve("State.o").toString()}),exe); assertEquals("",TestC.exec(exe)); } diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Eval2.java b/chapter28b/src/test/java/com/seaofnodes/simple/Eval2.java index 901a8990..fc87c13a 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/Eval2.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/Eval2.java @@ -1,9 +1,10 @@ package com.seaofnodes.simple; -import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.print.IRPrinter; import com.seaofnodes.simple.type.*; +import com.seaofnodes.simple.util.Ary; import com.seaofnodes.simple.util.SB; import com.seaofnodes.simple.util.Utils; import java.util.*; @@ -11,7 +12,7 @@ import java.util.*; public abstract class Eval2 { // A mapping from Node ID to evaluation data Object. - // Data Objsts are a boxed Long or Double; or an Object[] of fields, which + // Data Objects are a boxed Long or Double; or an Object[] of fields, which // themselves are just more Objects. private static class Frame { private static int UID=1; @@ -77,31 +78,24 @@ public abstract class Eval2 { } } - // Classic Closure; an environment (stack of frames) and an execution point - private static class Closure { - // Where to return too; either a CallEnd or a Function + // A Classic Continuation; the suspended caller and the control point to + // resume after a return. + private static class Continuation { + // Resume a CallEnd or a CLInit's successor, or exit at Stop. final CFGNode _cc; - // Stack frame at the execution point. Includes Closures which + // Stack frame at the execution point. Includes Continuations which // recursively include other stack frames - private Frame _frame; + final Frame _frame; - Closure(CFGNode cc, Frame frame) { - _cc = cc; // One of Fun, CallEnd or Stop (null) + Continuation(CFGNode cc, Frame frame) { + _cc = cc; _frame = frame; assert _frame._refcnt >=0; _frame._refcnt++; } - FunNode fun() { return (FunNode)_cc; } - CallEndNode cend() { return (CallEndNode)_cc; } - boolean isStop() { return _cc instanceof StopNode; } // or Stop - - // - void pushFrame() { - _frame = new Frame(_frame); - _frame._refcnt++; - } + boolean isStop() { return _cc instanceof StopNode; } @Override public String toString() { return _cc.toString(); @@ -119,12 +113,15 @@ public abstract class Eval2 { public static String eval( CodeGen code, long arg ) { return eval(code,arg,1000); } public static String eval( CodeGen code, long arg, int timeout ) { + code.driver(CodeGen.Phase.TypeCheck); if( code._start.uctrl()==null ) return ""; // The empty program SB trace = null; // new SB(); // TRACE, set to null for off, new SB() for on - // Force local scheduling phase - code.driver(CodeGen.Phase.LocalSched); // Set global, so don't have to pass everywhere CODE = code; + // Eval uses checked ideal IR, including CLInit, and does not run Lower. + // Objects loaded later are scheduled independently of this graph. + if( code._phase.ordinal()<CodeGen.Phase.Schedule.ordinal() ) code.scheduleForEval(); + code.driver(CodeGen.Phase.LocalSched); CLASSES.clear(); // Timer for when we run too long. Ticks once per backedge or function call @@ -134,18 +131,12 @@ public abstract class Eval2 { // fields, which themselves are just more Objects. F = new Frame(null); - // Start at the START - CFGNode BB = code._start, prior = null; - // Return from main hits Stop - F.put(BB,new Closure(code._stop,F)); - // For all <clinit> / Clazzes, install their uninitialized memory - for( FunNode fun : code._linker ) - if( fun!=null && fun.isClz() ) { - TypeStruct clz = ((TypeMemPtr)fun.sig().arg(0))._obj; - // Map from the <clinit> self parameter to the - // singleton class structure. - F.put(fun.parm(2),classObject(clz)); - } + FunNode entry = (FunNode)code._start.uctrl(); + VALUE_CACHE.setX(0,"$mem"); + VALUE_CACHE.setX(1,classObject(((TypeMemPtr)entry.sig().arg(0))._obj)); + VALUE_CACHE.setX(2,arg); + CFGNode BB = enter(entry,code._stop); + CFGNode prior = null; // ------------ @@ -158,7 +149,7 @@ public abstract class Eval2 { // Parallel assign Phis int i = BB instanceof RegionNode r - ? parallelAssignPhis(r,prior,arg,trace) + ? parallelAssignPhis(r,prior,trace) : 0; // Compute the data nodes until BB control @@ -170,33 +161,38 @@ public abstract class Eval2 { prior = BB; BB = BB.uctrl(); // Next unique control target switch( prior ) { - case StartNode start: F = new Frame(F); break; // Frame for the call to main, as-if Called case CallNode call : BB = call(call); break; + case CLInitNode init: BB = call(init); break; case NeverNode never: BB = never.cproj(0); break; // Synthetic exit is never taken. case IfNode iff : Object p = val(iff.pred()); BB = iff.cproj( p==null || (p instanceof Long x && x==0L) ? 1 : 0); break; - case ReturnNode ret : if( clj(ret.rpc()).isStop() ) return exit(ret); else BB = ret(ret); break; + case ReturnNode ret : if( continuation(ret).isStop() ) return exit(ret); else BB = ret(ret); break; case FunNode fun : if( loopCnt++ > timeout ) return null; break; // Timeout case LoopNode loop : if( loopCnt++ > timeout ) return null; break; // Timeout case CallEndNode cend: break; case CProjNode cproj: break; case RegionNode r : break; - default: throw Utils.TODO(); + default: throw Utils.TODO("Eval control: "+prior.getClass().getSimpleName()); } if( BB==null ) return null; assert !BB.isDead(); } } - - // Phi cache; size is limited to number of Phis at a merge point - private static final Object[] PHICACHE = new Object[256]; - static int parallelAssignPhis(RegionNode r, CFGNode prior, long arg, SB trace) { + // Scratch values for parallel Phi assignment and call arguments. Each use + // is consumed before the next block executes; loading only schedules code. + private static final Ary<Object> VALUE_CACHE = new Ary<>(Object.class); + static int parallelAssignPhis(RegionNode r, CFGNode prior, SB trace) { + + // enter() assigned parameters, including the return continuation, + // from runtime arguments rather than the compiler's call graph. + if( r instanceof FunNode ) { + int i = 0; + while( i<r.nOuts() && r.out(i) instanceof ParmNode ) i++; + return i; + } // Compute input path on Phis for this Region int path = r._inputs.find( prior ); - // Parameters read from prior frame, Phis from local frame - Frame frame = (r instanceof FunNode ? F._prior : F); - boolean isClz = r instanceof FunNode fun && fun.isClz(); // Parallel assign Phis. First parallel read and cache int i; @@ -205,26 +201,11 @@ public abstract class Eval2 { // non-Phi is the end of all Phis. if( !(r.out(i) instanceof PhiNode phi) ) break; - // Parms read from Call args - - // - except RPC is the actual prior frame - // - except entry <clinit> takes the external program argument - Object val; - if( isClz && prior instanceof StartNode && phi instanceof ParmNode parm && parm._idx==3 ) - PHICACHE[i] = arg; // Reading the initial arguments to <clinit> - else { - Node n = phi instanceof ParmNode parm - // RPC uses the prior Call directly; - // Parms may not be linked, so read call args directly - ? (parm._idx==0 ? prior : prior.in(parm._idx)) - // Phis read from path input - : phi.in(path); - // Read the frame before overwriting - PHICACHE[i] = frame.get(n); - } + VALUE_CACHE.setX(i,F.get(phi.in(path))); } // Parallel assign; might assign before read, so read from cache for( int j=0; j < i; j++ ) - traceData(F.put0(r.out(j),PHICACHE[j]),trace); + traceData(F.put0(r.out(j),VALUE_CACHE.at(j)),trace); // Return point in basic block past last Phi; the Phis have been // evaluated here, and now we need to evaluate the rest of the block return i; @@ -233,27 +214,67 @@ public abstract class Eval2 { // Make a call. private static CFGNode call( CallNode call ) { - // Set up the Return PC and return frame (which is just the current frame). - // Really making a Continuation, but it's just a Closure - Closure cont = new Closure(call.cend(),F); - // Set the return continuation into the current frame RPC - F.put(call, cont ); - // Next Frame to use - F = new Frame(F); - // Target function and environment - return CODE.link(tfp(call.fptr())); + int nargs = call.nargs()+1; // Include memory. + for( int i = 0; i<nargs; i++ ) VALUE_CACHE.setX(i,val(call.in(i+1))); + return enter(CODE.link(tfp(call.fptr())),call.cend()); + } + + // Interpret the flag-test/call diamond that Lower would emit. The taken + // arm uses the ordinary call entry and RPC continuation. The initializer + // itself initializes its parent and sets its own $init flag. + private static CFGNode call(CLInitNode init) { + CompUnit cu = init.cu(); + //if( cu==null ) return init.uctrl(); + assert cu != null; // Post-optimizer the remaining CLInits refer to real classes + Object[] obj = classObject(cu._clz); + if( obj[cu._clz.find("$init")].equals(1L) ) + return init.uctrl(); // Class already initialized! + VALUE_CACHE.setX(0,"$mem"); + VALUE_CACHE.setX(1,obj); + VALUE_CACHE.setX(2,0L); + return enter(CODE.link(cu),init.uctrl()); + } + + + private static FunNode enter(FunNode fun, CFGNode continuation) { + // If we find a Summary, then this Fun's code never got loaded. + // Load it now. + if( fun.summary()!=null ) { + int fidx = fun.sig().fidx(); + // Load the matching .o, and all its CompUnits, and all the + // included FunNodes, including a replacement for this one. + ElfReader elf = SummaryNode.load(CODE,fun); + // Object files contain checked ideal IR, before scheduling. This is + // the first schedule for this object's bodies, even though the caller + // is already executing. Schedule only the new graph: revisiting the + // caller could change nodes whose values live in an active Frame. + // No optimization, CLInit lowering, or finalization. + ListScheduler.sched(GlobalCodeMotion.forEval(CODE,(StartNode)elf._nodes.at(0),(StopNode)elf._nodes.last())); + // Get the new replacement FunNode + fun = CODE.link(fidx); + } + Continuation cont = new Continuation(continuation,F); + F=new Frame(F); + for( Node use : fun._outputs ) + if( use instanceof ParmNode parm ) + F.put0(parm,parm._idx==0 ? cont : VALUE_CACHE.at(parm._idx-1)); + return fun; } private static CFGNode ret( ReturnNode ret ) { // Fetch return expression value and continuation from current frame Object rez = val(ret.expr()); - Closure cont = clj(ret.rpc()); + Continuation cont = continuation(ret); // Lower Frame reference count; might delete frame F.dec(); // Lower ref count // Install replacement Frame; set return result over the CallEnd F = cont._frame; - F.put0(cont._cc,rez); // Set return value into CallEnd - return cont._cc; // Return CallEnd as the new control + if( cont._cc instanceof CallEndNode ) F.put0(cont._cc,rez); + return cont._cc; // Resume after the call. + } + + private static Continuation continuation(ReturnNode ret) { + return (Continuation)val(ret.rpc()); } private static String exit( ReturnNode ret ) { @@ -329,8 +350,6 @@ public abstract class Eval2 { static TypeFunPtr tfp(Node n) { return (TypeFunPtr)F.get(n); } - // Fetch and unbox a closure - static Closure clj(Node n) { return (Closure)F.get(n); } // Java box ints and floats. Other things can just be null or some informative string private static Object con( Type t ) { diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java b/chapter28b/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java index 501392f3..3c53c004 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java @@ -42,23 +42,26 @@ public class IncrementalBuildTest { assertNotNull("Reparse A before loading its stale inlined body",code._compunits.get("App/A")._src); assertNotNull(code._compunits.get("App/B")._src); compile(src,obj,"App/Main"); - checkNative(obj,new Ary<>(new String[]{obj.resolve("App.o").toString()})); + checkNative(obj); Path clean=Files.createDirectory(root.resolve("clean")); assertEquals("9",Eval2.eval(client(src,clean),0)); compile(src,clean,"App/Main"); - checkNative(clean,null); + checkNative(clean); } - @Test public void testBundledSourceChangeAndCycle() throws Exception { + @Test public void testSourceChangeAndCycle() throws Exception { Path root=fixture("bundle-"); Path src=root.resolve("src"), obj=root.resolve("obj"); write(src,"App/A","val value={int n -> n ? B.value(n-1) : 1; };",1000); write(src,"App/B","val value={int n -> n ? A.value(n-1) : 5; };",1000); write(src,"App/Main","return A.value(1);",1000); compile(src,obj,"App/A"); + assertEquals("One build timestamp for the cycle",Files.getLastModifiedTime(obj.resolve("App/A.o")), + Files.getLastModifiedTime(obj.resolve("App/B.o"))); stamp(obj.resolve("App/A.o"),3000); - assertFalse("A owns both sides of the cycle",Files.exists(obj.resolve("App/B.o"))); + assertTrue("Each side of the cycle owns an object",Files.exists(obj.resolve("App/B.o"))); + stamp(obj.resolve("App/B.o"),3000); CodeGen code=client(src,obj); assertEquals("5",Eval2.eval(code,0)); assertNull(code._compunits.get("App/A")._src); @@ -125,13 +128,15 @@ public class IncrementalBuildTest { .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); } - private static void checkNative(Path obj, Ary<String> libs) throws Exception { + private static void checkNative(Path obj) throws Exception { Path driver=obj.resolve("driver.c"); Files.writeString(driver,""" __attribute__((CALL_CONV)) extern long long run(void) __asm__("App.Main.run"); int main(void) { return run()==9 ? 0 : 1; } """); String exe=obj.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString(); + Ary<String> libs = new Ary<>(new String[]{obj.resolve("App.o").toString(), + obj.resolve("App/A.o").toString(),obj.resolve("App/B.o").toString(),obj.resolve("App/C.o").toString()}); TestC.linkExe(obj.resolve("App/Main.o").toString(), TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi",driver.toString(),libs,exe); assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java b/chapter28b/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java new file mode 100644 index 00000000..7d42f924 --- /dev/null +++ b/chapter28b/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java @@ -0,0 +1,113 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import java.io.IOException; +import java.nio.file.*; +import org.junit.Test; +import static org.junit.Assert.*; + +public class SelectiveLoadingTest { + @Test public void testCycleRefinementLoadsConditionHelper() throws Exception { + Path base = Path.of("build/objs/selective"); + Files.createDirectories(base); + Path root = Files.createTempDirectory(base,"cycle-"); + Path stubs = Files.createDirectory(root.resolve("stubs")); + Path lib = Files.createDirectory(root.resolve("lib")); + object(stubs,null,"A","int value=arg;"); + object(stubs,null,"B","int value=arg;"); + StringBuilder helper = new StringBuilder("val test={bool n -> "); + // Large parse, small final body: it will not request speculative loading. + for( int i = 0; i<80; i++ ) helper.append("val x").append(i).append("=n+0;"); + helper.append("return n;};"); + object(lib,null,"Predicate",helper.toString()); + Ary<String> inputs = new Ary<>(new String[]{stubs.toString(),lib.toString()}); + object(lib,inputs,"A","int value=1; if(Predicate.test(false)) value=B.value;"); + object(lib,inputs,"B","int value=A.value;"); + CodeGen code = new CodeGen(null,root.toString(),new Ary<>(new String[]{lib.toString()}), + "Main","return A.value;",123,true,TypeInteger.BOT).driver(CodeGen.Phase.FinalizeIR); + assertNotNull(function(code,"test").summary()); + assertNotNull("Summaries alone report a potential cycle",new ClassInitDependencies(code).check()); + code.driver(CodeGen.Phase.TypeCheck); + assertNull("The helper is outside A/B's apparent cycle",function(code,"test").summary()); + assertEquals("1",Eval2.eval(code,0)); + } + + private static void object(Path dir, Ary<String> paths, String name, String source) { + new CodeGen(null,dir.toString(),paths,name,source,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + } + + @Test public void testSummariesInliningAndStaticLink() throws Exception { + Path base = Path.of("build/objs/selective"); + Files.createDirectories(base); + Path root = Files.createTempDirectory(base,"library-"); + Path src = Files.createDirectory(root.resolve("src")); + Path lib = Files.createDirectory(root.resolve("lib")); + Path app = Files.createDirectory(root.resolve("app")); + write(src,"Lib","val api=Api; val work=Work; val unused=Unused;"); + write(src,"Lib/Api",""" + val p={int n -> Work.write_noInline(n);}; + val p_noInline={int n -> Work.write_noInline(n);}; + """); + write(src,"Lib/Work","val write_noInline={int n -> n+1;};"); + write(src,"Lib/Unused","val f={int n -> n*2;};"); + new CodeGen(src.toString(),lib.toString(),null,"Lib",null,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + for( String unit : new String[]{"Lib","Lib/Api","Lib/Work","Lib/Unused"} ) + assertTrue(unit,Files.isRegularFile(lib.resolve(unit+".o"))); + + Ary<String> paths = new Ary<>(new String[]{lib.toString()}); + CodeGen code = new CodeGen(null,app.toString(),paths,"Main", + "if(arg==0) return -1; return Lib.api.p_noInline(arg);",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertNotNull(function(code,"Lib.Api.p_noInline").summary()); + assertNotNull(function(code,"Lib.Work.write_noInline").summary()); + assertNotNull(function(code,"Lib.Unused.f").summary()); + assertEquals("-1",Eval2.eval(code,0)); + assertNotNull("An untaken branch does not load its target",function(code,"Lib.Api.p_noInline").summary()); + assertEquals("42",Eval2.eval(code,41)); + assertNull(function(code,"Lib.Work.write_noInline").summary()); + assertNotNull("Evaluation leaves unrelated objects unloaded",function(code,"Lib.Unused.f").summary()); + assertEquals("6",Eval2.eval(code,5)); + + code=new CodeGen(null,app.toString(),paths,"Main", + "val run={int n -> Lib.api.p(n);}; return run(arg);",123,true,TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); + assertNull("p requested Api's implementation",function(code,"Lib.Api.p").summary()); + assertNotNull("The remaining call needs a signature, not a body",function(code,"Lib.Work.write_noInline").summary()); + assertNotNull("A namespace reexport does not request IR",function(code,"Lib.Unused.f").summary()); + code.driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + Path driver = app.resolve("driver.c"); + Files.writeString(driver,""" + __attribute__((CALL_CONV)) extern long long run(long long); + int main(void) { return run(41)==42 ? 0 : 1; } + """); + String exe = app.resolve(TestC.OS.startsWith("Windows") ? "test.exe" : "test").toString(); + assertTrue("The compiler already resolved this transitive object", + TestC.linkObjs(code).contains(lib.resolve("Lib/Work.o").toRealPath().toString())); + assertEquals("",TestC.gcc(code, + TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi",driver.toString(),null,exe)); + + Files.delete(lib.resolve("Lib/Work.o")); + // Even an untaken branch requires a complete dependency closure for a + // fresh program. + try { + Eval2.eval(new CodeGen(null,app.toString(),paths,"Missing", + "if(arg==0) return -1; return Lib.api.p_noInline(arg);",123,true,TypeInteger.BOT),0); + fail("Missing transitive code must fail before evaluation"); + } catch( RuntimeException ex ) { assertTrue(ex.getMessage(),ex.getMessage().contains("Lib/Work")); } + } + + private static FunNode function(CodeGen code, String name) { + for( FunNode fun : code._linker ) if( fun!=null && name.equals(fun._name) ) return fun; + throw new AssertionError("Missing function "+name); + } + + private static void write(Path src, String unit, String text) throws IOException { + Path file = src.resolve(unit+".smp"); + Files.createDirectories(file.getParent()); + Files.writeString(file,text); + } +} diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java b/chapter28b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java index 4576bc94..4e8a0417 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java @@ -30,6 +30,7 @@ public class SeparateCompilationTest { CodeGen code=new CodeGen(null,null,new Ary<>(new String[]{lib.toString()}), "Client","return Callbacks.run(arg);",123,true,TypeInteger.BOT) .driver(CodeGen.Phase.Opto); + code.loadAllIR(); // This assertion inspects a body, not just its saved summary. assertNull(code._compunits.get("Callbacks")._src); ParmNode base=code._start.walk(n -> n instanceof ParmNode p && p._label.equals("base") ? p : null); assertNotNull(base); diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/Simple.java b/chapter28b/src/test/java/com/seaofnodes/simple/Simple.java index 7cfdf57d..1f1e8ab0 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/Simple.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/Simple.java @@ -257,7 +257,7 @@ Options: if ( !TestC.CPU_PORT.equals( cpu ) || !TestC.CALL_CONVENTION.equals( abi ) ) throw bad("cannot run code on not native target"); String exe = TestC.OS.startsWith("Windows") ? outBase+".exe" : outBase; - String result = TestC.gcc(outBase+".o", null, null, "", TestC.linkObjs(externPaths), exe); + String result = TestC.gcc(code, null, null, "", exe); System.out.print(result); } } diff --git a/chapter28b/src/test/java/com/seaofnodes/simple/TestC.java b/chapter28b/src/test/java/com/seaofnodes/simple/TestC.java index b08be824..54adf3af 100644 --- a/chapter28b/src/test/java/com/seaofnodes/simple/TestC.java +++ b/chapter28b/src/test/java/com/seaofnodes/simple/TestC.java @@ -2,6 +2,8 @@ package com.seaofnodes.simple; import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.codegen.CompUnit; +import com.seaofnodes.simple.codegen.ElfReader; import com.seaofnodes.simple.type.TypeInteger; import com.seaofnodes.simple.util.Ary; import com.seaofnodes.simple.util.Utils; @@ -10,6 +12,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashSet; import java.util.concurrent.TimeUnit; import static org.junit.Assert.*; @@ -117,13 +120,12 @@ public abstract class TestC { // build/objs/foo.exe - linked executable Windows // build/objs/foo - linked executable Linux String pathBase = "build/objs/"+base; - String obj = pathBase+".o"; String exe = pathBase+(OS.startsWith("Windows") ? ".exe" : ""); // Compile simple, emit ELF CodeGen code = new CheckedCodeGen(null,"build/objs",externPaths,base,src,126L,true,TypeInteger.BOT); code.driver( CPU_PORT, simple_conv, false, cfile==null ); - linkExe(obj, c_conv, cfile, linkObjs(externPaths), exe); + linkExe(linkObjs(code), c_conv, cfile, exe); // Allocation quality not degraded SpillStats.checkSpills(spills,code._regAlloc._spillScaled); @@ -137,7 +139,7 @@ public abstract class TestC { public static String gcc( String main, double ignore, String... objs ) throws IOException { String BLDDIR = "build/objs/test0/"; String exe = BLDDIR+main+(OS.startsWith("Windows") ? ".exe" : ""); - linkExe(objs[0],null,null,null,exe); + linkExe(new Ary<>(objs),null,null,exe); // Module tests intentionally use the program exit code as their result. ExecResult result = execute(null,0,exe); return result.exit==0 ? result.out : "exec exit code: "+result.exit; @@ -160,7 +162,21 @@ public abstract class TestC { return execStdin(stdin,0,args); } - public static void linkExe( String obj, String c_conv, String cfile, Ary<String> linkObjs, String exe ) throws IOException { + // Immediate compile/link: the compiler already resolved every Simple object. + public static String gcc(CodeGen code, String c_conv, String cfile, String stdin, String... args) throws IOException { + linkExe(linkObjs(code),c_conv,cfile,args[0]); + return execStdin(stdin,0,args); + } + + // Object-only tests supply every link input explicitly. + public static void linkExe( String entry, String c_conv, String cfile, Ary<String> libraries, String exe ) throws IOException { + Ary<String> objects = new Ary<>(String.class); + objects.add(entry); + if( libraries!=null ) objects.addAll(libraries); + linkExe(objects,c_conv,cfile,exe); + } + + private static void linkExe(Ary<String> objects, String c_conv, String cfile, String exe) throws IOException { // Compile the C program. Compiling code and constants in the low // 2Gig. Pointers are 64b BUT since always in the low 2G all the // high bits are zero - and Simple code can be emitted treating @@ -169,18 +185,16 @@ public abstract class TestC { params.add("gcc"); params.add(OS.startsWith("Windows") ? "-Wl,--image-base,0x400000" : "-no-pie"); if( cfile!=null ) params.add(cfile); // Associated C driver, usually has a `main` - params.add(obj); + params.addAll(objects); // Standalone Simple programs export `simple_main`; the small C // runtime owns the native main symbol and converts argc/argv into a // normal Simple array before making the ABI-qualified call. if( cfile==null ) params.add(runtimeObject()); - if( linkObjs != null ) - params.addAll(linkObjs.asAry()); params.addAll(new String[] { "-lm", // Picks up 'sqrt' for newtonFloat tests to compare - "-g", - "-o", - exe, + "-g", // debug symbols + "-o", // name the output file + exe, // name the output file }); // Calling convention for C calls, if any if( cfile!=null ) { @@ -254,20 +268,15 @@ public abstract class TestC { } } - static Ary<String> linkObjs(Ary<String> externPaths) { - if( externPaths == null ) - return null; - Ary<String> objs = new Ary<>(String.class); - for( String path : externPaths ) { - File file = new File(path); - if( file.isFile() && file.getName().endsWith(".o") ) - objs.add(file.toString()); - else if( file.isDirectory() ) { - File sys = new File(file,"sys.o"); - if( sys.isFile() ) - objs.add(sys.toString()); - } + // Gather all CompUnit files and all externally named symbols + static Ary<String> linkObjs(CodeGen code) throws IOException { + Ary<String> objNames = new Ary<>(String.class); + for( CompUnit cu : code._compunits.values() ) { + File obj = cu._obj; + if( obj==null && cu._src!=null ) // Source supplied as a string. + obj = new File(code._buildDir+"/"+(code._srcName==null ? "Test" : code._srcName)+".o"); + if( obj!=null ) objNames.add(obj.getCanonicalPath()); } - return objs.isEmpty() ? null : objs; + return objNames; } } |
