package x10.visit; import polyglot.ast.*; import polyglot.util.ErrorInfo; import polyglot.util.Position; import polyglot.util.CollectionUtil; import x10.util.CollectionFactory; import polyglot.visit.NodeVisitor; import polyglot.visit.DataFlow; import polyglot.visit.FlowGraph; import polyglot.visit.ContextVisitor; import polyglot.visit.InitChecker; import polyglot.visit.DataFlow.Item; import polyglot.visit.FlowGraph.EdgeKey; import polyglot.frontend.Job; import polyglot.types.Flags; import polyglot.types.Type; import polyglot.types.FieldDef; import polyglot.types.MethodDef; import polyglot.types.FieldInstance; import polyglot.types.ProcedureDef; import polyglot.types.QName; import polyglot.types.ClassDef; import polyglot.types.Ref; import polyglot.types.SemanticException; import polyglot.types.Name; import polyglot.types.Context; import polyglot.types.ProcedureDef_c; import polyglot.types.Types; import x10.ast.*; import x10.errors.Errors; import polyglot.types.TypeSystem; import polyglot.types.VarDef; import polyglot.types.LocalDef; import polyglot.types.ClassType; import polyglot.types.ContainerType; import x10.types.X10FieldDef; import x10.types.MethodInstance; import x10.types.X10ParsedClassType_c; import x10.types.X10ProcedureDef; import x10.types.X10MethodDef; import x10.types.X10ConstructorDef; import x10.types.X10FieldDef_c; import x10.types.checker.ThisChecker; import x10.util.Synthesizer; import java.util.HashMap; import java.util.Set; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.LinkedHashMap; import java.util.ArrayList; import java.util.Collections; import java.util.TreeSet; import java.util.TreeMap; import java.util.Arrays; import static polyglot.visit.InitChecker.*; /** * Checks correct object initialization, i.e., * - this cannot escape during construction * - correct usages of @NonEscaping and @NoThisAccess * - fields are not read before they're assigned. * - property(...) is called exactly once * * We do not track: * - static fields (see FwdReferenceChecker, and other checks are done in X10FieldAssign_c and X10FieldDecl_c) * - locals (see InitChecker) */ public class CheckEscapingThis extends NodeVisitor { public static long TIME = 0; public final static boolean GATHER_STATS = false; // Gather statistics for the initialization paper private static int ASYNC_INIT_COUNT = 0; private static HashSet<X10ProcedureDef> ALL_CTORS = new HashSet<X10ProcedureDef>(); private static HashSet<X10ProcedureDef> ALL_METHODS = new HashSet<X10ProcedureDef>(); private static HashSet<X10ProcedureDef> ALL_EXPLICIT_NON_ESCAPING_METHODS = new HashSet<X10ProcedureDef>(); private static HashSet<X10ProcedureDef> ALL_NON_ESCAPING_METHODS = new HashSet<X10ProcedureDef>(); private static HashSet<X10ProcedureDef> ALL_NO_THIS_ACCESS = new HashSet<X10ProcedureDef>(); private static class DataFlowItem extends BaseDataFlowItem<FieldDef> {} class FieldChecker extends DataFlow { private ProcedureDecl currDecl; private DataFlowItem init; private DataFlowItem finalResult; public FieldChecker(DataFlowItem init) { super(CheckEscapingThis.this.job, CheckEscapingThis.this.ts, CheckEscapingThis.this.nf, true /* forward analysis */, false /* perform dataflow when leaving CodeDecls, not when entering */); this.init = init; } protected Item createInitialItem(FlowGraph graph, Term node, boolean entry) { return init; } protected Item confluence(List<Item> items, List<EdgeKey> itemKeys, Term node, boolean entry, FlowGraph graph) { if (node instanceof ProcedureDecl) { List<Item> filtered = filterItemsNonException(items, itemKeys); if (filtered.isEmpty()) { // see XTENLANG-1851 DataFlowItem res = new DataFlowItem(); for (FieldDef d : init.initStatus.keySet()) res.initStatus.put(d, MinMaxInitCount.ONE); return res; } else if (filtered.size() == 1) { return (Item)filtered.get(0); } else { return confluence(filtered, node, entry, graph); } } return confluence(items, node, entry, graph); } protected Item confluence(List<Item> items, Term node, boolean entry, FlowGraph graph) { //if (items.size()==0) return INIT; //if (items.size()==1) return (DataFlowItem)items.get(0); assert items.size()>=2; boolean isAsync = !entry && node instanceof Async; boolean isUncounted = isAsync ? Lowerer.isUncountedAsync((TypeSystem)ts,(Async) node) : false; DataFlowItem res = new DataFlowItem(); res.initStatus.putAll(((DataFlowItem) items.get(0)).initStatus); for (int i=1; i<items.size(); i++) { DataFlowItem item = (DataFlowItem) items.get(i); for (Map.Entry<FieldDef, MinMaxInitCount> pair : item.initStatus.entrySet()) { final FieldDef key = pair.getKey(); MinMaxInitCount i1 = res.initStatus.get(key); MinMaxInitCount i2 = pair.getValue(); // Async must be handled here, because DataFlow first calls safeConfluence, and only then it calls flow: //p.inItem = this.safeConfluence(... //p.outItems = this.flow(... MinMaxInitCount i_res = isAsync ? i1.afterAsync(i2,isUncounted) : i1.afterIf(i2); res.initStatus.put(key,i_res); } } return res; } protected FlowGraph initGraph(CodeNode code, Term root) { currDecl = (ProcedureDecl)code; // we do not analyze closures (Closure_c) since we require that "this" does not escape there return super.initGraph(code,root); } protected Map<EdgeKey, Item> flow(List<Item> inItems, List<EdgeKey> inItemKeys, FlowGraph graph, Term n, boolean entry, Set<EdgeKey> edgeKeys) { return this.flowToBooleanFlow(inItems, inItemKeys, graph, n, entry, edgeKeys); } private DataFlowItem fieldReadWrite(boolean isAssign, FieldDef field, DataFlowItem inItem) { DataFlowItem res = new DataFlowItem(); res.initStatus.putAll(inItem.initStatus); final MinMaxInitCount valueBefore = inItem.initStatus.get(field); if (valueBefore!=null) { // can happen for fake fields, e.g., class Bar { val q= this.f; } MinMaxInitCount valueAfter = isAssign ? valueBefore.afterAssign() : valueBefore.afterRead(); res.initStatus.put(field,valueAfter); } return res; } public Map<EdgeKey, Item> flow(Item trueItem, Item falseItem, Item otherItem, FlowGraph graph, Term n, boolean entry, Set<EdgeKey> succEdgeKeys) { final DataFlowItem inItem = (DataFlowItem) safeConfluence(trueItem, FlowGraph.EDGE_KEY_TRUE, falseItem, FlowGraph.EDGE_KEY_FALSE, otherItem, FlowGraph.EDGE_KEY_OTHER, n, entry, graph); DataFlowItem res = inItem; if (trueItem == null) trueItem = inItem; if (falseItem == null) falseItem = inItem; if (entry) { return itemToMap(inItem, succEdgeKeys); } boolean isAssign = n instanceof FieldAssign; if (isAssign || n instanceof Field) { final FieldInstance fi = isAssign ? ((FieldAssign) n).fieldInstance() : ((Field) n).fieldInstance(); FieldDef field = fi.def(); if (field!=null && (isAssign ? isTargetThis((FieldAssign) n) : isTargetThis((Field) n))) { res = fieldReadWrite(isAssign, field, inItem); if (isAssign) { // make sure we assign to val fields at most once if (field.flags().isFinal()) { // check it is assigned exactly once final MinMaxInitCount maxInitCount = res.initStatus.get(field); if (maxInitCount.isIllegalVal()) reportError(new Errors.FinalFieldAlreadyInitialized(field.name(),n.position())); } } } } else if (n instanceof AssignPropertyCall) { res = fieldReadWrite(true, propertyRepresentative,inItem); // I don't need to recurse into the constraint of a X10CanonicalTypeNode because: // - in STATIC_CHECKS it doesn't generate any code for them, therefore there is nothing to check. // e.g. the following is legal: class A { val f1:A{this.f2==f1} = null; val f2:A = null; } // - in DYNAMIC_CHECKS it generates a cast, and we recurse into that cast next: } else if (n instanceof X10Cast) { X10Cast cast = (X10Cast)n; // convert constraint to Expr final Set<VarDef> exprs = Synthesizer.getLocals(cast.castType()); for (VarDef e : exprs) if (e instanceof FieldDef) { FieldDef field = (FieldDef) e; if (isTargetThis(field)) { res = fieldReadWrite(false, field, inItem); } } } else if (n instanceof ConstructorCall) { ConstructorCall ctorCall = (ConstructorCall) n; assert (ctorCall.kind()==ConstructorCall.SUPER) : "We do not analyze ctors with 'this()' calls, because they're always correct - everything must be initialized after the 'this()' call"; //assert res==INIT : "It must be the first statement in the ctor"; // Note that the super call should be the first statement, but I inline the field-inits before it. } else if (n instanceof X10Call) { MethodInfo info = getInfo((X10Call) n); if (info!=null) { res = new DataFlowItem(); res.initStatus.putAll(inItem.initStatus); for (FieldDef field : fields) { boolean isRead = info.read.contains(field); boolean isWrite = info.write.contains(field); boolean isWriteSeq = info.seqWrite.contains(field); final MinMaxInitCount before = res.initStatus.get(field); res.initStatus.put(field, before.afterSeqBlock(MinMaxInitCount.build(isRead,isWrite, isWriteSeq))); } } } else if (n instanceof Expr && ((Expr)n).type().isBoolean() && (n instanceof Binary || n instanceof Unary)) { final Map<EdgeKey, Item> map = flowBooleanConditions(trueItem, falseItem, inItem, graph, (Expr) n, succEdgeKeys); if (map!=null) return map; } else if (n instanceof ParExpr && ((ParExpr)n).type().isBoolean()) { return itemsToMap(trueItem, falseItem, inItem, succEdgeKeys); } else if (n instanceof Finish) { res = new DataFlowItem(); for (Map.Entry<FieldDef, MinMaxInitCount> pair : inItem.initStatus.entrySet()) { MinMaxInitCount before = pair.getValue(); res.initStatus.put(pair.getKey(), before.finish()); if (GATHER_STATS && before.isAsynInit()) { System.out.println("Async field init="+pair.getKey().position()); ASYNC_INIT_COUNT++; } } } if (res!=inItem) { MethodInfo info = allMethods.get(currDecl.procedureInstance()); final boolean ctor = isCtor(); if (!ctor) assert info!=null : currDecl; // can't read from an un-init var in the ctors (I want to catch it here, so I can give the exact position info) // can't read from any var if @NoThisAccess if (ctor) { for (FieldDef f : fields) { boolean readBefore = inItem.initStatus.get(f).isRead(); final MinMaxInitCount fRes = res.initStatus.get(f); if (!readBefore && fRes.isRead()) { // wasn't read before, and we read it now (either because of Field access, or X10Call) reportError(new Errors.CannotReadFromFieldBeforeDefiniteAssignment(f.name(),n.position())); // I want to report more errors with this field, so I remove the read status res.initStatus.put(f,MinMaxInitCount.build(false, fRes.isWrite(),fRes.isSeqWrite())); wasError = true; } } } } return itemToMap(res, succEdgeKeys); } private boolean isCtor() { return currDecl instanceof ConstructorDecl; } @Override protected void check(FlowGraph graph, Term n, boolean entry, Item inItem, Map<EdgeKey, Item> outItems) { DataFlowItem dfIn = (DataFlowItem)inItem; if (dfIn == null) dfIn = init; if (n == graph.root() && !entry) { assert n==currDecl : n; finalResult = dfIn; } } public void checkResult() { // finish method/ctor ProcedureDecl decl = currDecl; assert decl!=null; final ProcedureDef procDef = decl.procedureInstance(); // everything must be assigned in a ctor (and nothing read) if (isCtor()) { for (FieldDef f : fields) { // a VAR marked with @Uninitialized is not tracked if (!finalResult.initStatus.get(f).isSeqWrite() && !Types.isUninitializedField((X10FieldDef)f,(TypeSystem)ts)) { final Position pos = currDecl.position(); // could be an auto-generated ctor wasError = true; reportError(new Errors.FieldNameWasNotDefinitelyAssigned(f.flags().isProperty(), f.name(), pos.isCompilerGenerated() ? f.position() : pos)); } } } else { MethodInfo oldInfo = allMethods.get(procDef); assert oldInfo!=null : currDecl; assert !Types.isNoThisAccess((X10ProcedureDef)procDef,(TypeSystem)ts); MethodInfo newInfo = new MethodInfo(); for (Map.Entry<FieldDef, MinMaxInitCount> pair : finalResult.initStatus.entrySet()) { MinMaxInitCount val = pair.getValue(); final FieldDef f = pair.getKey(); if (val.isRead()) newInfo.read.add(f); if (!f.flags().isFinal()) { // NonEscaping methods can only write to VAR (not VAL) if (val.isWrite()) newInfo.write.add(f); if (val.isSeqWrite()) newInfo.seqWrite.add(f); } } // proof that the fix-point terminates: write set decreases while the read set increases assert oldInfo.write.containsAll(newInfo.write); assert oldInfo.seqWrite.containsAll(newInfo.seqWrite); assert newInfo.read.containsAll(oldInfo.read); // fixed-point reached? if (newInfo.read.equals(oldInfo.read) && newInfo.write.equals(oldInfo.write) && newInfo.seqWrite.equals(oldInfo.seqWrite)) { // no change! } else { wasChange = true; allMethods.put(procDef,newInfo); } } } } private MethodInfo getInfo(X10Call call) { final X10MethodDef def = (X10MethodDef) call.methodInstance().def(); if (isTargetThis(call) && findMethod(call)!=null && !Types.isNoThisAccess(def,ts)) { final MethodInfo info = allMethods.get(def); assert info!=null; return info; } return null; } private static boolean isProperty(FieldDef def) { return def.flags().isProperty(); } private static boolean isProperty(ProcedureDef def) { return ((ProcedureDef_c)def).flags().isProperty(); } private static boolean isPrivate(ProcedureDef def) { return ((ProcedureDef_c)def).flags().isPrivate(); } private boolean isPrivateOrFinal(ProcedureDef def) { if (isXlassFinal) return true; final Flags flags = ((ProcedureDef_c)def).flags(); return flags.isPrivate() || flags.isFinal(); } // Main visits all the classes, and type-checks them (verify that "this" doesn't escape, etc) public static class Main extends NodeVisitor { private final Job job; public Main(Job job) { this.job = job; } @Override public NodeVisitor enter(Node n) { if (n instanceof X10ClassDecl_c) { final X10ClassDecl_c classDecl_c = (X10ClassDecl_c) n; if (!classDecl_c.flags().flags().isInterface()) // I have nothing to analyze in an interface new CheckEscapingThis(classDecl_c,job, job.extensionInfo().typeSystem()); } return this; } } static enum CtorState { Start, SawCtor, SawProperty }; public class CheckCtor extends NodeVisitor { private CtorState state = CtorState.Start; private boolean wasSuperCall = false; private CheckCtor(X10ConstructorDecl_c ctor) { if (getConstructorCall(ctor)==null) { // There is no 'this(...)' or 'super(...)' calls, so we implicitly start after a 'super()' call state = CtorState.SawCtor; wasSuperCall = true; } } private void postCheck() { switch (state) { case Start: assert false : "There must be a super call (either explicit or implicit)"; case SawCtor: // We already report if: property(...) might not have been called //if (hasProperties && wasSuperCall) // reportError("You must call 'property(...)' at least once",ctor.position()); break; } } @Override public Node visitEdgeNoOverride(Node parent, Node n) { Position pos = n.position(); if (getMsg(n)!=null) { return n; // already checked } if (!canUseThis() && n instanceof X10Call) { final X10Call call = (X10Call) n; if (isTargetThis(call)) { if (Types.isNoThisAccess((X10MethodDef)call.methodInstance().def(),ts)) { // && X10TypeMixin.getNonEscapingReadsFrom((X10MethodDef)call.methodInstance().def(),ts)==null) { // @NonEscaping methods cannot write to any fields // even though we use "this.call(...)", this is legal // because the call doesn't read nor write to "this" } else { MethodInfo info = getInfo(call); if (info!=null && info.read.size()==0 && info.write.size()==0) { // ok } else { reportError("You can use 'this' before 'property(...)' to call only @NoThisAccess methods or NonEscaping methods that do not read nor write any fields.",pos); } } for (Expr e : call.arguments()) e.visit(this); return n; } } n.del().visitChildren(this); if (n instanceof ConstructorCall) { ConstructorCall constructorCall = (ConstructorCall) n; switch (state) { case Start: state = CtorState.SawCtor; wasSuperCall = constructorCall.kind()==ConstructorCall.SUPER; break; case SawCtor: reportError("Can only have a single 'this(...)' or 'super(...)' in a constructor",pos); break; case SawProperty: reportError("'this(...)' or 'super(...)' must come before 'property(...)'",pos); break; } } else if (n instanceof AssignPropertyCall) { switch (state) { case Start: assert false : "implicit super() call is handled at the beginning, see getConstructorCall"; case SawCtor: if (!wasSuperCall) reportError("You cannot call 'property(...)' after 'this(...)'",pos); else if (!hasProperties) { // This error is already reported: "The property initializer must have the same number of arguments as properties for the class." //reportError("You can call 'property(...)' only if the class defined properties",pos); } else { state = CtorState.SawProperty; } break; case SawProperty: reportError("You can call 'property(...)' at most once",pos); break; } } else if (isThis(n)) { Special special = (Special) n; if (special.kind()==Special.SUPER) { if (state==CtorState.Start) reportError("You can use 'super' only after 'super(...)'",pos); } else if (!canUseThis()) reportError((hasProperties ? "Can use 'this' only after 'property(...)'" : "Can use 'this' only after 'this(...)' or 'super(...)'")+" in "+parent, pos); } return n; } private boolean canUseThis() { return state==CtorState.SawProperty || // after call to 'property(...)' (!hasProperties && state==CtorState.SawCtor) || // after call to 'this(...)' or 'super(...)' if there are no properties (state==CtorState.SawCtor && !wasSuperCall); // after call to 'this(...)' } } // we gather info on every private/final/@NonEscaping method called during construction (@NoThisAccess do not access "this", so no need to analyze them) private static class MethodInfo { private final Set<FieldDef> read = CollectionFactory.newHashSet(); private final Set<FieldDef> write = CollectionFactory.newHashSet(); private final Set<FieldDef> seqWrite = CollectionFactory.newHashSet(); } private final Job job; private final NodeFactory nf; private final TypeSystem ts; private final X10ClassDecl_c xlass; private final boolean hasProperties; // this this class defined properties (excluding properties of the sueprclass). if so, there must be exactly one "property(...)" private final FieldDef propertyRepresentative; // tracking a single property field is enough (all of them are assigned together) private final boolean isXlassFinal; private final Type xlassType; // the keys are either X10ConstructorDecl_c or X10MethodDecl_c private final HashMap<ProcedureDef,MethodInfo> allMethods = new LinkedHashMap<ProcedureDef, MethodInfo>(); // all ctors and methods recursively called from allMethods on receiver "this" private final ArrayList<ProcedureDecl> dfsMethods = new ArrayList<ProcedureDecl>(); // to accelerate the fix-point alg // the set of all VAR and VAL fields (including one property representative), including those in the superclass because of super() call // (we need to check that VAL are read properly, and that VAR are written and read properly.) private final Set<FieldDef> fields = CollectionFactory.newHashSet(); private final Set<FieldDef> superFields = CollectionFactory.newHashSet(); // after the "super()" call, these fields are initialized private final DataFlowItem INIT = new DataFlowItem(); private final DataFlowItem CTOR_INIT = new DataFlowItem(); private boolean wasChange = true, wasError = false; // for fixed point alg private Set<FieldDef> globalRef = CollectionFactory.newHashSet();// There is one exception to the "this cannot escape" rule: val root = GlobalRef[...](this) private void checkGlobalRef(Node n) { // you cannot access a globalRef field via this (but you can assign to them) if (n instanceof Field) { Field f = (Field) n; FieldDef def = f.fieldInstance().def(); if (isTargetThis(f) && globalRef.contains(def)) reportError("Cannot use '"+def.name()+"' because a GlobalRef[...](this) cannot be used in a field initializer, constructor, or methods called from a constructor.",n.position()); } } public CheckEscapingThis(X10ClassDecl_c xlass, Job job, TypeSystem ts) { long start = System.currentTimeMillis(); this.job = job; this.ts = ts; nf = (NodeFactory)ts.extensionInfo().nodeFactory(); this.xlass = xlass; final List<PropertyDecl> props = xlass.properties(); hasProperties = props!=null && props.size()>0; propertyRepresentative = hasProperties ? props.get(0).fieldDef() : null; if (hasProperties) fields.add(propertyRepresentative); // adding one property representative (for our data flow, to make sure it property(...) is always called isXlassFinal = xlass.flags().flags().isFinal(); this.xlassType = Types.baseType(xlass.classDef().asType()); // calculate the set of all fields (including inherited fields) calcFields(); MinMaxInitCount notInited = MinMaxInitCount.build(false, false,false); MinMaxInitCount inited = MinMaxInitCount.build(false, true,true); for (FieldDef f : fields) { INIT.initStatus.put(f,notInited); CTOR_INIT.initStatus.put(f,notInited); } for (FieldDef field : superFields) { CTOR_INIT.initStatus.put(field, inited); } typeCheck(); TIME += Math.abs(System.currentTimeMillis()-start); } private static ArrayList<FieldDef> getInstanceFields(ClassDef currClass) { List<FieldDef> list = currClass.fields(); ArrayList<FieldDef> init = new ArrayList<FieldDef>(list.size()); for (FieldDef f : list) { if (!isProperty(f) && // not tracking property fields (checking property() call was done elsewhere) !f.flags().isStatic()) { // static fields are checked in FwdReferenceChecker init.add(f); } } return init; } private void calcFields() { final ClassDef myClassDef = xlass.classDef(); ClassDef currClass = myClassDef; while (currClass!=null) { final ArrayList<FieldDef> init = getInstanceFields(currClass); fields.addAll(init); if (myClassDef!=currClass) superFields.addAll(init); final Ref<? extends Type> superType = currClass.superType(); if (superType==null) break; currClass = superType.get().toClass().def(); } } private boolean isGlobalRefNewExpr(X10New_c new_c) { final TypeNode typeNode = new_c.objectType(); final List<Expr> args = new_c.arguments(); if (args.size()==1 && isThis(args.get(0)) && // the first and only argument is "this" typeNode instanceof X10CanonicalTypeNode_c) { // now checking the ctor is of GlobalRef X10CanonicalTypeNode_c tn = (X10CanonicalTypeNode_c) typeNode; final Type type = Types.baseType(tn.type()); if (type instanceof X10ParsedClassType_c) { X10ParsedClassType_c classType_c = (X10ParsedClassType_c) type; final QName qName = classType_c.def().fullName(); if (qName.equals(QName.make("x10.lang","GlobalRef"))) { // found the pattern! return true; } } } return false; } private void calcGlobalRefs(ArrayList<X10FieldDecl_c> nonStaticFields) { for (X10FieldDecl_c field : nonStaticFields) { boolean isGlobalRef = false; X10FieldDef fieldDef = field.fieldDef(); if (Types.isNonEscaping(fieldDef,ts)) isGlobalRef = true; final Expr init = field.init(); // check for the pattern: val/var someField = GlobalRef[...](this) if (init instanceof X10New_c) { X10New_c new_c = (X10New_c) init; if (isGlobalRefNewExpr(new_c)) isGlobalRef=true; } if (isGlobalRef) { // must be private if (!isXlassFinal && !field.flags().flags().isPrivate()) reportError("In order to use the pattern GlobalRef[...](this) the field must be private.",field.position()); globalRef.add(fieldDef); } } } private void typeCheck() { final X10ClassBody_c body = (X10ClassBody_c)xlass.body(); // visit all (non-static) field initializers and check that they do not have forward references nor that "this" escapes ArrayList<X10FieldDecl_c> nonStaticFields = new ArrayList<X10FieldDecl_c>(); for (ClassMember classMember : body.members()) { if (classMember instanceof X10FieldDecl_c) { X10FieldDecl_c field = (X10FieldDecl_c) classMember; if (field.flags().flags().isStatic()) continue; nonStaticFields.add(field); } } // Find globalRefs calcGlobalRefs(nonStaticFields); // inline the field-initializers in every ctor ArrayList<Stmt> fieldInits = new ArrayList<Stmt>(); final Position pos = Position.COMPILER_GENERATED; for (X10FieldDecl_c field : nonStaticFields) { final Expr init = field.init(); final X10FieldDef def = (X10FieldDef) field.fieldDef(); if (init==null) continue; final Special This = (Special) nf.Special(pos, Special_c.THIS).type(def.container().get().toType()); final FieldAssign fieldAssign = (FieldAssign) nf.FieldAssign(pos, This, field.name(), Assign_c.ASSIGN, init). fieldInstance(def.asInstance()). type(init.type()); fieldInits.add(nf.Eval(pos, fieldAssign)); if (!globalRef.contains(def)) init.visit(this); // field init are like a ctor } Block newInit = nf.Block(pos,fieldInits); // visit every ctor, every @NoThisAccess/@NonEscaping method, and every method recursively called from them, and check that this and super do not escape ArrayList<X10ConstructorDecl_c> allCtors = new ArrayList<X10ConstructorDecl_c>(); for (ClassMember classMember : body.members()) { if (classMember instanceof ProcedureDecl) { final ProcedureDecl proc = (ProcedureDecl) classMember; final X10ProcedureDef def = (X10ProcedureDef)proc.procedureInstance(); final Block procBody = proc.body(); if (def instanceof X10MethodDef) { X10MethodDef x10def = (X10MethodDef) def; boolean isNoThisAccess = Types.isNoThisAccess(x10def,ts); boolean isNonEscaping = Types.isNonEscaping(x10def,ts); if (GATHER_STATS) { ALL_METHODS.add(x10def); if (isNoThisAccess) ALL_NO_THIS_ACCESS.add(x10def); if (isNonEscaping) { ALL_NON_ESCAPING_METHODS.add(x10def); ALL_EXPLICIT_NON_ESCAPING_METHODS.add(x10def); } } // if we overrode a method with @NoThisAccess, then we must be annotated with @NoThisAccess // (NonEscaping is private/final, so cannot be overriden) if (!isNoThisAccess) { final MethodInstance instance = x10def.asInstance(); final Context emptyContext = ts.emptyContext(); final List<MethodInstance> overriddenMethods = instance.overrides(emptyContext); // Yoav and Igor thought hard and couldn't find an example where using an empty context vs. the correct context gives a different result. for (MethodInstance overriddenMI : overriddenMethods) { MethodDef overriddenDef = overriddenMI.def(); if (overriddenDef==def) continue; // me boolean overriddenIsNoThisAccess = Types.isNoThisAccess((X10MethodDef)overriddenDef,ts); if (overriddenIsNoThisAccess) { reportError("You must annotate "+proc+" with @NoThisAccess because it overrides a method annotated with that.", proc.position()); break; // one such error msg is enough } } } if (isNoThisAccess) { // NoThisAccess is stronger than NonEscaping so we check it first (in case someone wrote both annotations) // check "this" is not accessed at all if (procBody != null) { // native/abstract methods checkNoThis(procBody,"You cannot use 'this' or 'super' in a method annotated with @NoThisAccess"); } // No need to do procBody.visit(this) because "this"/"super" are not used in a NoThisAccess method. } else if (isNonEscaping) { if (!isPrivateOrFinal(x10def)) reportError("A @NonEscaping method must be private or final.", proc.position()); if (procBody!=null && !allMethods.containsKey(def)) { // for native methods/ctors, we don't have a body final MethodInfo info = new MethodInfo(); allMethods.put(def, info); procBody.visit(this); dfsMethods.add(proc); } } } else { if (procBody==null) continue; // native ctors assert proc instanceof X10ConstructorDecl_c : proc; final X10ConstructorDecl_c ctor = (X10ConstructorDecl_c) proc; allCtors.add(ctor); if (GATHER_STATS) ALL_CTORS.add(ctor.constructorDef()); procBody.visit(this); } } } // we still need to CheckCtor (make sure super, this and property is correct) //if (fields.size()==0) return; // done! all fields have an init, thus all reads are legal (and no writes must be done). // do init for the fixed point alg for (Map.Entry<ProcedureDef, MethodInfo> entry : allMethods.entrySet()) { MethodInfo info = entry.getValue(); info.write.addAll(fields); info.seqWrite.addAll(fields); } // run fix point alg: ctors do not need to be in the fixed point alg because nobody can call them directly final FieldChecker fieldChecker = new FieldChecker(INIT); while (wasChange && !wasError) { wasChange =false; // do a DFS: starting from private/final methods, and then the ctors (this will reach a fixed-point fastest) for (ProcedureDecl p : dfsMethods) { fieldChecker.dataflow(p); fieldChecker.checkResult(); } } // handle ctors and field initializers // make a new special ctor for field-init, and the ctors will use its data-flow for their INIT if (allCtors.size()>0) { // there should be at least one auto-generated ctor fieldChecker.init = CTOR_INIT; X10ConstructorDecl_c fieldInitCtor = (X10ConstructorDecl_c) allCtors.get(0).body(newInit); fieldChecker.dataflow(fieldInitCtor); fieldChecker.init = fieldChecker.finalResult; for (X10ConstructorDecl_c ctor : allCtors) { // check super, this, and property calls final CheckCtor checkCtor = new CheckCtor(ctor); ctor.visit(checkCtor); // we check both the body and signature because we want to make sure that "this" is not used in the method guard checkCtor.postCheck(); final ConstructorCall cc = getConstructorCall(ctor); if (cc!=null && cc.kind() == ConstructorCall.THIS) { // ignore in dataflow ctors that call other ctors (using "this(...)"). continue; } // X10ConstructorDecl_c newCtor = (X10ConstructorDecl_c) ctor.body( nf.Block(pos,newInit,ctor.body()) ); //reports errors in the field-inits multiple times (if we have multiple ctors) fieldChecker.dataflow(ctor); fieldChecker.checkResult(); } } if (GATHER_STATS) { System.out.println( " ASYNC_LOCAL_INIT_COUNT="+InitChecker.ASYNC_INIT_COUNT+ " ASYNC_FIELD_INIT_COUNT="+ASYNC_INIT_COUNT + " ALL_CTORS="+ALL_CTORS.size()+ " ALL_METHODS="+ALL_METHODS.size()+ " ALL_NON_ESCAPING_METHODS="+ALL_NON_ESCAPING_METHODS.size()+ " ALL_EXPLICIT_NON_ESCAPING_METHODS="+ALL_EXPLICIT_NON_ESCAPING_METHODS.size()+ " ALL_NO_THIS_ACCESS="+ALL_NO_THIS_ACCESS.size()); TreeMap<String,int[]> byFile = new TreeMap<String,int[]>(); HashSet[] ALL = {ALL_CTORS, ALL_METHODS, ALL_NON_ESCAPING_METHODS, ALL_EXPLICIT_NON_ESCAPING_METHODS, ALL_NO_THIS_ACCESS}; for (int i=0; i<ALL.length; i++) for (Object defO : ALL[i]) { X10ProcedureDef def = (X10ProcedureDef) defO; Position defPos = def.position(); if (defPos.isCompilerGenerated()) continue; String file = defPos.file(); if (!byFile.containsKey(file)) byFile.put(file, new int[ALL.length] ); byFile.get(file)[i]++; } for (String file : byFile.keySet()) { System.out.println(file+": "+ Arrays.toString(byFile.get(file))); } System.out.println(); } } public static ConstructorCall getConstructorCall(X10ConstructorDecl_c ctor) { // We can reuse ConstructorCallChecker, but for better efficiency, we just check it directly final Block ctorBody = ctor.body(); assert ctorBody!=null; final List<Stmt> stmts = ctorBody.statements(); for (Stmt s : stmts) { if (s instanceof ConstructorCall) { return (ConstructorCall) s; } } return null; } private X10MethodDecl_c findMethod(X10Call call) { MethodInstance mi2 = call.methodInstance(); final X10ClassBody_c body = (X10ClassBody_c)xlass.body(); for (ClassMember classMember : body.members()) { if (classMember instanceof X10MethodDecl_c) { X10MethodDecl_c mdecl = (X10MethodDecl_c) classMember; if (mdecl.body()==null) continue; // for native methods (like typeName in structs) final MethodDef md = mdecl.methodDef(); if (mi2.def().equals(md)) return mdecl; } } return null; } private void checkNoThis(Node n, String msg) { n.visit(new ThisCheckerIgnoringTypes(msg)); } private String getMsg(Node n) { if (n instanceof AtEach) return "'this' or 'super' cannot escape via an 'ateach' statement during construction."; if (n instanceof AtStmt) return "'this' or 'super' cannot escape via an 'at' statement during construction."; if (n instanceof AtExpr) return "'this' or 'super' cannot escape via an 'at' expression during construction."; if (n instanceof Closure) return "'this' or 'super' cannot escape via a closure during construction."; return null; } @Override public Node visitEdgeNoOverride(Node parent, Node n) { checkGlobalRef(n); // check globalRef usage in ctors and methods called from ctors String msg = getMsg(n); if (msg!=null) { // "this" cannot escape into an AT or closure checkNoThis(n,msg); return n; } if (n instanceof New) { New aNew = (New) n; if (aNew.qualifier()==null && aNew.body()!=null) { reportError("'this' cannot escape via an anonymous class during construction", n.position()); } } // You can access "this" for field access and field assignment. // field assignment: if (n instanceof FieldAssign) { FieldAssign f = (FieldAssign) n; if (isThis(f.target())) { Expr right = f.right(); if (right instanceof X10New_c && globalRef.contains(f.fieldInstance().def()) && isGlobalRefNewExpr((X10New_c)right)) return n; right.visit(this); return n; } } // field access: if (n instanceof Field) { final Field f = (Field) n; if (isThis(f.target())) return n; } // You can also access "this" as the receiver of property calls (because they are MACROS that are expanded to field access) // and as the receiver of private/final calls if (n instanceof X10Call) { final X10Call call = (X10Call) n; final MethodInstance methodInstance = call.methodInstance(); final X10ProcedureDef procDef = (X10ProcedureDef) methodInstance.def(); if (isThis(call.target())) { boolean hasNoThisAccess = Types.isNoThisAccess(procDef,ts); if (isProperty(procDef) || hasNoThisAccess) { // property-method calls and calls to @NoThisAccess are ok } else { // the method must be final or private (or @NoThisAccess) final Position callPos = call.position(); boolean isNonEscaping = Types.isNonEscaping(procDef,ts); X10MethodDecl_c method = findMethod(call); if (method==null) { // in the future: we could infer nonescaping from the superclass. The problem is that it is hard to understand the error messages that result from such inference // Igor: I think we should disallow the call to foo() when we infer that foo() escapes this. The error message may mention @NonEscaping -- once the user annotates foo() with @NonEscaping, the compiler will tell him/her where the potential points of escape are. if (!isNonEscaping) reportError("The call "+call+" is illegal because you can only call a superclass method during construction only if it is annotated with @NonEscaping.", callPos); } else { if (!isPrivateOrFinal(procDef) && !isNonEscaping) // if it is NonEscaping, we will already report the error: "A @NonEscaping method must be private or final." reportError("The call "+call+" is illegal because you can only call private/final @NonEscaping methods or @NoThisAccess methods during construction.", callPos); ProcedureDef pd = method.procedureInstance(); if (allMethods.containsKey(pd)) { // we already analyzed this method (or it is an error method) } else { if (!isNonEscaping && !isXlassFinal && !isPrivate(procDef)) job.compiler().errorQueue().enqueue(ErrorInfo.WARNING,"Method '"+procDef.signature()+"' is called during construction and therefore should be marked as @NonEscaping.", method.position()); final Block body = method.body(); if (body!=null) { allMethods.put(pd,new MethodInfo()); // prevent infinite recursion if (GATHER_STATS) ALL_NON_ESCAPING_METHODS.add(method.methodDef()); body.visit(this); dfsMethods.add(method); } } } } // it is enough to just recurse into the arguments (because the receiver is either this or super) for (Expr e : call.arguments()) e.visit(this); return n; } } // You cannot use "this" for anything else! if (isThis(n)) { reportError("'this' and 'super' cannot escape from a constructor or from methods called from a constructor",n.position()); } n.del().visitChildren(this); return n; } private void reportError(String s, Position p) { job.compiler().errorQueue().enqueue(ErrorInfo.SEMANTIC_ERROR,s,p); } private boolean isTargetThis(X10Call call) { final MethodInstance methodInstance = call.methodInstance(); final MethodDef def = methodInstance.def(); return !isProperty(def) && !def.flags().isStatic() && isThis(call.target()); } private boolean isTargetThis(FieldAssign f) { FieldDef def = f.fieldInstance().def(); return !isProperty(def) && !def.flags().isStatic() && isThis(f.target()); } private boolean isTargetThis(Field f) { FieldDef def = f.fieldInstance().def(); return isTargetThis(def) && isThis(f.target()); } private boolean isTargetThis(FieldDef def) { return !isProperty(def) && !def.flags().isStatic(); } private boolean isThis(Node n) { if (n==null || !(n instanceof Special)) return false; final Special special = (Special) n; final Type tt = Types.baseType(special.type()); if (!tt.isClass()) return false; ClassType type = tt.toClass(); // both this and super cannot escape // for "super.", it resolves to the superclass, so I need to go up the superclasses Type thisClass = xlassType; while (thisClass!=null) { if (!thisClass.isClass()) return false; final ClassType classType = thisClass.toClass(); if (type.def()==classType.def()) return true; final Type superClass = classType.superClass(); if (superClass==null) return false; thisClass = Types.baseType(superClass); } return false; } class ThisCheckerIgnoringTypes extends NodeVisitor { private final String errMsg; public ThisCheckerIgnoringTypes(String errMsg) { this.errMsg = errMsg; } @Override public Node override(Node n) { if (isThis(n)) { job.compiler().errorQueue().enqueue(ErrorInfo.SEMANTIC_ERROR,errMsg,n.position()); } return null; } } }