diff options
| author | Cliff Click <cliffc@acm.org> | 2026-10-31 09:40:01 -0700 |
|---|---|---|
| committer | Cliff Click <cliffc@acm.org> | 2026-10-31 09:40:01 -0700 |
| commit | 88404cb0316196dedb46eb42263128371debaba4 (patch) | |
| tree | 0979512f295162cf84026baa318e06ef9b60c968 /chapter28a/src | |
| parent | 0ac0c9e10287972ac3af5a4ee43d26f0d5b318fe (diff) | |
| download | Simple-88404cb0316196dedb46eb42263128371debaba4.tar.gz Simple-88404cb0316196dedb46eb42263128371debaba4.zip | |
Add source privacy tests and update test fixtures
Diffstat (limited to 'chapter28a/src')
11 files changed, 244 insertions, 46 deletions
diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/Chapter10Test.java b/chapter28a/src/test/java/com/seaofnodes/simple/Chapter10Test.java index 39c7040e..ca840088 100644 --- a/chapter28a/src/test/java/com/seaofnodes/simple/Chapter10Test.java +++ b/chapter28a/src/test/java/com/seaofnodes/simple/Chapter10Test.java @@ -245,8 +245,9 @@ if (arg) bar = null; if( bar ) bar.a = 1; return bar.a; """); - code.driver(CodeGen.Phase.FinalizeIR); - assertEquals("return Phi(Region,1,.a);", code.print()); + // An erroneous load stays intact instead of partially forwarding it. + try { code.driver(CodeGen.Phase.TypeCheck); fail(); } + catch( Parser.ParseException e ) { assertEquals("Might be null accessing 'a'",e.getMessage()); } } @Test diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/Chapter15Test.java b/chapter28a/src/test/java/com/seaofnodes/simple/Chapter15Test.java index c2071cb6..84bc28de 100644 --- a/chapter28a/src/test/java/com/seaofnodes/simple/Chapter15Test.java +++ b/chapter28a/src/test/java/com/seaofnodes/simple/Chapter15Test.java @@ -221,15 +221,15 @@ return rez;"""); CodeGen code = new CodeGen( """ // Can we define a forward-reference array? -struct _Tree { !_Tree?[]? !_kids; }; +struct _Tree { !_Tree?[]? !kids; }; !_Tree !root = new _Tree; -root._kids = new !_Tree?[2]; // NO BANG SO ARRAY IS OF IMMUTABLE TREES???? -root._kids[0] = new _Tree; +root.kids = new !_Tree?[2]; // NO BANG SO ARRAY IS OF IMMUTABLE TREES???? +root.kids[0] = new _Tree; return root; """); code.driver(CodeGen.Phase.TypeCheck); assertEquals("return Test._Tree;", code.print()); - assertEquals("Test._Tree{_kids=*Test._Tree {*[]*Test._Tree?[mutable]? !_kids; }?[ Test._Tree{_kids=null},null]}", Eval2.eval(code, 0)); + assertEquals("Test._Tree{kids=*Test._Tree {*[]*Test._Tree?[mutable]? !kids; }?[ Test._Tree{kids=null},null]}", Eval2.eval(code, 0)); } @Test diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/Chapter19Test.java b/chapter28a/src/test/java/com/seaofnodes/simple/Chapter19Test.java index d5fe207a..a54377fa 100644 --- a/chapter28a/src/test/java/com/seaofnodes/simple/Chapter19Test.java +++ b/chapter28a/src/test/java/com/seaofnodes/simple/Chapter19Test.java @@ -85,7 +85,7 @@ return 0; """ struct String { u8[~] !cs; - int _hashCode; + int cachedHash; // Public fixture storage: hashing helpers are outside String. new String = { u8[~] s -> cs=s; }; }; @@ -110,15 +110,15 @@ val _hashCodeString = { !String self -> // Return the String hashCode (cached, and never 0) val hashCode = { !String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); + self.cachedHash + ? self.cachedHash + : (self.cachedHash = _hashCodeString(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 MEM[ 2:___ 3:___ 4:___ 5:.cs=Parm_s(Test.String.String,*[]u8[final]); 6:._hashCode=0;]; return MEM[ 2:___ 3:___ 4:___ 5:___ 6:._hashCode=0;]; 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 MEM[ 2:___ 3:___ 4:___ 5:.cs=Parm_s(Test.String.String,*[]u8[final]); 6:.cachedHash=0;]; return MEM[ 2:___ 3:___ 4:___ 5:___ 6:.cachedHash=0;]; return Test.String; return Phi(Region,0,1,0,1); return Phi(Region,(!=0).cachedHash,Phi(Region,123456789,(!=0)Phi(Loop,0,(.[]+((Phi_hash<<5)-Phi_hash))))); ]", code.print()); assertEquals("4029215624828139541", Eval2.eval(code, 2)); } @@ -128,16 +128,16 @@ hashCode(new String("Hello, World!")); """ struct String { u8[~] !cs; - int _hashCode; + int cachedHash; // Public fixture storage: hashing helpers are outside String. new String = { u8[~] s -> cs=s; }; }; val f = { !String self -> 7; }; val hashCode = { !String self -> - self._hashCode + self.cachedHash ? 3 - : (self._hashCode = f(self)); + : (self.cachedHash = f(self)); }; hashCode(new String("Hello")); diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/Chapter20Test.java b/chapter28a/src/test/java/com/seaofnodes/simple/Chapter20Test.java index cdfd16a7..c1355bf2 100644 --- a/chapter28a/src/test/java/com/seaofnodes/simple/Chapter20Test.java +++ b/chapter28a/src/test/java/com/seaofnodes/simple/Chapter20Test.java @@ -227,7 +227,7 @@ return ary[1] * 1000 + ary[3]; // 1 * 1000 + 6 """ struct String { u8[] !cs; - int _hashCode; + int cachedHash; // Public fixture storage: hashing helpers are outside String. new String = { u8[] data -> cs=data; }; }; @@ -243,9 +243,9 @@ val equals = { !String !self, !String s -> // Return the String hashCode (cached, and never 0) val hashCode = { !String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); + self.cachedHash + ? self.cachedHash + : (self.cachedHash = _hashCodeString(self)); }; val _hashCodeString = { !String self -> diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/Chapter21Test.java b/chapter28a/src/test/java/com/seaofnodes/simple/Chapter21Test.java index 2fcf65e9..d5d208b6 100644 --- a/chapter28a/src/test/java/com/seaofnodes/simple/Chapter21Test.java +++ b/chapter28a/src/test/java/com/seaofnodes/simple/Chapter21Test.java @@ -354,7 +354,7 @@ return v0; """ struct String { u8[] !cs; - int _hashCode; + int cachedHash; // Public fixture storage: hashing helpers are outside String. new String = { u8[] data -> cs=data; }; }; @@ -370,9 +370,9 @@ val equals = { !String !self, !String s -> // Return the String hashCode (cached, and never 0) val hashCode = { !String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); + self.cachedHash + ? self.cachedHash + : (self.cachedHash = _hashCodeString(self)); }; val _hashCodeString = { !String self -> @@ -393,7 +393,7 @@ val _hashCodeString = { !String self -> """ struct String { u8[] !cs; - int _hashCode; + int cachedHash; // Public fixture storage: hashing helpers are outside String. new String = { u8[] data -> cs=data; }; }; @@ -409,9 +409,9 @@ val equals = { !String !self, !String s -> // Return the String hashCode (cached, and never 0) val hashCode = { !String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); + self.cachedHash + ? self.cachedHash + : (self.cachedHash = _hashCodeString(self)); }; val _hashCodeString = { !String self -> diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/PrivacyTest.java b/chapter28a/src/test/java/com/seaofnodes/simple/PrivacyTest.java new file mode 100644 index 00000000..21e7f2ff --- /dev/null +++ b/chapter28a/src/test/java/com/seaofnodes/simple/PrivacyTest.java @@ -0,0 +1,196 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.type.TypeInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.Test; +import static org.junit.Assert.*; + +public class PrivacyTest { + private static void rejects(String src, String message) { + for( long seed : new long[]{1,13,123} ) + try { + new CodeGen(src,seed,true).driver(CodeGen.Phase.TypeCheck); + fail("Accepted private access: "+src); + } catch( Parser.ParseException e ) { + assertTrue(e.getMessage(),e.getMessage().contains(message)); + assertNotNull(e._loc); + assertTrue(e.getMessage(),e.getMessage().contains(": error: ")); + } + } + + @Test public void testFieldsAndMethods() { + String box = "struct Box { int _x=7; val _get={ -> _x; }; }; val b=new Box(); "; + for( String use : new String[]{"return b._x;", + "b._x=8; return 0;", "b._x+=1; return 0;", "b._x++; return 0;", + "return b._get();", "val f=b._get; return 0;"} ) + rejects(box+use,"Private field"); + // Constant fields must not bypass the check in generic constant folding. + rejects("struct Box { val _x=7; }; return new Box()._x;","Private field"); + rejects("struct Box { flt _x=7.0; }; return new Box()._x;","Private field"); + + for( String use : new String[]{"b._x; return 0;", + "if(false) b._x; return 0;"} ) { + CodeGen dead = new CodeGen(box+use).driver(CodeGen.Phase.TypeCheck); + assertEquals("0",Eval2.eval(dead,0)); + } + + CodeGen code = new CodeGen(""" + struct Box { + int _x; + new Box={int n -> _x=n; ++_x;}; + val _get={ -> _x; }; + val sum={Box other -> _get()+other._x; }; + }; + val a=new Box(2); + return a.sum(new Box(3)); + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("7",Eval2.eval(code,0)); + // An underscore on a function-local variable does not make it a field. + code = new CodeGen("val run={int n -> int _x=n; ++_x; return _x;}; return run(arg);") + .driver(CodeGen.Phase.TypeCheck); + assertEquals("5",Eval2.eval(code,4)); + } + + @Test public void testNestedOwners() { + // One shared forward ref can have both legal and illegal source uses. + rejects("val own={ -> _x; }; struct Child { val get={ -> _x; }; }; val _x=7; return own()+new Child().get();","Private field"); + rejects("struct Child { val get={ -> _x; }; }; val _x=7; return new Child().get();","Private field"); + rejects("struct Child { val get={ -> _f(); }; }; val _f={ -> 7; }; return new Child().get();","Private field"); + rejects("int _x=7; struct Child { val get={ -> _x; }; }; return 0;","Private field"); + rejects("int _x=7; struct Child { val get={ -> _x=8; }; }; return 0;","Private field"); + rejects("int _x=7; struct Child { val get={ -> ++_x; }; }; return 0;","Private field"); + rejects(""" + struct Outer { + int _x=7; + struct Inner { val get={Outer o -> o._x;}; }; + }; + return 0; + ""","Private field"); + rejects(""" + struct Outer { + struct Inner { int _x=7; }; + val get={Inner i -> i._x;}; + }; + return 0; + ""","Private field"); + rejects(""" + struct A { int _x=7; }; + struct B { val get={A a -> a._x;}; }; + return 0; + ""","Private field"); + } + + @Test public void testPrivateClasses() { + // The private root class name is in scope throughout its own body, + // including nested classes, without exposing its private fields. + new CodeGen(null,null,"_Private", + "struct Child { val root={ -> _Private; }; }; return 0;", + 123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + CodeGen code = new CodeGen(""" + struct _Box { + int _x=7; + val get={ -> _x; }; + val sum={_Box b -> _x+b._x;}; + }; + val b=new _Box(); + return b.sum(new _Box()); + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("14",Eval2.eval(code,0)); + + String outer = "struct Outer { struct _Hidden { int x=7; struct Public { int x=8; }; }; }; "; + for( String use : new String[]{"return new Outer._Hidden();", + "Outer._Hidden? x; return 0;", "return Outer._Hidden;", + "return new Outer._Hidden.Public();"} ) + rejects(outer+use,"Private class"); + rejects("struct _Box { int _x=7; }; return new _Box()._x;","Private field"); + + + } + + static final String LIB = """ + struct _Hidden { int _x=7; val get={ -> _x; }; }; + val make={ -> new _Hidden(); }; + int _secret=11; + val read={ -> _secret; }; + { i32 -> i32 } _exit="C"; + """; + + @Test public void testSourceModules() throws Exception { + Path base = Path.of("build/objs/privacy"); + Files.createDirectories(base); + Path root = Files.createTempDirectory(base,"source-"); + Files.writeString(root.resolve("App.smp"),"return 0;"); + Files.createDirectory(root.resolve("App")); + Files.writeString(root.resolve("App/Lib.smp"),LIB); + Files.writeString(root.resolve("App/Client.smp"),"return Lib.make().get()+Lib.read();"); + CodeGen code = new CodeGen(root.toString(),root.resolve("obj").toString(), + "App/Client",null,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + assertEquals("18",Eval2.eval(code,0)); + for( String use : new String[]{"return Lib._secret;", "Lib._secret=0; return 0;", + "return Lib._exit;", "return new Lib._Hidden();", + "Lib._Hidden? h; return 0;", "return Lib._Hidden;", + "return Lib.make()._x;"} ) { + Files.writeString(root.resolve("App/Client.smp"),use); + try { + new CodeGen(root.toString(),root.resolve("obj").toString(), + "App/Client",null,123,true,TypeInteger.BOT).driver(CodeGen.Phase.TypeCheck); + fail("Accepted private access: "+use); + } catch( Parser.ParseException e ) { + assertTrue(use+": "+e.getMessage(),e.getMessage().contains("Private ")); + assertTrue(e.getMessage(),e.getMessage().contains("Client.smp:")); + } + } + } + + @Test public void testForwardReceiverAndUnusedAccess() { + for( String use : new String[]{"return make()._x;", "return make()._get();", + "val b=make(); b._x=9; return b;"} ) + rejects(""" + val run={ -> %s }; + struct Box { int _x=7; val _get={ -> _x; }; }; + val make={ -> new Box(); }; + return run(); + """.formatted(use),"Private field"); + CodeGen code = new CodeGen(""" + struct Box { + int _x=7; + val read={Box b -> identity(b)._x; }; + val identity={Box b -> b; }; + }; + val b=new Box(); + return b.read(b); + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("7",Eval2.eval(code,0)); + // Unused forward accesses disappear through ordinary DCE. + for( String use : new String[]{"make()._x;", "val f=make()._get;"} ) { + CodeGen dead = new CodeGen("val run={ -> "+use+" return 0; }; "+ + "struct Box { int _x=7; val _get={ -> _x; }; }; "+ + "val make={ -> new Box(); }; return run();").driver(CodeGen.Phase.TypeCheck); + assertEquals("0",Eval2.eval(dead,0)); + } + // A legal inlined setter must not erase the preceding illegal store. + rejects(""" + val run={ -> val b=make(); b._x=1; b.set(2); return b.get(); }; + struct Box { + int _x=7; + val set={int n -> _x=n;}; + val get={ -> _x;}; + }; + val make={ -> new Box(); }; + return run(); + ""","Private field"); + // A dead call has no return type; it must not strand a valid check. + code = new CodeGen(""" + struct Box { + int _x=7; + val read={Box b -> if(false) identity(b)._x; return 7; }; + val identity={Box b -> b; }; + }; + val b=new Box(); + return b.read(b); + """).driver(CodeGen.Phase.TypeCheck); + assertEquals("7",Eval2.eval(code,0)); + } +} diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testString.smp b/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testString.smp index 951cdc32..3a4b349e 100644 --- a/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testString.smp +++ b/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter20Test-testString.smp @@ -1,7 +1,7 @@ struct _String { u8[] cs; new _String={ u8[] b -> cs=b; }; - int _hashCode; + int cachedHash; // External helpers deliberately use public storage. }; val _equals = { !_String !self, !_String s -> @@ -14,9 +14,9 @@ val _equals = { !_String !self, !_String s -> }; val _hashCode = { !_String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); + self.cachedHash + ? self.cachedHash + : (self.cachedHash = _hashCodeString(self)); }; val _hashCodeString = { !_String self -> diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testString.smp b/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testString.smp index a18a5b88..d7b0e73b 100644 --- a/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testString.smp +++ b/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter21AllocTest-testString.smp @@ -1,7 +1,7 @@ // -*- mode: java; -*- struct _String { u8[] cs; - int _hashCode; + int cachedHash; // External helpers deliberately use public storage. new _String = { u8[] data -> cs=data; }; }; @@ -17,9 +17,9 @@ val equals = { !_String !self, !_String s -> // Return the String hashCode (cached, and never 0) val hashCode = { !_String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); + self.cachedHash + ? self.cachedHash + : (self.cachedHash = _hashCodeString(self)); }; val _hashCodeString = { !_String self -> diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testStringExport.smp b/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testStringExport.smp index ee5a0560..486a02e2 100644 --- a/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testStringExport.smp +++ b/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter21Test-testStringExport.smp @@ -1,6 +1,6 @@ struct String { u8[] cs; - int _hashCode; + int cachedHash; // External helpers deliberately use public storage. new String = { u8[] data -> cs=data; }; }; @@ -16,9 +16,9 @@ val equals = { !String !self, !String s -> // Return the String hashCode (cached, and never 0) val hashCode = { !String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); + self.cachedHash + ? self.cachedHash + : (self.cachedHash = _hashCodeString(self)); }; val _hashCodeString = { !String self -> diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter23AllocTest-testString.smp b/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter23AllocTest-testString.smp index 49c80a99..eb9c52b2 100644 --- a/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter23AllocTest-testString.smp +++ b/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter23AllocTest-testString.smp @@ -1,7 +1,7 @@ // -*- mode: java; -*- struct _String { u8[] cs; - int _hashCode; + int cachedHash; // External helpers deliberately use public storage. new _String = { u8[] data -> cs=data; }; }; @@ -17,9 +17,9 @@ val equals = { !_String !self, !_String s -> // Return the String hashCode (cached, and never 0) val hashCode = { !_String self -> - self._hashCode - ? self._hashCode - : (self._hashCode = _hashCodeString(self)); + self.cachedHash + ? self.cachedHash + : (self.cachedHash = _hashCodeString(self)); }; val _hashCodeString = { !_String self -> diff --git a/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testBubbleSort.smp b/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testBubbleSort.smp index f292254e..a5ae22a9 100644 --- a/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testBubbleSort.smp +++ b/chapter28a/src/test/java/com/seaofnodes/simple/spill/Chapter24AllocTest-testBubbleSort.smp @@ -16,7 +16,8 @@ i32 MAP_PRIVATE = 2; i32 MAP_ANON = 32; - val _copy = { u8[] dst, int doff, u8[~] src, int soff, int len -> + // Shared by the module and its nested collection classes. + val copy = { u8[] dst, int doff, u8[~] src, int soff, int len -> for( int i=0; i<len; i++ ) dst[i+doff] = src[i+soff]; return dst; @@ -31,7 +32,7 @@ int newlen = buf# * 2; while( len+len2 >= newlen ) newlen *= 2; u8[] !buf2 = new u8[newlen]; - _copy(buf2,0,buf,0,buf#); + copy(buf2,0,buf,0,buf#); buf = buf2; self; }; @@ -45,7 +46,7 @@ // Add a plain array to the aryu8 val addAry = { u8[~] ary -> _grow(ary#); - _copy(buf,len,ary,0,ary#); + copy(buf,len,ary,0,ary#); len += ary#; self; }; @@ -176,7 +177,7 @@ struct _aryi64 { } // Compress var dst = new u8[len]; - return _copy(dst,0,buf,0,len); + return copy(dst,0,buf,0,len); }; // -*- mode: java; -*- |
