diff options
Diffstat (limited to 'chapter-escape/src')
4 files changed, 130 insertions, 14 deletions
diff --git a/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java b/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java index f22b73f1..19dd1c29 100644 --- a/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java +++ b/chapter-escape/src/main/java/com/seaofnodes/simple/Parser.java @@ -267,7 +267,7 @@ public class Parser { // Pre-call the function from Start, with worse-case arguments. This // represents all the future, yet-to-be-parsed functions calls and // external calls. - _scope.push(new Kind.Func()); + _scope.push(new Kind.Func(_privateFun!=null)); ctrl(fun); // Scope control from function // Unknown callers supply whole memory; field aliases are discovered lazily. _scope.mem(new ParmNode(ScopeNode.MEM0,1,TypeMem.BOT,fun,new ProjNode(_code._start,1,ScopeNode.MEM0).peephole()).peephole()); @@ -768,6 +768,7 @@ public class Parser { // The alloc() call added the allocation scope !_scope.canInit(def) ) throw error("Cannot reassign final '"+name+"'"); + checkSharedField(def); // Parse assignment expression checkCaptureWrite(def); @@ -869,6 +870,14 @@ public class Parser { throw error("a not-null/non-zero expression"); t = expr._type; if( !xfinal ) t = inferredType(t); // Widen if not final + // A final field is still a local variable during construction. + // Its constant default is a value, not a one-value declaration + // type: later assignments and loop Phis may change it. + else if( _scope.inConstructor() && !(t instanceof TypeFunPtr) ) { + Type wide=inferredType(t); + t = t instanceof TypeMemPtr ptr && ptr.notNull() + ? TypeMemPtr.make((byte)2,((TypeMemPtr)wide)._obj,false).withAccess(ptr._ro) : wide; + } } // expr is a constant function if( t instanceof TypeFunPtr && expr._type instanceof TypeFunPtr tfp && tfp.isConstant() ) { @@ -948,7 +957,10 @@ public class Parser { Var v = _scope.var(index++); if( v.isFRef() ) continue; Node value = _scope.in(v); - boolean constant = v._final && v._init && value._type.isConstant(); + // Only function declarations are shared across instances. A final + // data field can change in a named or inline constructor, so a + // constant default does not justify folding every future read. + boolean constant = v._final && v._init && value._type instanceof TypeFunPtr && value._type.isConstant(); Field field = Field.make(v._name,v.type(),_code.getALIAS(),v._final,constant); shape=shape.add(field); TYPES.put(_nestedType,TypeMemPtr.make(shape)); @@ -1452,8 +1464,9 @@ public class Parser { if( name!=null ) { Var n = _scope.lookup(name); if( n != null && !(n.type() instanceof TypeMemPtr) ) { - if( n._final ) + if( n._final && !_scope.canInit(n) ) throw error("Cannot reassign final '"+n._name+"'"); + checkSharedField(n); Node expr = liftExpr(peep(new AddNode(_scope.in(n),con(delta))),n.type(),true); _scope.update(n,expr); return expr; @@ -1545,8 +1558,9 @@ public class Parser { if( ch==0 ) // Normal primary, check for postfix updates return parsePostfix(rvalue); // Assign-update direct into Scope - if( var._final ) + if( var._final && !_scope.canInit(var) ) throw error("Cannot reassign final '"+var._name+"'"); + checkSharedField(var); checkCaptureWrite(var); Node op = opAssign(ch,rvalue, var.type() ); _scope.update(var,op); @@ -1600,6 +1614,24 @@ public class Parser { throw error("Cannot assign captured variable '"+var._name+"' in an initializer"); } + // Inline constructors have local field slots too, but shared function + // declarations have no instance slot to publish. Do not silently discard + // an assignment to one of those declarations. + private void checkSharedField(Var var) { + if( !_scope.canInit(var) ) return; + if( _scope.kind(var) instanceof Kind.Define ) { + checkPrivateWrite(var._name); + return; + } + for( int i=_scope.depth()-1; i>0; i-- ) + if( _scope._kinds.at(i) instanceof Kind.Alloc alloc ) { + Field field=alloc._tmp._obj.field(var._name); + if( field!=null && field._one ) + throw error("Cannot reassign shared function '"+var._name+"'"); + return; + } + } + // Check for assign-update, "x += e0". // Returns "x + e0"; the caller assigns value. // if ch is postfix, then lhs is kept and caller will unkeep and return it. @@ -1980,6 +2012,7 @@ public class Parser { if( v.type() instanceof TypeFunPtr ctor && ctor.ret() instanceof TypeMem ) throw error("A constructor is only called by new"); if( matchOpx('=','=') ) { + checkPrivateWrite(name); Node value=parseAsgn().keep(); checkPublication(value); _scope.update(v,liftExpr(value,v.type(),false)); @@ -1990,6 +2023,7 @@ public class Parser { throw error("Cannot read uninitialized field '"+name+"'"); char ch=_lexer.matchOperAssign(); if( ch!=0 ) { + checkPrivateWrite(name); Node op=opAssign(ch,value,v.type()); checkPublication(op); _scope.update(v,op); @@ -2000,6 +2034,12 @@ public class Parser { return parsePostfix(value); } + private void checkPrivateWrite(String name) { + Field field=((TypeMemPtr)TYPES.get(_nestedType))._obj.field(name); + if( field!=null && field._one ) + throw error("Cannot reassign shared function '"+name+"'"); + } + // zero/sign extend. "i" is limited to either classic unsigned (min==0) or // classic signed (min=minus-power-of-2); max=power-of-2-minus-1. private Node zsMask(Node val, Type t ) { diff --git a/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java b/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java index 3c1e9776..f65a822d 100644 --- a/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java +++ b/chapter-escape/src/main/java/com/seaofnodes/simple/node/ScopeNode.java @@ -39,7 +39,10 @@ public class ScopeNode extends Node { // Basic block scoping public static class Block extends Kind { } // Function scope - public static class Func extends Kind { } + public static class Func extends Kind { + public final boolean _constructor; + public Func(boolean constructor) { _constructor=constructor; } + } // Struct definition, mapping types to fields public static class Define extends Kind { public Define(TypeMemPtr tmp) { _tmp=tmp; } public final TypeMemPtr _tmp; } // Struct allocation, mapping values to fields @@ -137,15 +140,25 @@ public class ScopeNode extends Node { } - // Allocation fields live in the scope immediately outside the constructor - // block. Nested blocks retain that permission; surrounding bindings do not. + // Fields are writable local values until construction ends, even when the + // published field is final. Ordinary methods and final local variables do + // not inherit that permission. This also decides which loop Phis we need. public boolean canInit(Var v) { - for( int i=_kinds._len-1; i>0; i-- ) - if( _kinds.at(i) instanceof Kind.Alloc ) + for( int i=_kinds._len-1; i>=0; i-- ) { + Kind k=_kinds.at(i); + if( k instanceof Kind.Alloc ) return v._idx >= _kinds.at(i-1)._lexSize && v._idx < _kinds.at(i)._lexSize; + if( k instanceof Kind.Define ) return kind(v)==k; + if( k instanceof Kind.Func fun ) + return fun._constructor && i>0 && _kinds.at(i-1) instanceof Kind.Define def && kind(v)==def; + } return false; } + private boolean loopVar(Var v) { + return !v._final || (canInit(v) && !v.type().isHighOrConst()); + } + public boolean inConstructor() { return _kinds.last() instanceof Kind.Define; } public boolean inAllocation () { return _kinds.last() instanceof Kind.Alloc ; } public boolean inFunction () { return _kinds.last() instanceof Kind.Func ; } @@ -294,7 +307,7 @@ public class ScopeNode extends Node { // Memory is one binding, with the same lazy loop Phi as scalar names. for( int i=1; i<nIns(); i++ ) - dup.addDef(loop && !_vars.at(i)._final ? this : in(i)); + dup.addDef(loop && loopVar(var(i)) ? this : in(i)); return dup; } @@ -356,7 +369,7 @@ public class ScopeNode extends Node { // Fill in the backedge of any inserted Phis void _endLoop( ScopeNode scope, Node back, Node exit ) { for( int i=1; i<nIns(); i++ ) { - if( _vars.at(i)._final ) continue; // Final vars did not get modified in the loop + if( !loopVar(var(i)) ) continue; if( _vars.at(i).type().isHighOrConst() ) continue; // Cannot lift higher than a constant, so no Phi if( back.in(i) != scope ) { PhiNode phi = (PhiNode)in(i); diff --git a/chapter-escape/src/main/java/com/seaofnodes/simple/type/Field.java b/chapter-escape/src/main/java/com/seaofnodes/simple/type/Field.java index 96e6d766..9bd85ba6 100644 --- a/chapter-escape/src/main/java/com/seaofnodes/simple/type/Field.java +++ b/chapter-escape/src/main/java/com/seaofnodes/simple/type/Field.java @@ -17,10 +17,10 @@ public class Field extends Type { public Type _t; // Unique memory alias, not sensibly part of a "type" but very convenient here. public int _alias; - // Field must be written to exactly once, no more, no less + // Field is immutable after construction; constructor locals may change public boolean _final; - // Field is final in the declaration; value is the same for all instances - // and will be moved to the class object + // Shared function declaration, with the same value for every instance. + // A final data field is still per-instance, even with a constant default. public boolean _one; private static final Ary<Field> FREE = new Ary<>(Field.class); diff --git a/chapter-escape/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java b/chapter-escape/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java new file mode 100644 index 00000000..e91dc384 --- /dev/null +++ b/chapter-escape/src/test/java/com/seaofnodes/simple/FinalConstructionTest.java @@ -0,0 +1,63 @@ +package com.seaofnodes.simple; + +import com.seaofnodes.simple.codegen.CodeGen; +import com.seaofnodes.simple.node.StoreNode; +import org.junit.Test; +import static org.junit.Assert.*; + +public class FinalConstructionTest { + private static final String LOOP = """ + val step={int x -> x*2+1;}; + struct C { + val x=1; + new C={int n -> + x=0; x=1; + for(int i=0; i<n; i++) x=step(x); + }; + val get={->x;}; + }; + return new C(arg).get(); + """; + + @Test public void testFinalConstructorLocals() 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; + }); + 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;", + "struct C { val x=1; new C={int n -> for(int i=0;i<n;i++) ++x;}; }; return new C(arg).x;", + "struct C { val x=1.5; new C={->x=2.5;}; }; return new C().x;", + "struct C { val x=\"a\"; new C={int n -> for(int i=0;i<n;i++) x=\"b\";}; }; return new C(arg).x[0];", + "struct C { val x=new int[1]; new C={->x=new int[2]; x=new int[3];}; }; return new C().x#;" + }; + String[] results={"2","8","4","2.5","98","3"}; + for( int i=0; i<sources.length; i++ ) + assertEquals(results[i],Eval2.eval(new CodeGen(sources[i]).driver(CodeGen.Phase.TypeCheck),3)); + + String[] rejected={ + "struct C { val x=1; }; val c=new C(); c.x=2; return c.x;", + "struct C { new C={->val x=1; x=2;}; }; return new C();", + "struct C { val f={->1;}; new C={->f={->2;};}; }; return new C().f();" + }; + for( String src : rejected ) { + try { new CodeGen(src).driver(CodeGen.Phase.TypeCheck); fail(src); } + catch( Parser.ParseException e ) { assertTrue(e.getMessage(),e.getMessage().startsWith("Cannot")); } + } + + var dir=java.nio.file.Path.of("build/objs"); + java.nio.file.Files.createDirectories(dir); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.smp"),"val buildFinal={int arg ->"+LOOP+"};"); + java.nio.file.Files.writeString(dir.resolve("finalConstruction.c"),""" + extern long long buildFinal(long long); + int main(void) { return buildFinal(0)!=1 || buildFinal(3)!=15 || buildFinal(5)!=63; } + """); + TestC.run("build/objs","finalConstruction","",-1); + } +} |
