summaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
Diffstat (limited to 'src/test')
-rw-r--r--src/test/java/com/seaofnodes/simple/BoundaryTest.java7
-rw-r--r--src/test/java/com/seaofnodes/simple/CallEndTest.java85
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter10Test.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter17bTest.java3
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter20Test.java9
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter21Test.java11
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter25Test.java548
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter28bTest.java29
-rw-r--r--src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java202
-rw-r--r--src/test/java/com/seaofnodes/simple/Eval2.java167
-rw-r--r--src/test/java/com/seaofnodes/simple/IncrementalBuildTest.java192
-rw-r--r--src/test/java/com/seaofnodes/simple/IndirectCallTest.java32
-rw-r--r--src/test/java/com/seaofnodes/simple/ModulesTest.java248
-rw-r--r--src/test/java/com/seaofnodes/simple/NativeExecutionTest.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/ParseAllTest.java42
-rw-r--r--src/test/java/com/seaofnodes/simple/PhaseTest.java27
-rw-r--r--src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java263
-rw-r--r--src/test/java/com/seaofnodes/simple/SeparateCompilationTest.java91
-rw-r--r--src/test/java/com/seaofnodes/simple/Simple.java16
-rw-r--r--src/test/java/com/seaofnodes/simple/SpillStats.java10
-rw-r--r--src/test/java/com/seaofnodes/simple/StaticDataTest.java19
-rw-r--r--src/test/java/com/seaofnodes/simple/TestC.java59
-rw-r--r--src/test/java/com/seaofnodes/simple/TypeTest.java14
-rw-r--r--src/test/java/com/seaofnodes/simple/codegen/IndirectCallTestSupport.java17
-rw-r--r--src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java4
-rw-r--r--src/test/smp/examples/A_helloWorld.smp2
-rw-r--r--src/test/smp/examples/BubbleSort.smp36
-rw-r--r--src/test/smp/examples/Capitalize.smp22
-rw-r--r--src/test/smp/examples/Dijkstra.smp72
-rw-r--r--src/test/smp/examples/FileIO.smp50
-rw-r--r--src/test/smp/separate/client/Main.smp6
-rw-r--r--src/test/smp/separate/library/Counters.smp12
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; };
+};