diff options
| author | Cliff Click <cliffc@acm.org> | 2026-11-01 10:33:55 -0800 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-11-01 10:33:55 -0800 |
| commit | f4bd723d929e53475f7990374dca7b7dfcbbdc42 (patch) | |
| tree | 7bb080119e7b88ad4ac9b30af019fe6a5e4ba9dd /chapter29/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java | |
| parent | 0ac0c9e10287972ac3af5a4ee43d26f0d5b318fe (diff) | |
| download | Simple-main.tar.gz Simple-main.zip | |
Add Chapter 29 for tech debt cleanup with the parser splitmain
Keep Chapter 28b intact and carry the grammar-oriented parser package, Var move, IterPeeps move, and parser regression tests into Chapter 29. Defer privacy and exclude its experimental tests and fixture changes.
Register the chapter in the indexes, IntelliJ modules and run configurations, Maven reactor, roadmap, and linear-history publisher. Windows and Linux each pass 573 tests, release builds, 199 allocation replays, and 29 native checks.
Diffstat (limited to 'chapter29/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java')
| -rw-r--r-- | chapter29/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java | 585 |
1 files changed, 585 insertions, 0 deletions
diff --git a/chapter29/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java b/chapter29/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java new file mode 100644 index 00000000..88ebb6b7 --- /dev/null +++ b/chapter29/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java @@ -0,0 +1,585 @@ +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; +import com.seaofnodes.simple.type.TypeFunPtr; +import com.seaofnodes.simple.type.Field; +import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeTuple; +import com.seaofnodes.simple.util.Ary; +import java.nio.file.Files; +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; +import org.junit.runners.model.Statement; +import static org.junit.Assert.*; + +public class ClassInitDependenciesTest { + @Test public void testPrivateInitializerCallEdges() throws Exception { + for( long seed : new long[]{1,123,987654321} ) { + Path root = tempDir("private-roots-"); + Path classes = Files.createDirectories(root.resolve("Root")); + Files.writeString(root.resolve("Root.smp"),"val _dead = _Dead; val read = { -> _Live.value; }; return 0;"); + Files.writeString(classes.resolve("_Live.smp"),"int value = 17+arg;"); + Files.writeString(classes.resolve("_Dead.smp"),"val child = Child; int value = 99;"); + Path children = Files.createDirectories(classes.resolve("_Dead")); + Files.writeString(children.resolve("Child.smp"),"int value = 42;"); + CodeGen code = new CodeGen(root.toString(),root.resolve("obj").toString(),null, + "Root",null,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + FunNode live = code.link(code._compunits.get("Root/_Live")); + assertNotNull(live); + assertFalse("A local init call is not an unknown caller",live.unknownCallers()); + assertTrue(live.in(1) instanceof CProjNode call && call.in(0) instanceof CLInitNode); + for( FunNode fun : code._linker ) + assertTrue(fun==null || fun.isDead() || + fun._compunit!=code._compunits.get("Root/_Dead") && + fun._compunit!=code._compunits.get("Root/_Dead/Child")); + assertEquals("0",Eval2.eval(code,99)); + new CodeGen(root.toString(),root.resolve("obj").toString(),null, + "Root",null,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.Encoding,"x86_64_v2","SystemV"); + } + } + + private final ArrayList<Path> dirs = new ArrayList<>(); + + @Rule public final TestRule cleanup = (test, description) -> new Statement() { + @Override public void evaluate() throws Throwable { + // Keep all fixtures if compilation or any later assertion fails. + test.evaluate(); + for( Path dir : dirs ) { + Path[] files; + try( var paths=Files.walk(dir) ) { + files=paths.sorted(Comparator.reverseOrder()).toArray(Path[]::new); + } + for( Path file : files ) Files.delete(file); + } + } + }; + + private Path tempDir(String prefix) throws Exception { + Path build=Path.of("build/init-deps"); + Files.createDirectories(build); + Path dir=Files.createTempDirectory(build,prefix); + dirs.add(dir); + return dir; + } + + private CodeGen compile(String entry, Map<String,String> sources, long seed) throws Exception { + Path root=tempDir("case-"); + Files.writeString(root.resolve("Root.smp"),"val entry="+entry+";"); + for( var source : sources.entrySet() ) { + Path path=root.resolve("Root/"+source.getKey()+".smp"); + Files.createDirectories(path.getParent()); + Files.writeString(path,source.getValue()); + } + return new CodeGen(root.toString(),root.resolve("obj").toString(),null, + "Root",null,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + } + + private void cycle(String entry, Map<String,String> sources, String expected) throws Exception { + expected="Root."+expected.replace(" -> "," -> Root."); + for( long seed : new long[]{1,123,987654321} ) { + try { compile(entry,sources,seed); fail("Expected initialization cycle"); } + catch( Parser.ParseException ex ) { + assertCycle(ex.getMessage(),expected); + } + } + } + + // The report lists internal SCC edges, including ordinary helper calls. + private HashMap<String,HashSet<String>> reportEdges(String message) { + String[] lines=message.split("\n"); + assertEquals(message,"Cyclic class initialization:",lines[0]); + HashMap<String,HashSet<String>> edges=new HashMap<>(); + for( int i=1; i<lines.length; i++ ) { + if( !lines[i].startsWith(" ") || lines[i].startsWith(" ") ) continue; // Diagnostic/source/caret. + String[] edge=lines[i].trim().split(" -> "); + 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<String,HashSet<String>> edges, String from, String to, HashSet<String> 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) { + HashMap<String,HashSet<String>> edges=reportEdges(message); + String[] path=expected.split(" -> "); + for( int i=1; i<path.length; i++ ) + assertTrue("Missing "+path[i-1]+" -> "+path[i]+" in:\n"+message, + reportedTouch(edges,path[i-1],path[i],new HashSet<>())); + } + + // Follow ordinary helpers, stopping at the next initializer. This checks + // the class prerequisite relation without requiring a total init order. + private boolean touches(CodeGen code, ClassInitDependencies deps, String from, String to) { + int src=code.link(code._compunits.get(from)).sig().fidx(); + int dst=code.link(code._compunits.get(to)).sig().fidx(); + return touches(code,deps._edgess,src,dst,new BitSet()); + } + + private boolean touches(CodeGen code, int[][] edges, int src, int dst, BitSet seen) { + if( src>=edges.length || seen.get(src) || edges[src]==null ) return false; + seen.set(src); + for( int edge : edges[src] ) { + if( edge==dst ) return true; + FunNode fun=code.link(edge); + if( fun!=null && !fun.isClz() && touches(code,edges,edge,dst,seen) ) return true; + } + return false; + } + + @Test public void testEdgesAndPassiveReferences() throws Exception { + CodeGen code = compile("A",Map.of("A","int value=B.value;", "B","int value=arg|1;"),123); + ClassInitDependencies deps=new ClassInitDependencies(code); + assertTrue(touches(code,deps,"Root/A","Root/B")); + code=compile("A",Map.of("A","val b=B;", "B","val a=A;"),123); + deps=new ClassInitDependencies(code); + assertFalse(touches(code,deps,"Root/A","Root/B")); + assertFalse(touches(code,deps,"Root/B","Root/A")); + assertTrue(touches(code,deps,"Root/A","Root")); + assertTrue(touches(code,deps,"Root/B","Root")); + } + + @Test public void testCycles() throws Exception { + cycle("A",Map.of("A","val get_noInline={ ->\n B.value;\n};\nint value=get_noInline();", + "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","int value=B.value;", "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","val get={ -> B.value; }; int value=get();", + "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","int value=get(); val get={ -> B.value; };", + "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","int value=0; if(arg) value=B.value;", + "B","int value=A.value;"),"A -> B -> A"); + cycle("A",Map.of("A","val f=arg ? { -> B.value; } : { -> 0; }; int value=f();", + "B","int value=A.value;"),"A -> B -> A"); + cycle("P",Map.of("P","int value=A.value;", "P/A","int value=arg;"),"P -> P.A -> P"); + } + + @Test public void testSiblingsAndNonreturningCall() throws Exception { + cycle("P",Map.of("P","val a=A; val b=B;", "P/A","int value=B.value;", + "P/B","int value=A.value;"),"P.A -> P.B -> P.A"); + cycle("A",Map.of("A","val get={ -> val x=B.value; while(1) {} return x; }; int value=get();", + "B","int value=A.value;"),"A -> B -> A"); + // An out-of-line nonreturning call retains its continuation's touches. + cycle("A",Map.of( + "A","int value=1; val stop_noInline={ -> while(1) {} return 1; }; stop_noInline(); return B.value;", + "B","int value=A.value;"),"A -> B -> A"); + String source="val stop_noInline={ -> while(1) {} return 1; }; stop_noInline(); return 2;"; + CodeGen code=new CodeGen(source).driver(CodeGen.Phase.TypeCheck); + assertNull(Eval2.eval(code,0,10)); // Evaluation reaches its loop budget. + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) + new CodeGen(source).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + + @Test public void testOptimizedDependencies() throws Exception { + for( long seed : new long[]{1,123,987654321} ) { + // SCCP learns this condition from a call; the parser cannot decide it. + CodeGen code=compile("A",Map.of( + "A","val test_noInline={ -> false; }; int value=1; if(test_noInline()) value=B.value;", + "B","int value=A.value;"),seed); + ClassInitDependencies deps=new ClassInitDependencies(code); + assertFalse(touches(code,deps,"Root/A","Root/B")); + assertTrue(touches(code,deps,"Root/B","Root/A")); + + code=compile("A",Map.of( + "A","val test_noInline={ -> false; }; val f=test_noInline() ? { -> B.value; } : { -> 1; }; int value=f();", + "B","int value=A.value;"),seed); + assertFalse(touches(code,new ClassInitDependencies(code),"Root/A","Root/B")); + + // An unused helper's body must not become an initializer dependency. + code=compile("A",Map.of("A","val unused={ -> B.value; }; int value=1;", + "B","int value=A.value;"),seed); + assertFalse(touches(code,new ClassInitDependencies(code),"Root/A","Root/B")); + } + // Discarding the value of a field read cannot discard its active touch. + cycle("A",Map.of("A","int value=1; B.value; return 0;", + "B","int value=2; A.value; return 0;"),"A -> B -> A"); + } + + @Test public void testRecursionAndNativeCalls() throws Exception { + CodeGen code=compile("A",Map.of("A","val f={int n -> n ? f(n-1) : B.value; }; int value=f(arg);", + "B","int value=arg|1;"),123); + assertTrue(touches(code,new ClassInitDependencies(code),"Root/A","Root/B")); + code=compile("A",Map.of("A","{int} native=\"C\"; return native();"),123); + + Path dir=tempDir("import-"); + new CodeGen(null,dir.toString(),null,"B","int value=1;",123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "A","return B.value;",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertTrue(touches(code,new ClassInitDependencies(code),"A","B")); + } + + private CodeGen object(Path dir, Path imports, String name, String source) { + return new CodeGen(null,dir.toString(),new Ary<>(new String[]{imports.toString()}), + name,source,123,true,TypeInteger.BOT) + .driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + } + + private CodeGen module(Path dir, Path imports, String name, Map<String,String> sources) throws Exception { + Path src=tempDir("import-"); + for( var entry : sources.entrySet() ) { + Path file=src.resolve(entry.getKey()+".smp"); + Files.createDirectories(file.getParent()); + Files.writeString(file,entry.getValue()); + } + return new CodeGen(src.toString(),dir.toString(),new Ary<>(new String[]{imports.toString()}), + name,null,123,true,TypeInteger.BOT).driver(TestC.CPU_PORT,TestC.CALL_CONVENTION,false,false); + } + + @Test public void testImportedInitialization() throws Exception { + Path dir=tempDir("import-"), stubs=tempDir("import-"); + object(stubs,stubs,"A","int value=arg;"); + object(stubs,stubs,"B","int value=arg;"); + // Each object is independently acyclic. Combining their summaries + // must recheck the graph rather than trust either object's local order. + String asrc="int value=arg;\nvalue=B.value;"; + CodeGen acode=module(dir,stubs,"A",Map.of("A",asrc)); + String afile=acode._compunits.get("A").sourceFile(); + module(dir,stubs,"B",Map.of("B","int value=arg; value=A.value;")); + for( String entry : new String[]{"A","B"} ) { + CodeGen code = new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","return "+entry+".value;",123,true,TypeInteger.BOT); + try { + code.driver(CodeGen.Phase.TypeCheck); + fail("Expected imported initialization cycle"); + } catch( Parser.ParseException ex ) { + assertCycle(ex.getMessage(),"A -> B -> A"); + assertNull(code.getElf(code._compunits.get("A")._obj)._nodes); + assertNull(code.getElf(code._compunits.get("B")._obj)._nodes); + } + } + // The summary reports the dependency chain even without source files. + 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"); + } + } + + @Test public void testImportedDiamondAndHelpers() throws Exception { + Path diamond=tempDir("import-"); + CodeGen dcode=object(diamond,diamond,"D","int value=arg;"); + var data=dcode._encoding._data.entries().stream().filter(obj -> obj.symbol.equals("D.$class")).findFirst().orElseThrow(); + var dclz=dcode._compunits.get("D")._clz; + assertEquals(0,dcode._encoding._sdata.buf()[data.offset+dclz.offset(dclz.find("$init"))]); + object(diamond,diamond,"B","int value=D.value;"); + object(diamond,diamond,"C","int value=D.value;"); + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(null,diamond.toString(),new Ary<>(new String[]{diamond.toString()}), + "Main","int value=B.value+C.value;",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + CompUnit b=code._compunits.get("B"), d=code._compunits.get("D"); + ClassInitDependencies deps=new ClassInitDependencies(code); + assertTrue(touches(code,deps,"B","D")); + assertTrue(touches(code,deps,"C","D")); + assertTrue(touches(code,deps,"Main","B")); + assertTrue(touches(code,deps,"Main","C")); + Field flag=d._clz.field("$init"); + assertSame(TypeInteger.U1,flag._t); + assertFalse(flag._final); + assertNotEquals(flag._alias,b._clz.field("$init")._alias); + // Loading just the header/types uses the same alias identity and + // does not require allocating any graph nodes. + ElfReader elf=ElfReader.load(diamond.resolve("D.o").toFile(),d); + assertEquals(flag._alias,elf.loadPublicTypes(code).field("$init")._alias); + assertNull(elf._nodes); + } + object(diamond,diamond,"Helper","val get={ -> D.value; }; val get_noInline={ -> D.value; };"); + for( String name : new String[]{"get","get_noInline"} ) { + CodeGen code=new CodeGen(null,diamond.toString(),new Ary<>(new String[]{diamond.toString()}), + "Main","return Helper."+name+"();",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + ClassInitDependencies deps=new ClassInitDependencies(code); + assertTrue(touches(code,deps,"Main","D")); + assertFalse(touches(code,deps,"Helper","D")); + } + object(diamond,diamond,"Wrapper","val get_noInline={ -> Helper.get_noInline(); };"); + CodeGen wrapped=new CodeGen(null,diamond.toString(),new Ary<>(new String[]{diamond.toString()}), + "Main","return Wrapper.get_noInline();",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertTrue(touches(wrapped,new ClassInitDependencies(wrapped),"Main","D")); + } + + @Test public void testInterleavedModules() throws Exception { + // Two objects, each providing a parent and children. Their orders + // must interleave: P.B precedes Q.A, which precedes P.A. + Path modules=tempDir("import-"), qstub=tempDir("import-"); + module(qstub,qstub,"Q",Map.of("Q","val A=A;", "Q/A","int value=arg;")); + module(modules,qstub,"P",Map.of("P","val A=A; val B=B;", + "P/A","int value=arg; value=Q.A.value;", "P/B","int value=arg;")); + module(modules,modules,"Q",Map.of("Q","val A=A;", "Q/A","int value=arg; value=P.B.value;")); + CodeGen combined=new CodeGen(null,modules.toString(),new Ary<>(new String[]{modules.toString()}), + "Main","return P.A.value;",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + ClassInitDependencies deps=new ClassInitDependencies(combined); + assertTrue(touches(combined,deps,"Q/A","P/B")); + assertTrue(touches(combined,deps,"P/A","Q/A")); + } + + @Test public void testCycleReportDirection() throws Exception { + 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 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"); + 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 testInitializationMemory() throws Exception { + String[] sources={ + "int value=B.value+B.value;", + "int value; if(arg) value=B.value; else value=B.value+1; value=value+B.value;", + "int value; if(arg) value=B.value; value=value+B.value;" + }; + int[] counts={1,2,2}; + for( long seed : new long[]{1,123,987654321} ) + for( int i=0; i<sources.length; i++ ) { + CodeGen code=compile("A",Map.of("A",sources[i],"B","int value=arg;"),seed); + FunNode fun=code.link(code._compunits.get("Root/A")); + CompUnit b=code._compunits.get("Root/B"); + int[] touches={0}; + code._stop.walk(n -> { + if( n instanceof CLInitNode init && init.fun()==fun && init.cu()==b ) { + touches[0]++; + TypeTuple state=(TypeTuple)init._type; + assertSame(TypeMem.BOT,state._types[1]); + TypeMem flag=(TypeMem)state._types[2]; + assertEquals(b._clz.field("$init")._alias,flag._alias); + assertSame(TypeInteger.TRUE,flag._t); + assertNotNull(init.ready()); + } + return null; + }); + assertEquals(sources[i],counts[i],touches[0]); + } + } + + @Test public void testForwardInstanceTouch() { + String src=""" + struct S { int x=7; }; + val _get={ -> _make().x; }; + val _make={ -> new S; }; + return _get(); + """; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(src,seed,true).driver(CodeGen.Phase.Parse); + CLInitNode init=code._stop.walk(n -> n instanceof CLInitNode c ? c : null); + assertNotNull("The unresolved receiver needs a speculative check",init); + assertNotNull("The readiness load is present from parsing",init.ready()); + code.driver(CodeGen.Phase.TypeCheck); + assertNull("An instance needs neither CLInit nor its speculative $init load", + code._stop.walk(n -> n instanceof CLInitNode || + (n instanceof LoadNode ld && ld._name.equals("$init")) ? n : null)); + assertEquals("7",Eval2.eval(code,0)); + } + } + + @Test public void testInitializerFlagStore() throws Exception { + // A same-named struct must retain the file class's hidden flag. + CodeGen code=compile("A",Map.of("A","struct A { int value; }; int value=arg;"),123); + CompUnit a=code._compunits.get("Root/A"); + Field flag=a._clz.field("$init"); + assertNotNull(flag); + assertFalse(flag._final); + FunNode fun=code.link(a); + int[] stores={0}; + code._stop.walk(n -> { + if( n instanceof StoreNode st && st._name.equals("$init") && st._alias==flag._alias ) { + stores[0]++; + assertSame(TypeInteger.TRUE,st.val()._type); + assertSame(fun,CFGNode.earlyCFG(st).fun()); + } + return null; + }); + assertEquals(1,stores[0]); + } + + @Test public void testInitializationExecution() throws Exception { + Path dir=tempDir("execution-"); + object(dir,dir,"State","int count=0;"); + object(dir,dir,"B","State.count++; int value=17+arg;"); + String src="int value=arg ? B.value+B.value : 0; return value+100*State.count;"; + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main",src,seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertNotNull(code._stop.walk(n -> n instanceof CLInitNode ? n : null)); + assertEquals("0",Eval2.eval(code,0)); + assertNotNull("Eval executes CLInit without lowering it",code._stop.walk(n -> n instanceof CLInitNode ? n : null)); + assertEquals("134",Eval2.eval(code,1)); + } + + object(dir,dir,"Main","val run={int take -> "+src.replace("arg ?","take ?")+" };"); + Path cfile=dir.resolve("check.c"); + Files.writeString(cfile,""" + #include <stdint.h> + __attribute__((CALL_CONV)) extern int64_t run(int64_t take); + int main(void) { + return run(0)!=0 || run(1)!=134 || run(1)!=134 || run(0)!=100; + } + """); + String exe=dir.resolve(TestC.OS.startsWith("Windows") ? "check.exe" : "check").toString(); + TestC.linkExe(dir.resolve("Main.o").toString(),TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", + cfile.toString(),new Ary<>(new String[]{dir.resolve("B.o").toString(),dir.resolve("State.o").toString()}),exe); + assertEquals("",TestC.exec(exe)); + for( String cpu : new String[]{"arm","riscv"} ) + new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main",src,123,true,TypeInteger.BOT).driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + + @Test public void testParentInitializationExecution() throws Exception { + Path dir=tempDir("execution-"); + object(dir,dir,"State","int count=0;"); + CodeGen library = module(dir,dir,"P",Map.of("P","val C=C; State.count=State.count*10+1;", + "P/C","State.count=State.count*10+2; int value=arg; return State.count;")); + // Direct entry into the child has no surrounding CLInit call/check. + // Its body must establish the parent itself, including an imported parent. + CodeGen child = new CodeGen(library._modDir,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "P/C",null,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertEquals("12",Eval2.eval(child,99)); + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","return P.C.value+State.count;",seed,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertEquals("12",Eval2.eval(code,99)); + } + object(dir,dir,"Main","val run={ -> P.C.value+State.count; };"); + Path cfile=dir.resolve("check.c"); + Files.writeString(cfile,""" + #include <stdint.h> + __attribute__((CALL_CONV)) extern int64_t run(void); + int main(void) { return run()!=12 || run()!=12; } + """); + String exe=dir.resolve(TestC.OS.startsWith("Windows") ? "check.exe" : "check").toString(); + TestC.linkExe(dir.resolve("Main.o").toString(),TestC.CALL_CONVENTION.equals("win64") ? "ms_abi" : "sysv_abi", + cfile.toString(),new Ary<>(new String[]{dir.resolve("P.o").toString(),dir.resolve("P/C.o").toString(), + dir.resolve("State.o").toString()}),exe); + assertEquals("",TestC.exec(exe)); + } + + @Test public void testNonreturningObject() throws Exception { + Path dir=tempDir("nonreturning-"); + object(dir,dir,"Spin","val spin_noInline={ -> while(1) {} return 7; };"); + CodeGen code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","Spin.spin_noInline(); return 7;",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertNull(Eval2.eval(code,0,10)); + } + + @Test public void testImportedPrivateInitializer() throws Exception { + Path dir=tempDir("private-"); + module(dir,dir,"Library",Map.of("Library","val read={ -> _Hidden.value; };", + "Library/_Hidden","int value=17+arg;")); + CodeGen code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","return Library.read();",123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertEquals("17",Eval2.eval(code,99)); + // Even a request stopping at Select must drop imported bodies. They + // remain available only to the ideal evaluator above, not this object. + for( String cpu : new String[]{"x86_64_v2","arm","riscv"} ) { + CodeGen selected=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Main","return Library.read();",123,true,TypeInteger.BOT).driver(CodeGen.Phase.Select,cpu,"SystemV"); + assertNull(selected._serial); + for( CompUnit cu : selected._compunits.values() ) + if( cu._src==null ) assertNull(cu._stop); + selected.driver(CodeGen.Phase.Encoding,cpu,"SystemV"); + } + } + + @Test public void testOverlappingFunctionAndClassCycles() throws Exception { + // Fixed function graph: Opto/inlining must not erase the overlap that + // 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.<clinit>", "B.<clinit>"}; + for( int i=0; i<names.length; i++ ) { + TypeFunPtr sig=TypeFunPtr.make1((byte)2,false,new Type[0],TypeInteger.BOT,i+f); + CompUnit cu=code._compunits.get(i==3 ? "Root/B" : "Root/A"); + FunNode fun=new FunNode(null,sig,names[i],cu); + fun._type=Type.CONTROL; + code._linker.setX(i+f,fun); + } + ClassInitDependencies deps=new ClassInitDependencies(code); + 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[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[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"); + // A failed check leaves an SCC on the stack. A later check must start + // fresh, and must not treat edges into completed SCCs as back edges. + deps._edgess[b]=null; + assertNull(deps.check()); + } +} |
