From 7914681413f3e3562733786aa02b1fc7adfbe9b0 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Mon, 10 Aug 2026 11:11:18 -0700 Subject: Chapter 28b: Separate Compilation Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki Co-authored-by: Claude Opus 5 --- .../java/com/seaofnodes/simple/BoundaryTest.java | 7 +- .../java/com/seaofnodes/simple/Chapter10Test.java | 2 + .../java/com/seaofnodes/simple/Chapter17bTest.java | 3 +- .../java/com/seaofnodes/simple/Chapter25Test.java | 528 +++++++++++++++++++++ .../simple/ClassInitDependenciesTest.java | 165 ++++++- .../java/com/seaofnodes/simple/ModulesTest.java | 248 ---------- .../com/seaofnodes/simple/NativeExecutionTest.java | 1 + src/test/java/com/seaofnodes/simple/PhaseTest.java | 27 +- .../seaofnodes/simple/SeparateCompilationTest.java | 90 ++++ src/test/java/com/seaofnodes/simple/Simple.java | 16 +- .../java/com/seaofnodes/simple/SpillStats.java | 10 +- .../java/com/seaofnodes/simple/StaticDataTest.java | 19 +- src/test/java/com/seaofnodes/simple/TestC.java | 2 +- .../simple/codegen/RegAllocTestSupport.java | 4 +- 14 files changed, 819 insertions(+), 303 deletions(-) create mode 100644 src/test/java/com/seaofnodes/simple/Chapter25Test.java delete mode 100644 src/test/java/com/seaofnodes/simple/ModulesTest.java create mode 100644 src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java (limited to 'src/test/java/com/seaofnodes') 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/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/Chapter25Test.java b/src/test/java/com/seaofnodes/simple/Chapter25Test.java new file mode 100644 index 00000000..31a616b5 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/Chapter25Test.java @@ -0,0 +1,528 @@ +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 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 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[]{dir+"/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 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", "sys/adt", "sys/adt/bitset", "sys/ary", + "sys/aryi64", "sys/aryu8", "sys/char", "sys/io", "sys/libc", "sys/scan"},sys_elf._deps); + 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_FILE.toString(),"-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")); + + TestC.linkExe(obj,null,null,new Ary<>(new String[]{SYS_FILE.toString()}),exe); + String rez = TestC.exec(TestC.TEST_TIMEOUT_SECONDS,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/ClassInitDependenciesTest.java b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java index 6b4a987d..47e10f97 100644 --- a/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java +++ b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java @@ -57,7 +57,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 +194,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 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 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,28 +439,30 @@ 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)); 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 - __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 { @@ -376,29 +471,49 @@ public class ClassInitDependenciesTest { 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;")); 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 - __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("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/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 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/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/SeparateCompilationTest.java b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java new file mode 100644 index 00000000..4576bc94 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java @@ -0,0 +1,90 @@ +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); + 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 + __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..7cfdf57d 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 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 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(outBase+".o", null, null, "", TestC.linkObjs(externPaths), 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 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 @@ -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..b08be824 100644 --- a/src/test/java/com/seaofnodes/simple/TestC.java +++ b/src/test/java/com/seaofnodes/simple/TestC.java @@ -120,7 +120,7 @@ public abstract class TestC { 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); 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 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(); -- cgit v1.2.3