/* * Copyright 2008 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.SourceInfo; import com.google.gwt.dev.jjs.SourceOrigin; import com.google.gwt.dev.jjs.ast.Context; import com.google.gwt.dev.jjs.ast.JArrayRef; import com.google.gwt.dev.jjs.ast.JArrayType; import com.google.gwt.dev.jjs.ast.JBinaryOperation; import com.google.gwt.dev.jjs.ast.JBinaryOperator; import com.google.gwt.dev.jjs.ast.JCastOperation; import com.google.gwt.dev.jjs.ast.JCharLiteral; import com.google.gwt.dev.jjs.ast.JClassType; import com.google.gwt.dev.jjs.ast.JExpression; import com.google.gwt.dev.jjs.ast.JInstanceOf; import com.google.gwt.dev.jjs.ast.JMethod; import com.google.gwt.dev.jjs.ast.JMethodCall; import com.google.gwt.dev.jjs.ast.JModVisitor; import com.google.gwt.dev.jjs.ast.JNullLiteral; import com.google.gwt.dev.jjs.ast.JNullType; import com.google.gwt.dev.jjs.ast.JPrimitiveType; import com.google.gwt.dev.jjs.ast.JProgram; import com.google.gwt.dev.jjs.ast.JReferenceType; import com.google.gwt.dev.jjs.ast.JType; import com.google.gwt.dev.jjs.ast.JTypeOracle; import com.google.gwt.dev.jjs.ast.JVisitor; import com.google.gwt.dev.jjs.ast.js.JsCastMap; import com.google.gwt.dev.jjs.ast.js.JsCastMap.JsQueryType; import com.google.gwt.dev.util.collect.Lists; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; /** * Replace cast and instanceof operations with calls to the Cast class. Depends * on {@link CatchBlockNormalizer}, {@link CompoundAssignmentNormalizer}, * {@link JsoDevirtualizer}, and {@link LongCastNormalizer} having already run. * * <p> * Object and String always get a queryId of 0 and 1, respectively. The 0 * queryId always means "always succeeds". In practice, we never generate an * explicit cast with a queryId of 0; it is only used for array store checking, * where the 0 queryId means that anything can be stored into an Object[]. * </p> * <p> * JavaScriptObject has a queryId of -1, which again is only used for array * store checking, to ensure that a non-JSO is not stored into a * JavaScriptObject[]. * </p> */ public class CastNormalizer { private class AssignTypeCastabilityVisitor extends JVisitor { private final Set<JReferenceType> alreadyRan = new HashSet<JReferenceType>(); private final IdentityHashMap<JReferenceType, JsCastMap> castableTypesMap = new IdentityHashMap<JReferenceType, JsCastMap>(); private final List<JArrayType> instantiatedArrayTypes = new ArrayList<JArrayType>(); private final Map<JReferenceType, Set<JReferenceType>> queriedTypes = new IdentityHashMap<JReferenceType, Set<JReferenceType>>(); { JTypeOracle typeOracle = program.typeOracle; for (JArrayType arrayType : program.getAllArrayTypes()) { if (typeOracle.isInstantiatedType(arrayType)) { instantiatedArrayTypes.add(arrayType); } } // Force entries for Object and String. recordCastInternal(program.getTypeJavaLangObject(), program.getTypeJavaLangObject()); recordCastInternal(program.getTypeJavaLangString(), program.getTypeJavaLangObject()); } public void computeTypeCastabilityMaps() { List<JReferenceType> sortedQueryTypes = sortQueryTypes(); queryIdsByType = assignQueryIds(sortedQueryTypes); // do String first (which will pull in Object also, it's superclass). computeSourceType(program.getTypeJavaLangString()); assert (castableTypesMap.size() == 2); /* * Compute the list of classes than can successfully satisfy cast * requests, along with the set of types they can be successfully cast to. * Do it in super type order. */ for (JReferenceType type : program.getDeclaredTypes()) { if (type instanceof JClassType) { computeSourceType(type); } } for (JArrayType type : instantiatedArrayTypes) { computeSourceType(type); } // pass our info to JProgram program.initTypeInfo(castableTypesMap); program.recordQueryIds(queryIdsByType, sortedQueryTypes); } /* * If this expression could possibly generate an ArrayStoreException, we * must record a query on the element type being assigned to. */ @Override public void endVisit(JBinaryOperation x, Context ctx) { if (x.getOp().isAssignment() && x.getLhs() instanceof JArrayRef) { // first, calculate the transitive closure of all possible runtime types // the lhs could be JArrayRef lhsArrayRef = (JArrayRef) x.getLhs(); JType elementType = lhsArrayRef.getType(); if (elementType instanceof JNullType) { // will generate a null pointer exception instead return; } // primitives are statically correct if (!(elementType instanceof JReferenceType)) { return; } // element type being final means the assignment is statically correct if (((JReferenceType) elementType).isFinal()) { return; } /* * For every instantiated array type that could -in theory- be the * runtime type of the lhs, we must record a cast from the rhs to the * prospective element type of the lhs. */ JTypeOracle typeOracle = program.typeOracle; JType rhsType = x.getRhs().getType(); assert (rhsType instanceof JReferenceType); JArrayType lhsArrayType = lhsArrayRef.getArrayType(); for (JArrayType arrayType : instantiatedArrayTypes) { if (typeOracle.canTheoreticallyCast(arrayType, lhsArrayType)) { JType itElementType = arrayType.getElementType(); if (itElementType instanceof JReferenceType) { recordCast(itElementType, x.getRhs()); } } } } } @Override public void endVisit(JCastOperation x, Context ctx) { if (disableCastChecking) { return; } if (x.getCastType() != program.getTypeNull()) { recordCast(x.getCastType(), x.getExpr()); } } @Override public void endVisit(JInstanceOf x, Context ctx) { assert (x.getTestType() != program.getTypeNull()); recordCast(x.getTestType(), x.getExpr()); } private Map<JReferenceType, Integer> assignQueryIds(List<JReferenceType> sortedQueryTypes) { Map<JReferenceType, Integer> result = new IdentityHashMap<JReferenceType, Integer>(); int queryId = 0; for (JReferenceType queryType : sortedQueryTypes) { result.put(queryType, queryId++); } // JSO's marker queryId is -1 (used for array stores). JClassType jsoType = program.getJavaScriptObject(); if (jsoType != null) { result.put(jsoType, -1); } return result; } private boolean canTriviallyCastJsoSemantics(JReferenceType type, JReferenceType qType) { type = type.getUnderlyingType(); qType = qType.getUnderlyingType(); if (type instanceof JArrayType && qType instanceof JArrayType) { JArrayType aType = (JArrayType) type; JArrayType aqType = (JArrayType) qType; return program.typeOracle.canTriviallyCast(type, qType) || (program.isJavaScriptObject(aType.getLeafType()) && program .isJavaScriptObject(aqType.getLeafType())); } return program.typeOracle.canTriviallyCast(type, qType) || (program.isJavaScriptObject(type) && program.isJavaScriptObject(qType)); } /** * Create the data for JSON table to capture the mapping from a class to its * query types. */ private void computeSourceType(JReferenceType type) { if (type == null || alreadyRan.contains(type)) { return; } assert (type == type.getUnderlyingType()); alreadyRan.add(type); // Visit super type. if (type instanceof JClassType) { computeSourceType(((JClassType) type).getSuperClass()); } if (!program.typeOracle.isInstantiatedType(type) || program.isJavaScriptObject(type)) { return; } // Find all possible query types which I can satisfy Set<JsQueryType> castableTypes = new TreeSet<JsQueryType>(JSQUERY_COMPARATOR); /* * NOTE: non-deterministic iteration over HashSet and HashMap. Okay * because we're sorting the results. */ for (JReferenceType qType : queriedTypes.keySet()) { Set<JReferenceType> querySet = queriedTypes.get(qType); /** * Handles JSO[] -> JSO[] case now that canCastTrivially doesn't deal * with JSO cross-casts anymore. */ if (canTriviallyCastJsoSemantics(type, qType)) { for (JReferenceType argType : querySet) { if (canTriviallyCastJsoSemantics(type, argType) || program.isJavaScriptObject(qType)) { int queryId = queryIdsByType.get(qType); // Ignore Object (id 0) which is always true. if (queryId > 0) { castableTypes.add(new JsQueryType(SourceOrigin.UNKNOWN, qType, queryId)); } break; } } } } /* * Don't add an entry for empty answer sets, except for Object and String * which require entries. */ if (castableTypes.isEmpty() && type != program.getTypeJavaLangObject() && type != program.getTypeJavaLangString()) { return; } // add an entry for me castableTypesMap.put(type, new JsCastMap(SourceOrigin.UNKNOWN, Lists.create(castableTypes), program.getJavaScriptObject())); } private void recordCast(JType targetType, JExpression rhs) { if (targetType instanceof JReferenceType) { targetType = ((JReferenceType) targetType).getUnderlyingType(); // unconditional cast b/c it would've been a semantic error earlier JReferenceType rhsType = ((JReferenceType) rhs.getType()).getUnderlyingType(); // don't record a type for trivial casts that won't generate code if (program.typeOracle.canTriviallyCast(rhsType, (JReferenceType) targetType)) { return; } // If the target type is a JavaScriptObject, don't record an id. if (program.isJavaScriptObject(targetType)) { return; } recordCastInternal((JReferenceType) targetType, rhsType); } } private void recordCastInternal(JReferenceType toType, JReferenceType rhsType) { toType = toType.getUnderlyingType(); rhsType = rhsType.getUnderlyingType(); Set<JReferenceType> querySet = queriedTypes.get(toType); if (querySet == null) { querySet = new HashSet<JReferenceType>(); queriedTypes.put(toType, querySet); } querySet.add(rhsType); } /** * Sort into alphabetical, except Object and String which must come first. */ private List<JReferenceType> sortQueryTypes() { // Initial name-only sort. List<JReferenceType> sortedQueryTypes = new ArrayList<JReferenceType>(queriedTypes.keySet()); Collections.sort(sortedQueryTypes, new HasNameSort()); // Used LinkedHashSet to move Object and String to the front. LinkedHashSet<JReferenceType> tempSortedQueryTypes = new LinkedHashSet<JReferenceType>(); // Reserve query id 0 for java.lang.Object (for array stores on JSOs). tempSortedQueryTypes.add(program.getTypeJavaLangObject()); /* * Reserve query id 1 for java.lang.String to facilitate the mashup case. * Also, facilitates detecting an object as a Java String (see Cast.java) * Multiple GWT modules need to modify String's prototype the same way. */ tempSortedQueryTypes.add(program.getTypeJavaLangString()); // Add the rest. tempSortedQueryTypes.addAll(sortedQueryTypes); sortedQueryTypes = new ArrayList<JReferenceType>(tempSortedQueryTypes); return sortedQueryTypes; } } /** * Explicitly convert any char or long type expressions within a concat * operation into strings because normal JavaScript conversion does not work * correctly. */ private class ConcatVisitor extends JModVisitor { @Override public void endVisit(JBinaryOperation x, Context ctx) { if (x.getOp() == JBinaryOperator.CONCAT) { JExpression newLhs = convertString(x.getLhs()); JExpression newRhs = convertString(x.getRhs()); if (newLhs != x.getLhs() || newRhs != x.getRhs()) { JBinaryOperation newExpr = new JBinaryOperation(x.getSourceInfo(), program.getTypeJavaLangString(), JBinaryOperator.CONCAT, newLhs, newRhs); ctx.replaceMe(newExpr); } } else if (x.getOp() == JBinaryOperator.ASG_CONCAT) { JExpression newRhs = convertString(x.getRhs()); if (newRhs != x.getRhs()) { JBinaryOperation newExpr = new JBinaryOperation(x.getSourceInfo(), program.getTypeJavaLangString(), JBinaryOperator.ASG_CONCAT, x.getLhs(), newRhs); ctx.replaceMe(newExpr); } } } private JExpression convertString(JExpression expr) { JPrimitiveType charType = program.getTypePrimitiveChar(); if (expr.getType() == charType) { if (expr instanceof JCharLiteral) { JCharLiteral charLit = (JCharLiteral) expr; return program.getLiteralString(expr.getSourceInfo(), new char[]{charLit.getValue()}); } else { // Replace with Cast.charToString(c) JMethodCall call = new JMethodCall(expr.getSourceInfo(), null, program .getIndexedMethod("Cast.charToString")); call.addArg(expr); return call; } } else if (expr.getType() == program.getTypePrimitiveLong()) { // Replace with LongLib.toString(l) JMethodCall call = new JMethodCall(expr.getSourceInfo(), null, program .getIndexedMethod("LongLib.toString")); call.addArg(expr); return call; } return expr; } } /** * Handle integral divide operations which may have floating point results. */ private class DivVisitor extends JModVisitor { @Override public void endVisit(JBinaryOperation x, Context ctx) { JType type = x.getType(); if (x.getOp() == JBinaryOperator.DIV && type != program.getTypePrimitiveFloat() && type != program.getTypePrimitiveDouble()) { /* * If the numerator was already in range, we can assume the output is * also in range. Therefore, we don't need to do the full conversion, * but rather a narrowing int conversion instead. */ String methodName = "Cast.narrow_" + type.getName(); JMethod castMethod = program.getIndexedMethod(methodName); JMethodCall call = new JMethodCall(x.getSourceInfo(), null, castMethod, type); x.setType(program.getTypePrimitiveDouble()); call.addArg(x); ctx.replaceMe(call); } } } /** * Replaces all casts and instanceof operations with calls to implementation * methods. */ private class ReplaceTypeChecksVisitor extends JModVisitor { @Override public void endVisit(JCastOperation x, Context ctx) { JExpression replaceExpr; JType toType = x.getCastType(); JExpression expr = x.getExpr(); if (disableCastChecking && toType instanceof JReferenceType) { // Just leave the cast in, GenerateJavaScriptAST will ignore it. return; } SourceInfo info = x.getSourceInfo(); if (toType instanceof JNullType) { /** * A null type cast is used as a placeholder value to indicate that the * user tried a cast that couldn't possibly work. Typically this means * either the statically resolvable arg type is incompatible with the * target type, or the target type was globally uninstantiable. * * See {@link com.google.gwt.dev.jjs.impl.TypeTightener.TightenTypesVisitor#endVisit(JCastOperation, * Context)} * * We handle this cast by throwing a ClassCastException, unless the * argument is null. */ JMethod method = program.getIndexedMethod("Cast.throwClassCastExceptionUnlessNull"); // Note, we must update the method call to return the null type. JMethodCall call = new JMethodCall(info, null, method, toType); call.addArg(expr); replaceExpr = call; } else if (toType instanceof JReferenceType) { JExpression curExpr = expr; JReferenceType refType = ((JReferenceType) toType).getUnderlyingType(); JReferenceType argType = (JReferenceType) expr.getType(); if (program.typeOracle.canTriviallyCast(argType, refType) || (program.typeOracle.isEffectivelyJavaScriptObject(argType) && program.typeOracle .isEffectivelyJavaScriptObject(refType))) { // just remove the cast replaceExpr = curExpr; } else { // A cast is still needed. Substitute the appropriate Cast implementation. JMethod method; boolean isJsoCast = program.typeOracle.isEffectivelyJavaScriptObject(refType); if (isJsoCast) { // A cast to a concrete JSO subtype method = program.getIndexedMethod("Cast.dynamicCastJso"); } else if (program.typeOracle.isDualJsoInterface(refType)) { // An interface that should succeed when the object is a JSO method = program.getIndexedMethod("Cast.dynamicCastAllowJso"); } else { // A regular cast method = program.getIndexedMethod("Cast.dynamicCast"); } // override the type of the called method with the target cast type JMethodCall call = new JMethodCall(info, null, method, toType); call.addArg(curExpr); if (!isJsoCast) { call.addArg(new JsQueryType(info, refType, queryIdsByType.get(refType))); } replaceExpr = call; } } else { /* * See JLS 5.1.3: if a cast narrows from one type to another, we must * call a narrowing conversion function. EXCEPTION: we currently have no * way to narrow double to float, so don't bother. */ JPrimitiveType tByte = program.getTypePrimitiveByte(); JPrimitiveType tChar = program.getTypePrimitiveChar(); JPrimitiveType tShort = program.getTypePrimitiveShort(); JPrimitiveType tInt = program.getTypePrimitiveInt(); JPrimitiveType tLong = program.getTypePrimitiveLong(); JPrimitiveType tFloat = program.getTypePrimitiveFloat(); JPrimitiveType tDouble = program.getTypePrimitiveDouble(); JType fromType = expr.getType(); String methodName = null; if (tLong == fromType && tLong != toType) { if (tByte == toType || tShort == toType || tChar == toType) { /* * We need a double call here, one to convert long->int, and another * one to narrow. Construct the inner call here and fall through to * do the narrowing conversion. */ JMethod castMethod = program.getIndexedMethod("LongLib.toInt"); JMethodCall call = new JMethodCall(info, null, castMethod); call.addArg(expr); expr = call; fromType = tInt; } else if (tInt == toType) { methodName = "LongLib.toInt"; } else if (tFloat == toType || tDouble == toType) { methodName = "LongLib.toDouble"; } } if (toType == tLong && fromType != tLong) { // Longs get special treatment. if (tByte == fromType || tShort == fromType || tChar == fromType || tInt == fromType) { methodName = "LongLib.fromInt"; } else if (tFloat == fromType || tDouble == fromType) { methodName = "LongLib.fromDouble"; } } else if (tByte == fromType) { if (tChar == toType) { methodName = "Cast.narrow_" + toType.getName(); } } else if (tShort == fromType) { if (tByte == toType || tChar == toType) { methodName = "Cast.narrow_" + toType.getName(); } } else if (tChar == fromType) { if (tByte == toType || tShort == toType) { methodName = "Cast.narrow_" + toType.getName(); } } else if (tInt == fromType) { if (tByte == toType || tShort == toType || tChar == toType) { methodName = "Cast.narrow_" + toType.getName(); } } else if (tFloat == fromType || tDouble == fromType) { if (tByte == toType || tShort == toType || tChar == toType || tInt == toType) { methodName = "Cast.round_" + toType.getName(); } } if (methodName != null) { JMethod castMethod = program.getIndexedMethod(methodName); JMethodCall call = new JMethodCall(info, null, castMethod, toType); call.addArg(expr); replaceExpr = call; } else { // Just remove the cast replaceExpr = expr; } } ctx.replaceMe(replaceExpr); } @Override public void endVisit(JInstanceOf x, Context ctx) { JReferenceType argType = (JReferenceType) x.getExpr().getType(); JReferenceType toType = x.getTestType(); // Only tests on run-time types are supported assert (toType == toType.getUnderlyingType()); if (program.typeOracle.canTriviallyCast(argType, toType) // don't depend on type-tightener having run || (program.typeOracle.isEffectivelyJavaScriptObject(argType) && program.typeOracle .isEffectivelyJavaScriptObject(toType))) { // trivially true if non-null; replace with a null test JNullLiteral nullLit = program.getLiteralNull(); JBinaryOperation eq = new JBinaryOperation(x.getSourceInfo(), program.getTypePrimitiveBoolean(), JBinaryOperator.NEQ, x.getExpr(), nullLit); ctx.replaceMe(eq); } else { JMethod method; boolean isJsoCast = false; if (program.typeOracle.isDualJsoInterface(toType)) { method = program.getIndexedMethod("Cast.instanceOfOrJso"); } else if (program.typeOracle.isEffectivelyJavaScriptObject(toType)) { isJsoCast = true; method = program.getIndexedMethod("Cast.instanceOfJso"); } else { method = program.getIndexedMethod("Cast.instanceOf"); } JMethodCall call = new JMethodCall(x.getSourceInfo(), null, method); call.addArg(x.getExpr()); if (!isJsoCast) { call.addArg(new JsQueryType(x.getSourceInfo(), toType, queryIdsByType.get(toType))); } ctx.replaceMe(call); } } } private static final Comparator<JsQueryType> JSQUERY_COMPARATOR = new Comparator<JsQueryType>() { @Override public int compare(JsQueryType o1, JsQueryType o2) { return o1.getQueryId() - o2.getQueryId(); } }; public static void exec(JProgram program, boolean disableCastChecking) { new CastNormalizer(program, disableCastChecking).execImpl(); } private final boolean disableCastChecking; private final JProgram program; private Map<JReferenceType, Integer> queryIdsByType; private CastNormalizer(JProgram program, boolean disableCastChecking) { this.program = program; this.disableCastChecking = disableCastChecking; } private void execImpl() { { ConcatVisitor visitor = new ConcatVisitor(); visitor.accept(program); } { DivVisitor visitor = new DivVisitor(); visitor.accept(program); } { AssignTypeCastabilityVisitor assigner = new AssignTypeCastabilityVisitor(); assigner.accept(program); assigner.computeTypeCastabilityMaps(); } { ReplaceTypeChecksVisitor replacer = new ReplaceTypeChecksVisitor(); replacer.accept(program); } } }