From 9b9fadcfa74b087a695156573ea28b89a166b18b Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Sat, 17 Oct 2026 09:00:10 -0700 Subject: Class cycles init checks --- .../simple/ClassInitDependenciesTest.java | 141 +++++++++++++++++---- 1 file changed, 116 insertions(+), 25 deletions(-) (limited to 'chapter25/src/test/java/com/seaofnodes') diff --git a/chapter25/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java b/chapter25/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java index 1dec3312..e3af8cce 100644 --- a/chapter25/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java +++ b/chapter25/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java @@ -12,7 +12,10 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.BitSet; import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -66,18 +69,34 @@ public class ClassInitDependenciesTest { } } - // The cycle can start at any member, but must follow dependency direction. + // The report lists internal SCC edges, including ordinary helper calls. + private HashMap> reportEdges(String message) { + String[] lines=message.split("\n"); + assertEquals(message,"Cyclic class initialization:",lines[0]); + HashMap> edges=new HashMap<>(); + for( int i=1; i "); + assertEquals(message,2,edge.length); + edges.computeIfAbsent(edge[0],key -> new HashSet<>()).add(edge[1]); + } + return edges; + } + + // Follow helpers in the diagnostic, stopping at the next class initializer. + private boolean reportedTouch(HashMap> edges, String from, String to, HashSet seen) { + if( !seen.add(from) || !edges.containsKey(from) ) return false; + for( String dst : edges.get(from) ) + if( dst.equals(to) || (dst.contains(" [fidx ") && reportedTouch(edges,dst,to,seen)) ) return true; + return false; + } + private void assertCycle(String message, String expected) { - String prefix="Cyclic class initialization: "; - assertTrue(message,message.startsWith(prefix)); - String actual=message.substring(prefix.length()).trim(); + HashMap> edges=reportEdges(message); String[] path=expected.split(" -> "); - for( int start=0; start rotated=new ArrayList<>(); - for( int i=0; i ",rotated)) ) return; - } - fail("Expected cycle "+expected+" (up to rotation), got "+actual); + for( int i=1; i "+path[i]+" in:\n"+message, + reportedTouch(edges,path[i-1],path[i],new HashSet<>())); } // Follow ordinary helpers, stopping at the next initializer. This checks @@ -204,8 +223,10 @@ public class ClassInitDependenciesTest { object(stubs,stubs,"B","int value=arg;"); // Each object is independently acyclic. Combining their real bodies // must recheck the graph rather than trust either object's local order. - object(dir,stubs,"A","int value=arg;\nvalue=B.value;"); - object(dir,stubs,"B","int value=arg; value=A.value;"); + String asrc="int value=arg;\nvalue=B.value;"; + CodeGen acode=module(dir,stubs,"A",Map.of("A",asrc)); + String afile=acode._compunits.get("A").sourceFile(); + module(dir,stubs,"B",Map.of("B","int value=arg; value=A.value;")); for( String entry : new String[]{"A","B"} ) { try { new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), @@ -213,8 +234,21 @@ public class ClassInitDependenciesTest { fail("Expected imported initialization cycle"); } catch( Parser.ParseException ex ) { assertCycle(ex.getMessage(),"A -> B -> A"); + assertTrue(ex.getMessage(),ex.getMessage().contains("value=B.value;\n -------^")); + assertTrue(ex.getMessage(),ex.getMessage().contains("\n"+afile+":2:8: error: cyclic class initialization\n")); } } + // The imported object must still report its saved path and offset + // when the original source is no longer available. + Files.delete(Path.of(afile)); + try { + new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","return A.value;",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + fail("Expected imported initialization cycle"); + } catch( Parser.ParseException ex ) { + assertCycle(ex.getMessage(),"A -> B -> A"); + assertTrue(ex.getMessage(),ex.getMessage().contains("\n"+afile+": error: cyclic class initialization (source unavailable at byte offset "+asrc.indexOf('.')+")")); + } } @Test public void testImportedDiamondAndHelpers() throws Exception { @@ -276,31 +310,88 @@ public class ClassInitDependenciesTest { @Test public void testCycleReportDirection() throws Exception { cycle("A",Map.of("A","int value=B.value;", "B","int value=C.value;", "C","int value=A.value;"),"A -> B -> C -> A"); + for( long seed : new long[]{1,123,987654321} ) { + // Stack order A,B,C is not a cycle: B has no edge to C. All four + // internal edges must be shown; edges to Root/D must be omitted. + try { + compile("A",Map.of("A","int value=B.value+C.value+D.value;", + "B","int value=A.value;", "C","int value=A.value;", + "D","int value=arg;"),seed); + fail("Expected initialization cycle"); + } catch( Parser.ParseException ex ) { + assertEquals(Map.of("Root.A",Set.of("Root.B","Root.C"), + "Root.B",Set.of("Root.A"), "Root.C",Set.of("Root.A")), + reportEdges(ex.getMessage())); + } + } } - @Test public void testOverlappingFunctionAndClassCycles() { - // Fixed function graph: Opto/inlining must not erase the overlap that - // exercises SCC traversal. f -> g -> f is legal; f -> A -> g -> f isn't. - CodeGen code=new CodeGen("return 0;").driver(CodeGen.Phase.Opto); + @Test public void testCycleSourceLocations() throws Exception { + for( String name : new String[]{"read", "read_noInline"} ) { + try { + compile("A",Map.of("A","int value=Helper."+name+"();", + "Helper","val "+name+"={ ->\n B.value;\n};", + "B","int value=A.value;"),123); + fail("Expected initialization cycle"); + } catch( Parser.ParseException ex ) { + String message=ex.getMessage(); + assertCycle(message,"Root.A -> Root.B -> Root.A"); + // Inlining must keep Helper's source identity, not substitute A. + assertTrue(message,message.contains("Helper.smp:2:4")); + assertTrue(message,message.contains(" B.value;\n ---^")); + if( name.endsWith("noInline") ) { + assertTrue(message,message.contains("A.smp:1:31")); + assertTrue(message,message.contains("int value=Helper.read_noInline();")); + } + } + } + } + + @Test public void testSourceError() { + String src="// π\r\n\treturn 1;\r\n"; + CodeGen code=new CodeGen(src).driver(CodeGen.Phase.Parse); CompUnit cu=code._compunits.get("Test"); - code._linker.clear(); - String[] names={"f", "g", "A."}; + int pos=src.substring(0,src.indexOf('1')).getBytes(java.nio.charset.StandardCharsets.UTF_8).length; + assertEquals("Test.smp:2:9: error: example\n return 1;\n ---------------^",SourceError.format(cu,pos,"example")); + assertEquals("Test.smp:3:1: error: example\n \n ^",SourceError.format(cu,src.getBytes(java.nio.charset.StandardCharsets.UTF_8).length,"example")); + } + + @Test public void testSelfInitialization() throws Exception { + for( String name : new String[]{"read","read_noInline"} ) + for( long seed : new long[]{1,123,987654321} ) + compile("A",Map.of("A","int value=arg; val "+name+"={ -> A.value; }; int result="+name+"();"),seed); + } + + @Test public void testOverlappingFunctionAndClassCycles() throws Exception { + // Fixed function graph: Opto/inlining must not erase the overlap that + // exercises SCC traversal. One clinit is legal, two in the SCC are not. + CodeGen code=compile("A",Map.of("A","val b=B; int value=arg;", "B","int value=arg;"),123); + int f=code._linker._len, g=f+1, a=f+2, b=f+3; + String[] names={"f", "g", "A.", "B."}; for( int i=0; i g - deps._edgess[3]=new int[]{2}; // g -> f + deps._edgess[f]=new int[]{g}; // f -> g + deps._edgess[g]=new int[]{f}; // g -> f assertNull("Ordinary mutual recursion is legal",deps.check()); - deps._edgess[4]=new int[]{3}; // A -> g - deps._edgess[2]=new int[]{3,4}; + deps._edgess[a]=new int[]{a}; // A -> A + assertNull("A singleton initializer self-edge is legal",deps.check()); + deps._edgess[a]=new int[]{g}; // A -> g + deps._edgess[f]=new int[]{g,a}; + assertNull("A cycle containing only one initializer is legal",deps.check()); + deps._edgess[a]=new int[]{b}; // A -> B + deps._edgess[b]=new int[]{g}; // B -> g -> f -> A String forward=deps.check(); - deps._edgess[2]=new int[]{4,3}; + deps._edgess[f]=new int[]{a,g}; String reverse=deps.check(); assertTrue("Both successor orders must reject the class cycle: "+forward+", "+reverse, forward!=null && reverse!=null); + assertCycle(forward,"Root.A -> Root.B -> Root.A"); + assertCycle(reverse,"Root.A -> Root.B -> Root.A"); } } -- cgit v1.2.3