diff options
Diffstat (limited to 'src/test')
32 files changed, 1794 insertions, 504 deletions
diff --git a/src/test/java/com/seaofnodes/simple/BoundaryTest.java b/src/test/java/com/seaofnodes/simple/BoundaryTest.java index 68d8175f..b736ccb3 100644 --- a/src/test/java/com/seaofnodes/simple/BoundaryTest.java +++ b/src/test/java/com/seaofnodes/simple/BoundaryTest.java @@ -75,14 +75,9 @@ public class BoundaryTest { } } - @Test public void testLibrarySource() { - CodeGen code=new CodeGen("return sys.char.toupper('a');").driver(CodeGen.Phase.TypeCheck); - assertEquals("65",Eval2.eval(code,0)); - } - @Test public void testLazyTables() { for( long seed : new long[]{1,123,987654321} ) { - CodeGen code=new CodeGen("src/test/smp/lazy","build/objs","App/Main",null,seed,true,TypeInteger.BOT) + CodeGen code=new CodeGen("src/test/smp/lazy","build/objs",null,"App/Main",null,seed,true,TypeInteger.BOT) .driver(CodeGen.Phase.TypeCheck); assertEquals("0",Eval2.eval(code,0)); assertEquals("125",Eval2.eval(code,1)); diff --git a/src/test/java/com/seaofnodes/simple/CallEndTest.java b/src/test/java/com/seaofnodes/simple/CallEndTest.java new file mode 100644 index 00000000..a174bdf7 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/CallEndTest.java @@ -0,0 +1,85 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import org.junit.Test; +import static org.junit.Assert.*; + +public class CallEndTest { + @Test public void testReturnWideningMonotonicity() { + new CodeGen("return 0;").parse(); + TypeFunPtr sig = TypeFunPtr.make1((byte)2,true,TypeFunPtr.TEMPTY,TypeInteger.I32,100); + FunNode fun = new FunNode(null,sig,"f",null); + Node value = ConstantNode.raw(Type.TOP); + ReturnNode ret = new ReturnNode(ConstantNode.raw(Type.CONTROL), + ConstantNode.raw(TypeMem.START),value,null,fun); + Type[] inputs = new Type[]{Type.TOP,TypeInteger.TOP,TypeInteger.I32.dual(), + TypeInteger.ZERO,TypeInteger.constant(Long.MAX_VALUE), + TypeInteger.make(-1,1,(byte)0),TypeInteger.make(-1,1,(byte)1), + TypeInteger.make(-1,1,(byte)2),TypeInteger.make(-1,1,(byte)3), + TypeInteger.I32,TypeInteger.BOT,TypeScalar.BOT,Type.BOTTOM}; + Type[] outputs = new Type[inputs.length]; + for( int i=0; i<inputs.length; i++ ) { + value._type = inputs[i]; + outputs[i] = ret.compute(); + } + for( int i=0; i<inputs.length; i++ ) + for( int j=0; j<inputs.length; j++ ) + if( inputs[i].isa(inputs[j]) ) + assertTrue(inputs[i]+" -> "+inputs[j],outputs[i].isa(outputs[j])); + } + + // Growing the target set must not narrow the result, even when the linked + // return has exhausted its integer widening budget. + @Test public void testUnknownTargetMonotonicity() { + CodeGen code = new CodeGen("return 0;").parse(); + TypeFunPtr sig = TypeFunPtr.make1((byte)2,true,TypeFunPtr.TEMPTY,TypeInteger.BOT,100); + FunNode fun = new FunNode(null,sig,"f",null); + code._linker.setX(100,fun); + Node ctrl = ConstantNode.raw(Type.CONTROL); + Node mem = ConstantNode.raw(TypeMem.START); + Node value = ConstantNode.raw(TypeInteger.I32); + ReturnNode ret = new ReturnNode(ctrl,mem,value,null,fun); + ret._type = ret.compute(); + Node fptr = ConstantNode.raw(sig.makeFrom(TypeInteger.I32)); + CallNode call = new CallNode(null,ctrl,mem,fptr).init(); + CallEndNode end = new CallEndNode(call,TypeRPC.BOT); + end.addDef(ret); + Type linked = end.compute(); + TypeFunPtr unknown = TypeFunPtr.make(false,TypeFunPtr.TEMPTY,TypeInteger.I32); + assertTrue(fptr._type.isa(unknown)); + fptr._type = unknown; + assertTrue("Adding unknown targets must only lose precision",linked.isa(end.compute())); + } + + @Test public void testUnlinkedCallIsPhaseIndependent() { + CodeGen code = new CodeGen("return 0;").parse(); + TypeFunPtr sig = TypeFunPtr.make1((byte)2,true,TypeFunPtr.TEMPTY,TypeInteger.I32,100); + CallNode call = new CallNode(null,ConstantNode.raw(Type.CONTROL), + ConstantNode.raw(TypeMem.START),ConstantNode.raw(sig)).init(); + CallEndNode end = new CallEndNode(call,TypeRPC.BOT); + Type expected = TypeTuple.make(Type.CONTROL,TypeMem.BOT,TypeInteger.I32); + for( CodeGen.Phase phase : new CodeGen.Phase[]{CodeGen.Phase.Parse, + CodeGen.Phase.Opto,CodeGen.Phase.IterPostOpto} ) { + code._phase = phase; + assertSame(phase.toString(),expected,end.compute()); + } + } + + @Test public void testExternalTargets() { + CodeGen code = new CodeGen("return 0;").parse(); + code.externFunc(100,"first"); + code.externFunc(101,"second"); + TypeFunPtr sig = TypeFunPtr.make1((byte)2,true,TypeFunPtr.TEMPTY,TypeInteger.I32,100); + Node fptr = ConstantNode.raw(sig); + CallNode call = new CallNode(null,ConstantNode.raw(Type.CONTROL), + ConstantNode.raw(TypeMem.START),fptr).init(); + CallEndNode end = new CallEndNode(call,TypeRPC.BOT); + Type first = end.compute(); + fptr._type = sig.meet(sig.makeFrom(101)); + assertTrue(first.isa(end.compute())); + assertSame(TypeInteger.I32,((TypeTuple)end.compute()).ret()); + assertSame(Type.BOTTOM,((TypeMem)((TypeTuple)end.compute()).mem())._t); + } +} diff --git a/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/src/test/java/com/seaofnodes/simple/Chapter10Test.java index 39c7040e..6892db0c 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter10Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter10Test.java @@ -39,6 +39,7 @@ public class Chapter10Test { } + private static final String NULLABLE_POINT_SOURCE = """ struct Point { int x; new Point = { int v -> x = v; }; }; !Point?[] !points = new !Point?[2]; @@ -373,6 +374,7 @@ return ret.v0; } + @Test public void testModGlobalInConstructor() { CodeGen code = new CodeGen( diff --git a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java index 62dbbde4..4019fd38 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java +++ b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java @@ -300,7 +300,7 @@ A x = new A; for( String fields : new String[] { "!B !b; new A = { -> if (arg) b = new B; };", "!B !b; new A = { -> if (arg) return 0; b = new B; };", - "!B !b; new A = { !B !b -> b = new B; };", // Mutable argument shadows field + "!B !b; new A = { !B !b -> b = new B; };", // Argument shadows field "new A = { -> }; !B !b;", // Field declared later } ) { CodeGen code = new CodeGen("struct B {}; struct A { "+fields+" }; return 0;"); @@ -538,6 +538,7 @@ for(;;arg++;) {} } + // --------------------------------------------------------------- @Test public void testForward0() { diff --git a/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/src/test/java/com/seaofnodes/simple/Chapter20Test.java index cdfd16a7..1001e691 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter20Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter20Test.java @@ -216,7 +216,7 @@ for( int i=0; i<ary#-1; i++ ) return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6 """; testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);"); - testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));"); + testCPU(src,"riscv" , "SystemV", 9,"return (add,.[],(mul,.[],1000));"); testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));"); } @@ -256,9 +256,10 @@ val _hashCodeString = { !String self -> return hash; }; """; - testCPU(src,"x86_64_v2", "SystemV",12,null); - testCPU(src,"riscv" , "SystemV",6,null); - testCPU(src,"arm" , "SystemV",6,null); + // The implicit return exposes _hashCodeString, so retain its body too. + testCPU(src,"x86_64_v2", "SystemV",21,null); + testCPU(src,"riscv" , "SystemV",9,null); + testCPU(src,"arm" , "SystemV",7,null); } @Test diff --git a/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/src/test/java/com/seaofnodes/simple/Chapter21Test.java index 2fcf65e9..d5b41ac9 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter21Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -324,7 +324,7 @@ for( int i=0; i<ary#-1; i++ ) return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6 """; testCPU(src,"x86_64_v2", "SystemV",-1,"return mov(.[]);"); - testCPU(src,"riscv" , "SystemV", 7,"return (add,.[],(mul,.[],1000));"); + testCPU(src,"riscv" , "SystemV", 9,"return (add,.[],(mul,.[],1000));"); testCPU(src,"arm" , "SystemV", 5,"return (add,.[],(mul,.[],1000));"); } @@ -383,9 +383,10 @@ val _hashCodeString = { !String self -> return hash; }; """; - testCPU(src,"x86_64_v2", "SystemV",12,null); - testCPU(src,"riscv" , "SystemV",6,null); - testCPU(src,"arm" , "SystemV",6,null); + // The implicit return exposes _hashCodeString, so retain its body too. + testCPU(src,"x86_64_v2", "SystemV",21,null); + testCPU(src,"riscv" , "SystemV",9,null); + testCPU(src,"arm" , "SystemV",7,null); } @Test public void testStringExport() throws IOException { @@ -422,7 +423,7 @@ val _hashCodeString = { !String self -> return hash; }; """; - TestC.runC(src, "stringHash", "",12); + TestC.runC(src, "stringHash", "",21); } @Test public void testLoop2() { diff --git a/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/src/test/java/com/seaofnodes/simple/Chapter25Test.java new file mode 100644 index 00000000..f94f641b --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/Chapter25Test.java @@ -0,0 +1,548 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.isa.eval.EvalRisc5; + +import com.seaofnodes.isa.eval.EvalArm64; + +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 org.junit.Ignore; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class Chapter25Test { + + @Test public void testFlagSpills() throws IOException { + com.seaofnodes.simple.codegen.FlagSpillTestSupport.run(); + } + + @Ignore + @Test + public void testJig() { + String src = "{ i32 -> i32 } close = \"C\"; val main = { u8[] a -> if (a# > 1) return close(0); return a[0]; };"; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.Encoding,"riscv","SystemV"); + assertEquals("return (arg+3);", code.print()); + assertEquals("3",Eval2.eval(code,0)); + } + + @Test public void testConditionalClassInitX86() throws IOException { + Path build=Path.of("build/objs"); + Files.createDirectories(build); + Path dir=Files.createTempDirectory(build,"init_flags_"); + // Keep B separately compiled so its first-touch check survives. + new CodeGen(null,dir.toString(),null,"B","int value=17+arg;",123,true,TypeInteger.BOT) + .driver("x86_64_v2","win64",false,false); + // GCM can hoist the fused B.$init compare above the test of take. + // Its flags then cross another flag definition and need a legal split. + new CheckedCodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","val run={int take -> return take ? B.value : 0; };",123,true,TypeInteger.BOT) + .driver(CodeGen.Phase.Encoding,"x86_64_v2","win64"); + // Remove successful fixtures; preserve the object if compilation fails. + Files.delete(dir.resolve("B.o")); + Files.delete(dir); + } + + @Test public void testPublicInstanceMethod() { + String src = """ + struct Scan { + val visible = { u8 c -> c==255; }; + val _hidden = { u8 c -> c==1; }; + }; + struct _Private { + val hiddenClassMethod = { u8 c -> c==2; }; + }; + return new Scan.visible(44); + """; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.Parse); + FunNode visible = null; + int checked = 0; + for( FunNode fun : code._linker ) { + if( fun==null || fun._name==null ) continue; + if( fun._name.equals("visible") ) { + assertTrue(code.publicFIDX(fun.sig().fidx())); + visible = fun; + checked++; + } + if( fun._name.equals("_hidden") || fun._name.equals("hiddenClassMethod") ) { + assertFalse(code.publicFIDX(fun.sig().fidx())); + checked++; + } + } + assertEquals(3,checked); + code.driver(CodeGen.Phase.TypeCheck); + assertTrue(visible.unknownCallers()); + assertEquals(TypeInteger.U8,visible.parm(3)._type); + assertEquals("0",Eval2.eval(code,0)); + } + } + + @Test public void testNeverReturnPointerRiscV() { + new CodeGen(""" + struct _S { int x; }; + !_S !s = new _S; + if(arg) while(1) { s.x += arg; } + return s; + """).driver(CodeGen.Phase.Encoding,"riscv","SystemV"); + } + + @Test public void testInlineNeverExit() { + String[] sources={ + "val _spin={ -> while(1) {} return 1; }; _spin(); return 2;", + // Two callers require cloning, including the non-exiting arm. + "val _spin={int x -> if(x) while(1) {} return 7; }; return _spin(arg)+_spin(arg);" + }; + for( int i=0; i<sources.length; i++ ) { + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(sources[i],seed,true).driver(CodeGen.Phase.TypeCheck); + assertNull("Calls should inline",code._start.walk(n -> n instanceof CallNode ? n : null)); + assertNotNull("The non-exiting path must survive",code._start.walk(n -> n instanceof LoopNode && n!=code._start ? n : null)); + if( i==1 ) assertEquals("14",Eval2.eval(code,0,10)); + assertNull(Eval2.eval(code,1,10)); + } + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) + new CheckedCodeGen(sources[i]).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + } + + @Test + public void testExternData() throws IOException { + String src = """ + i32 counter="C"; + i8 small="C"; + f64 fraction="C"; + {int} bump="C"; + val test_global={ -> + val before=counter; + counter=before; + counter=++counter+1; + counter+=1; + val mid=bump(); + small++; + fraction=fraction+0.5; + return before*10000+mid*100+counter; + }; + """; + TestC.runC(src,"extern_data","",-1); + } + + @Test + public void testExternDataEncoding() { + for( String cpu : new String[]{"arm","riscv"} ) { + CodeGen code = new CheckedCodeGen("i32 counter=\"C\"; counter=counter+3; return counter;"); + // Exercise signed low-12 relocation arithmetic, past the first page. + int addr = 0x3800; + code._externDataAddresses.put("counter",addr); + code.driver(cpu,"SystemV",true,false); + byte[] image = new byte[1<<20]; + var bits = code._encoding._bits; + System.arraycopy(bits.buf(),0,image,0,bits.size()); + if( cpu.equals("arm") ) { + EvalArm64 arm = new EvalArm64(image,1<<16); + arm.st4(addr,-7); + assertEquals(0,arm.step(1000)); + assertEquals(-4,arm.regs[0]); + assertEquals(-4,arm.ld4s(addr)); + } else { + EvalRisc5 r5 = new EvalRisc5(image,1<<16); + r5.st4(addr,-7); + assertEquals(0,r5.step(1000)); + assertEquals(-4,r5.regs[10]); + assertEquals(-4,r5.ld4s(addr)); + } + } + } + + @Test + public void testExternDataImports() throws IOException { + Path dir = Files.createTempDirectory(Path.of("build/objs"),"extern_data_"); + String globals = "i32 counter=\"C\"; val read={ -> counter; };"; + CodeGen globalCode = new CodeGen(null,dir.toString(),null,"Globals",globals,126,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + var clz = globalCode.compunit()._clz; + int counter = clz.find("counter"); + assertTrue(clz._fields[counter]._extern); + assertEquals(-1,clz.offset(counter)); + assertEquals(clz.remove(counter).size(),clz.size()); + // Load the serialized declaration and ideal graph from Globals.o. + String src = """ + i32 counter="C"; + val test_global={ -> + val before=Globals.counter; + Globals.counter=before+1; + counter+=2; + return before*100+Globals.read(); + }; + """; + Ary<String> paths = new Ary<>(new String[]{dir.toString()}); + for( boolean imported : new boolean[]{true,false} ) { + Path root = dir.resolve(imported ? "binary" : "source"); + Files.createDirectories(root.resolve("Main")); + if( !imported ) Files.writeString(root.resolve("Main/Globals.smp"),globals); + 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[]{imported ? dir+"/Globals.o" : root+"/Main/Globals.o"}), + root+"/program"+(TestC.OS.startsWith("Windows") ? ".exe" : ""))); + } + } + + @Test public void testPrintingConstantPool() throws Exception { + com.seaofnodes.simple.codegen.PrintRegTestSupport.checkConstantPool(); + } + + private static final String SYS_BLDDIR = "build/objs/lib_"+TestC.CPU_ABI; + private static final File SYS_FILE = new File(SYS_BLDDIR+"/sys.o"); + + @Test + public void testForwardConstructor() { + CodeGen code = new CodeGen("src/test/java/com/seaofnodes/simple/test_smp/forward_ctor", + "build/objs/forward_ctor_parse",null, + "m",null,123L,true,TypeInteger.BOT); + code.driver(CodeGen.Phase.TypeCheck); + } + + @Test public void testTypeOnlyFileReference() throws IOException { + Path base = Path.of("build/objs"); + Files.createDirectories(base); + Path root = Files.createTempDirectory(base,"type-only-"); + Files.createDirectory(root.resolve("A")); + Files.writeString(root.resolve("A/B.smp"),"int x = 0;"); + // B declares only a class initializer. Using its name as a type + // must not publish a nonexistent instance initializer during Opto. + for( String src : new String[]{ + "B? b = null; return 0;", + "val readB = { B b -> 0; }; return 0;" + } ) { + Files.writeString(root.resolve("A.smp"),src); + CodeGen code = new CodeGen(root.toString(),root.resolve("obj").toString(),null, + "A",null,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertEquals("0",Eval2.eval(code,0)); + } + } + + + @Test + public void testPostfixFieldUpdate() { + String src = """ + struct V { + u32 !len = 0; + i64[] !buf; + new V = { i64[] b -> buf=b; }; + val grow = { i64 n -> self; }; + val add = { i64 x -> grow(1).buf[len++] = x; self; }; + }; + return new V(new i64[1]).add(7).len; + """; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } + + @Test + public void testStringEscapes() { + String src = "u8[~] s=\"A\\n\\t\\r\\\\\\\"B\"; " + + "return s#==7 && s[0]=='A' && s[1]==10 && s[2]==9 && s[3]==13 && " + + "s[4]==92 && s[5]==34 && s[6]=='B';"; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } + + @Test + public void testCharacterEscapes() { + CodeGen code = new CodeGen("return '\\n'==10 && '\\t'==9 && '\\r'==13;") + .driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } + + @Test + public void testFuzzerUnresolvedNumericLoop() { + CodeGen code = new CodeGen(""" + arg=0; + while(arg+0) + while(-arg) + arg=0; + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("return 0;",code.print()); + } + + @Test + public void testFuzzerBadLCA() { + new CodeGen(""" + int v0=arg; + if(v0&&--v0?0:arg) {} + """).driver(CodeGen.Phase.LocalSched); + } + + @Test @Ignore + public void testModule0() throws IOException { + String MODDIR = "src/test/java/com/seaofnodes/simple/test0"; + String BLDDIR = "build/objs/test0"; + String a_obj = BLDDIR+"/A.o"; + String ab_obj = BLDDIR+"/A/B.o"; + // Remove any prior results so the test runs from scratch. + delELFiles(new File(BLDDIR)); + writeB(MODDIR,5); + + // Compile MODDIR/A.smp into MODDIR/A.o + // Since A refers to B also: + // Compile MODDIR/A/B.smp into MODDIR/A/B.o + CodeGen code1 = new CodeGen(MODDIR, BLDDIR,null, + "A",null,123L,true,TypeInteger.BOT); + code1.driver(CodeGen.Phase.Export,TestC.CPU_PORT,TestC.CALL_CONVENTION); + + // Verify produces A.o, A/B.o + File a_file = new File( a_obj); + File ab_file = new File(ab_obj); + assertTrue( a_file.exists() ); + assertTrue( ab_file.exists() ); + long a_msec1 = a_file.lastModified(); + long ab_msec1 = ab_file.lastModified(); + + // Link and execute: arg is true, so compute "5+1" as the exit code + String rez1 = TestC.gcc("A", 1.2, a_obj, ab_obj); + assertEquals("exec exit code: 6",rez1); + + // Compile again A, expecting both A.o and A/B.o to be up-to-date and not compiled + CodeGen code2 = new CodeGen(MODDIR, BLDDIR, null, + "A",null,123L,true,TypeInteger.BOT); + code2.driver(CodeGen.Phase.Export,TestC.CPU_PORT,TestC.CALL_CONVENTION); + + assertTrue( a_file.exists() ); + assertTrue( ab_file.exists() ); + long a_msec2 = a_file.lastModified(); + long ab_msec2 = ab_file.lastModified(); + + assertEquals( a_msec1, a_msec2 ); + assertEquals( ab_msec1, ab_msec2 ); + + // Touch A.smp and recompile. A/B.o should not recompile. + new File(MODDIR+"/A.smp").setLastModified(System.currentTimeMillis()); + CodeGen code3 = new CodeGen(MODDIR, BLDDIR,null, + "A",null,123L,true,TypeInteger.BOT); + code3.driver(CodeGen.Phase.Export,TestC.CPU_PORT,TestC.CALL_CONVENTION); + + long a_msec3 = a_file.lastModified(); + long ab_msec3 = ab_file.lastModified(); + assertTrue ( a_msec1 < a_msec3 ); + assertEquals( ab_msec1, ab_msec3 ); + + // Link and execute: arg is true, so compute "5+1" as the exit code + String rez3 = TestC.gcc("A", 1.2, a_obj, ab_obj); + assertEquals("exec exit code: 6",rez3); + + + // Modify B.smp and recompile A/B.o; it should recompile and A.o should not. + writeB(MODDIR,7); + CodeGen code4 = new CodeGen(MODDIR, BLDDIR,null, + "A/B",null,123L,true,TypeInteger.BOT); + code4.driver(CodeGen.Phase.Export,TestC.CPU_PORT,TestC.CALL_CONVENTION); + + long a_msec4 = a_file.lastModified(); + long ab_msec4 = ab_file.lastModified(); + assertEquals( a_msec3, a_msec4 ); + assertTrue ( ab_msec3 < ab_msec4 ); + + // Link and execute: uses stale A.o, so remains '6' not '7+1' == 8 + String rez4 = TestC.gcc("A", 1.2, a_obj, ab_obj); + assertEquals("exec exit code: 6",rez4); + + // Recompile A.o, it should recompile despite not being touched because + // it depends on A/B.o which recompiled in the prior step. + CodeGen code5 = new CodeGen(MODDIR, BLDDIR,null, + "A",null,123L,true,TypeInteger.BOT); + code5.driver(CodeGen.Phase.Export,TestC.CPU_PORT,TestC.CALL_CONVENTION); + + long a_msec5 = a_file.lastModified(); + long ab_msec5 = ab_file.lastModified(); + assertTrue ( a_msec4 < a_msec5 ); + assertEquals( ab_msec4, ab_msec5 ); + + // Link and execute: updates A.o from B.o inlining, without compiling B.o + String rez5 = TestC.gcc("A", 1.2, a_obj, ab_obj); + assertEquals("exec exit code: 8",rez5); + // Reset for next time + writeB(MODDIR,5); + } + + private void writeB(String MODDIR, int x) throws IOException { + var bsmp = new FileWriter(MODDIR+"/A/B.smp"); + bsmp.write("val x="+x+";\n"); + bsmp.close(); + } + + + // Recursive search (TODO: gzip, archives) and delete all .o files + private void delELFiles( File dir) { + if( dir.isDirectory() ) + for( File f : dir.listFiles() ) + delELFiles(f); + else if( dir.getName().endsWith(".o") ) + dir.delete(); + } + + + @Test + public void testSys() { + assertTrue("Missing "+SYS_FILE+"; run make tests_sys", SYS_FILE.exists()); + + // Can read the ELF files + CodeGen code1 = new CodeGen("return 0;"); + ElfReader sys_elf = ElfReader.load(SYS_FILE, null); + sys_elf.loadPublicTypes(code1); + + // Elf files are sane + + // Sys depends on io, libc, char, collections, and array utilities. + 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); + } + + @Test + public void testConstantClassInitialization() throws IOException { + TestC.runSF("val x = 42; return 0;","constantClassInt","",0); + TestC.runSF("val f = { -> 7; }; return 0;","constantClassFunction","",0); + } + + @Test + public void testHelloWorld() throws IOException { + String expected = "Hello, World!\n"; + String prog = "return sys.io.p(\""+expected+"\") - "+expected.length()+";"; + // tests_raw1 concurrently builds Chapter22Test's helloWorld executable. + TestC.run(prog,"helloWorldSys",new Ary<>(new String[]{SYS_BLDDIR}), + TestC.CALL_CONVENTION, null, null, expected,0); + } + + @Test + public void testHelloWorldDriver() throws Exception { + String out = "build/objs/helloWorldDriver/"; + Simple.main(new String[]{"-L",SYS_BLDDIR,"-o",out,"--norun","src/test/smp/examples/A_helloWorld.smp"}); + assertTrue(Files.isRegularFile(Path.of(out,"A_helloWorld.o"))); + } + + @Test + public void testHelloWorldDriverLibFile() throws Exception { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + PrintStream saved = System.out; + try( PrintStream capture = new PrintStream(output,true,StandardCharsets.UTF_8) ) { + System.setOut(capture); + Simple.main(new String[]{"-L",SYS_BLDDIR,"-o","build/objs/helloWorldDriverLibFile/", + "src/test/smp/examples/A_helloWorld.smp"}); + } finally { + System.setOut(saved); + } + assertEquals("Hello, World!\n",output.toString(StandardCharsets.UTF_8)); + } + + @Test + public void testHelloWorldNoInline() throws Exception { + String base = "helloWorldNoInline"; + String expected = "Hello, World!\n"; + String prog = "return sys.io.p_noInline(\""+expected+"\") - "+expected.length()+";"; + CodeGen code = new CheckedCodeGen(null,"build/objs",new Ary<>(new String[]{SYS_BLDDIR}), + base,prog,123L,true,TypeInteger.BOT); + code.driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,true); + + String obj = "build/objs/"+base+".o"; + String exe = "build/objs/"+base+(TestC.OS.startsWith("Windows") ? ".exe" : ""); + String syms = TestC.exec(TestC.TEST_TIMEOUT_SECONDS,"nm",obj); + assertTrue(syms, syms.contains(" U sys.io.p_noInline")); + + String rez = TestC.gcc(code,null,null,null,exe); + assertEquals(expected,rez); + } + + @Test + public void testRedirectedRead() throws IOException { + String src = """ +u8[] buf = new u8[10]; +i64 ptr = buf; +int rez = sys.libc.read(0,ptr,buf#); +return rez < buf# ? 0 : sys.libc._exit(-2); +"""; + TestC.run(src,"redirectedRead",new Ary<>(new String[]{SYS_BLDDIR}), + TestC.CALL_CONVENTION, null, null, + "abc", "", -1); + } + + @Test + public void testErrnoAccessor() throws IOException { + String src = "val status=sys.libc.close(-1); return status != -1 || sys.libc.errno()==0;"; + TestC.run(src,"errnoAccessor",new Ary<>(new String[]{SYS_BLDDIR}), + TestC.CALL_CONVENTION,null,null,"",-1); + } + + @Test + public void testBubbles() throws IOException { + String src = Files.readString( Path.of("src/test/smp/examples/BubbleSort.smp")); + String exe = TestC.compile(src,"BubbleSort",new Ary<>(new String[]{SYS_BLDDIR}), + TestC.CALL_CONVENTION,null,null,-1); + assertEquals("[-17, 2, 3, 999]\n", + TestC.exec(exe,"[3, 2,-17, 999 ]")); + assertEquals("[1, 2, 3, 4, 4, 5]\n", + TestC.exec(exe,"[4, 5, 3, 1, 4, 2]")); + assertEquals("[1, 2, 3, 4, 5]\n", + TestC.exec(exe,"[1, 2, 3, 4, 5]")); + assertEquals("[1, 2, 3, 4, 5, 6, 7, 8, 9]\n", + TestC.exec(exe,"[9, 8, 7, 6, 5, 4, 3, 2, 1]")); + + String usage = "Usage: please provide a list of at least two integers to sort in the format \"[1, 2, 3, 4, 5]\"\n"; + assertEquals(usage,TestC.exec(exe)); + assertEquals(usage,TestC.exec(exe,"")); + assertEquals(usage,TestC.exec(exe,"[1]")); + assertEquals(usage,TestC.exec(exe,"[4 5 3]")); + } + + @Test + public void testCapitalize() throws IOException { + String src = Files.readString(Path.of("src/test/smp/examples/Capitalize.smp")); + String exe = TestC.compile(src,"Capitalize",new Ary<>(new String[]{SYS_BLDDIR}), + TestC.CALL_CONVENTION,null,null,-1); + assertEquals("Hello world\n",TestC.exec(exe,"hello world")); + assertEquals("Hello World\n",TestC.exec(exe,"Hello World")); + assertEquals("123 apples\n",TestC.exec(exe,"123 apples")); + assertEquals("Usage: please provide a string\n",TestC.exec(exe)); + assertEquals("Usage: please provide a string\n",TestC.exec(exe,"")); + assertEquals("Use quotes around multiple strings.\n",TestC.exec(exe,"hello","world")); + } + + @Test + public void testDijkstra() throws IOException { + String src = Files.readString(Path.of("src/test/smp/examples/Dijkstra.smp")); + String matrix = "[0, 2, 0, 6, 0, 2, 0, 3, 8, 5, 0, 3, 0, 0, 7, 6, 8, 0, 0, 9, 0, 5, 7, 9, 0]"; + String exe = TestC.compile(src,"Dijkstra",new Ary<>(new String[]{SYS_BLDDIR}), + TestC.CALL_CONVENTION,null,null,-1); + assertEquals("2\n",TestC.exec(exe,matrix,"0","1")); + String usage = "Usage: please provide three inputs: a serialized matrix, a source node and a destination node\n"; + assertEquals("7\n",TestC.exec(exe,matrix,"0","4")); + assertEquals(usage,TestC.exec(exe)); + assertEquals(usage,TestC.exec(exe,"","","")); + assertEquals(usage,TestC.exec(exe,"[1, 0, 3, 0, 5, 1]","1","2")); + assertEquals(usage,TestC.exec(exe,"[0, 0, 0, 0]","0","1")); + } + + @Test + public void testFileIO() throws IOException { + String src = Files.readString(Path.of("src/test/smp/examples/FileIO.smp")); + String exe = TestC.compile(src,"FileIO",new Ary<>(new String[]{SYS_BLDDIR}), + TestC.CALL_CONVENTION,null,null,-1); + assertEquals("File I/O succeeded\n",TestC.exec(exe)); + } +} diff --git a/src/test/java/com/seaofnodes/simple/Chapter28bTest.java b/src/test/java/com/seaofnodes/simple/Chapter28bTest.java new file mode 100644 index 00000000..e0424573 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/Chapter28bTest.java @@ -0,0 +1,29 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import org.junit.Ignore; +import org.junit.Test; +// +import static org.junit.Assert.*; + +public class Chapter28bTest { + + @Ignore + @Test + public void testJig() { + String src = """ +struct C { + int x; + new C = { int n -> + while(n--) + if(n) x=1; + }; +}; +return new C(arg).x; +"""; + 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/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java index 6b4a987d..09391e4f 100644 --- a/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java +++ b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java @@ -25,6 +25,31 @@ import org.junit.runners.model.Statement; import static org.junit.Assert.*; public class ClassInitDependenciesTest { + @Test public void testPrivateInitializerCallEdges() throws Exception { + for( long seed : new long[]{1,123,987654321} ) { + Path root = tempDir("private-roots-"); + Path classes = Files.createDirectories(root.resolve("Root")); + Files.writeString(root.resolve("Root.smp"),"val _dead = _Dead; val read = { -> _Live.value; }; return 0;"); + Files.writeString(classes.resolve("_Live.smp"),"int value = 17+arg;"); + Files.writeString(classes.resolve("_Dead.smp"),"val child = Child; int value = 99;"); + Path children = Files.createDirectories(classes.resolve("_Dead")); + Files.writeString(children.resolve("Child.smp"),"int value = 42;"); + CodeGen code = new CodeGen(root.toString(),root.resolve("obj").toString(),null, + "Root",null,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + FunNode live = code.link(code._compunits.get("Root/_Live")); + assertNotNull(live); + assertFalse("A local init call is not an unknown caller",live.unknownCallers()); + assertTrue(live.in(1) instanceof CProjNode call && call.in(0) instanceof CLInitNode); + for( FunNode fun : code._linker ) + assertTrue(fun==null || fun.isDead() || + fun._compunit!=code._compunits.get("Root/_Dead") && + fun._compunit!=code._compunits.get("Root/_Dead/Child")); + assertEquals("0",Eval2.eval(code,99)); + new CodeGen(root.toString(),root.resolve("obj").toString(),null, + "Root",null,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.Encoding,"x86_64_v2","SystemV"); + } + } + private final ArrayList<Path> dirs = new ArrayList<>(); @Rule public final TestRule cleanup = (test, description) -> new Statement() { @@ -57,7 +82,7 @@ public class ClassInitDependenciesTest { Files.createDirectories(path.getParent()); Files.writeString(path,source.getValue()); } - return new CodeGen(root.toString(),root.resolve("obj").toString(), + return new CodeGen(root.toString(),root.resolve("obj").toString(),null, "Root",null,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); } @@ -194,26 +219,119 @@ public class ClassInitDependenciesTest { assertTrue(touches(code,new ClassInitDependencies(code),"Root/A","Root/B")); code=compile("A",Map.of("A","{int} native=\"C\"; return native();"),123); + Path dir=tempDir("import-"); + new CodeGen(null,dir.toString(),null,"B","int value=1;",123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "A","return B.value;",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertTrue(touches(code,new ClassInitDependencies(code),"A","B")); } - private void object(Path dir, Path unused, String name, String source) throws Exception { - Path file=dir.resolve("Root/"+name+".smp"); - Files.createDirectories(file.getParent()); - Files.writeString(file,source); + private CodeGen object(Path dir, Path imports, String name, String source) { + return new CodeGen(null,dir.toString(),new Ary<>(new String[]{imports.toString()}), + name,source,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); } - private void module(Path dir, Path unused, String name, Map<String,String> sources) throws Exception { - for( var entry : sources.entrySet() ) object(dir,dir,entry.getKey(),entry.getValue()); + private CodeGen module(Path dir, Path imports, String name, Map<String,String> sources) throws Exception { + Path src=tempDir("import-"); + for( var entry : sources.entrySet() ) { + Path file=src.resolve(entry.getKey()+".smp"); + Files.createDirectories(file.getParent()); + Files.writeString(file,entry.getValue()); + } + return new CodeGen(src.toString(),dir.toString(),new Ary<>(new String[]{imports.toString()}), + name,null,123,true,TypeInteger.BOT).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); } - private CodeGen entry(Path dir, String source, long seed) throws Exception { - Files.writeString(dir.resolve("Root.smp"),""); - object(dir,dir,"Main",source); - return new CodeGen(dir.toString(),dir.toString(),"Root/Main",null,seed,true,TypeInteger.BOT); + @Test public void testImportedInitialization() throws Exception { + Path dir=tempDir("import-"), stubs=tempDir("import-"); + object(stubs,stubs,"A","int value=arg;"); + object(stubs,stubs,"B","int value=arg;"); + // Each object is independently acyclic. Combining their real bodies + // must recheck the graph rather than trust either object's local order. + String asrc="int value=arg;\nvalue=B.value;"; + CodeGen acode=module(dir,stubs,"A",Map.of("A",asrc)); + String afile=acode._compunits.get("A").sourceFile(); + module(dir,stubs,"B",Map.of("B","int value=arg; value=A.value;")); + for( String entry : new String[]{"A","B"} ) { + try { + new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","return "+entry+".value;",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + fail("Expected imported initialization cycle"); + } catch( Parser.ParseException ex ) { + assertCycle(ex.getMessage(),"A -> B -> A"); + assertTrue(ex.getMessage(),ex.getMessage().contains("value=B.value;\n -------^")); + assertTrue(ex.getMessage(),ex.getMessage().contains("\n"+afile+":2:8: error: cyclic class initialization\n")); + } + } + // The imported object must still report its saved path and offset + // when the original source is no longer available. + Files.delete(Path.of(afile)); + try { + new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","return A.value;",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + fail("Expected imported initialization cycle"); + } catch( Parser.ParseException ex ) { + assertCycle(ex.getMessage(),"A -> B -> A"); + assertTrue(ex.getMessage(),ex.getMessage().contains("\n"+afile+": error: cyclic class initialization (source unavailable at byte offset "+asrc.indexOf('.')+")")); + } } - private CodeGen emit(Path dir, String name, String source) throws Exception { - return entry(dir,source,123).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + @Test public void testImportedDiamondAndHelpers() throws Exception { + Path diamond=tempDir("import-"); + CodeGen dcode=object(diamond,diamond,"D","int value=arg;"); + var data=dcode._encoding._data.entries().stream().filter(obj -> obj.symbol.equals("D.$class")).findFirst().orElseThrow(); + var dclz=dcode._compunits.get("D")._clz; + assertEquals(0,dcode._encoding._sdata.buf()[data.offset+dclz.offset(dclz.find("$init"))]); + object(diamond,diamond,"B","int value=D.value;"); + object(diamond,diamond,"C","int value=D.value;"); + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(null,diamond.toString(),new Ary<>(new String[]{diamond.toString()}), + "Main","int value=B.value+C.value;",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + CompUnit b=code._compunits.get("B"), d=code._compunits.get("D"); + ClassInitDependencies deps=new ClassInitDependencies(code); + assertTrue(touches(code,deps,"B","D")); + assertTrue(touches(code,deps,"C","D")); + assertTrue(touches(code,deps,"Main","B")); + assertTrue(touches(code,deps,"Main","C")); + Field flag=d._clz.field("$init"); + assertSame(TypeInteger.U1,flag._t); + assertFalse(flag._final); + assertNotEquals(flag._alias,b._clz.field("$init")._alias); + // Loading just the header/types uses the same alias identity and + // does not require allocating any graph nodes. + ElfReader elf=ElfReader.load(diamond.resolve("D.o").toFile(),d); + assertEquals(flag._alias,elf.loadPublicTypes(code).field("$init")._alias); + assertNull(elf._nodes); + } + object(diamond,diamond,"Helper","val get={ -> D.value; }; val get_noInline={ -> D.value; };"); + for( String name : new String[]{"get","get_noInline"} ) { + CodeGen code=new CodeGen(null,diamond.toString(),new Ary<>(new String[]{diamond.toString()}), + "Main","return Helper."+name+"();",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + ClassInitDependencies deps=new ClassInitDependencies(code); + assertTrue(touches(code,deps,"Main","D")); + assertFalse(touches(code,deps,"Helper","D")); + } + object(diamond,diamond,"Wrapper","val get_noInline={ -> Helper.get_noInline(); };"); + CodeGen wrapped=new CodeGen(null,diamond.toString(),new Ary<>(new String[]{diamond.toString()}), + "Main","return Wrapper.get_noInline();",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertTrue(touches(wrapped,new ClassInitDependencies(wrapped),"Main","D")); + } + + @Test public void testInterleavedModules() throws Exception { + // Two objects, each providing a parent and children. Their orders + // must interleave: P.B precedes Q.A, which precedes P.A. + Path modules=tempDir("import-"), qstub=tempDir("import-"); + module(qstub,qstub,"Q",Map.of("Q","val A=A;", "Q/A","int value=arg;")); + module(modules,qstub,"P",Map.of("P","val A=A; val B=B;", + "P/A","int value=arg; value=Q.A.value;", "P/B","int value=arg;")); + module(modules,modules,"Q",Map.of("Q","val A=A;", "Q/A","int value=arg; value=P.B.value;")); + CodeGen combined=new CodeGen(null,modules.toString(),new Ary<>(new String[]{modules.toString()}), + "Main","return P.A.value;",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + ClassInitDependencies deps=new ClassInitDependencies(combined); + assertTrue(touches(combined,deps,"Q/A","P/B")); + assertTrue(touches(combined,deps,"P/A","Q/A")); } @Test public void testCycleReportDirection() throws Exception { @@ -346,59 +464,87 @@ public class ClassInitDependenciesTest { object(dir,dir,"B","State.count++; int value=17+arg;"); String src="int value=arg ? B.value+B.value : 0; return value+100*State.count;"; for( long seed : new long[]{1,123,987654321} ) { - CodeGen code=entry(dir,src,seed).driver(CodeGen.Phase.TypeCheck); + CodeGen code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "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)); } - emit(dir,"Main","val run={int take -> "+src.replace("arg ?","take ?")+" };"); + object(dir,dir,"Main","val run={int take -> "+src.replace("arg ?","take ?")+" };"); Path cfile=dir.resolve("check.c"); Files.writeString(cfile,""" #include <stdint.h> - __attribute__((CALL_CONV)) extern int64_t run(int64_t take) __asm__("Root.Main.run"); + __attribute__((CALL_CONV)) extern int64_t run(int64_t take); int main(void) { return run(0)!=0 || run(1)!=134 || run(1)!=134 || run(0)!=100; } """); String exe=dir.resolve(TestC.OS.startsWith("Windows") ? "check.exe" : "check").toString(); - TestC.linkExe(dir.resolve("Root/Main.o").toString(),TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", - cfile.toString(),null,exe); + TestC.linkExe(dir.resolve("Main.o").toString(),TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", + cfile.toString(),new Ary<>(new String[]{dir.resolve("B.o").toString(),dir.resolve("State.o").toString()}),exe); assertEquals("",TestC.exec(exe)); for( String cpu : new String[]{"arm","riscv"} ) - entry(dir,src,123).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main",src,123,true,TypeInteger.BOT).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); } @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=entry(dir,"return P.C.value+State.count;",seed).driver(CodeGen.Phase.TypeCheck); + 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); assertEquals("12",Eval2.eval(code,99)); } - emit(dir,"Main","val run={ -> P.C.value+State.count; };"); + object(dir,dir,"Main","val run={ -> P.C.value+State.count; };"); Path cfile=dir.resolve("check.c"); Files.writeString(cfile,""" #include <stdint.h> - __attribute__((CALL_CONV)) extern int64_t run(void) __asm__("Root.Main.run"); + __attribute__((CALL_CONV)) extern int64_t run(void); int main(void) { return run()!=12 || run()!=12; } """); String exe=dir.resolve(TestC.OS.startsWith("Windows") ? "check.exe" : "check").toString(); - TestC.linkExe(dir.resolve("Root/Main.o").toString(),TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", - cfile.toString(),null,exe); + 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("P/C.o").toString(), + dir.resolve("State.o").toString()}),exe); assertEquals("",TestC.exec(exe)); } @Test public void testNonreturningObject() throws Exception { Path dir=tempDir("nonreturning-"); object(dir,dir,"Spin","val spin_noInline={ -> while(1) {} return 7; };"); - CodeGen code=entry(dir,"Spin.spin_noInline(); return 7;",123).driver(CodeGen.Phase.TypeCheck); + CodeGen code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","Spin.spin_noInline(); return 7;",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); assertNull(Eval2.eval(code,0,10)); } + @Test public void testImportedPrivateInitializer() throws Exception { + Path dir=tempDir("private-"); + module(dir,dir,"Library",Map.of("Library","val read={ -> _Hidden.value; };", + "Library/_Hidden","int value=17+arg;")); + CodeGen code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","return Library.read();",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertEquals("17",Eval2.eval(code,99)); + // Even a request stopping at Select must drop imported bodies. They + // remain available only to the ideal evaluator above, not this object. + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) { + CodeGen selected=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","return Library.read();",123,true,TypeInteger.BOT).driver(CodeGen.Phase.Select,cpu,"SystemV"); + assertNull(selected._serial); + for( CompUnit cu : selected._compunits.values() ) + if( cu._src==null ) assertNull(cu._stop); + selected.driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + } @Test public void testOverlappingFunctionAndClassCycles() throws Exception { // Fixed function graph: Opto/inlining must not erase the overlap that diff --git a/src/test/java/com/seaofnodes/simple/Eval2.java b/src/test/java/com/seaofnodes/simple/Eval2.java index 901a8990..fc87c13a 100644 --- a/src/test/java/com/seaofnodes/simple/Eval2.java +++ b/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/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java b/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java new file mode 100644 index 00000000..d8f7ef34 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java @@ -0,0 +1,192 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.*; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.util.Ary; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileTime; +import org.junit.Test; +import static org.junit.Assert.*; + +public class IncrementalBuildTest { + @Test public void testRequiredTypesBelongToTheirSource() throws Exception { + Path base = Path.of("build/objs/incremental"); + Files.createDirectories(base); + Path root = Files.createTempDirectory(base,"required-types-"); + Path src = Files.createDirectory(root.resolve("src")), obj = Files.createDirectory(root.resolve("obj")); + write(src,"App","val marker = 0;",1000); + write(src,"App/Main","return A.x+C.y;",1000); + write(src,"App/A","int x = 0; val readB = { B b -> b.x; };",1000); + write(src,"App/B","int x = 0;",1000); + write(src,"App/C","int y = 0;",1000); + CodeGen code = new CodeGen(src.toString(),obj.toString(),null,"App/Main",null,123,true,TypeInteger.BOT) + .parse(); + ParseAllTestSupport.checkRequiredTypes(code); + } + + @Test public void testTransitiveSourceChange() throws Exception { + Path base=Path.of("build/objs/incremental"); + Files.createDirectories(base); + Path root=Files.createTempDirectory(base,"chain-"); + Path src=Files.createDirectory(root.resolve("src")); + Path obj=Files.createDirectory(root.resolve("obj")); + write(src,"App","val marker=0;",1000); + write(src,"App/B","val value={ -> 5; };",1000); + write(src,"App/A","val value={ -> B.value()+1; };",1000); + write(src,"App/C","val value={ -> A.value()+1; };",1000); + write(src,"App/Main","val run={ -> C.value(); }; return run();",1000); + compile(src,obj,"App"); + stamp(obj.resolve("App.o"),1500); + compile(src,obj,"App/B"); + stamp(obj.resolve("App/B.o"),2000); + compile(src,obj,"App/A"); + stamp(obj.resolve("App/A.o"),3000); + compile(src,obj,"App/C"); + stamp(obj.resolve("App/C.o"),3500); + CodeGen code=client(src,obj); + assertNull("Reuse unchanged A",code._compunits.get("App/A")._src); + assertNull("Reuse unchanged B",code._compunits.get("App/B")._src); + assertNull("Reuse unchanged C",code._compunits.get("App/C")._src); + assertEquals("7",Eval2.eval(code,0)); + + write(src,"App/B","val value={ -> 7; };",4000); + code=client(src,obj); + assertEquals("9",Eval2.eval(code,0)); + assertNotNull(code._compunits.get("App/C")._src); + 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); + + Path clean=Files.createDirectory(root.resolve("clean")); + assertEquals("9",Eval2.eval(client(src,clean),0)); + compile(src,clean,"App/Main"); + checkNative(clean); + } + + @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); + 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); + assertNull(code._compunits.get("App/B")._src); + + write(src,"App/B","val value={int n -> n ? A.value(n-1) : 7; };",4000); + code=client(src,obj); + assertEquals("7",Eval2.eval(code,0)); + assertNotNull(code._compunits.get("App/A")._src); + assertNotNull(code._compunits.get("App/B")._src); + } + + @Test public void testMissingDependency() throws Exception { + Path root=fixture("missing-"); + Path src=root.resolve("src"), obj=root.resolve("obj"); + write(src,"App/A","val value={ -> B.value()+1; };",1000); + write(src,"App/B","val value={ -> 5; };",1000); + write(src,"App/Main","return A.value();",1000); + compile(src,obj,"App/B"); + stamp(obj.resolve("App/B.o"),2000); + compile(src,obj,"App/A"); + stamp(obj.resolve("App/A.o"),3000); + Files.delete(obj.resolve("App/B.o")); + CodeGen code=client(src,obj); + assertEquals("6",Eval2.eval(code,0)); + assertNotNull(code._compunits.get("App/A")._src); + assertNotNull(code._compunits.get("App/B")._src); + Files.delete(src.resolve("App/B.smp")); + try { + client(src,obj); + fail("Missing source and object must not reuse A's inlined body"); + } catch( RuntimeException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("Undefined name 'B'")); + } + } + + @Test public void testLibraryDependency() throws Exception { + Path root=fixture("library-"); + Path src=root.resolve("src"), obj=root.resolve("obj"); + Path lib=Files.createDirectory(root.resolve("lib")); + var paths=new Ary<>(new String[]{lib.toString(),obj.toString()}); + library(lib,5); + stamp(lib.resolve("LibB.o"),2000); + write(src,"App/A","val value={ -> LibB.value()+1; };",1000); + write(src,"App/Main","return A.value();",1000); + new CodeGen(src.toString(),obj.toString(),paths,"App/A",null,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + stamp(obj.resolve("App/A.o"),3000); + CodeGen code=new CodeGen(src.toString(),obj.toString(),paths,"App/Main",null,123,true,TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); + assertEquals("6",Eval2.eval(code,0)); + assertNull("Unchanged library inputs permit reuse",code._compunits.get("App/A")._src); + + library(lib,7); + stamp(lib.resolve("LibB.o"),4000); + code=new CodeGen(src.toString(),obj.toString(),paths,"App/Main",null,123,true,TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); + assertEquals("8",Eval2.eval(code,0)); + assertNotNull(code._compunits.get("App/A")._src); + } + + private static void library(Path lib, int value) { + new CodeGen(null,lib.toString(),null,"LibB","val value={ -> "+value+"; };",123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + } + + 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)); + } + + private static Path fixture(String prefix) throws Exception { + Path base=Path.of("build/objs/incremental"); + Files.createDirectories(base); + Path root=Files.createTempDirectory(base,prefix); + Path src=Files.createDirectory(root.resolve("src")); + Path obj=Files.createDirectory(root.resolve("obj")); + write(src,"App","val marker=0;",1000); + compile(src,obj,"App"); + stamp(obj.resolve("App.o"),1500); + return root; + } + + private static void write(Path src, String name, String text, long time) throws Exception { + Path path=src.resolve(name+".smp"); + Files.createDirectories(path.getParent()); + Files.writeString(path,text); + stamp(path,time); + } + + private static void stamp(Path path, long time) throws Exception { + Files.setLastModifiedTime(path,FileTime.fromMillis(time)); + } + + private static void compile(Path src, Path obj, String name) { + new CodeGen(src.toString(),obj.toString(),null,name,null,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + } + + private static CodeGen client(Path src, Path obj) { + return new CodeGen(src.toString(),obj.toString(),null,"App/Main",null,123,true,TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); + } +} diff --git a/src/test/java/com/seaofnodes/simple/IndirectCallTest.java b/src/test/java/com/seaofnodes/simple/IndirectCallTest.java deleted file mode 100644 index e65ce3d1..00000000 --- a/src/test/java/com/seaofnodes/simple/IndirectCallTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.codegen.RegMask; -import com.seaofnodes.simple.codegen.IndirectCallTestSupport; -import com.seaofnodes.simple.node.*; -import com.seaofnodes.simple.type.Type; -import com.seaofnodes.simple.type.TypeFunPtr; -import org.junit.Test; -import static org.junit.Assert.*; - -public class IndirectCallTest { - @Test public void testTargetOperand() { - for( String cpu : new String[]{"x86_64_v2","riscv","arm"} ) { - CodeGen code = new CodeGen("return 0;").driver(CodeGen.Phase.Select,cpu,"SystemV"); - Node target = PhiNode.make("target",TypeFunPtr.BOT,code._start); - target._type = TypeFunPtr.BOT; - CallNode ideal = new CallNode(null,code._start,null,target); - CallNode call = (CallNode)code._mach.instSelect(ideal); - // The last input is the call target, not another argument. - assertEquals("callr",((MachNode)call).op()); - RegMask mask = ((MachNode)call).regmap(call.nIns()-1); - assertTrue(mask.test(5)); - assertTrue(mask.test(6)); - if( cpu.equals("arm") ) IndirectCallTestSupport.checkArmEncoding(code,call); - // Machine operands can lose their ideal type (e.g. a spill reload). - // The call still needs the original signature for argument masks. - target._type = Type.BOTTOM; - assertSame(TypeFunPtr.BOT,call.tfp()); - } - } -} diff --git a/src/test/java/com/seaofnodes/simple/ModulesTest.java b/src/test/java/com/seaofnodes/simple/ModulesTest.java deleted file mode 100644 index be54b43d..00000000 --- a/src/test/java/com/seaofnodes/simple/ModulesTest.java +++ /dev/null @@ -1,248 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.isa.eval.EvalRisc5; - -import com.seaofnodes.isa.eval.EvalArm64; - -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; -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 org.junit.Ignore; -import org.junit.Test; - -import static org.junit.Assert.*; - -public class ModulesTest { - - @Test public void testFlagSpills() throws IOException { - com.seaofnodes.simple.codegen.FlagSpillTestSupport.run(); - } - - @Ignore - @Test - public void testJig() { - String src = "{ i32 -> i32 } close = \"C\"; val main = { u8[] a -> if (a# > 1) return close(0); return a[0]; };"; - CodeGen code = new CodeGen(src).driver(CodeGen.Phase.Encoding,"riscv","SystemV"); - assertEquals("return (arg+3);", code.print()); - assertEquals("3",Eval2.eval(code,0)); - } - - @Test public void testConditionalClassInitX86() throws IOException { - Path build=Path.of("build/objs"); - Files.createDirectories(build); - Path dir=Files.createTempDirectory(build,"init_flags_"); - Files.createDirectories(dir.resolve("Main")); - Files.writeString(dir.resolve("Main/B.smp"),"int value=17+arg;"); - // GCM can hoist the fused B.$init compare above the test of take. - // Its flags then cross another flag definition and need a legal split. - new CheckedCodeGen(dir.toString(),dir.toString(), - "Main","val run={int take -> return take ? B.value : 0; };",123,true,TypeInteger.BOT) - .driver(CodeGen.Phase.Encoding,"x86_64_v2","win64"); - // Remove successful fixtures; preserve the object if compilation fails. - Files.delete(dir.resolve("Main/B.smp")); - Files.delete(dir.resolve("Main")); - Files.delete(dir); - } - - @Test public void testPublicInstanceMethod() { - String src = """ - struct Scan { - val visible = { u8 c -> c==255; }; - val _hidden = { u8 c -> c==1; }; - }; - struct _Private { - val hiddenClassMethod = { u8 c -> c==2; }; - }; - return new Scan.visible(44); - """; - for( long seed : new long[]{1,123,987654321} ) { - CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.Parse); - FunNode visible = null; - int checked = 0; - for( FunNode fun : code._linker ) { - if( fun==null || fun._name==null ) continue; - if( fun._name.equals("visible") ) { - assertTrue(code.publicFIDX(fun.sig().fidx())); - visible = fun; - checked++; - } - if( fun._name.equals("_hidden") || fun._name.equals("hiddenClassMethod") ) { - assertFalse(code.publicFIDX(fun.sig().fidx())); - checked++; - } - } - assertEquals(3,checked); - code.driver(CodeGen.Phase.TypeCheck); - assertTrue(visible.unknownCallers()); - assertEquals(TypeInteger.U8,visible.parm(3)._type); - assertEquals("0",Eval2.eval(code,0)); - } - } - - @Test public void testNeverReturnPointerRiscV() { - new CodeGen(""" - struct _S { int x; }; - !_S !s = new _S; - if(arg) while(1) { s.x += arg; } - return s; - """).driver(CodeGen.Phase.Encoding,"riscv","SystemV"); - } - - @Test public void testInlineNeverExit() { - String[] sources={ - "val _spin={ -> while(1) {} return 1; }; _spin(); return 2;", - // Two callers require cloning, including the non-exiting arm. - "val _spin={int x -> if(x) while(1) {} return 7; }; return _spin(arg)+_spin(arg);" - }; - for( int i=0; i<sources.length; i++ ) { - for( long seed : new long[]{1,123,987654321} ) { - CodeGen code=new CodeGen(sources[i],seed,true).driver(CodeGen.Phase.TypeCheck); - assertNull("Calls should inline",code._start.walk(n -> n instanceof CallNode ? n : null)); - assertNotNull("The non-exiting path must survive",code._start.walk(n -> n instanceof LoopNode && n!=code._start ? n : null)); - if( i==1 ) assertEquals("14",Eval2.eval(code,0,10)); - assertNull(Eval2.eval(code,1,10)); - } - for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) - new CheckedCodeGen(sources[i]).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); - } - } - - @Test - public void testExternData() throws IOException { - String src = """ - i32 counter="C"; - i8 small="C"; - f64 fraction="C"; - {int} bump="C"; - val test_global={ -> - val before=counter; - counter=before; - counter=++counter+1; - counter+=1; - val mid=bump(); - small++; - fraction=fraction+0.5; - return before*10000+mid*100+counter; - }; - """; - TestC.runC(src,"extern_data","",-1); - } - - @Test - public void testExternDataEncoding() { - for( String cpu : new String[]{"arm","riscv"} ) { - CodeGen code = new CheckedCodeGen("i32 counter=\"C\"; counter=counter+3; return counter;"); - // Exercise signed low-12 relocation arithmetic, past the first page. - int addr = 0x3800; - code._externDataAddresses.put("counter",addr); - code.driver(cpu,"SystemV",true,false); - byte[] image = new byte[1<<20]; - var bits = code._encoding._bits; - System.arraycopy(bits.buf(),0,image,0,bits.size()); - if( cpu.equals("arm") ) { - EvalArm64 arm = new EvalArm64(image,1<<16); - arm.st4(addr,-7); - assertEquals(0,arm.step(1000)); - assertEquals(-4,arm.regs[0]); - assertEquals(-4,arm.ld4s(addr)); - } else { - EvalRisc5 r5 = new EvalRisc5(image,1<<16); - r5.st4(addr,-7); - assertEquals(0,r5.step(1000)); - assertEquals(-4,r5.regs[10]); - assertEquals(-4,r5.ld4s(addr)); - } - } - } - - - @Test public void testPrintingConstantPool() throws Exception { - com.seaofnodes.simple.codegen.PrintRegTestSupport.checkConstantPool(); - } - - - @Test - public void testForwardConstructor() { - CodeGen code = new CodeGen("src/test/java/com/seaofnodes/simple/test_smp/forward_ctor", - "build/objs/forward_ctor_parse", - "m",null,123L,true,TypeInteger.BOT); - code.driver(CodeGen.Phase.TypeCheck); - } - - - @Test - public void testPostfixFieldUpdate() { - String src = """ - struct V { - u32 !len = 0; - i64[] !buf; - new V = { i64[] b -> buf=b; }; - val grow = { i64 n -> self; }; - val add = { i64 x -> grow(1).buf[len++] = x; self; }; - }; - return new V(new i64[1]).add(7).len; - """; - CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("1",Eval2.eval(code,0)); - } - - @Test - public void testStringEscapes() { - String src = "u8[~] s=\"A\\n\\t\\r\\\\\\\"B\"; " + - "return s#==7 && s[0]=='A' && s[1]==10 && s[2]==9 && s[3]==13 && " + - "s[4]==92 && s[5]==34 && s[6]=='B';"; - CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); - assertEquals("1",Eval2.eval(code,0)); - } - - @Test - public void testCharacterEscapes() { - CodeGen code = new CodeGen("return '\\n'==10 && '\\t'==9 && '\\r'==13;") - .driver(CodeGen.Phase.TypeCheck); - assertEquals("1",Eval2.eval(code,0)); - } - - @Test - public void testFuzzerUnresolvedNumericLoop() { - CodeGen code = new CodeGen(""" - arg=0; - while(arg+0) - while(-arg) - arg=0; - """).driver(CodeGen.Phase.TypeCheck); - assertEquals("return 0;",code.print()); - } - - @Test - public void testFuzzerBadLCA() { - new CodeGen(""" - int v0=arg; - if(v0&&--v0?0:arg) {} - """).driver(CodeGen.Phase.LocalSched); - } - - - // Recursive search (TODO: gzip, archives) and delete all .o files - - - @Test - public void testConstantClassInitialization() throws IOException { - TestC.runSF("val x = 42; return 0;","constantClassInt","",0); - TestC.runSF("val f = { -> 7; }; return 0;","constantClassFunction","",0); - } - - -} diff --git a/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java b/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java index fa914d28..f3f65183 100644 --- a/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java +++ b/src/test/java/com/seaofnodes/simple/NativeExecutionTest.java @@ -48,6 +48,7 @@ public class NativeExecutionTest { @Test public void testHelloWorldArtifacts() throws IOException { new Chapter22Test().testHelloWorld(); + new Chapter25Test().testHelloWorld(); // Model a parallel run paused between Chapter 22's link and execution. // Chapter 25 must not replace the executable Chapter 22 is about to run. String exe = "build/objs/helloWorld"+(TestC.OS.startsWith("Windows") ? ".exe" : ""); diff --git a/src/test/java/com/seaofnodes/simple/ParseAllTest.java b/src/test/java/com/seaofnodes/simple/ParseAllTest.java deleted file mode 100644 index f74ee7cd..00000000 --- a/src/test/java/com/seaofnodes/simple/ParseAllTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.seaofnodes.simple; - -import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.codegen.ParseAllTestSupport; -import com.seaofnodes.simple.type.TypeInteger; -import java.nio.file.Files; -import java.nio.file.Path; -import org.junit.Test; -import static org.junit.Assert.*; - -public class ParseAllTest { - @Test public void testTypeOnlyFileHasNoConstructor() throws Exception { - Path base = Path.of("build/objs"); - Files.createDirectories(base); - Path root = Files.createTempDirectory(base,"type-only-"); - Files.createDirectory(root.resolve("A")); - Files.writeString(root.resolve("A/B.smp"),"int x = 0;"); - for( String src : new String[]{ - "B? b = null; return 0;", - "val readB = { B b -> 0; }; return 0;"} ) { - Files.writeString(root.resolve("A.smp"),src); - CodeGen code = new CodeGen(root.toString(),root.resolve("obj").toString(), - "A",null,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); - assertEquals("0",Eval2.eval(code,0)); - } - } - - @Test public void testRequiredTypesBelongToTheirSource() throws Exception { - Path base = Path.of("build/objs"); - Files.createDirectories(base); - Path root = Files.createTempDirectory(base,"required-types-"); - Files.createDirectory(root.resolve("App")); - Files.writeString(root.resolve("App.smp"),"val marker = 0;"); - Files.writeString(root.resolve("App/Main.smp"),"return A.x+C.y;"); - Files.writeString(root.resolve("App/A.smp"),"int x = 0; val readB = { B b -> b.x; };"); - Files.writeString(root.resolve("App/B.smp"),"int x = 0;"); - Files.writeString(root.resolve("App/C.smp"),"int y = 0;"); - CodeGen code = new CodeGen(root.toString(),root.resolve("obj").toString(), - "App/Main",null,123,true,TypeInteger.BOT).parse(); - ParseAllTestSupport.checkRequiredTypes(code); - } -} diff --git a/src/test/java/com/seaofnodes/simple/PhaseTest.java b/src/test/java/com/seaofnodes/simple/PhaseTest.java index 5ff7fcd6..7252378a 100644 --- a/src/test/java/com/seaofnodes/simple/PhaseTest.java +++ b/src/test/java/com/seaofnodes/simple/PhaseTest.java @@ -22,22 +22,43 @@ public class PhaseTest { code.driver(Phase.TypeCheck); code.driver(Phase.TypeCheck); // A repeated request does not rerun anything. code.driver(Phase.LocalSched); - assertEquals("Parse Iter Opto IterPostOpto FinalizeIR TypeCheck Lower LoopTree Schedule LocalSched", + assertEquals("Parse Iter Opto IterPostOpto FinalizeIR TypeCheck Lower LoopTree Unlink Schedule LocalSched", String.join(" ",phases)); + assertNull(code._serial); assertNull(code._mach); assertEquals("45",Eval2.eval(code,10)); } - @Test public void testEncodingAndResume() { + @Test public void testEncodingWithoutSerialization() { for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) { CodeGen code = new CodeGen(SRC).driver(Phase.TypeCheck); - code.driver(Phase.LoopTree,cpu,"SystemV"); + code.driver(Phase.Unlink,cpu,"SystemV"); code.driver(Phase.Select,cpu,"SystemV"); code.driver(Phase.Encoding,cpu,"SystemV"); + assertNull(code._serial); byte[] staged = code._encoding._bits.toByteArray(); CodeGen direct = new CodeGen(SRC).driver(Phase.Encoding,cpu,"SystemV"); assertArrayEquals(staged,direct._encoding._bits.toByteArray()); } } + @Test public void testExplicitSerializationAndMemoryExport() { + CodeGen code = new CodeGen(SRC).driver(Phase.Serialize); + assertNotNull(code._serial); + assertEquals(Phase.Serialize,code._phase); + code.driver(Phase.Encoding,"riscv","SystemV"); + assertNotNull(code._serial); + + CodeGen memory = new CodeGen(SRC).driver("riscv","SystemV",true,false); + assertNull(memory._serial); + assertEquals(Phase.Export,memory._phase); + + CodeGen late = new CodeGen(SRC).driver(Phase.Encoding,"riscv","SystemV"); + try { + late.exportELF(false,false); + fail("Cannot recover ideal IR after lowering and instruction selection"); + } catch( IllegalStateException expected ) { + assertTrue(expected.getMessage().contains("Serialize before Lower")); + } + } } diff --git a/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java b/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java new file mode 100644 index 00000000..d8fad3d5 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java @@ -0,0 +1,263 @@ +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 testSharedConstantsInSeparateObjects() throws Exception { + Path root = library("shared-constants-"); + Path src = Files.createDirectory(root.resolve("src")); + Path obj = Files.createDirectory(root.resolve("obj")); + write(src,"Lib","val sum_noInline = { int n -> Left.at_noInline(n)+Right.at_noInline(n); };"); + write(src,"Lib/Left","val at_noInline = { int n -> n+1000000000000; };"); + write(src,"Lib/Right","val at_noInline = { int n -> n*2+1000000000000; };"); + // Both bodies share a constant in the compiling graph. Each saved + // object must contain it without pulling in the other unit's body. + new CodeGen(src.toString(),obj.toString(),null,"Lib",null,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + CodeGen code = new CodeGen(null,null,new Ary<>(new String[]{obj.toString()}), + "Use","return Lib.sum_noInline(arg);",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertNull(code._compunits.get("Lib/Left")._src); + assertNull(code._compunits.get("Lib/Right")._src); + assertEquals("2000000000000",Eval2.eval(code,0)); + assertEquals("2000000000003",Eval2.eval(code,1)); + } + + @Test public void testUnreachableImportIsNotARoot() throws Exception { + Path root = library("dead-import-"); + Ary<String> paths = new Ary<>(new String[]{root.toString()}); + object(root,paths,"Unused","val run_noInline = { int n -> n+1; };"); + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(null,root.toString(),paths,"Main",""" + val _false_noInline = { -> false; }; + if( _false_noInline() ) return Unused.run_noInline(arg); + return 7; + """,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + CompUnit unused = code._compunits.get("Unused"); + assertNotNull("Parsing loaded the declaration",unused); + for( FunNode fun : code._linker ) + assertTrue(fun==null || fun.isDead() || fun._compunit!=unused); + assertEquals("7",Eval2.eval(code,0)); + } + } + + private static Path library(String prefix) throws IOException { + Path base = Files.createDirectories(Path.of("build/objs/selective")); + return Files.createTempDirectory(base,prefix); + } + + @Test public void testImportedCallbackChain() throws Exception { + Path root = library("callback-chain-"); + Ary<String> paths = new Ary<>(new String[]{root.toString()}); + object(root,paths,"L0","val relay_noInline = { {int->int} f -> f; };"); + for( int i = 1; i<6; i++ ) + object(root,paths,"L"+i,"val relay_noInline = { {int->int} f -> L"+(i-1)+".relay_noInline(f); };"); + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(null,root.toString(),paths,"Main",""" + val _increment_noInline = { int n -> n+1; }; + val _decrement_noInline = { int n -> n-1; }; + val f = L5.relay_noInline(arg ? _increment_noInline : _decrement_noInline); + return f(41); + """,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertNotNull(function(code,"relay_noInline").summary()); + assertEquals("42",Eval2.eval(code,1)); + assertEquals("40",Eval2.eval(code,0)); + } + } + + @Test public void testImportedCallbackReturn() throws Exception { + Path root = library("callback-return-"); + Ary<String> paths = new Ary<>(new String[]{root.toString()}); + object(root,paths,"Relay","val relay = { {int->int} f -> f; };"); + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(null,root.toString(),paths,"Main",""" + val _increment_noInline = { int n -> n+1; }; + val f = Relay.relay(_increment_noInline); + return f(arg); + """,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertNull(function(code,"relay").summary()); + assertEquals("42",Eval2.eval(code,41)); + assertEquals("1",Eval2.eval(code,0)); + } + } + + @Test public void testImportedFunctionField() throws Exception { + Path root = library("function-field-"); + Ary<String> paths = new Ary<>(new String[]{root.toString()}); + object(root,paths,"Targets","var chosen = { int n -> n+1; };"); + object(root,paths,"Apply","val run = { int n -> val f = Targets.chosen; return f(n); };"); + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(null,root.toString(),paths,"Main", + "return Apply.run(arg);",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertNull(function(code,"run").summary()); + assertEquals("42",Eval2.eval(code,41)); + assertEquals("1",Eval2.eval(code,0)); + } + } + + @Test public void testImportedUnknownCallback() throws Exception { + Path root = library("unknown-callback-"); + Ary<String> paths = new Ary<>(new String[]{root.toString()}); + object(root,paths,"Apply",""" + val apply = { {int->int} f,int n -> f(n); }; + val apply_noInline = { {int->int} f,int n -> f(n); }; + """); + for( String target : new String[]{"apply","apply_noInline","forward_noInline"} ) + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(null,root.toString(),paths,"Main", + "val _increment_noInline = { int n -> n+1; }; "+ + "val forward_noInline = { {int->int} f,int n -> Apply.apply_noInline(f,n); }; return "+ + (target.equals("forward_noInline") ? target : "Apply."+target)+"(_increment_noInline,arg);", + seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertEquals("42",Eval2.eval(code,41)); + assertEquals("1",Eval2.eval(code,0)); + } + CodeGen nativeCode = new CodeGen(null,root.toString(),paths,"NativeCallback",""" + val _increment_noInline = { int n -> n+1; }; + val run = { int n -> Apply.apply_noInline(_increment_noInline,n); }; + """,123,true,TypeInteger.BOT).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + Path driver = root.resolve("driver.c"); + Files.writeString(driver,""" + __attribute__((CALL_CONV)) extern long long run(long long); + int main(void) { return run(41)==42 && run(0)==1 ? 0 : 1; } + """); + String exe = root.resolve(TestC.OS.startsWith("Windows") ? "test.exe" : "test").toString(); + assertEquals("",TestC.gcc(nativeCode, + TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi",driver.toString(),null,exe)); + } + + @Test public void testUnknownCallbackEncoding() { + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) + new CodeGen("val apply = { {int->int} f,int n -> f(n); };") + .driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + + @Test public void testImportedCallbackInitCycle() throws Exception { + Path root = library("callback-cycle-"); + Path src = Files.createDirectory(root.resolve("src")); + Ary<String> paths = new Ary<>(new String[]{root.toString()}); + object(root,paths,"Apply","val apply_noInline = { {int->int} f,int n -> f(n); };"); + write(src,"Root","val entry = A.value;"); + write(src,"Root/A","val callback = { int n -> B.value; }; int value = Apply.apply_noInline(callback,1);"); + write(src,"Root/B","int value = A.value;"); + for( long seed : new long[]{1,123,987654321} ) { + try { + new CodeGen(src.toString(),root.toString(),paths,"Root",null,seed,true,TypeInteger.BOT) + .driver(CodeGen.Phase.TypeCheck); + fail("The unknown callback can close an initialization cycle"); + } catch( Parser.ParseException ex ) { + assertTrue(ex.getMessage(),ex.getMessage().startsWith("Cyclic class initialization:")); + assertTrue(ex.getMessage(),ex.getMessage().contains("Root.A")); + assertTrue(ex.getMessage(),ex.getMessage().contains("Root.B")); + assertTrue(ex.getMessage(),ex.getMessage().contains("unknown call")); + } + } + } + + @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/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java new file mode 100644 index 00000000..4e8a0417 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java @@ -0,0 +1,91 @@ +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.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; + +public class SeparateCompilationTest { + @Test public void testImportedNativeExposure() throws Exception { + Path build=Path.of("build/objs/separate"); + Files.createDirectories(build); + Path lib=Files.createTempDirectory(build,"callbacks-"); + String src=""" + val run={int n -> + val power={int base,int exponent -> + if(exponent<=0) return 1; + return base*power(base,exponent-1); + }; + { {int int -> int} -> int } retain="C"; + retain(power); + return power(2,n); + }; + """; + new CodeGen(null,lib.toString(),null,"Callbacks",src,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + 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); + assertSame("Native code may call the saved callback with any base",TypeInteger.BOT,base._type); + code.driver(CodeGen.Phase.TypeCheck); + } + + // The client root contains no library source. The saved constructor must + // keep local loop values, and retained calls must use the library's storage. + @Test public void testImportedConstructorAndState() throws Exception { + Path fixtures=Path.of("src/test/smp/separate"); + Path build=Path.of("build/objs/separate"); + Files.createDirectories(build); + Path lib=Files.createTempDirectory(build,"library-"); + Path client=Files.createTempDirectory(build,"client-"); + new CodeGen(fixtures.resolve("library").toString(),lib.toString(),null, + "Counters",null,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + assertTrue(Files.isRegularFile(lib.resolve("Counters.o"))); + assertFalse(Files.exists(fixtures.resolve("client/Counters.smp"))); + var paths=new Ary<>(new String[]{lib.toString()}); + for( long seed : new long[]{2,13,123} ) { + CodeGen code=new CodeGen(fixtures.resolve("client").toString(),client.toString(),paths, + "Main",null,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + CompUnit imported=code._compunits.get("Counters"); + assertNotNull(imported); + assertNull("Library source must not be parsed",imported._src); + assertEquals(lib.resolve("Counters.o").toRealPath(),imported._obj.toPath().toRealPath()); + assertEquals("0",Eval2.eval(code,0)); + assertEquals("107",Eval2.eval(code,3)); + assertEquals("131",Eval2.eval(code,5)); + } + new CodeGen(fixtures.resolve("client").toString(),client.toString(),paths, + "Main",null,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + Path driver=client.resolve("driver.c"); + Files.writeString(driver,""" + #include <stdio.h> + __attribute__((CALL_CONV)) extern long long run(long long); + int main(void) { + long long args[]={0,3,5,0,3}, expected[]={0,107,231,0,307}; + for(int i=0;i<5;i++) { + long long actual=run(args[i]); + if(actual!=expected[i]) { + printf("call %d: got %lld, expected %lld\\n",i,actual,expected[i]); + return 1; + } + } + return 0; + } + """); + String exe=client.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString(); + TestC.linkExe(client.resolve("Main.o").toString(), + TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi",driver.toString(), + new Ary<>(new String[]{lib.resolve("Counters.o").toString()}),exe); + assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); + } +} diff --git a/src/test/java/com/seaofnodes/simple/Simple.java b/src/test/java/com/seaofnodes/simple/Simple.java index 437fcd81..1f1e8ab0 100644 --- a/src/test/java/com/seaofnodes/simple/Simple.java +++ b/src/test/java/com/seaofnodes/simple/Simple.java @@ -31,6 +31,7 @@ Options: '/', then all result files use relative paths to 'output', using a input file root-relative path. If --root is missing, the one result file is named 'output' + -L path - add an external Simple object search path --eval - (slowly) evaluate the compiled code in emulator --run - run the compiled code natively; this is the default --main - export the top-level initializer as C `main` @@ -104,6 +105,7 @@ Options: String cpu = null; String abi = null; String out = null; + ArrayList<String> libPaths = new ArrayList<>(); // Parse command line int i; for( i = 0; i < args.length; i++ ) { @@ -144,6 +146,9 @@ Options: case "--root": if( root!=null || i+1>=args.length ) bad_usage(); root=args[++i]; break; + case "-L": if (i + 1 >= args.length || args[i + 1].charAt(0) == '-') bad_usage(); + libPaths.add(args[++i]); + break; case "--cpu": if (cpu != null || i + 1 >= args.length || args[i + 1].charAt(0) == '-') bad_usage(); cpu = args[++i]; break; @@ -218,11 +223,16 @@ Options: if( cpu == null ) cpu = TestC.CPU_PORT; if( abi == null ) abi = TestC.CALL_CONVENTION; - // Read input file + String simpleHome = System.getenv("SIMPLE_HOME"); + if( simpleHome != null && !simpleHome.isEmpty() ) + libPaths.add(Path.of(simpleHome,cpu+"_"+abi).toString()); + + // Keep file-backed units so discovery sees parents and siblings. src = input_filename.isEmpty() ? getInput() : null; // Compilation pipeline - CodeGen code = new CodeGen(modPath.toString(), outPath.toString(), srcName, src, 456, true, TypeInteger.BOT ); + Ary<String> externPaths = libPaths.isEmpty() ? null : new Ary<>(libPaths.toArray(new String[0])); + CodeGen code = new CodeGen(modPath.toString(), outPath.toString(), externPaths, srcName, src, 456, true, TypeInteger.BOT ); code.driver(do_eval && !do_codegen ? Phase.TypeCheck : Phase.LastPhase, cpu,abi, false, emit_main || do_run || do_eval, dump); @@ -247,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(null), exe); + String result = TestC.gcc(code, null, null, "", exe); System.out.print(result); } } diff --git a/src/test/java/com/seaofnodes/simple/SpillStats.java b/src/test/java/com/seaofnodes/simple/SpillStats.java index 944f7ef5..d43f1a36 100644 --- a/src/test/java/com/seaofnodes/simple/SpillStats.java +++ b/src/test/java/com/seaofnodes/simple/SpillStats.java @@ -20,7 +20,7 @@ public class SpillStats extends RunListener { @Override public void testFinished(Description test) { _test = null; } public static void record25(CodeGen code) { - if( ACTIVE==null || ACTIVE._test==null || ACTIVE._test.getTestClass()!=ModulesTest.class ) return; + if( ACTIVE==null || ACTIVE._test==null || ACTIVE._test.getTestClass()!=Chapter25Test.class ) return; ACTIVE.record(code,"Chapter25",ACTIVE._test.getMethodName(),code._mach.getClass().getSimpleName(),code._callingConv); } @@ -54,7 +54,7 @@ public class SpillStats extends RunListener { String[] row = line.split("\t"); try { String src = Files.readString(dir.resolve(row[5])); - CodeGen code = new CheckedCodeGen(null,null,null,src,123L,true,TypeInteger.BOT) + CodeGen code = new CheckedCodeGen(null,null,null,null,src,123L,true,TypeInteger.BOT) .driver(CodeGen.Phase.RegAlloc,row[2],row[3]); ACTIVE.record(code,row[0],row[1],row[2],row[3]); } catch( Throwable error ) { @@ -65,7 +65,7 @@ public class SpillStats extends RunListener { } // Include the library's allocation even when make's sys.o is up to date. try { - CodeGen code = new CheckedCodeGen("src/main/smp","build/spill-sys","sys", + CodeGen code = new CheckedCodeGen("src/main/smp","build/spill-sys",null,"sys", Files.readString(Path.of("src/main/smp/sys.smp")),456L,true,TypeInteger.BOT) .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION); ACTIVE.record(code,"Chapter25","sys",TestC.CPU_PORT,TestC.CALL_CONVENTION); @@ -75,7 +75,7 @@ public class SpillStats extends RunListener { } JUnitCore junit = new JUnitCore(); junit.addListener(ACTIVE); - Result result = junit.run(ModulesTest.class); + Result result = junit.run(Chapter25Test.class); for( Failure failure : result.getFailures() ) System.err.println(failure.getTrace()); failures += result.getFailureCount(); if( failures!=0 ) System.out.println("FAILED: totals are not a complete comparison."); @@ -89,4 +89,4 @@ public class SpillStats extends RunListener { ACTIVE = null; if( failures!=0 ) System.exit(1); } -} +}
\ No newline at end of file diff --git a/src/test/java/com/seaofnodes/simple/StaticDataTest.java b/src/test/java/com/seaofnodes/simple/StaticDataTest.java index b8f5f1d1..88d0246d 100644 --- a/src/test/java/com/seaofnodes/simple/StaticDataTest.java +++ b/src/test/java/com/seaofnodes/simple/StaticDataTest.java @@ -69,7 +69,7 @@ public class StaticDataTest { @Test public void testNativeImage() throws Exception { Path dir=Path.of("build/objs/static-data"); Files.createDirectories(dir); - CodeGen code=new CodeGen(null,dir.toString(),"Test",SRC,123L,true, + CodeGen code=new CodeGen(null,dir.toString(),null,"Test",SRC,123L,true, TypeInteger.BOT) .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); TypeStruct ts=(TypeStruct)root(code).type; @@ -102,7 +102,7 @@ public class StaticDataTest { for( int i=0; i<2; i++ ) { String name=i==0 ? "A" : "B"; String src="{ i32 -> i32 } abs = \"C\"; val get"+name+"={ -> abs; }; return 0;"; - CodeGen code=new CodeGen(null,dir.toString(),name,src,123L,true,TypeInteger.BOT) + CodeGen code=new CodeGen(null,dir.toString(),null,name,src,123L,true,TypeInteger.BOT) .driver(CodeGen.Phase.TypeCheck); assertNull("The native address is initialized in the static image", code._stop.walk(n -> n instanceof StoreNode st && st._name.equals("abs") ? n : null)); @@ -140,18 +140,19 @@ public class StaticDataTest { } } - // Two source files must reference the same mutable class object. + // Two separately compiled users must reference the same mutable class object. @Test public void testSharedClass() throws Exception { Path dir=Path.of("build/objs/static-data-shared"); Files.createDirectories(dir); - Files.createDirectories(dir.resolve("Main")); + Path lib=dir.resolve("lib"); + Files.createDirectories(lib); + var paths=new Ary<>(new String[]{lib.toString()}); String[] names={"Globals","A","B"}; String[] sources={"int count=0;", "val a={ -> Globals; };", "val b={ -> Globals; };"}; for( int i=0; i<names.length; i++ ) - Files.writeString(dir.resolve("Main/"+names[i]+".smp"),sources[i]); - new CodeGen(dir.toString(),dir.toString(),"Main","val a={ -> A.a(); }; val b={ -> B.b(); };",123L,true,TypeInteger.BOT) - .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + new CodeGen(null,(i==0?lib:dir).toString(),i==0?null:paths,names[i],sources[i],123L,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); Path driver=dir.resolve("driver.c"); Files.writeString(driver,""" #include <stdint.h> @@ -164,9 +165,9 @@ public class StaticDataTest { } """); String exe=dir.resolve(TestC.OS.startsWith("Windows")?"test.exe":"test").toString(); - TestC.linkExe(dir.resolve("Main.o").toString(), + TestC.linkExe(lib.resolve("Globals.o").toString(), TestC.CALL_CONVENTION.equals("win64")?"ms_abi":"sysv_abi", - driver.toString(),null,exe); + driver.toString(),new Ary<>(new String[]{dir+"/A.o",dir+"/B.o"}),exe); assertEquals("",TestC.exec(TestC.TEST_TIMEOUT_SECONDS,exe)); } } diff --git a/src/test/java/com/seaofnodes/simple/TestC.java b/src/test/java/com/seaofnodes/simple/TestC.java index 164168f6..54adf3af 100644 --- a/src/test/java/com/seaofnodes/simple/TestC.java +++ b/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",base,src,126L,true,TypeInteger.BOT); + 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; } } diff --git a/src/test/java/com/seaofnodes/simple/TypeTest.java b/src/test/java/com/seaofnodes/simple/TypeTest.java index f6099607..113676ff 100644 --- a/src/test/java/com/seaofnodes/simple/TypeTest.java +++ b/src/test/java/com/seaofnodes/simple/TypeTest.java @@ -7,6 +7,20 @@ import static org.junit.Assert.assertSame; public class TypeTest { + @Test public void testBoundedBits() { + int[] sparse = XInt.make(XInt.make(2),65); + for( int[] bits : new int[][]{XInt.EMPTY,XInt.FULL,sparse,XInt.dual(sparse)} ) + for( int limit : new int[]{0,1,32,65,66,100} ) { + int next = XInt.next(bits,-1,limit); + for( int bit=0; bit<limit; bit++ ) + if( XInt.bit(bits,bit) ) { + Assert.assertEquals(bit,next); + next = XInt.next(bits,next,limit); + } + Assert.assertEquals(-1,next); + } + } + // Removing zero must not reset the widening history of a remaining range. @Test public void testNonZeroWidening() { for( byte widen=0; widen<=3; widen++ ) { diff --git a/src/test/java/com/seaofnodes/simple/codegen/IndirectCallTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/IndirectCallTestSupport.java deleted file mode 100644 index 3c27036a..00000000 --- a/src/test/java/com/seaofnodes/simple/codegen/IndirectCallTestSupport.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.seaofnodes.simple.codegen; - -import com.seaofnodes.simple.node.*; -import static org.junit.Assert.*; - -public class IndirectCallTestSupport { - public static void checkArmEncoding(CodeGen code, CallNode call) { - Encoding enc = new Encoding(code) { - @Override public short reg(Node n) { - assertSame(call.fptr(),n); - return 7; - } - }; - ((MachNode)call).encoding(enc); - assertEquals(0xd63f00e0,enc.read4(0)); // BLR X7 - } -} diff --git a/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java index 4398827e..292f1ae7 100644 --- a/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java +++ b/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java @@ -169,9 +169,9 @@ public class RegAllocTestSupport { // 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, + public CheckedCodeGen(String modDir, String buildDir, com.seaofnodes.simple.util.Ary<String> externPaths, String srcName, String src, long seed, boolean reset, TypeInteger arg) { - super(modDir,buildDir,srcName,src,seed,reset,arg); + super(modDir,buildDir,externPaths,srcName,src,seed,reset,arg); } @Override public CodeGen regAlloc() { super.regAlloc(); diff --git a/src/test/smp/examples/A_helloWorld.smp b/src/test/smp/examples/A_helloWorld.smp new file mode 100644 index 00000000..4ed6abdf --- /dev/null +++ b/src/test/smp/examples/A_helloWorld.smp @@ -0,0 +1,2 @@ +sys.io.p("Hello, World!\n"); +return 0; diff --git a/src/test/smp/examples/BubbleSort.smp b/src/test/smp/examples/BubbleSort.smp new file mode 100644 index 00000000..ca6906b5 --- /dev/null +++ b/src/test/smp/examples/BubbleSort.smp @@ -0,0 +1,36 @@ + +// An implementation of +// https://sampleprograms.io/projects/bubble-sort/ +// +// Bubble sort, with reading (parsing) and writing (formatting) the data. + +val sort = { !sys.aryi64 nums -> + sys.aryi64?! more = nums; + while( more ) { + more = null; + for( var i = 0; i < nums.len-1; i++ ) + if( nums.buf[i] > nums.buf[i+1] ) + more = nums.swap(i,i+1); + } +}; + +// Read one process argument of the form "[ 4, 3, 2, 1]" and print it sorted. +// args[0] is the executable name, as with native C argc/argv. +val main = { u8[~][] args -> + val usage = { -> + sys.io.p("Usage: please provide a list of at least two integers to sort in the format \"[1, 2, 3, 4, 5]\"\n"); + return 0; + }; + if( args# != 2 ) return usage(); + + var nums = sys.aryi64.parseAryI64(args[1]); + if( !nums ) return usage(); + if( nums.len < 2 ) return usage(); + + // Bubble sort array of integers + sort(nums); + + // Print the same bracketed array-list format accepted as input. + nums.str().add('\n').write(1); + return 0; +}; diff --git a/src/test/smp/examples/Capitalize.smp b/src/test/smp/examples/Capitalize.smp new file mode 100644 index 00000000..8e31c18f --- /dev/null +++ b/src/test/smp/examples/Capitalize.smp @@ -0,0 +1,22 @@ + +// An implementation of +// https://sampleprograms.io/projects/capitalize/ + +val main = { u8[~][] args -> + if( args# > 2 ) { + sys.io.p("Use quotes around multiple strings.\n"); + return 0; + } + if( args# != 2 || args[1]# == 0 ) { + sys.io.p("Usage: please provide a string\n"); + return 0; + } + + u8[~] input = args[1]; + var output = new u8[input# + 1]; + sys.ary.copy(output,0,input,0,input#); + output[0] = sys.char.toupper(input[0]); + output[input#] = '\n'; + sys.io.p(output); + return 0; +}; diff --git a/src/test/smp/examples/Dijkstra.smp b/src/test/smp/examples/Dijkstra.smp new file mode 100644 index 00000000..fd595eb5 --- /dev/null +++ b/src/test/smp/examples/Dijkstra.smp @@ -0,0 +1,72 @@ + +// An implementation of +// https://sampleprograms.io/projects/dijkstra/ + +val shortestPath = { !sys.aryi64 matrix, int nodes, int src, int dst -> + i64 infinity = (1 << 63)-1; + var dist = new i64[nodes]; + var visited = new sys.adt.bitset(); + // All nodes are infinitely far from each other + for( int i=0; i<nodes; i++ ) dist[i] = infinity; + // Starting point is zero far from starting point + dist[src] = 0; + + i64 result = -1; + int count = 0; + while( count < nodes && result < 0 ) { + int nearest = -1; + i64 best = infinity; + for( int node=0; node<nodes; node++ ) + if( !visited.get(node) && dist[node] < best ) { + nearest = node; + best = dist[node]; + } + if( nearest < 0 ) count = nodes; + else { + if( nearest == dst ) return best; + visited.set(nearest); + for( int node=0; node<nodes; node++ ) { + i64 weight = matrix.get(nearest*nodes + node); + if( weight > 0 && !visited.get(node) && best+weight < dist[node] ) + dist[node] = best+weight; + } + } + count++; + } + return result; +}; + +val usage = { -> + sys.io.p("Usage: please provide three inputs: a serialized matrix, a source node and a destination node\n"); + return 0; +}; + +val main = { u8[~][] args -> + // Exactly 3 args + if( args# != 4 ) return usage(); + // First is a serialized square matrix of positive numbers + var matrix = sys.aryi64.parseAryI64(args[1]); + if( !matrix ) return usage(); + if( !matrix.len ) return usage(); + // All numbers are positive + for( int i=0; i<matrix.len; i++ ) + if( matrix.get(i) < 0 ) return usage(); + // Matrix is square + int nodes = 0; + while( nodes*nodes < matrix.len ) nodes++; + if( nodes*nodes != matrix.len ) + return usage(); // Not a square matrix + + // src,dst are values within the matrix + i64 src = sys.aryu8.atoi(args[2]); + i64 dst = sys.aryu8.atoi(args[3]); + if( src < 0 || src >= nodes || dst < 0 || dst >= nodes ) + return usage(); // Source and destination land within the graph + + i64 cost = shortestPath(matrix,nodes,src,dst); + if( cost >= 0 ) { + new sys.aryu8().num(cost).add('\n').write(1); + return 0; + } + return usage(); +}; diff --git a/src/test/smp/examples/FileIO.smp b/src/test/smp/examples/FileIO.smp new file mode 100644 index 00000000..e48d435b --- /dev/null +++ b/src/test/smp/examples/FileIO.smp @@ -0,0 +1,50 @@ + +// An implementation of +// https://sampleprograms.io/projects/file-input-output/ + +val FILE = "output.txt"; + +val main = { u8[~][] args -> + var str = new sys.aryu8(); + str.addAry("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n"); + str.addAry("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n"); + str.addAry("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n"); + str.addAry("Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n"); + + // Create + i32 fd = sys.io.create(FILE); + if( fd == -1 ) return _error(); + // Write + int wrote = sys.io.writeOff(fd,str.buf,0,str.len); + if( wrote == -1 ) return _error(); + if( wrote != str.len ) return _error2(-2); + // Close + if( sys.io.close(fd) != 0 ) return _error(); + + // Open + i32 fd2 = sys.io.open(FILE); + if( fd2 == -1 ) return _error(); + // Read + var got = sys.io.read(fd2); + if( got# != str.len ) return _error2(-3); + // Close + if( sys.io.close(fd2) != 0 ) return _error(); + + // Check contents are equal + if( sys.ary.cmp(got,0,str.buf,0,str.len) ) + return _error2(-4); + + // Remove + if( sys.io.remove(FILE) != 0 ) return _error(); + + sys.io.p("File I/O succeeded\n"); + return 0; +}; + +val _error2 = { i32 errno -> + sys.io.remove(FILE); + new sys.aryu8().addAry("Failed with error code ").num(errno).add('\n').write(sys.libc.STDERR); + sys.libc._exit(errno); +}; + +val _error = { -> _error2(sys.libc.errno()); }; diff --git a/src/test/smp/separate/client/Main.smp b/src/test/smp/separate/client/Main.smp new file mode 100644 index 00000000..077f6575 --- /dev/null +++ b/src/test/smp/separate/client/Main.smp @@ -0,0 +1,6 @@ +val run={int n -> + if(!n) return 0; + val c=new Counters.Counter(n); + return c.get()+100*Counters.next_noInline(); +}; +return run(arg); diff --git a/src/test/smp/separate/library/Counters.smp b/src/test/smp/separate/library/Counters.smp new file mode 100644 index 00000000..a3e36562 --- /dev/null +++ b/src/test/smp/separate/library/Counters.smp @@ -0,0 +1,12 @@ +// A library can keep shared state and publish instance constructors. +int calls=0; +val next_noInline={ -> calls++; return calls; }; + +struct Counter { + val value=1; + new Counter={int n -> + value=0; + for(int i=0; i<n; i++) value=value*2+1; + }; + val get={ -> value; }; +}; |
