diff options
| author | Cliff Click <cliffc@acm.org> | 2026-11-03 09:34:31 -0800 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-11-03 09:34:31 -0800 |
| commit | 8d92810d9a2545d851d5986715a98fda24bb3c3a (patch) | |
| tree | 1450eeb00da92bafcad6ed1bed7a586df7e974e4 /chapter29/src/test | |
| parent | bfd9af8f9518a6fe87cbb706813cd87c8dc5ea90 (diff) | |
| download | Simple-8d92810d9a2545d851d5986715a98fda24bb3c3a.tar.gz Simple-8d92810d9a2545d851d5986715a98fda24bb3c3a.zip | |
Early version of safe using partial construciton
Diffstat (limited to 'chapter29/src/test')
11 files changed, 443 insertions, 25 deletions
diff --git a/chapter29/src/test/java/com/seaofnodes/simple/ArithNodeTest.java b/chapter29/src/test/java/com/seaofnodes/simple/ArithNodeTest.java index e912f224..b28a6af3 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/ArithNodeTest.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/ArithNodeTest.java @@ -9,6 +9,29 @@ import org.junit.Test; import static org.junit.Assert.*; public class ArithNodeTest { + @Test public void testConstantFoldsRequireInitializedInputs() { + new CodeGen("return 0;"); + Node x = ConstantNode.seed(TypeScalar.BOT); + Node zero = ConstantNode.seed(TypeInteger.ZERO); + for( Node op : new Node[]{new MulNode(x,zero,(byte)1),new AndNode(null,x,zero), + new SubNode(x,x,(byte)1),new BoolNode.EQ(x,x,(byte)1), + new BoolNode.NE(x,x,(byte)1),new BoolNode.ULT(x,x),new NotNode(x)} ) { + assertSame(op.label(),TypeScalar.BOT,op.compute()); + Node replacement = op.idealize(); + assertTrue(op.label(),replacement==null || !replacement.compute().isConstant()); + } + // Once the family is known, compute owns the annihilator fold. + x._type = TypeInteger.BOT; + assertSame(TypeInteger.ZERO,new MulNode(x,zero,(byte)1).compute()); + assertSame(TypeInteger.TRUE,new BoolNode.EQ(x,x,(byte)1).compute()); + assertSame(TypeInteger.FALSE,new BoolNode.NE(x,x,(byte)1).compute()); + assertSame(TypeInteger.FALSE,new BoolNode.ULT(x,x).compute()); + x._type = TypeFloat.F64; + assertSame(TypeInteger.BOOL,new BoolNode.EQ(x,x,(byte)2).compute()); + x._type = TypeFloat.constant(Double.NaN); + assertSame(TypeInteger.FALSE,new BoolNode.EQ(x,x,(byte)2).compute()); + } + @Test public void testUnresolvedInput() { new CodeGen("return 0;").driver(CodeGen.Phase.Opto); Node lhs = ConstantNode.seed(TypeInteger.TRUE); @@ -16,9 +39,9 @@ public class ArithNodeTest { ArithNode add = new AddNode(lhs,rhs); assertSame(TypeInteger.constant(2),add.compute()); lhs._type = TypeScalar.BOT; - assertSame(Type.BOTTOM,add.compute()); + assertSame(TypeScalar.BOT,add.compute()); add.setMode((byte)1); - assertSame(Type.BOTTOM,add.compute()); + assertSame(TypeScalar.BOT,add.compute()); } @Test public void testFloatPrecision() { @@ -26,6 +49,10 @@ public class ArithNodeTest { assertEquals("1.6777217E7",Eval2.eval(code,16777216)); code = new CodeGen("f32 x = arg; f32 y = x+1.0; return y;").driver(CodeGen.Phase.TypeCheck); assertEquals("1.6777216E7",Eval2.eval(code,16777216)); + code = new CodeGen("flt x = 0.0; flt y = 0.0; if(arg) { x = 1.5; y = 3.5; } else { x = 2.5; y = 4.5; } return x+y;") + .driver(CodeGen.Phase.TypeCheck); + assertEquals("7.0",Eval2.eval(code,0)); + assertEquals("5.0",Eval2.eval(code,1)); } @Test public void testMonotoneCompute() { diff --git a/chapter29/src/test/java/com/seaofnodes/simple/BoundaryTest.java b/chapter29/src/test/java/com/seaofnodes/simple/BoundaryTest.java index de502371..10b7517e 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/BoundaryTest.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/BoundaryTest.java @@ -24,10 +24,7 @@ public class BoundaryTest { CodeGen code=new CodeGen(src).driver(CodeGen.Phase.LocalSched); assertEquals("2018",Eval2.eval(code,19)); assertEquals("0",Eval2.eval(code,0)); - code._stop.walk(n -> { - if( n instanceof StoreNode st ) assertEquals(0,st.cfg0().loopDepth()); - return null; - }); + // Private memory follows the loop; stores need not move to its exit. code=new CodeGen("struct Byte { new Byte={int n -> self.x=n; self.x+=3;}; u8 x; }; return new Byte(255).x;").driver(CodeGen.Phase.TypeCheck); assertEquals("2",Eval2.eval(code,0)); // data is still unset when the constructor reads and updates x. diff --git a/chapter29/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/chapter29/src/test/java/com/seaofnodes/simple/Chapter10Test.java index 2c9a69a7..1a954110 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/Chapter10Test.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/Chapter10Test.java @@ -158,7 +158,7 @@ else return v; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("Stop[ return Test.Vector2D; return MEM[ 2:___ 3:.x=0; 4:.y=0;]; return Test.Vector2D; ]", code.print()); + assertEquals("Stop[ return Test.Vector2D; return MemTop; return Test.Vector2D; ]", code.print()); } @Test @@ -196,7 +196,7 @@ while (arg) { return bar.a; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("Stop[ return Phi(Loop,0,(Phi_a+2)); return MEM[ 2:___ 3:.a=0;]; return Test.Bar; ]", code.print()); + assertEquals("Stop[ return Phi(Loop,0,(Phi_a+2)); return MemTop; return Test.Bar; ]", code.print()); } @Test @@ -318,7 +318,7 @@ while( i.x < i.len ) { return sum; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("Stop[ return Phi(Loop,0,(Phi(Loop,0,(Phi_x+1))+Phi_sum)); return MEM[ 2:___ 3:.x=0; 4:.len=0;]; return Test.Iter; ]", code.print()); + assertEquals("Stop[ return Phi(Loop,0,(Phi(Loop,0,(Phi_x+1))+Phi_sum)); return MemTop; return Test.Iter; ]", code.print()); } @@ -337,7 +337,7 @@ while(arg) { return ret; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("Stop[ return Phi(Loop,Test.s0,Phi(Region,Test.s0,Phi_ret)); return MEM[ 2:___ 3:.v0=0;]; return Test.s0; ]", code.print()); + assertEquals("Stop[ return Phi(Loop,Test.s0,Phi(Region,Test.s0,Phi_ret)); return MemTop; return Test.s0; ]", code.print()); } @Test @@ -390,7 +390,7 @@ s0 b = new s0(); return a.x * 100 + b.x * 10 + cnt; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("Stop[ return 112; return MEM[ 2:___ 3:___ 4:.x=1;]; return Test.s0; ]", code.print()); + assertEquals("Stop[ return 112; return MemTop; return Test.s0; ]", code.print()); } @@ -433,7 +433,7 @@ else return new s0; if(new s0.f0) return 0; """); code.driver(CodeGen.Phase.TypeCheck); - assertEquals("Stop[ return Test.s0; return MEM[ 2:___ 3:.f0=0;]; return Test.s0; ]", code.print()); + assertEquals("Stop[ return Test.s0; return MemTop; return Test.s0; ]", code.print()); } @Test @@ -461,7 +461,7 @@ s0 v1 = v0; return v1; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("Stop[ return (const)Test.s0; return MEM[ 2:___ 3:.f0=0;]; return Test.s0; ]", code.print()); + assertEquals("Stop[ return (const)Test.s0; return MemTop; return Test.s0; ]", code.print()); } diff --git a/chapter29/src/test/java/com/seaofnodes/simple/Chapter12Test.java b/chapter29/src/test/java/com/seaofnodes/simple/Chapter12Test.java index fae7611d..156e091d 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/Chapter12Test.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/Chapter12Test.java @@ -101,7 +101,7 @@ N n = new N(new M); return n.next; """); code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("Stop[ return (const)Test.M; return MEM[ 2:___ 3:.m=0;]; return Test.M; return MEM[ 2:___ 3:___ 4:___ 5:___ 6:___ 7:.next=Parm_m(Test.N.N,*Test.M {i64 !m; }); 8:.i=0;]; return MEM[ 2:___ 3:___ 4:___ 5:___ 6:___ 7:___ 8:.i=0;]; return Test.N; ]", code.print()); + assertEquals("Stop[ return (const)Test.M; return MemTop; return Test.M; return MemTop; return MemTop; return Test.N; ]", code.print()); } @Test diff --git a/chapter29/src/test/java/com/seaofnodes/simple/Chapter18Test.java b/chapter29/src/test/java/com/seaofnodes/simple/Chapter18Test.java index e5813eae..ecb2b934 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/Chapter18Test.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/Chapter18Test.java @@ -347,7 +347,7 @@ for(;;) { } """); code.driver(Phase.LocalSched); - assertEquals("Stop[ return Top; return Parm_i(x,i64); ]", code.print()); + assertEquals("return Top;", code.print()); assertNull( Eval2.eval( code, 0 ) ); // Infinite loop, <clinit> never exits } diff --git a/chapter29/src/test/java/com/seaofnodes/simple/Chapter19Test.java b/chapter29/src/test/java/com/seaofnodes/simple/Chapter19Test.java index d5fe207a..e6249d82 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/Chapter19Test.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/Chapter19Test.java @@ -118,7 +118,7 @@ val hashCode = { !String self -> hashCode(new String("Hello, World!")); """; CodeGen code = new CodeGen(src).driver(Phase.LocalSched); - assertEquals("Stop[ return Phi(Region,123456789,(!=0)Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash)))); return MEM[ 2:___ 3:___ 4:___ 5:.cs=Parm_s(Test.String.String,*[]u8[final]); 6:._hashCode=0;]; return MEM[ 2:___ 3:___ 4:___ 5:___ 6:._hashCode=0;]; return Test.String; return Phi(Region,0,1,0,1); return Phi(Region,(!=0)._hashCode,Phi(Region,123456789,(!=0)Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash))))); ]", code.print()); + assertEquals("Stop[ return Phi(Region,123456789,(!=0)Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash)))); return MemTop; return MemTop; return Test.String; return Phi(Region,0,1,0,1); return Phi(Region,(!=0)._hashCode,Phi(Region,123456789,(!=0)Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash))))); ]", code.print()); assertEquals("4029215624828139541", Eval2.eval(code, 2)); } diff --git a/chapter29/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java b/chapter29/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java new file mode 100644 index 00000000..cea6ca91 --- /dev/null +++ b/chapter29/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java @@ -0,0 +1,135 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.parser.Parser; +import org.junit.Test; +import static org.junit.Assert.*; + +public class ConstructionGroupTest { + private static final String CHILD = """ + struct Child { + Test.Parent? parent; + int value = 5; + new Child = { Test.Parent? ^par -> parent = par; }; + }; + val read = { Child c -> return c.value; }; + """; + + private String parent(String body) { + return CHILD+"struct Parent { Child child; int~ payload; new Parent = { int n -> "+body+" }; };"; + } + + private void rejects(String src, String message) { + try { + new CodeGen(src).parse(); + fail("Accepted: "+src); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains(message)); + } + } + + @Test public void testCyclesAndIndependentAllocations() { + String src = parent("val detached = new Child(null); payload = n+read(detached); child = new Child(self);")+ + "val p = new Parent(arg); val local = new Child(p); val par = local.parent; return (par ? par.payload : 0)+p.child.value;"; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck); + assertEquals("17",Eval2.eval(code,7)); + assertEquals("10",Eval2.eval(code,0)); + } + new CodeGen(src.replace("Child","Child_noInline")) + .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION,true,false,0); + } + + @Test public void testNoIncompleteLookupOrEscape() { + // This lookup supplies a live result. Inlining must not turn self + // complete inside the body before checking the use of child. + rejects(parent("child = new Child(self); payload = child.parent==null;")+ + "return new Parent(arg).payload;","incomplete object"); + rejects(parent("payload = 1; child = new Child(self); int x = child.parent==null;")+"return 0;","incomplete object"); + rejects(parent("payload = 1; child = new Child(self); read(child);")+"return 0;","ordinary function"); + rejects(parent("payload = 1; child = new Child(self);").replace("parent = par;","parent = par; value = par.payload;")+ + "return 0;","incomplete object"); + rejects(""" + struct Box { Test.C? value; }; val box = new Box; + struct C { int~ x; new C = { -> box.value = self; x = 1; }; }; + return 0; + ""","ordinary object"); + rejects("struct C { C next = self; int~ x; new C = { -> int y = next.x; x = 7; }; }; return 0;", + "incomplete object"); + } + + @Test public void testAliasesAndLoopBackedges() { + String src = parent("val alias = self; alias.payload = n; child = new Child(alias);")+"return new Parent(arg).child.value;"; + assertEquals("5",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),3)); + rejects(parent("payload = 1; child = new Child(self); Child? !c = null; while(n--) { if(c) read(c); c = child; }")+ + "return 0;","ordinary function"); + rejects(parent("payload = 1; child = new Child(self); Parent? !p = null; while(n--) { val c = new Child(p); read(c); p = self; }")+ + "return 0;","ordinary function"); + rejects(parent("payload = 1; child = new Child(self); val c = n ? new Child(null) : child; read(c);")+ + "return 0;","ordinary function"); + } + + @Test public void testConstructorOnlyQualifier() { + rejects("struct C {}; val f = { C ^c -> return 0; }; return 0;","constructor"); + rejects("struct C { new C = { int ^x -> }; }; return 0;","struct parameter"); + rejects(parent("payload = 1; child = new Child(self);").replace("^par","par")+"return 0;","without '^'"); + CodeGen code = new CodeGen(parent("payload = 1; child = new Child(self);")+"return 0;") + .driver(CodeGen.Phase.TypeCheck); + try { + code.serialize(); + fail("Exported a constructor qualifier without its contract"); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("Separate compilation")); + } + } + + @Test public void testCompleteLoopAllocations() { + // Optimistic types prove the cycle p -> new Link(p) -> p complete. + String src = """ + struct Link { + Link? next; + int value = 5; + new Link = { Link? ^n -> next = n; }; + }; + struct Root { + Link head; + int value; + new Root = { int n -> + Link? !p = null; + while(n--) p = new Link(p); + if(!p) p = new Link(null); + value = p.value; + head = p; + }; + }; + val root = new Root(arg); + return root.value + root.head.value; + """; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("10",Eval2.eval(code,0)); + assertEquals("10",Eval2.eval(code,3)); + // Dead calls discard their arguments before a construction result exists. + assertEquals("0",Eval2.eval(new CodeGen(CHILD+"struct Parent {}; return 0; new Child(null);") + .driver(CodeGen.Phase.TypeCheck),0)); + } + + @Test public void testDeclaredQualifierAndOwnReceiver() { + // A complete caller does not grant permission to read a ^ formal. + rejects(""" + struct Parent { int payload = 7; }; + struct Child { + int value; + new Child = { Parent ^par -> value = par.payload; }; + }; + return new Child(new Parent).value; + ""","incomplete object"); + // The self exception belongs to the receiver, not every pointer of its class. + rejects(""" + struct C { + int !value; + new C = { !C? ^other -> if(other) other.value = 1; value = 2; }; + }; + return new C(new C(null)).value; + ""","incomplete object"); + } +} diff --git a/chapter29/src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java b/chapter29/src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java new file mode 100644 index 00000000..c570e643 --- /dev/null +++ b/chapter29/src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java @@ -0,0 +1,222 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.parser.Parser; +import org.junit.Test; +import static org.junit.Assert.*; + +@org.junit.Ignore("Deferred: inferred field requirements and incomplete-object traversal") +public class ConstructionRequirementsTest { + private static final String CHILD = """ + struct Child { + Test.Parent parent; + int~ width; + new Child = { Test.Parent ^par -> + parent = par; + width = par.width; + }; + }; + """; + + private String parent(String body) { + return CHILD+"struct Parent { Child child; int~ width; new Parent = { int n -> "+body.replace("arg","n")+" }; };"; + } + + private void rejects(String src, String message) { + try { + new CodeGen(src).parse(); + fail("Accepted: "+src); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains(message)); + } + } + + @Test public void testCycleAndRequiredField() { + String src = parent("width = arg+10; child = new Child(self);")+"return new Parent(arg).child.parent.child.width;"; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("17",Eval2.eval(code,7)); + assertEquals("10",Eval2.eval(code,0)); + code = new CodeGen(parent("width = arg+10; child = new Child(self);")+ + "val p = new Parent(arg); return new Child(p).parent.child.width;").driver(CodeGen.Phase.TypeCheck); + assertEquals("17",Eval2.eval(code,7)); + code = new CodeGen(CHILD+"struct Parent { int width = 10; Child child = new Child(self); }; return new Parent().child.width;") + .driver(CodeGen.Phase.TypeCheck); + assertEquals("10",Eval2.eval(code,0)); + rejects(parent("child = new Child(self); width = 10;")+"return 0;","requires initialized field 'width'"); + rejects(parent("if(arg) width = 10; child = new Child(self); width = 20;")+"return 0;","requires initialized field 'width'"); + code = new CodeGen(parent("child = new Child(self); width = 10;").replace("width = par.width;","width = 5;")+ + "return new Parent(arg).child.parent.width;").driver(CodeGen.Phase.TypeCheck); + assertEquals("10",Eval2.eval(code,0)); + // Even if the comparison folds using the non-null declared type, + // accessing child still requires that field to have been initialized. + rejects(parent("width = 10; child = new Child(self);").replace("width = par.width;","width = par.child==null;")+ + "return 0;","requires initialized field 'child'"); + } + + @Test public void testNoEscapeOrFieldLaundering() { + rejects("struct C { C next = self; int~ payload; new C = { -> payload = next.payload; }; }; return 0;", + "might not be initialized"); + CodeGen code = new CodeGen("struct C { C next = self; int payload; new C = { -> payload = 17; payload = next.payload; }; }; return new C().next.payload;") + .driver(CodeGen.Phase.TypeCheck); + assertEquals("17",Eval2.eval(code,0)); + rejects(""" + struct C { + C next; int~ payload; + new C = { -> next = self; payload = next.payload; }; + }; return 0; + ""","might not be initialized"); + rejects(parent("width = 10; child = new Child(self); use(child);")+ + "val use = { Child c -> return c.parent.width; }; return 0;","ordinary function"); + rejects(CHILD+""" + struct Parent { + Child child; int~ width; + new Parent = { -> width = 10; child = new Child(self); int x = child.parent.width; }; + }; return 0; + ""","Traversing an unfinished reference field"); + rejects(""" + struct C { + int x; + val get = { -> return x; }; + new C = { -> x = get(); }; + }; return 0; + ""","ordinary function"); + rejects(""" + struct Box { Test.C? value; }; val box = new Box; + struct C { new C = { -> box.value = self; }; }; return 0; + ""","ordinary object"); + } + + @Test public void testConstructorOnlyQualifier() { + rejects("struct C {}; val f = { C ^c -> return 0; }; return 0;","constructor"); + rejects("struct C { new C = { int ^x -> }; }; return 0;","struct parameter"); + String src = parent("width = 10; child = new Child(self);")+"return 0;"; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + try { + code.serialize(); + fail("Exported an unrecorded construction contract"); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("Separate compilation")); + } + } + + @Test public void testTransitiveRequirementsAndAliases() { + String child = """ + struct Leaf { + int~ width; + new Leaf = { Test.Parent ^par -> val alias = par; width = alias.width; }; + }; + struct Child { + Test.Parent parent; Leaf leaf; + new Child = { Test.Parent ^par -> parent = par; leaf = new Leaf(par); }; + }; + """; + String parent = "struct Parent { Child child; int~ width; new Parent = { int n -> %s }; };"; + String good = child+parent.formatted("width = n+10; val alias = self; child = new Child(alias);"); + CodeGen code = new CodeGen(good+"return new Parent(arg).child.leaf.width;").driver(CodeGen.Phase.TypeCheck); + assertEquals("17",Eval2.eval(code,7)); + rejects(child+parent.formatted("child = new Child(self); width = 10;")+"return 0;", + "requires initialized field 'width'"); + } + + @Test public void testLoopsAndUninlinedConstructor() { + String src = parent("width = 1; for(int i = 0; i < arg; i++) width += 2; child = new Child(self);")+ + "return new Parent(arg).child.width;"; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(src.replace("Child","Child_noInline"),seed,true).driver(CodeGen.Phase.TypeCheck); + assertEquals("7",Eval2.eval(code,3)); + assertEquals("1",Eval2.eval(code,0)); + } + new CodeGen(src.replace("Child","Child_noInline")) + .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION,true,false,0); + rejects(parent("for(int i = 0; i < arg; i++) width = 2; child = new Child(self); width = 10;")+"return 0;", + "requires initialized field 'width'"); + // The assignment on the backedge introduces the unfinished alias + // after the call was parsed. It must not pass as an ordinary Child. + rejects(parent("width = 10; child = new Child(self); Child? !c = null; while(arg--) { use(c); c = child; }")+ + "val use = { Child? c -> return 0; }; return 0;","ordinary function"); + } + + @Test public void testNullableFieldAndDuplicateArguments() { + String src = """ + struct Child { + Test.Parent parent; int~ width; + new Child = { Test.Parent ^a, Test.Parent ^b -> + parent = a; width = a.width+b.width+(a.optional==null); + }; + }; + struct Parent { + Child child; int width; Child? optional; + new Parent = { -> width = 10; child = new Child(self,self); }; + }; + return new Parent().child.width; + """; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + assertEquals("21",Eval2.eval(code,0)); + } + + @Test public void testNullableUnfinishedParameter() { + String child = """ + struct Child { + Test.Parent? parent; int width; + new Child = { Test.Parent? ^par -> + parent = par; width = par ? par.width : 0; + }; + }; + val read = { Child c -> return c.width; }; + """; + String parent = """ + struct Parent { + Child child; int~ width; + new Parent = { int n -> + val detached = new Child(null); + width = n+read(detached); + child = new Child(self); + }; + }; + """; + CodeGen code = new CodeGen(child+parent+""" + val p = new Parent(arg); + val leaf = new Child(p); + return read(leaf)+read(p.child); + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("14",Eval2.eval(code,7)); + assertEquals("0",Eval2.eval(code,0)); + rejects(child+parent.replace("width = n+read(detached);","").replace("child = new Child(self);", + "child = new Child(self); width = n;"),"requires initialized field 'width'"); + rejects(child+parent.replace("child = new Child(self);","child = new Child(self); read(child);"), + "ordinary function"); + try { + new CodeGen(child.replace("par ? par.width : 0","par.width")+parent+ + "return new Child(null).width;").driver(CodeGen.Phase.TypeCheck); + fail("Dereferenced null constructor argument"); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().toLowerCase().contains("null")); + } + } + + @Test public void testDistinctCallTimeMemory() { + String src = CHILD+""" + struct Parent { + Child child; Child second; int~ width; + new Parent = { int n -> + width = n+1; child = new Child(self); + width = n+2; second = new Child(self); + width = n+3; + }; + }; + val p = new Parent(arg); return p.child.width*100+p.second.width*10+p.width; + """; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(src.replace("Child","Child_noInline"),seed,true).driver(CodeGen.Phase.TypeCheck); + assertEquals("123",Eval2.eval(code,0)); + assertEquals("456",Eval2.eval(code,3)); + } + new CodeGen(src.replace("Child","Child_noInline")) + .driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION,true,false,0); + String loop = parent("width = 1; child = new Child(self); for(int i = 0; i < arg; i++) { width += 1; child = new Child(self); } width += 10;")+ + "val p = new Parent(arg); return p.child.width*100+p.width;"; + CodeGen code = new CodeGen(loop.replace("Child","Child_noInline")).driver(CodeGen.Phase.TypeCheck); + assertEquals("414",Eval2.eval(code,3)); + assertEquals("111",Eval2.eval(code,0)); + } +} diff --git a/chapter29/src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java b/chapter29/src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java index 6a229a4f..cbb9ee37 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java @@ -6,6 +6,48 @@ import org.junit.Test; import static org.junit.Assert.*; public class ConstructorCompletionTest { + @Test public void testFieldMemoryDuringInitialization() { + CodeGen code = new CodeGen(""" + struct A { + int x = 1; + x = 2; + int y = self.x; + new A = { -> self.x = 3; y += x; }; + }; + val a = new A; return a.x*10+a.y; + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("35",Eval2.eval(code,0)); + code = new CodeGen(""" + struct A { + int[] data = new int[3]; + new A = { -> for(int i = 0; i < 3; i++) data[i] = i+1; }; + }; + return new A().data[2]; + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("3",Eval2.eval(code,0)); + for( String body : new String[]{ + "int~ x; int y = x; new A = { -> x = 7; };", + "int~ x; int y = x*0; new A = { -> x = 7; };", + // Constructor entry includes the initializer's early return. + // Its later Store of 2 must not excuse the earlier read of x. + "int~ x; if(arg) return 0; x = 1; new A = { -> int y = x; x = 2; };" + } ) + try { + new CodeGen("struct A { "+body+" }; return 0;").parse(); + fail("Read a field before its constructor wrote it"); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains("might not be initialized")); + } + } + + @Test public void testRequiredNumberBesideNullableDefault() { + CodeGen code = new CodeGen(""" + struct A { int~ x; A? p; new A = { -> x = 10; }; }; + return new A().x; + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("10",Eval2.eval(code,0)); + } + private void rejects(String fields, String field, boolean explicit) { String src = "struct B {}; struct A { "+fields+" }; return 0;"; try { diff --git a/chapter29/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/chapter29/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java index 38b94693..66fdacc3 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java @@ -2,7 +2,6 @@ package com.seaofnodes.simple; import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.simple.codegen.CodeGen; -import com.seaofnodes.simple.node.StoreNode; import org.junit.Test; import static org.junit.Assert.*; @@ -20,16 +19,12 @@ public class FinalConstructionTest { return new C(arg).get(); """; - @Test public void testFinalConstructorLocals() throws java.io.IOException { + @Test public void testFinalConstructorFields() throws java.io.IOException { var code=new CodeGen(LOOP).driver(CodeGen.Phase.LocalSched); assertEquals("1",Eval2.eval(code,0)); assertEquals("15",Eval2.eval(code,3)); assertEquals("63",Eval2.eval(code,5)); - code._stop.walk(n -> { - if( n instanceof StoreNode st && st._name.equals("x") ) - assertEquals("Final field writes belong after the loop",0,st.cfg0().loopDepth()); - return null; - }); + // Writes are ordinary Stores and may remain inside the loop. String[] sources={ "struct C { val x=1; new C={->x=1; x=2;}; }; return new C().x;", "struct C { int ~x; new C={int n -> self.x=1; for(int i=0;i<n;i++) self.x*=2;}; }; return new C(arg).x;", diff --git a/chapter29/src/test/java/com/seaofnodes/simple/TypeTest.java b/chapter29/src/test/java/com/seaofnodes/simple/TypeTest.java index b4055eef..68239396 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/TypeTest.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/TypeTest.java @@ -147,7 +147,7 @@ public class TypeTest { Assert.assertNotEquals(ptr1, ptr2); Assert.assertEquals(ptr1, ptr1.dual().dual()); - Assert.assertEquals(TypeMemPtr.makeNullable(TypeStruct.BOT).withAccess(false), ptr1.meet(ptr2nil)); + Assert.assertEquals(TypeMemPtr.makeNullable(TypeStruct.BOT).withAccess(false).withPartial(false), ptr1.meet(ptr2nil)); TypeMemPtr TOP = TypeMemPtr.TOP; TypeMemPtr BOT = TypeMemPtr.makeNullable(TypeStruct.BOT); @@ -156,7 +156,7 @@ public class TypeTest { Type PTR_meet_NULL = NULL.meet(PTR); Assert.assertEquals(BOT, PTR_meet_NULL); Type ptr1_meet_ptr2 = ptr1.meet(ptr2); - Assert.assertEquals(PTR.withAccess(false), ptr1_meet_ptr2); + Assert.assertEquals(PTR.withAccess(false).withPartial(false), ptr1_meet_ptr2); Type NULL_join_ptr1 = NULL.join(ptr1); Assert.assertEquals(TypePtr.XNPTR, NULL_join_ptr1); Type NULL_join_PTR = PTR.join(NULL); |
