From 772e3a1036d571de55b178ebd6d11ee5140d4e36 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Mon, 10 Aug 2026 11:11:18 -0700 Subject: Chapter 29: Tech Debt Cleanup Co-authored-by: Hsiang-Ying Fu <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: fuhsnn <66062782+fuhsnn@users.noreply.github.com> Co-authored-by: Symious <14933944+symious@users.noreply.github.com> Co-authored-by: Jun Kawasaki Co-authored-by: Claude Opus 5 --- .../java/com/seaofnodes/simple/ArithNodeTest.java | 32 ++- .../java/com/seaofnodes/simple/BoundaryTest.java | 6 +- .../java/com/seaofnodes/simple/CallEndTest.java | 54 +++++ .../java/com/seaofnodes/simple/Chapter10Test.java | 1 + .../java/com/seaofnodes/simple/Chapter12Test.java | 13 ++ .../java/com/seaofnodes/simple/Chapter17bTest.java | 13 +- .../java/com/seaofnodes/simple/Chapter18Test.java | 2 + .../java/com/seaofnodes/simple/Chapter19Test.java | 2 +- .../java/com/seaofnodes/simple/Chapter20Test.java | 2 + .../java/com/seaofnodes/simple/Chapter22Test.java | 1 + .../java/com/seaofnodes/simple/Chapter25Test.java | 1 + .../simple/ClassInitDependenciesTest.java | 1 + .../seaofnodes/simple/CompoundAssignmentTest.java | 1 + .../seaofnodes/simple/ConstructionGroupTest.java | 162 +++++++++++++++ .../simple/ConstructionRequirementsTest.java | 222 +++++++++++++++++++++ .../simple/ConstructorCompletionTest.java | 119 +++++++++++ src/test/java/com/seaofnodes/simple/Eval2.java | 2 +- .../seaofnodes/simple/FinalConstructionTest.java | 10 +- .../java/com/seaofnodes/simple/ParserTest.java | 33 +++ .../com/seaofnodes/simple/PartialReadTest.java | 155 ++++++++++++++ .../seaofnodes/simple/SelectiveLoadingTest.java | 1 + src/test/java/com/seaofnodes/simple/TypeTest.java | 24 ++- .../simple/codegen/RegAllocTestSupport.java | 25 +++ .../java/com/seaofnodes/simple/fuzzer/Fuzzer.java | 2 +- .../com/seaofnodes/simple/fuzzer/FuzzerUtils.java | 2 +- .../seaofnodes/simple/fuzzer/ScriptGenerator.java | 4 +- 26 files changed, 864 insertions(+), 26 deletions(-) create mode 100644 src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java create mode 100644 src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java create mode 100644 src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java create mode 100644 src/test/java/com/seaofnodes/simple/ParserTest.java create mode 100644 src/test/java/com/seaofnodes/simple/PartialReadTest.java (limited to 'src/test/java/com/seaofnodes/simple') diff --git a/src/test/java/com/seaofnodes/simple/ArithNodeTest.java b/src/test/java/com/seaofnodes/simple/ArithNodeTest.java index e912f224..1e6c6fc0 100644 --- a/src/test/java/com/seaofnodes/simple/ArithNodeTest.java +++ b/src/test/java/com/seaofnodes/simple/ArithNodeTest.java @@ -9,6 +9,30 @@ 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), + new ConvertNode(TypeInteger.BOT,x),new ToIntegerNode(x),new ToFloatNode(x),new PtrToIntNode(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 +40,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 +50,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/src/test/java/com/seaofnodes/simple/BoundaryTest.java b/src/test/java/com/seaofnodes/simple/BoundaryTest.java index e196b93f..10b7517e 100644 --- a/src/test/java/com/seaofnodes/simple/BoundaryTest.java +++ b/src/test/java/com/seaofnodes/simple/BoundaryTest.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.*; @@ -23,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/src/test/java/com/seaofnodes/simple/CallEndTest.java b/src/test/java/com/seaofnodes/simple/CallEndTest.java index a174bdf7..f57fd96a 100644 --- a/src/test/java/com/seaofnodes/simple/CallEndTest.java +++ b/src/test/java/com/seaofnodes/simple/CallEndTest.java @@ -7,6 +7,60 @@ import org.junit.Test; import static org.junit.Assert.*; public class CallEndTest { + // An unknown value can become a private object or callback. Both escape + // sinks must reserve those possibilities before the value is known. + @Test public void testUnknownEscapesRefineToPrivateValues() { + CodeGen code = new CodeGen("return 0;").parse(); + code._phase = CodeGen.Phase.IterPostOpto; + StopCUNode stop = code.compunit()._stop; + stop._type = TypeTuple.make(Type.CONTROL,TypeMem.START,Type.BOTTOM); + ReturnNode ret = (ReturnNode)stop.in(0); + TypeFunPtr callback = TypeFunPtr.make1((byte)2,true,TypeFunPtr.TEMPTY,TypeInteger.BOT,100); + TypeMemPtr object = TypeMemPtr.make(TypeStruct.make("_Private",false, + Field.make("callback",callback,100,false))); + code.externFunc(101,"retain"); + TypeFunPtr retain = TypeFunPtr.make1((byte)2,true, + new Type[]{TypeScalar.BOT},TypeInteger.BOT,101); + Node value = ConstantNode.raw(TypeScalar.BOT); + CallNode call = new CallNode(null,ConstantNode.raw(Type.CONTROL), + ConstantNode.raw(TypeMem.START),value,ConstantNode.raw(retain)); + call._type = Type.CONTROL; + GlobalEscapeNode global = new GlobalEscapeNode(); + global._type = TypeMem.START; + for( Type broad : new Type[]{Type.BOTTOM,TypeScalar.BOT} ) { + value._type = broad; + ret._type = TypeTuple.make(Type.CONTROL,TypeMem.START,broad); + TypeTuple unknownStop = stop.compute(); + TypeMem unknownGlobal = global.compute(); + assertSame(XInt.FULL,((TypeMem)unknownStop.mem())._escFs); + assertSame(XInt.FULL,((TypeMem)unknownStop.mem())._escAs); + assertSame(XInt.FULL,unknownGlobal._escFs); + assertSame(XInt.FULL,unknownGlobal._escAs); + for( Type precise : new Type[]{object,callback} ) { + assertTrue(precise.isa(broad)); + value._type = precise; + ret._type = TypeTuple.make(Type.CONTROL,TypeMem.START,precise); + assertTrue(stop.compute().isa(unknownStop)); + assertTrue(global.compute().isa(unknownGlobal)); + } + } + } + + @Test public void testUnresolvedOffsetInInlineBody() { + CodeGen code = new CodeGen("val f = { int[] data -> return data#; }; return f;").parse(); + for( FunNode fun : code._linker ) + if( fun!=null && "f".equals(fun._name) ) { + // Keep the offset unresolved: an early inline must copy it + // with its data parameter, not share it with the old body. + FldOffNode off = new FldOffNode(fun.parm(3),"<<",null).init(); + fun.ret().setDef(2,off); + assertSame(Type.BOTTOM,off._type); + assertTrue("Unresolved field offset belongs to the function",fun.body().get(off._nid)); + return; + } + fail("Missing test function"); + } + @Test public void testReturnWideningMonotonicity() { new CodeGen("return 0;").parse(); TypeFunPtr sig = TypeFunPtr.make1((byte)2,true,TypeFunPtr.TEMPTY,TypeInteger.I32,100); diff --git a/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/src/test/java/com/seaofnodes/simple/Chapter10Test.java index 165eba21..eadaf9fe 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter10Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter10Test.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.simple.codegen.CodeGen; import org.junit.Test; import static org.junit.Assert.assertEquals; diff --git a/src/test/java/com/seaofnodes/simple/Chapter12Test.java b/src/test/java/com/seaofnodes/simple/Chapter12Test.java index 973b15d9..c6817308 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter12Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter12Test.java @@ -79,6 +79,19 @@ return f0.i.f.i.i; assertEquals("return 17;", code.print()); } + @Test public void testPrivateRecursiveReturn() { + CodeGen code = new CodeGen(""" + struct _A { !Test._B? !b; }; + struct _B { !_A? !a; }; + !_A !_a = new _A; + !_B !_b = new _B; + _a.b = _b; + _b.a = _a; + return _a.b.a.b; + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("return Test._B;",code.print()); + } + @Test public void testNullRef0() { 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 4019fd38..98197b77 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter17bTest.java +++ b/src/test/java/com/seaofnodes/simple/Chapter17bTest.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.simple.codegen.CodeGen; import org.junit.Ignore; import org.junit.Test; @@ -292,7 +293,9 @@ struct A { !B !b; new A = { -> A x = new A; """); try { code.driver(CodeGen.Phase.TypeCheck); fail(); } - catch( Exception e ) { assertEquals("Field 'b' might not be initialized",e.getMessage()); } + // The unused constructor return value can die; its unwritten field + // still fails the completion check at TypeCheck. + catch( Exception e ) { assertEquals("'Test.A' is not fully initialized, field 'b' is only partially set in the constructor",e.getMessage()); } } @Test @@ -304,7 +307,7 @@ A x = new A; "new A = { -> }; !B !b;", // Field declared later } ) { CodeGen code = new CodeGen("struct B {}; struct A { "+fields+" }; return 0;"); - try { code.parse(); fail(fields); } + try { code.driver(CodeGen.Phase.TypeCheck); fail(fields); } catch( Parser.ParseException e ) { assertEquals("'Test.A' is not fully initialized, field 'b' is only partially set in the constructor",e.getMessage()); } } } @@ -313,7 +316,7 @@ A x = new A; public void testDefaultConstructorFields() { for( String field : new String[] { "!B !b;", "!B !b;" } ) { CodeGen code = new CodeGen("struct B {}; struct A { "+field+" }; return new A;"); - try { code.parse(); fail(field); } + try { code.driver(CodeGen.Phase.TypeCheck); fail(field); } catch( Parser.ParseException e ) { assertEquals("'Test.A' is not fully initialized, field 'b' needs to be set in a constructor",e.getMessage()); } } for( String fields : new String[] { @@ -549,7 +552,7 @@ struct _A { }; return new _A; """); - code.driver(CodeGen.Phase.FinalizeIR); + code.driver(CodeGen.Phase.TypeCheck); assertEquals("return Test._A;", code.print()); assertEquals("Test._A{f1=null,f2=null}", Eval2.eval(code,0)); } @@ -564,7 +567,7 @@ struct _A { }; return new _A(new !B?[0]).array_of_b; """); - code.driver(CodeGen.Phase.FinalizeIR); + code.driver(CodeGen.Phase.TypeCheck); assertEquals("return []*B?;", code.print()); assertEquals("*B {}?[]", Eval2.eval(code,0)); } diff --git a/src/test/java/com/seaofnodes/simple/Chapter18Test.java b/src/test/java/com/seaofnodes/simple/Chapter18Test.java index 8e109991..ecb2b934 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter18Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter18Test.java @@ -1,4 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.parser.Var; +import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.simple.type.*; import com.seaofnodes.simple.node.*; diff --git a/src/test/java/com/seaofnodes/simple/Chapter19Test.java b/src/test/java/com/seaofnodes/simple/Chapter19Test.java index 3cc00922..50ca8922 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter19Test.java +++ b/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 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 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/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/src/test/java/com/seaofnodes/simple/Chapter20Test.java index 7eb1f255..f5eb2b36 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter20Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter20Test.java @@ -79,6 +79,7 @@ return f(s); @Test public void testAllocatorUnion() { com.seaofnodes.simple.codegen.RegAllocTestSupport.union(); } @Test public void testAllocatorCopyClobber() throws Exception { com.seaofnodes.simple.codegen.RegAllocTestSupport.copyClobber(); } @Test public void testAllocatorCommutativePhi() { com.seaofnodes.simple.codegen.RegAllocTestSupport.commutativePhi(); } + @Test public void testAllocatorCommutativeLoop() { com.seaofnodes.simple.codegen.RegAllocTestSupport.commutativeLoop(); } @Test public void testAllocatorKills() { com.seaofnodes.simple.codegen.RegAllocTestSupport.killWithoutResult(); } @Test public void testAllocatorDependencies() { com.seaofnodes.simple.codegen.RegAllocTestSupport.nullUseMask(); } @Test public void testAllocatorCloneClass() { com.seaofnodes.simple.codegen.RegAllocTestSupport.cloneRegisterClass(); } @@ -255,6 +256,7 @@ val hashCode = { !String self -> ? self._hashCode : (self._hashCode = _hashCodeString(self)); }; + """; // Return the public hashCode wrapper; keep its helper private. testCPU(src,"x86_64_v2", "SystemV",12,null); diff --git a/src/test/java/com/seaofnodes/simple/Chapter22Test.java b/src/test/java/com/seaofnodes/simple/Chapter22Test.java index 00d77f65..e9a668a7 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter22Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter22Test.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.isa.eval.EvalRisc5; import com.seaofnodes.isa.eval.EvalArm64; diff --git a/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/src/test/java/com/seaofnodes/simple/Chapter25Test.java index 1ad1e129..5f615282 100644 --- a/src/test/java/com/seaofnodes/simple/Chapter25Test.java +++ b/src/test/java/com/seaofnodes/simple/Chapter25Test.java @@ -5,6 +5,7 @@ import com.seaofnodes.isa.eval.EvalRisc5; import com.seaofnodes.isa.eval.EvalArm64; import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.simple.codegen.RegAllocTestSupport.CheckedCodeGen; import com.seaofnodes.simple.codegen.ElfReader; import com.seaofnodes.simple.codegen.ParseAll; diff --git a/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java index dfd7a78c..dae64fb0 100644 --- a/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java +++ b/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.Type; diff --git a/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java b/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java index e2d822d0..285d4293 100644 --- a/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java +++ b/src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.simple.codegen.CodeGen; import org.junit.Test; import static org.junit.Assert.*; diff --git a/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java b/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java new file mode 100644 index 00000000..e544fcad --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java @@ -0,0 +1,162 @@ +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).driver(CodeGen.Phase.TypeCheck); + 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 testIncompleteChildReadAfterInlining() { + // Child's own slots are initialized, even though its parent remains + // partial. Reading that slot does not complete the parent reference. + String src = parent("child = new Child(self); payload = child.parent==null;")+ + "return new Parent(arg).payload;"; + assertEquals("0",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),0)); + } + + @Test public void testInitializerFeedsConstructor() { + // Private memory comes from the initializer's real call edge. An + // unknown-caller default must neither erase it nor poison its fields. + String src = "struct C { int !x = 7; new C = { -> x = x+1; }; }; return new C().x;"; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck); + assertEquals("8",Eval2.eval(code,0)); + assertEquals("Stop[ return 8; return Test.C; ]",code.print()); + } + } + + @Test public void testNoIncompleteLookupOrEscape() { + 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;","Might be null"); + 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 = { -> x = next.x; }; }; return new C().x;", + "might not be initialized"); + } + + @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 testPartialQualifier() { + new CodeGen("struct C {}; val f = { C ^c -> return 0; }; return 0;").driver(CodeGen.Phase.TypeCheck); + 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 public constructor's unknown callers promise no initialized slots. + for( String body : new String[]{"value = par.payload;", "value = par.payload*0;", + "int x = par.payload; value = x*0;", "value = par.payload==par.payload;"} ) + rejects(""" + struct Parent { int payload = 7; }; + struct Child { + int value; + new Child = { Parent ^par -> BODY }; + }; + return new Child(new Parent).value; + """.replace("BODY",body),"might not be initialized"); + // 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"); + } + + @Test public void testUnusedIncompleteReadCanDie() { + String src = """ + struct Parent { int payload = 7; }; + struct Child { int value = 5; + new Child = { Parent ^par -> int unused = par.payload; }; + }; + return new Child(new Parent).value; + """; + assertEquals("5",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),0)); + } +} diff --git a/src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java b/src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java new file mode 100644 index 00000000..c570e643 --- /dev/null +++ b/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/src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java b/src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java new file mode 100644 index 00000000..a5fc236b --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java @@ -0,0 +1,119 @@ +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 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; int seen; if(arg) return 0; x = 1; new A = { -> seen = x; x = 2; };" + } ) + try { + new CodeGen("struct A { "+body+" }; return 0;").driver(CodeGen.Phase.TypeCheck); + 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 { + new CodeGen(src).driver(CodeGen.Phase.TypeCheck); + fail(src); + } catch( Parser.ParseException e ) { + assertEquals("'Test.A' is not fully initialized, field '"+field+ + (explicit ? "' is only partially set in the constructor" : + "' needs to be set in a constructor"),e.getMessage()); + } + } + + @Test public void testMissingNumericFields() { + for( String type : new String[]{"int", "u8", "f32"} ) { + rejects(type+"~ x;","x",false); + rejects(type+"~ x; new A = { -> };","x",true); + // Converting the merged BOTTOM to the numeric storage type used + // to hide the missing assignment from the returned memory. + rejects(type+"~ x; new A = { -> if(arg) x = 1; };","x",true); + rejects(type+"~ x; new A = { -> if(arg) return 0; x = 1; };","x",true); + rejects(type+"~ x; new A = { -> while(arg--) x = 1; };","x",true); + } + } + + @Test public void testInitializerAndConstructorReturns() { + rejects("B b;","b",false); + rejects("new A = { -> if(arg) b = new B; }; B b;","b",true); + String[] fields = { + "int~ x; new A = { -> if(arg) { x = 3; return 0; } x = 7; };", + // has no value for x; the user constructor supplies it. + "new A = { -> x = 7; }; int~ x;", + // The user constructor inherits the declaration's default. + "new A = { -> }; int~ x = 7;", + "B? b; int x;", + }; + String[][] values = {{"7","3"}, {"7","7"}, {"7","7"}, {"0","0"}}; + for( int i = 0; i while(true) {} }; }; return 0;").driver(CodeGen.Phase.TypeCheck); + rejects("int~ x; new A = { -> if(arg) return 0; while(true) {} };","x",true); + CodeGen code = new CodeGen(""" + struct A { + int~ x; + new A = { -> if(arg) { x = 7; return 0; } while(true) {} }; + }; + return new A().x; + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("7",Eval2.eval(code,1)); + } + + @Test public void testEarlierConstructorBinding() { + // The older binding spelling is parsed before the following field. + // Its Return lacks that field, so completion must consult too. + rejects("val A = { -> }; B b;","b",true); + rejects("int~ x; val A = { -> if(arg) x = 1; };","x",true); + new CodeGen("struct B {}; struct A { val A = { -> }; B b = new B; }; return 0;").driver(CodeGen.Phase.TypeCheck); + new CodeGen("struct A { val A = { -> while(true) {} }; int~ x; }; return 0;").driver(CodeGen.Phase.TypeCheck); + } +} diff --git a/src/test/java/com/seaofnodes/simple/Eval2.java b/src/test/java/com/seaofnodes/simple/Eval2.java index eea5b023..8d0e3d5b 100644 --- a/src/test/java/com/seaofnodes/simple/Eval2.java +++ b/src/test/java/com/seaofnodes/simple/Eval2.java @@ -371,7 +371,7 @@ public abstract class Eval2 { yield new Object[0]; } - } else if( Parser.startsClzPrefix(tmp._obj._name) ) { + } else if( TypeStruct.startsClzPrefix(tmp._obj._name) ) { yield classObject(tmp._obj); } else { // Constant object with all zeros diff --git a/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java index f583e2d2..66fdacc3 100644 --- a/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java +++ b/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java @@ -1,7 +1,7 @@ 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.*; @@ -19,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 2; }()"}; + String[] results = {"6", "82", "4", "4"}; + for( int i = 0; i x=\"}\"#; }; }; return new Box().x;") + .driver(CodeGen.Phase.TypeCheck); + assertEquals("1",Eval2.eval(code,0)); + } + + @Test public void testUnclosedDeferredFragment() { + try { + new CodeGen("for(int i=0; i<2; i++").driver(CodeGen.Phase.TypeCheck); + fail("Accepted an unterminated for update"); + } catch( Parser.ParseException e ) { + assertEquals("Unclosed for update",e.getMessage()); + } + } +} diff --git a/src/test/java/com/seaofnodes/simple/PartialReadTest.java b/src/test/java/com/seaofnodes/simple/PartialReadTest.java new file mode 100644 index 00000000..a6230b69 --- /dev/null +++ b/src/test/java/com/seaofnodes/simple/PartialReadTest.java @@ -0,0 +1,155 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.InitViewNode; +import com.seaofnodes.simple.node.ConstructNode; +import com.seaofnodes.simple.parser.Parser; +import org.junit.Test; +import static org.junit.Assert.*; + +public class PartialReadTest { + private static String program(String body, String helper) { + return "struct _C { int~ x; int~ y; new _C = { int n -> " + body + + " }; }; val _read_noInline = { _C ^p -> " + helper + + " }; return new _C(arg).y;"; + } + private static void rejects(String source, String message) { + try { + new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + fail("Accepted " + source); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains(message)); + } + } + @Test public void testLocalRead() { + String src = program("x=n; y=_read_noInline(self);", "return p.x;"); + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck); + assertNull(code._stop.walk(n -> n instanceof InitViewNode ? n : null)); + assertEquals("42",Eval2.eval(code,42)); + } + new CodeGen(src).driver(CodeGen.Phase.Encoding,TestC.CPU_PORT,TestC.CALL_CONVENTION,true,false,0); + } + @Test public void testRedundantViewsFoldBeforeTypeCheck() { + String src=program("x=n; y=0; y=_read_noInline(self);", "return p.x;"); + CodeGen code=new CodeGen(src).driver(CodeGen.Phase.IterPostOpto); + assertNull(code._stop.walk(n -> n instanceof InitViewNode ? n : null)); + assertNull(code._stop.walk(n -> n instanceof ConstructNode c && !c._partial ? n : null)); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("31",Eval2.eval(code,31)); + + // Here the view still masks y. A valid program need not initialize + // every field before lending self, so survival is not itself an error. + code=new CodeGen(program("x=n; y=_read_noInline(self);", "return p.x;")) + .driver(CodeGen.Phase.IterPostOpto); + assertNotNull(code._stop.walk(n -> n instanceof InitViewNode ? n : null)); + code.driver(CodeGen.Phase.TypeCheck); + assertEquals("31",Eval2.eval(code,31)); + } + @Test public void testInliningKeepsBorrowRestriction() { + rejects(""" + struct _C { + int !x; + new _C={ int n -> x=n; if(!n) _write(self); }; + }; + val _write={ !_C ^p -> if(p.x) p.x=9; return p.x; }; + val a=new _C(0); val b=new _C(1); + return _write(b)+a.x; + ""","incomplete object"); + } + @Test public void testMissingAndBranchField() { + for( String use : new String[]{"p.x", "p.x*0", "p.x==p.x"} ) + rejects(program("y=_read_noInline(self); x=n;", "return "+use+";"),"might not be initialized"); + rejects(program("if(n) x=n; y=_read_noInline(self); x=0;", "return p.x;"),"might not be initialized"); + } + @Test public void testActualValueFromMemory() { + for( String body : new String[]{"x=1; val p=self; x=2; y=_read_noInline(p);", + "val p=self; x=2; y=_read_noInline(p);"} ) { + CodeGen code = new CodeGen(program(body,"return p.x;")).driver(CodeGen.Phase.TypeCheck); + assertEquals("2",Eval2.eval(code,0)); + } + } + @Test public void testBorrowedWrite() { + rejects(program("x=n; y=_read_noInline(self);", "p.x=9; return p.x;") + .replace("int~ x", "int !x").replace("_C ^p", "!_C ^p"),"incomplete object"); + } + @Test public void testUnknownCallerCannotRead() { + rejects(program("x=n; y=_read_noInline(self);", "return p.x;") + .replace("_read_noInline","read_noInline"),"might not be initialized"); + } + @Test public void testForwardingAndMergedCallers() { + String src = program("x=n; y=_read_noInline(self);", "return _next_noInline(p);") + + " val _next_noInline = { _C ^p -> return p.x; };"; + assertEquals("17",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),17)); + rejects(program("y=_read_noInline(self); x=n; y=_read_noInline(self);", "return p.x;"),"might not be initialized"); + } + @Test public void testLoops() { + String src = program("x=1; for(int i=0; i parent=p; seen=p.x; }; + }; + struct _Parent { + int~ x; + _Child child; + new _Parent = { int n -> x=n; child=new _Child(self); }; + }; + val p=new _Parent(arg); return p.child.seen+p.child.parent.x; + """; + assertEquals("22",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),11)); + rejects(src.replace("x=n; child=new _Child(self);","child=new _Child(self); x=n;"),"might not be initialized"); + } + @Test public void testInlineReadAndWrite() { + String src=program("x=n; y=_read_noInline(self);","return p.x;").replace("_noInline",""); + assertEquals("19",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),19)); + rejects(src.replace("return p.x;","p.x=9; return p.x;").replace("int~ x","int !x").replace("_C ^p","!_C ^p"),"incomplete object"); + rejects(src.replace("x=n; y=", "y=").replace("(self);", "(self); x=n;"),"might not be initialized"); + } + @Test public void testArrayAndNestedReference() { + String src=""" + struct _C { + int[] data; + int~ y; + new _C = { int n -> data=new int[1]; data[0]=n; y=_read_noInline(self); }; + }; + val _read_noInline={ _C ^p -> return p.data[0]; }; + return new _C(arg).y; + """; + assertEquals("23",Eval2.eval(new CodeGen(src).driver(CodeGen.Phase.TypeCheck),23)); + // A set reference slot is not proof that its referent is initialized. + rejects("struct D { int x=7; }; "+src.replace("int[] data","D data") + .replace("data=new int[1]; data[0]=n;","data=new D;") + .replace("p.data[0]","p.data.x"),"might not be initialized"); + } + @Test public void testCompleteBorrowCannotPublishPartial() { + rejects(""" + struct _C { int x=7; }; + struct Box { _C? p; }; val box=new Box; + val _store={ !_C ^dst, _C ^src -> box.p=src; return dst.x; }; + return _store(new _C,new _C); + ""","ordinary object"); + } + @Test public void testSameClassMemory() { + String src=program("x=n; y=n ? _read_noInline(self) : x;", + "val other=new _C(0); return p.x+other.y;"); + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck); + assertEquals("29",Eval2.eval(code,29)); + assertEquals("0",Eval2.eval(code,0)); + } + } +} diff --git a/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java b/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java index fa14fc2c..88c11101 100644 --- a/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java +++ b/src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java @@ -1,5 +1,6 @@ package com.seaofnodes.simple; +import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.simple.codegen.*; import com.seaofnodes.simple.node.*; import com.seaofnodes.simple.type.TypeInteger; diff --git a/src/test/java/com/seaofnodes/simple/TypeTest.java b/src/test/java/com/seaofnodes/simple/TypeTest.java index b4055eef..16b75fdc 100644 --- a/src/test/java/com/seaofnodes/simple/TypeTest.java +++ b/src/test/java/com/seaofnodes/simple/TypeTest.java @@ -7,6 +7,26 @@ import static org.junit.Assert.assertSame; public class TypeTest { + @Test public void testOpaqueExternalEscapes() { + TypeMem defaults = TypeMem.START.makeFrom(XInt.make(2),XInt.make(3)); + TypeMem prior = TypeMem.START.makeFrom(XInt.make(5),XInt.make(7)); + TypeMemPtr opaque = TypeMemPtr.makeNullable(TypeStruct.forward("OpaqueExternal")); + TypeMem escaped = prior.escapesFrom(opaque,defaults); + assertSame(XInt.meet(prior._escFs,defaults._escFs),escaped._escFs); + assertSame(XInt.meet(prior._escAs,defaults._escAs),escaped._escAs); + // Before type closure the same name might still resolve locally. + assertSame(XInt.FULL,prior.escapesFrom(opaque,TypeMem.BOT)._escFs); + for( Type unknown : new Type[]{Type.BOTTOM,TypeScalar.BOT,TypePtr.PTR, + TypeMemPtr.make(TypeStruct.open("LocalStillOpen"))} ) { + TypeMem broad = prior.escapesFrom(unknown,defaults); + assertSame(XInt.FULL,broad._escFs); + assertSame(XInt.FULL,broad._escAs); + } + // Concrete private callbacks must not be restricted to public names. + TypeFunPtr callback = TypeFunPtr.make1((byte)2,true,TypeFunPtr.TEMPTY,TypeInteger.BOT,11); + assertSame(XInt.make(11),TypeMem.START.escapesFrom(callback,defaults)._escFs); + } + // Aggregate finality is carried by its fields. Canonicalizing the unused // aggregate bit must commute with dual, even for named (non-high) structs. @Test public void testAggregateMemoryDual() { @@ -147,7 +167,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 +176,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); diff --git a/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java b/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java index 292f1ae7..adbb10a5 100644 --- a/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java +++ b/src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java @@ -1,6 +1,7 @@ package com.seaofnodes.simple.codegen; import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.node.cpus.x86_64_v2.SubX86; import com.seaofnodes.simple.node.cpus.x86_64_v2.x86_64_v2; import com.seaofnodes.simple.type.TypeInteger; import static org.junit.Assert.*; @@ -124,6 +125,30 @@ public class RegAllocTestSupport { assertTrue(BuildLRG.run(0,new RegAlloc(code))); } + public static void commutativeLoop() { + for( String op : new String[]{"+","*","^","&","|"} ) + for( boolean reversed : new boolean[]{false,true} ) { + String src = "return { u8[] data -> int h = 1; for(int i = 0; i < data#; i++) " + + "h = ((h<<5)-h) "+op+" data[i]; return h; };"; + CodeGen code = new CodeGen(src).driver(CodeGen.Phase.LocalSched,"x86_64_v2","SystemV"); + // Exercise both destructive operands regardless of ideal-node + // numbering. The shifted value overlaps the old loop hash. + int found = 0; + for( CFGNode bb : code._cfg ) + for( Node n : bb.outs() ) + if( n instanceof MachNode mach && mach.commutes() && mach.twoAddress()==1 && + (n.in(1) instanceof SubX86 || n.in(2) instanceof SubX86) ) { + if( (n.in(1) instanceof SubX86)==reversed ) n.swap12(); + found++; + } + assertEquals(op,1,found); + code.driver(CodeGen.Phase.RegAlloc,"x86_64_v2","SystemV"); + checkRegisters(code); + assertEquals(op+" reversed="+reversed,8,code._regAlloc._spillScaled); + code.encode(); + } + } + private static class CallOp extends CallNode implements MachNode { CallOp(Node ctrl, Node def) { super(null,ctrl,null,def,def); } @Override public RegMask regmap(int i) { return i==2 ? A : i==3 ? B : null; } diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java index 55c2c040..73ba6afd 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java @@ -1,6 +1,6 @@ package com.seaofnodes.simple.fuzzer; -import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.simple.Eval2; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.node.Node; diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java index 9fe2e405..c38c423d 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java @@ -1,6 +1,6 @@ package com.seaofnodes.simple.fuzzer; -import com.seaofnodes.simple.IterPeeps; +import com.seaofnodes.simple.codegen.IterPeeps; import com.seaofnodes.simple.codegen.CodeGen; import com.seaofnodes.simple.node.Node; import com.seaofnodes.simple.node.StopNode; diff --git a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java index c5e94047..78fb8e44 100644 --- a/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java +++ b/src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java @@ -1,6 +1,6 @@ package com.seaofnodes.simple.fuzzer; -import com.seaofnodes.simple.Parser; +import com.seaofnodes.simple.parser.Parser; import com.seaofnodes.simple.node.ScopeNode; import com.seaofnodes.simple.type.TypeInteger; import java.util.ArrayList; @@ -8,7 +8,7 @@ import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.function.Predicate; -import static com.seaofnodes.simple.Parser.KEYWORDS; +import static com.seaofnodes.simple.parser.Parser.KEYWORDS; /** -- cgit v1.2.3