summaryrefslogtreecommitdiff
path: root/src/test
diff options
context:
space:
mode:
authorCliff Click <cliffc@acm.org>2026-08-10 11:11:18 -0700
committerCliff Click <cliffc@acm.org>2026-11-12 23:28:58 -0800
commit772e3a1036d571de55b178ebd6d11ee5140d4e36 (patch)
treef81fd53601df21871adb7500f8dc2b8fa3a829a2 /src/test
parent15b634f47b300cb611b30554ce390ac20f149f8f (diff)
downloadSimple-772e3a1036d571de55b178ebd6d11ee5140d4e36.tar.gz
Simple-772e3a1036d571de55b178ebd6d11ee5140d4e36.zip
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 <jun784@gmail.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'src/test')
-rw-r--r--src/test/java/com/seaofnodes/simple/ArithNodeTest.java32
-rw-r--r--src/test/java/com/seaofnodes/simple/BoundaryTest.java6
-rw-r--r--src/test/java/com/seaofnodes/simple/CallEndTest.java54
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter10Test.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter12Test.java13
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter17bTest.java13
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter18Test.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter19Test.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter20Test.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter22Test.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/Chapter25Test.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/CompoundAssignmentTest.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/ConstructionGroupTest.java162
-rw-r--r--src/test/java/com/seaofnodes/simple/ConstructionRequirementsTest.java222
-rw-r--r--src/test/java/com/seaofnodes/simple/ConstructorCompletionTest.java119
-rw-r--r--src/test/java/com/seaofnodes/simple/Eval2.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/FinalConstructionTest.java10
-rw-r--r--src/test/java/com/seaofnodes/simple/ParserTest.java33
-rw-r--r--src/test/java/com/seaofnodes/simple/PartialReadTest.java155
-rw-r--r--src/test/java/com/seaofnodes/simple/SelectiveLoadingTest.java1
-rw-r--r--src/test/java/com/seaofnodes/simple/TypeTest.java24
-rw-r--r--src/test/java/com/seaofnodes/simple/codegen/RegAllocTestSupport.java25
-rw-r--r--src/test/java/com/seaofnodes/simple/fuzzer/Fuzzer.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/fuzzer/FuzzerUtils.java2
-rw-r--r--src/test/java/com/seaofnodes/simple/fuzzer/ScriptGenerator.java4
26 files changed, 864 insertions, 26 deletions
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; };",
+ // <init> 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<fields.length; i++ ) {
+ CodeGen code = new CodeGen("struct B {}; struct A { "+fields[i]+" }; return new A().x;")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals(values[i][0],Eval2.eval(code,0));
+ assertEquals(values[i][1],Eval2.eval(code,1));
+ }
+ }
+
+ @Test public void testNonreturningPaths() {
+ // An initializer parsed in dead enclosing code can have TOP memory
+ // rather than a struct-shaped result.
+ new CodeGen("while(false) { struct A { int x; }; } return 0;").driver(CodeGen.Phase.TypeCheck);
+ new CodeGen("struct A { int~ x; new A = { -> 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 <init> 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<n;i++) self.x*=2;}; }; return new C(arg).x;",
diff --git a/src/test/java/com/seaofnodes/simple/ParserTest.java b/src/test/java/com/seaofnodes/simple/ParserTest.java
new file mode 100644
index 00000000..364979f2
--- /dev/null
+++ b/src/test/java/com/seaofnodes/simple/ParserTest.java
@@ -0,0 +1,33 @@
+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 ParserTest {
+ @Test public void testDeferredFragments() {
+ // The for update is scanned before the body, then parsed after it.
+ // Delimiters in strings, characters and comments must not end the scan.
+ String[] updates = {"n=n+\") (\"#", "n=n+')'", "n=n+/* ) */2", "n=n+{ -> 2; }()"};
+ String[] results = {"6", "82", "4", "4"};
+ for( int i = 0; i<updates.length; i++ ) {
+ CodeGen code = new CodeGen("int n=0; for(int i=0; i<2; "+updates[i]+") i++; return n;")
+ .driver(CodeGen.Phase.TypeCheck);
+ assertEquals(updates[i],results[i],Eval2.eval(code,0));
+ }
+ // Declared constructor bodies use the same scanner with brace nesting.
+ CodeGen code = new CodeGen("struct Box { int x; new Box={ -> 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<n; i++) x+=2; 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);
+ assertEquals("7",Eval2.eval(code,3));
+ assertEquals("1",Eval2.eval(code,0));
+ }
+ rejects(program("for(int i=0; i<n; i++) x=n; y=_read_noInline(self); x=0;", "return p.x;"),"might not be initialized");
+ }
+ @Test public void testNoPublication() {
+ rejects("struct Box { Test._C? !p; }; val box=new Box; " +
+ program("x=n; y=_read_noInline(self);", "box.p=p; return p.x;"),"ordinary object");
+ }
+ @Test public void testConstructorReadAndRetention() {
+ String src = """
+ struct _Child {
+ Test._Parent parent;
+ int~ seen;
+ new _Child = { Test._Parent ^p -> 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;
/**