From ce3b04603d7af995004291d9a7eb668f313e6772 Mon Sep 17 00:00:00 2001 From: Cliff Click Date: Fri, 6 Nov 2026 16:06:02 -0800 Subject: Cleanup public escapes Drop "frozen" public escapes, and recompute in Start/Stop so Opto gets a precise escape set. Rename XInt.free -> XInt.alloc Fix some String tests to not escape by default --- .../java/com/seaofnodes/simple/Chapter20Test.java | 23 +++-- .../java/com/seaofnodes/simple/Chapter21Test.java | 38 +++---- .../java/com/seaofnodes/simple/Chapter25Test.java | 66 +++++++++--- .../simple/ClassInitDependenciesTest.java | 22 ++++ .../java/com/seaofnodes/simple/EscapeFlowTest.java | 112 +++++++++++++++++++++ 5 files changed, 215 insertions(+), 46 deletions(-) create mode 100644 chapter29/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java (limited to 'chapter29/src/test/java/com/seaofnodes/simple') diff --git a/chapter29/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/chapter29/src/test/java/com/seaofnodes/simple/Chapter20Test.java index 6cf92dc3..f5eb2b36 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/Chapter20Test.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/Chapter20Test.java @@ -242,13 +242,6 @@ val equals = { !String !self, !String s -> return true; }; -// Return the String hashCode (cached, and never 0) -val hashCode = { !String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); -}; - val _hashCodeString = { !String self -> int hash=0; for( int i=0; i< self.cs#; i++ ) @@ -256,11 +249,19 @@ val _hashCodeString = { !String self -> if( !hash ) hash = 123456789; return hash; }; + +// Return the String hashCode (cached, and never 0) +val hashCode = { !String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; + """; - // The implicit return exposes _hashCodeString, so retain its body too. - testCPU(src,"x86_64_v2", "SystemV",21,null); - testCPU(src,"riscv" , "SystemV",9,null); - testCPU(src,"arm" , "SystemV",7,null); + // Return the public hashCode wrapper; keep its helper private. + testCPU(src,"x86_64_v2", "SystemV",12,null); + testCPU(src,"riscv" , "SystemV",8,null); + testCPU(src,"arm" , "SystemV",6,null); } @Test diff --git a/chapter29/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter29/src/test/java/com/seaofnodes/simple/Chapter21Test.java index f1dc54e2..e15c2581 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/Chapter21Test.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -368,13 +368,6 @@ val equals = { !String !self, !String s -> return true; }; -// Return the String hashCode (cached, and never 0) -val hashCode = { !String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); -}; - val _hashCodeString = { !String self -> int hash=0; for( int i=0; i< self.cs#; i++ ) @@ -382,11 +375,18 @@ val _hashCodeString = { !String self -> if( !hash ) hash = 123456789; return hash; }; + +// Return the String hashCode (cached, and never 0) +val hashCode = { !String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; """; - // The implicit return exposes _hashCodeString, so retain its body too. - testCPU(src,"x86_64_v2", "SystemV",21,null); - testCPU(src,"riscv" , "SystemV",9,null); - testCPU(src,"arm" , "SystemV",7,null); + // Return the public hashCode wrapper; keep its helper private. + testCPU(src,"x86_64_v2", "SystemV",12,null); + testCPU(src,"riscv" , "SystemV",8,null); + testCPU(src,"arm" , "SystemV",6,null); } @Test public void testStringExport() throws IOException { @@ -408,13 +408,6 @@ val equals = { !String !self, !String s -> return true; }; -// Return the String hashCode (cached, and never 0) -val hashCode = { !String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); -}; - val _hashCodeString = { !String self -> int hash=0; for( int i=0; i< self.cs#; i++ ) @@ -422,8 +415,15 @@ val _hashCodeString = { !String self -> if( !hash ) hash = 123456789; return hash; }; + +// Return the String hashCode (cached, and never 0) +val hashCode = { !String self -> + self._hashCode + ? self._hashCode + : (self._hashCode = _hashCodeString(self)); +}; """; - TestC.runC(src, "stringHash", "",21); + TestC.runC(src, "stringHash", "",12); } @Test public void testLoop2() { diff --git a/chapter29/src/test/java/com/seaofnodes/simple/Chapter25Test.java b/chapter29/src/test/java/com/seaofnodes/simple/Chapter25Test.java index f5eba8f5..5f615282 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/Chapter25Test.java +++ b/chapter29/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; @@ -12,6 +13,8 @@ import com.seaofnodes.simple.node.CallNode; import com.seaofnodes.simple.node.FunNode; import com.seaofnodes.simple.node.LoopNode; import com.seaofnodes.simple.type.TypeInteger; +import com.seaofnodes.simple.type.TypeMem; +import com.seaofnodes.simple.type.TypeTuple; import com.seaofnodes.simple.type.XInt; import com.seaofnodes.simple.util.Ary; import java.io.File; @@ -73,21 +76,25 @@ public class Chapter25Test { for( long seed : new long[]{1,123,987654321} ) { CodeGen code = new CodeGen(src,seed,true).driver(CodeGen.Phase.Parse); FunNode visible = null; + int[] privateFidxs = XInt.EMPTY; int checked = 0; for( FunNode fun : code._linker ) { if( fun==null || fun._name==null ) continue; if( fun._name.equals("visible") ) { - assertTrue(code.publicFIDX(fun.sig().fidx())); visible = fun; checked++; } if( fun._name.equals("_hidden") || fun._name.equals("hiddenClassMethod") ) { - assertFalse(code.publicFIDX(fun.sig().fidx())); + privateFidxs = XInt.make(privateFidxs,fun.sig().fidx()); checked++; } } assertEquals(3,checked); code.driver(CodeGen.Phase.TypeCheck); + TypeMem outside = ((TypeTuple)code._start._type).mem(); + assertTrue(XInt.bit(outside._escFs,visible.sig().fidx())); + for( int fidx = XInt.next(privateFidxs,0); fidx>=0; fidx = XInt.next(privateFidxs,fidx) ) + assertFalse(XInt.bit(outside._escFs,fidx)); assertTrue(visible.unknownCallers()); assertEquals(TypeInteger.U8,visible.parm(3)._type); assertEquals("0",Eval2.eval(code,0)); @@ -101,25 +108,52 @@ public class Chapter25Test { val _private = { int x -> x+1; }; return 0; """).parse(); - int checked = 0; + int published = -1, unpublished = -1; for( FunNode fun : code._linker ) { if( fun==null || fun._name==null ) continue; - if( fun._name.equals("_hidden") ) { - // A public field can publish a privately named function. - assertTrue(code.publicFIDX(fun.sig().fidx())); - checked++; - } - if( fun._name.equals("_private") ) { - assertFalse(code.publicFIDX(fun.sig().fidx())); - checked++; - } + if( fun._name.equals("_hidden") ) published = fun.sig().fidx(); + if( fun._name.equals("_private") ) unpublished = fun.sig().fidx(); } - assertEquals(2,checked); + assertTrue(published>=0 && unpublished>=0); var clz = code.compunit()._clz; - assertTrue(XInt.bit(code.publicAliases(),clz.field("visible")._alias)); - assertFalse(XInt.bit(code.publicAliases(),clz.field("_hidden")._alias)); - assertFalse(XInt.bit(code.publicAliases(),clz.field("_private")._alias)); code.driver(CodeGen.Phase.TypeCheck); + TypeMem outside = ((TypeTuple)code._start._type).mem(); + // A public field can publish a privately named function. + assertTrue(XInt.bit(outside._escFs,published)); + assertFalse(XInt.bit(outside._escFs,unpublished)); + assertTrue(XInt.bit(outside._escAs,clz.field("visible")._alias)); + assertFalse(XInt.bit(outside._escAs,clz.field("_hidden")._alias)); + assertFalse(XInt.bit(outside._escAs,clz.field("_private")._alias)); + } + + @Test public void testImportedEscapeExclusions() throws IOException { + Path build=Path.of("build/objs"); + Files.createDirectories(build); + Path dir=Files.createTempDirectory(build,"escape_exclusions_"); + CodeGen library=new CodeGen(null,dir.toString(),null,"Lib",""" + val _hidden_noInline={int n -> n+100;}; + val apply_noInline={{int->int} f,int n -> f(n);}; + return apply_noInline; + """,123,true,TypeInteger.BOT).driver("x86_64_v2","win64",false,false); + int savedHidden=((com.seaofnodes.simple.type.TypeFunPtr) + library.compunit()._clz.field("_hidden_noInline")._t).fidx(); + // Allocate unrelated IDs before loading Lib so saved exclusions must + // remap by owner/slot, not by their numeric positions in the file. + CodeGen code=new CodeGen(null,dir.toString(),new Ary<>(new String[]{dir.toString()}), + "Client",""" + val _other={int n -> n-1;}; + val inc={int n -> n+1;}; + return Lib.apply_noInline(inc,arg); + """,987654321,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + var clz=(com.seaofnodes.simple.type.TypeStruct)Parser.TYPES.get("class:Lib"); + int hidden=((com.seaofnodes.simple.type.TypeFunPtr)clz.field("_hidden_noInline")._t).fidx(); + assertNotEquals(savedHidden,hidden); + TypeMem outside=((TypeTuple)code._start._type).mem(); + assertTrue(XInt.isHigh(outside._escFs)); + assertFalse(XInt.bit(outside._escFs,hidden)); + assertEquals("42",Eval2.eval(code,41)); + Files.delete(dir.resolve("Lib.o")); + Files.delete(dir); } @Test public void testNeverReturnPointerRiscV() { diff --git a/chapter29/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java b/chapter29/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java index 88ebb6b7..dae64fb0 100644 --- a/chapter29/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java +++ b/chapter29/src/test/java/com/seaofnodes/simple/ClassInitDependenciesTest.java @@ -158,6 +158,28 @@ public class ClassInitDependenciesTest { assertTrue(touches(code,deps,"Root/B","Root")); } + @Test public void testUnknownCallbackCycle() throws Exception { + for( long seed : new long[]{1,123,987654321} ) { + try { + compile("A",Map.of( + "A", """ + { {int->int} -> {int->int} } bounce="C"; + val callback_noInline={int n -> B.value;}; + val f=bounce(callback_noInline); + int value=f(1); + """, + "B", "int value=A.value;"),seed); + fail("Expected callback initialization cycle"); + } catch( Parser.ParseException ex ) { + String message=ex.getMessage(); + assertTrue(message,message.startsWith("Cyclic class initialization:")); + assertTrue(message,message.contains(" -> unknown call")); + assertTrue(message,message.contains("unknown call -> ")); + assertTrue(message,message.contains("Root.B -> Root.A")); + } + } + } + @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"); diff --git a/chapter29/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java b/chapter29/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java new file mode 100644 index 00000000..71a281b5 --- /dev/null +++ b/chapter29/src/test/java/com/seaofnodes/simple/EscapeFlowTest.java @@ -0,0 +1,112 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.*; +import com.seaofnodes.simple.type.*; +import org.junit.Test; +import static org.junit.Assert.*; + +public class EscapeFlowTest { + // A cofinite outside set must not revive private allocation helpers. + @Test public void testOpaqueRoundTrip() { + for( long seed : new long[]{1,123,987654321} ) { + CodeGen code = new CodeGen(""" + struct _A { B? !b; new _A = { B? p -> b=p; }; }; + val wrap = { B? b -> return new _A(b); }; + return wrap; + """,seed,true).driver(CodeGen.Phase.TypeCheck); + TypeMem outside = ((TypeTuple)code._start._type).mem(); + assertTrue(XInt.isHigh(outside._escFs)); + assertNotSame(XInt.FULL,outside._escFs); + for( FunNode fun : code._linker ) + if( fun!=null && fun._name!=null && fun._name.contains("_A.") ) + assertFalse(fun._name,fun.unknownCallers()); + } + } + + // Foreign callbacks remain possible. The default callback argument reads + // Start memory rather than a frozen declaration constant. + @Test public void testUnknownCallerCallback() { + CodeGen code = new CodeGen(""" + val _hidden_noInline = { int n -> return n+2; }; + val apply_noInline = { {int->int} f,int n -> return f(n); }; + return apply_noInline; + """).parse(); + TypeMemPtr self = (TypeMemPtr)named(code,"apply_noInline").parm(2).in(1)._type; + assertTrue(self._one); + assertEquals("class:Test",self._obj._name); + code.driver(CodeGen.Phase.TypeCheck); + FunNode apply = named(code,"apply_noInline"); + assertTrue(apply.unknownCallers()); + ParmNode callback = apply.parm(3); + assertTrue(callback.in(1) instanceof ProjNode); + TypeFunPtr type = (TypeFunPtr)callback._type; + assertTrue(XInt.isHigh(type.fidxs())); + assertSame(TypeInteger.BOT,apply.ret().expr()._type); + // Only and apply escape; forwarding #self must not expose _hidden. + assertEquals("Stop[ return { apply_noInline}; return #2; ]",code.print()); + } + + @Test public void testNativeCallbackRoundTrip() { + CodeGen code = new CodeGen(""" + val _callback_noInline = { int n -> return n+1; }; + { {int->int} -> {int->int} } bounce = "C"; + return bounce(_callback_noInline)(arg); + """).driver(CodeGen.Phase.TypeCheck); + FunNode callback = named(code,"_callback_noInline"); + assertTrue(callback.unknownCallers()); + assertTrue(XInt.bit(((TypeTuple)code._start._type).mem()._escFs,callback.sig().fidx())); + } + + // Call remains CONTROL. Changing only the outside set must wake CallEnd + // and extend the possible callback returned by the native factory. + @Test public void testOutsideReturnDependency() { + CodeGen code = new CodeGen("return 0;").parse(); + code._phase = CodeGen.Phase.Opto; + code.externFunc(100,"factory"); + TypeFunPtr callback = TypeFunPtr.make(false,TypeFunPtr.TEMPTY,TypeInteger.BOT); + TypeFunPtr factory = TypeFunPtr.make1((byte)2,true,TypeFunPtr.TEMPTY,callback,100); + CallNode call = new CallNode(null,ConstantNode.raw(Type.CONTROL), + ConstantNode.raw(TypeMem.START),ConstantNode.raw(factory)).init(); + CallEndNode end = new CallEndNode(call,TypeRPC.BOT); + int[] foreign = XInt.dual(XInt.make(7)); + code._start._type = TypeTuple.make(Type.CONTROL,TypeMem.START.makeFrom(foreign,XInt.EMPTY),TypeInteger.BOT); + TypeTuple before = (TypeTuple)end.compute(); + assertFalse(XInt.bit(((TypeFunPtr)before.ret()).fidxs(),7)); + boolean depends = false; + for( int i = 0; i n=x; }; }; + return new Box(arg).n; + """).parse(); + FunNode allocator = named(code,"Test.Box."); + int[] helpers = XInt.EMPTY; + for( FunNode fun : code._linker ) + if( fun!=null && fun.sig().ret() instanceof TypeMem ) + helpers = XInt.make(helpers,fun.sig().fidx()); + assertNotSame(XInt.EMPTY,helpers); + code.driver(CodeGen.Phase.TypeCheck); + TypeMem outside = ((TypeTuple)code._start._type).mem(); + assertTrue(allocator.unknownCallers()); + assertTrue(XInt.bit(outside._escFs,allocator.sig().fidx())); + for( int fidx = XInt.next(helpers,0); fidx>=0; fidx = XInt.next(helpers,fidx) ) + assertFalse(XInt.bit(outside._escFs,fidx)); + assertEquals("42",Eval2.eval(code,42)); + } + + private static FunNode named(CodeGen code, String name) { + for( FunNode fun : code._linker ) + if( fun!=null && name.equals(fun._name) ) return fun; + throw new AssertionError("Missing function "+name); + } +} -- cgit v1.2.3