/* * Copyright 2014 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.dev.jjs.impl; import com.google.gwt.dev.jjs.ast.Context; import com.google.gwt.dev.jjs.ast.JClassType; import com.google.gwt.dev.jjs.ast.JConstructor; import com.google.gwt.dev.jjs.ast.JField; import com.google.gwt.dev.jjs.ast.JFieldRef; import com.google.gwt.dev.jjs.ast.JInterfaceType; import com.google.gwt.dev.jjs.ast.JMethod; import com.google.gwt.dev.jjs.ast.JMethodCall; import com.google.gwt.dev.jjs.ast.JParameter; import com.google.gwt.dev.jjs.ast.JParameterRef; import com.google.gwt.dev.jjs.ast.JProgram; import com.google.gwt.dev.jjs.ast.JThisRef; import com.google.gwt.dev.jjs.ast.JType; import com.google.gwt.dev.jjs.ast.JVisitor; import com.google.gwt.dev.util.log.speedtracer.CompilerEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; import com.google.gwt.thirdparty.guava.common.base.Predicate; import com.google.gwt.thirdparty.guava.common.collect.Sets; import java.util.Set; /** * Determines conservatively which classes can potentially see uninitialized values of their * subclasses' fields. * <p> * This simple conservative analysis relies on the fact that when: <ul> * <li> (1) there are no virtual calls on "this" in any of the initialization methods * (constructors, init) of all the superclasses, and </li> * <li> (2) "this" does not escape through a parameter to other methods, and </li> * <li>(3) "this" is not aliased (stored into another field, variable or array)</li> * </ul> * then uninitialized values for subclass fields can never be seen. * <p> * This analysis is used to strengthen the nullness analysis performed by {@link TypeTightener} and * to hoist initialization of instance fields to the prototype in {@link GenerateJavaScriptAST}. */ public class ComputePotentiallyObservableUninitializedValues { private static final String NAME = ComputePotentiallyObservableUninitializedValues.class.getSimpleName(); private final JProgram program; private final Set<JType> classesWhoseFieldsCanBeObservedUninitialized = Sets.newHashSet(); private ComputePotentiallyObservableUninitializedValues(JProgram program) { this.program = program; } /** * Perform the analysis to compute which fields can be observed uninitialized. */ public static Predicate<JField> analyze(JProgram program) { return new ComputePotentiallyObservableUninitializedValues(program).analyzeImpl(); } private Predicate<JField> analyzeImpl() { SpeedTracerLogger.Event optimizeEvent = SpeedTracerLogger.start(CompilerEventType.OPTIMIZE, "optimizer", NAME); CanObserveSubclassUninitializedFieldsVisitor visitor = new CanObserveSubclassUninitializedFieldsVisitor(); visitor.accept(program); Set<JType> classesThatCanPotentiallyObserveUninitializedSubclassFields = visitor.classesThatCanPotentiallyObserveUninitializedSubclassFields; for (JType type : classesThatCanPotentiallyObserveUninitializedSubclassFields) { if (classesWhoseFieldsCanBeObservedUninitialized.contains(type)) { // Already processed. continue; } classesWhoseFieldsCanBeObservedUninitialized.addAll(program.getSubclasses(type)); } optimizeEvent.end(); return new Predicate<JField>() { @Override public boolean apply(JField field) { return isUninitializedValueObservable(field); } }; } private boolean isUninitializedValueObservable(JField x) { if (x.getLiteralInitializer() != null && (x.isFinal() || x.isStatic())) { // Static and final fields that are initialized to a (value) literal can not be observed in // uninitialized state. return false; } if (x.isStatic()) { // Static fields can potentially be observed uninitialized if clinit dependencies are // cyclical. return true; } return classesWhoseFieldsCanBeObservedUninitialized.contains(x.getEnclosingType()); } private class CanObserveSubclassUninitializedFieldsVisitor extends JVisitor { private JClassType currentClass; private JParameter devirtualizedThis; private Set<JType> classesThatCanPotentiallyObserveUninitializedSubclassFields = Sets.newHashSet(); @Override public void endVisit(JClassType x, Context ctx) { assert currentClass == x; currentClass = null; } @Override public void endVisit(JConstructor x, Context ctx) { assert currentClass == x.getEnclosingType(); assert devirtualizedThis == null; } @Override public void endVisit(JMethod x, Context ctx) { assert currentClass == x.getEnclosingType(); devirtualizedThis = null; } @Override public void endVisit(JThisRef x, Context ctx) { // Seen a reference to "this" that can potentially escape or be used as instance in a // method call. classesThatCanPotentiallyObserveUninitializedSubclassFields.add(currentClass); } public void endVisit(JParameterRef x, Context ctx) { if (x.getParameter() == devirtualizedThis) { // Seen a reference to devirtualized "this" that can potentially escape or be used as // instance in a method call. classesThatCanPotentiallyObserveUninitializedSubclassFields.add(currentClass); } } @Override public boolean visit(JClassType x, Context ctx) { assert currentClass == null; currentClass = x; return true; } @Override public boolean visit(JConstructor x, Context ctx) { // Only look at constructor bodies. assert currentClass == x.getEnclosingType(); return true; } @Override public boolean visit(JFieldRef x, Context ctx) { if (isFieldReferenceThroughThis(x) && isFieldDeclaredInCurrentClassOrSuper(x)) { // Accessing fields through this (or devirtualized this) from the current class or // any super is ok, no further checks are needed. // A subclass field ref can leak into superclass methods when optimizations are enabled. return false; } return true; } @Override public boolean visit(JInterfaceType x, Context ctx) { // No need to examine interfaces. return false; } @Override public boolean visit(JMethod x, Context ctx) { assert currentClass == x.getEnclosingType(); if (isInitMethod(x)) { return true; } if (isDevirtualizedInitMethod(x) && x.getParams().size() > 0 && x.getParams().get(0).getType() == currentClass) { devirtualizedThis = x.getParams().get(0); } // Do not explore the method body if it is not a constructor or the instance initializer. return false; } @Override public boolean visit(JMethodCall x, Context ctx) { // This is a method call inside a constructor. assert currentClass != null; // Calls to this/super constructors and instance initializers are okay, as they will also // get examined. if ((x.getTarget().isConstructor()) && x.getInstance() instanceof JThisRef || isInitMethod(x.getTarget())) { // Make sure "this" references do not escape through parameters accept(x.getArgs()); return false; } // The instance initializers are always devirtualized, handle them specially. if (isDevirtualizedInitMethod(x.getTarget()) && x.getArgs().size() > 0 && x.getArgs().get(0) instanceof JThisRef) { // Make sure "this" references do not escape through parameters other than the first. accept(x.getArgs().subList(1, x.getArgs().size())); return false; } if (!x.getTarget().isStatic() && !x.getTarget().isFinal() && x.getInstance() instanceof JThisRef) { // This is polymorphic method call on this, hence it is potentially unsafe. classesThatCanPotentiallyObserveUninitializedSubclassFields.add(currentClass); return false; } // This is a static call, if there is no "this" references in its parameters then it might // be ok. return true; } private boolean isDevirtualizedInitMethod(JMethod method) { return method.isStatic() && method.getName().equals(GwtAstBuilder.STATIC_INIT_METHOD_NAME) && method.getEnclosingType() == currentClass; } private boolean isInitMethod(JMethod method) { return !method.isStatic() && method.getName().equals(GwtAstBuilder.INIT_NAME_METHOD_NAME) && method.getEnclosingType() == currentClass; } private boolean isFieldReferenceThroughThis(JFieldRef x) { return x.getInstance() instanceof JThisRef || x.getInstance() instanceof JParameterRef && ((JParameterRef) x.getInstance()).getParameter() == devirtualizedThis; } private boolean isFieldDeclaredInCurrentClassOrSuper(JFieldRef x) { JClassType enclosingClass = (JClassType) x.getField().getEnclosingType(); return currentClass == enclosingClass || program.typeOracle.isSuperClass(enclosingClass, currentClass); } } }