Refactor JsInliner to support FORCE_INLINE.

Change-Id: I596daf7eaf04046648834f95cb2c706478953d8d
diff --git a/dev/BUILD b/dev/BUILD
index 996a4e1..bc36f70 100644
--- a/dev/BUILD
+++ b/dev/BUILD
@@ -185,6 +185,7 @@
             "core/src/com/google/gwt/dev/PrecompilationResult.java",
             "core/src/com/google/gwt/dev/StringAnalyzableTypeEnvironment.java",
             "core/src/com/google/gwt/dev/cfg/**/*.java",
+            "core/src/com/google/gwt/dev/common/**/*.java",
             "core/src/com/google/gwt/dev/javac/**/*.java",
             "core/src/com/google/gwt/dev/jdt/**/*.java",
             "core/src/com/google/gwt/dev/jjs/**/*.java",
diff --git a/dev/core/src/com/google/gwt/dev/common/InliningMode.java b/dev/core/src/com/google/gwt/dev/common/InliningMode.java
new file mode 100644
index 0000000..2e2f2f2
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/common/InliningMode.java
@@ -0,0 +1,23 @@
+/*
+ * 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.common;
+
+/**
+ * Used to mark methods to be handled  in specific ways by inliners.
+ */
+public enum InliningMode {
+  NORMAL, DO_NOT_INLINE, FORCE_INLINE;
+}
diff --git a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
index 5bef255..7798bc2 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/JavaToJavaScriptCompiler.java
@@ -133,6 +133,7 @@
 import com.google.gwt.dev.js.JsBreakUpLargeVarStatements;
 import com.google.gwt.dev.js.JsDuplicateCaseFolder;
 import com.google.gwt.dev.js.JsDuplicateFunctionRemover;
+import com.google.gwt.dev.js.JsForceInliningChecker;
 import com.google.gwt.dev.js.JsIncrementalNamer;
 import com.google.gwt.dev.js.JsInliner;
 import com.google.gwt.dev.js.JsLiteralInterner;
@@ -392,6 +393,9 @@
       // (7) Optimize the JS AST.
       final Set<JsNode> inlinableJsFunctions = jjsMapAndInlineableFunctions.getRight();
       optimizeJs(inlinableJsFunctions);
+      if (options.getOptimizationLevel() > OptionOptimize.OPTIMIZE_LEVEL_DRAFT) {
+        JsForceInliningChecker.check(logger, jjsmap, jsProgram);
+      }
 
       // TODO(stalcup): move to normalization
       // Must run before code splitter and namer.
@@ -497,7 +501,8 @@
     }
   }
 
-  private void optimizeJs(Set<JsNode> inlinableJsFunctions) throws InterruptedException {
+  private void optimizeJs(Set<JsNode> inlinableJsFunctions)
+      throws InterruptedException, UnableToCompleteException {
     if (shouldOptimize()) {
       optimizeJsLoop(inlinableJsFunctions);
       JsDuplicateCaseFolder.exec(jsProgram);
@@ -994,7 +999,8 @@
     return new BufferedInputStream(new GZIPInputStream(artifact.getContents(TreeLogger.NULL)));
   }
 
-  private void optimizeJsLoop(Collection<JsNode> toInline) throws InterruptedException {
+  private void optimizeJsLoop(Collection<JsNode> toInline)
+      throws InterruptedException, UnableToCompleteException {
     int optimizationLevel = options.getOptimizationLevel();
     List<OptimizerStats> allOptimizerStats = Lists.newArrayList();
     int counter = 0;
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java
index 538684e..1071a3c 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JMethod.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.dev.jjs.ast;
 
+import com.google.gwt.dev.common.InliningMode;
 import com.google.gwt.dev.jjs.InternalCompilerException;
 import com.google.gwt.dev.jjs.SourceInfo;
 import com.google.gwt.dev.jjs.SourceOrigin;
@@ -45,7 +46,7 @@
    * patterns, then it will marked as {@code UNDEFINED} to be later signaled as error in
    * {@link com.google.gwt.dev.jjs.impl.JsInteropRestrictionChecker}.
    */
-  public static enum JsPropertyAccessorType {
+  public enum JsPropertyAccessorType {
     GETTER, SETTER, UNDEFINED;
   }
 
@@ -61,7 +62,8 @@
   private String exportNamespace;
   private JsPropertyAccessorType jsPropertyType;
   private Specialization specialization;
-  private boolean inliningAllowed = true;
+  private InliningMode inliningMode = InliningMode.NORMAL;
+  private boolean preventDevirtualization = false;
   private boolean hasSideEffects = true;
   private boolean defaultMethod = false;
 
@@ -211,11 +213,15 @@
   }
 
   public boolean isInliningAllowed() {
-    return inliningAllowed;
+    return inliningMode != InliningMode.DO_NOT_INLINE;
   }
 
-  public void setInliningAllowed(boolean inliningAllowed) {
-    this.inliningAllowed = inliningAllowed;
+  public InliningMode getInliningMode() {
+    return inliningMode;
+  }
+
+  public void setInliningMode(InliningMode inliningMode) {
+    this.inliningMode = inliningMode;
   }
 
   public boolean hasSideEffects() {
@@ -234,6 +240,14 @@
     return defaultMethod;
   }
 
+  public boolean isDevirtualizationAllowed() {
+    return !preventDevirtualization;
+  }
+
+  public void disallowDevirtualization() {
+    this.preventDevirtualization = true;
+  }
+
   /**
    * AST representation of @SpecializeMethod.
    */
diff --git a/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java b/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java
index 7558c08..391b7ba 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/ast/JProgram.java
@@ -16,6 +16,7 @@
 package com.google.gwt.dev.jjs.ast;
 
 import com.google.gwt.dev.MinimalRebuildCache;
+import com.google.gwt.dev.common.InliningMode;
 import com.google.gwt.dev.jjs.Correlation.Literal;
 import com.google.gwt.dev.jjs.InternalCompilerException;
 import com.google.gwt.dev.jjs.SourceInfo;
@@ -291,30 +292,11 @@
   private FragmentPartitioningResult fragmentPartitioningResult;
 
   /**
-   * Set of method that are pinned and should be skipped by optimizations such as
-   * inlining, statification and prunned.
-   */
-  private Set<JMethod> pinnedMethods = Sets.newHashSet();
-
-  /**
-   * Returns true if the inliner should try to inline {@code method}.
-   */
-  public boolean isInliningAllowed(JMethod method) {
-    return !pinnedMethods.contains(method) && method.isInliningAllowed();
-  }
-
-  /**
-   * Returns true if {@link MakeCallsStatic} should try to statify {@code method}.
-   */
-  public boolean isDevitualizationAllowed(JMethod method) {
-    return !pinnedMethods.contains(method);
-  }
-
-  /**
    * Add a pinned method.
    */
   public void addPinnedMethod(JMethod method) {
-    pinnedMethods.add(method);
+    method.setInliningMode(InliningMode.DO_NOT_INLINE);
+    method.disallowDevirtualization();
   }
 
   public JProgram(MinimalRebuildCache minimalRebuildCache) {
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/Devirtualizer.java b/dev/core/src/com/google/gwt/dev/jjs/impl/Devirtualizer.java
index 7202c08..3da8a7e 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/Devirtualizer.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/Devirtualizer.java
@@ -267,7 +267,7 @@
     JMethod devirtualMethod = new JMethod(sourceInfo, prefix + "__devirtual$",
         inClass, method.getType(), false, true, true, AccessModifier.PUBLIC);
     // TODO(rluble): DoNotInline should be carried over if 'any' of the targets is marked so.
-    devirtualMethod.setInliningAllowed(method.isInliningAllowed());
+    devirtualMethod.setInliningMode(method.getInliningMode());
     devirtualMethod.setBody(new JMethodBody(sourceInfo));
     devirtualMethod.setSynthetic();
     inClass.addMethod(devirtualMethod);
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
index db07ccc..4722805 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GenerateJavaScriptAST.java
@@ -21,6 +21,7 @@
 import com.google.gwt.dev.MinimalRebuildCache;
 import com.google.gwt.dev.PrecompileTaskOptions;
 import com.google.gwt.dev.cfg.PermutationProperties;
+import com.google.gwt.dev.common.InliningMode;
 import com.google.gwt.dev.jjs.HasSourceInfo;
 import com.google.gwt.dev.jjs.InternalCompilerException;
 import com.google.gwt.dev.jjs.SourceInfo;
@@ -742,11 +743,6 @@
 
     private final JsName prototype = objectScope.declareName("prototype");
 
-    // JavaScript functions that arise from methods that were not inlined in the Java AST
-    // NOTE: We use a LinkedHashSet to preserve the order of insertion. So that the following passes
-    // that use this result are deterministic.
-    private final Set<JsNode> functionsForJsInlining = Sets.newLinkedHashSet();
-
     {
       globalTemp.setObfuscatable(false);
       prototype.setObfuscatable(false);
@@ -1238,14 +1234,7 @@
 
       javaMethodForJSFunction.put(jsFunc, x);
 
-      if (!program.isInliningAllowed(x)) {
-        jsProgram.disallowInlining(jsFunc);
-      }
-
-      // Collect the resulting function to be considered by the JsInliner.
-      if (methodsForJsInlining.contains(x)) {
-        functionsForJsInlining.add(jsFunc);
-      }
+      jsFunc.setInliningMode(x.getInliningMode());
 
       List<JsParameter> params = popList(x.getParams().size()); // params
 
@@ -1391,8 +1380,11 @@
          */
         qualifiedMethodName.setQualifier(names.get(method).makeRef(x.getSourceInfo()));
       } else {
-        // Regular super call. This calls are always static and optimizations normally statify them.
-        // They can appear in completely unoptimized code, hence need to be handled here.
+        // These are regular super method call. These calls are always dispatched statically and
+        // optimizations will statify them (except in a few cases, like being target of
+        // {@link Impl.getNameOf}.
+        // For the most part these calls only appear in completely unoptimized code, hence need to
+        // be handled here.
 
         // Construct JCHSU.getPrototypeFor(type).polyname
         // TODO(rluble): Ideally we would want to construct the inheritance chain the JS way and
@@ -1403,8 +1395,6 @@
         JsInvocation getPrototypeCall = constructInvocation(x.getSourceInfo(),
             "JavaClassHierarchySetupUtil.getClassPrototype",
             convertJavaLiteral(typeMapper.get(superMethodTargetType)));
-        // getClassPrototype is a JSNI call, so we are enabling inlining
-        methodsForJsInlining.add(currentMethod);
 
         JsNameRef methodNameRef = polymorphicNames.get(method).makeRef(x.getSourceInfo());
         methodNameRef.setQualifier(getPrototypeCall);
@@ -3135,15 +3125,22 @@
     }
   }
 
-  private class RecordJSInlinableMethods extends JVisitor {
+  private class CollectJsFunctionsForInlining extends JVisitor {
 
+    // JavaScript functions that arise from methods that were not inlined in the Java AST
+    // NOTE: We use a LinkedHashSet to preserve the order of insertion. So that the following passes
+    // that use this result are deterministic.
+    private Set<JsNode> functionsForJsInlining = Sets.newLinkedHashSet();
     private JMethod currentMethod;
 
     @Override
     public void endVisit(JMethod x, Context ctx) {
       if (x.isNative()) {
-        // These are methods that were not considered by the Java method inliner.
-        methodsForJsInlining.add(x);
+        // These are methods whose bodies where not traversed by the Java method inliner.
+        JsFunction function = methodBodyMap.get(x.getBody());
+        if (function != null && function.getBody() != null) {
+          functionsForJsInlining.add(function);
+        }
       }
 
       currentMethod = null;
@@ -3152,11 +3149,17 @@
     @Override
     public void endVisit(JMethodCall x, Context ctx) {
       JMethod target = x.getTarget();
-      if (program.isInliningAllowed(target) && (target.isNative()
-          || program.getIndexedTypes().contains(target.getEnclosingType()))) {
-        // the currentMethod calls a method that was not considered by the Java MethodInliner; these
-        // include JSNI methods and methods whose calls were inserted by normalization passes.
-        methodsForJsInlining.add(currentMethod);
+      if (target.isInliningAllowed() && (target.isNative()
+          || program.getIndexedTypes().contains(target.getEnclosingType())
+          || target.getInliningMode() == InliningMode.FORCE_INLINE)) {
+        // These are either: 1) callsites to JSNI functions, in which case MethodInliner did not
+        // attempt to inline; 2) inserted by normalizations passes AFTER all inlining or 3)
+        // calls to methods annotated with @ForceInline that were not inlined by the simple
+        // MethodInliner.
+        JsFunction function = methodBodyMap.get(currentMethod.getBody());
+        if (function != null && function.getBody() != null) {
+          functionsForJsInlining.add(function);
+        }
       }
     }
 
@@ -3165,6 +3168,11 @@
       currentMethod = x;
       return true;
     }
+
+    public Set<JsNode> getFunctionsForJsInlining() {
+      accept(program);
+      return functionsForJsInlining;
+    }
   }
 
   /**
@@ -3325,12 +3333,6 @@
   private Map<String, JsName> indexedFields = Maps.newHashMap();
 
   /**
-   * Methods where inlining hasn't happened yet because they are native or contain calls to native
-   * methods. See {@link RecordJSInlinableMethods}.
-   */
-  private Set<JMethod> methodsForJsInlining = Sets.newHashSet();
-
-  /**
    * Contains JsNames for all interface methods. A special scope is needed so
    * that independent classes will obfuscate their interface implementation
    * methods the same way.
@@ -3538,7 +3540,6 @@
     if (!incremental) {
       // TODO(rluble): pull out this analysis and make it a Java AST optimization pass.
       new RecordCrossClassCallsAndConstructorLiveness().accept(program);
-      new RecordJSInlinableMethods().accept(program);
     }
 
     // Map class literals to their respective types.
@@ -3558,7 +3559,10 @@
     JavaToJavaScriptMap jjsMap = new JavaToJavaScriptMapImpl(program.getDeclaredTypes(),
         names, typeForStatMap, vtableInitForMethodMap);
 
-    return Pair.create(jjsMap, generator.functionsForJsInlining);
+    Set<JsNode> functionsForJsInlining = incremental ? Collections.<JsNode>emptySet() :
+        new CollectJsFunctionsForInlining().getFunctionsForJsInlining();
+
+    return Pair.create(jjsMap, functionsForJsInlining);
   }
 
   private JsFunction getJsFunctionFor(JMethod jMethod) {
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java
index baaed7e..91c89df 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/GwtAstBuilder.java
@@ -16,6 +16,7 @@
 package com.google.gwt.dev.jjs.impl;
 
 import com.google.gwt.dev.CompilerContext;
+import com.google.gwt.dev.common.InliningMode;
 import com.google.gwt.dev.javac.JSORestrictionsChecker;
 import com.google.gwt.dev.javac.JdtUtil;
 import com.google.gwt.dev.javac.JsInteropUtil;
@@ -234,6 +235,9 @@
 import java.util.Map.Entry;
 import java.util.Set;
 
+import javaemul.internal.annotations.DoNotInline;
+import javaemul.internal.annotations.ForceInline;
+
 /**
  * Constructs a GWT Java AST from a single isolated compilation unit. The AST is
  * not associated with any {@link com.google.gwt.dev.jjs.ast.JProgram} and will
@@ -2811,7 +2815,7 @@
       JNewArray valuesArrayCopy = JNewArray.createInitializers(info, enumArrayType, initializers);
       if (type.getEnumList().size() > MAX_INLINEABLE_ENUM_SIZE) {
         // Only inline values() if it is small.
-        method.setInliningAllowed(false);
+        method.setInliningMode(InliningMode.DO_NOT_INLINE);
       }
       JjsUtils.replaceMethodBody(method, valuesArrayCopy);
     }
@@ -4088,17 +4092,19 @@
   private void processAnnotations(AbstractMethodDeclaration x,
       JMethod method) {
     maybeAddMethodSpecialization(x, method);
-    maybeSetDoNotInline(x, method);
+    maybeSetInliningMode(x, method);
     maybeSetHasNoSideEffects(x, method);
     if (isJsInteropEnabled) {
       JsInteropUtil.maybeSetJsInteropProperties(method, x.annotations);
     }
   }
 
-  private void maybeSetDoNotInline(AbstractMethodDeclaration x,
+  private void maybeSetInliningMode(AbstractMethodDeclaration x,
       JMethod method) {
-    if (JdtUtil.getAnnotation(x.binding, "javaemul.internal.annotations.DoNotInline") != null) {
-      method.setInliningAllowed(false);
+    if (JdtUtil.getAnnotation(x.binding, DoNotInline.class.getCanonicalName()) != null) {
+      method.setInliningMode(InliningMode.DO_NOT_INLINE);
+    } else if (JdtUtil.getAnnotation(x.binding, ForceInline.class.getCanonicalName()) != null) {
+      method.setInliningMode(InliningMode.FORCE_INLINE);
     }
   }
 
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java b/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java
index 2e4570b..3c018ee 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/MakeCallsStatic.java
@@ -167,7 +167,7 @@
       JMethod newMethod =
           new JMethod(sourceInfo, newName, enclosingType, returnType, false, true, true, x
               .getAccess());
-      newMethod.setInliningAllowed(x.isInliningAllowed());
+      newMethod.setInliningMode(x.getInliningMode());
       newMethod.setHasSideEffects(x.hasSideEffects());
       newMethod.setSynthetic();
       newMethod.addThrownExceptions(x.getThrownExceptions());
@@ -279,7 +279,7 @@
         return false;
       }
 
-      if (!program.isDevitualizationAllowed(method)) {
+      if (!method.isDevirtualizationAllowed()) {
         // Method has been specifically excluded from statification.
         return false;
       }
diff --git a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java
index da71a3e..9e09942 100644
--- a/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java
+++ b/dev/core/src/com/google/gwt/dev/jjs/impl/MethodInliner.java
@@ -136,7 +136,7 @@
         return InlineResult.BLACKLIST;
       }
 
-      if (!program.isInliningAllowed(method)) {
+      if (!method.isInliningAllowed()) {
         return InlineResult.BLACKLIST;
       }
 
diff --git a/dev/core/src/com/google/gwt/dev/js/JsForceInliningChecker.java b/dev/core/src/com/google/gwt/dev/js/JsForceInliningChecker.java
new file mode 100644
index 0000000..00b215a
--- /dev/null
+++ b/dev/core/src/com/google/gwt/dev/js/JsForceInliningChecker.java
@@ -0,0 +1,81 @@
+/*
+ * 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.js;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.common.InliningMode;
+import com.google.gwt.dev.jjs.ast.JMethod;
+import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
+import com.google.gwt.dev.js.ast.JsContext;
+import com.google.gwt.dev.js.ast.JsFunction;
+import com.google.gwt.dev.js.ast.JsProgram;
+import com.google.gwt.dev.js.ast.JsVisitor;
+
+/**
+ * Makes sure methods marked with @ForceInline are actually inlined in optimized mode.
+ */
+public class JsForceInliningChecker {
+  private static final String NAME = JsForceInliningChecker.class.getSimpleName();
+
+  /**
+   * Determines if the evaluation of a JsNode may be affected by side effects.
+   */
+  private static class ForceInliningCheckerVisitor extends JsVisitor {
+
+    private final TreeLogger logger;
+    private boolean error = false;
+    private JavaToJavaScriptMap javaToJavaScriptMap;
+
+    private ForceInliningCheckerVisitor(
+        TreeLogger logger, JavaToJavaScriptMap javaToJavaScriptMap) {
+      this.logger = logger;
+      this.javaToJavaScriptMap = javaToJavaScriptMap;
+    }
+
+    @Override
+    public void endVisit(JsFunction x, JsContext ctx) {
+      if (x.getInliningMode() == InliningMode.FORCE_INLINE) {
+        JMethod originalMethod = javaToJavaScriptMap.nameToMethod(x.getName());
+        String methodName = originalMethod != null ? originalMethod.toString() :
+            x.getName().getShortIdent();
+        logger.log(Type.ERROR, "Function " + methodName
+            + " is marked as @ForceInline but it could not be inlined");
+        error = true;
+      }
+    }
+  }
+
+  /**
+   * Static entry point used by JavaToJavaScriptCompiler.
+   */
+  public static void check(TreeLogger logger, JavaToJavaScriptMap javaToJavaScriptMap,
+      JsProgram program) throws UnableToCompleteException {
+    ForceInliningCheckerVisitor visitor =
+        new ForceInliningCheckerVisitor(logger, javaToJavaScriptMap);
+    visitor.accept(program);
+    if (visitor.error) {
+      throw new UnableToCompleteException();
+    }
+  }
+
+  /**
+   * Utility class.
+   */
+  private JsForceInliningChecker() {
+  }
+}
diff --git a/dev/core/src/com/google/gwt/dev/js/JsInliner.java b/dev/core/src/com/google/gwt/dev/js/JsInliner.java
index 50af027..969cd3c 100644
--- a/dev/core/src/com/google/gwt/dev/js/JsInliner.java
+++ b/dev/core/src/com/google/gwt/dev/js/JsInliner.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.dev.js;
 
+import com.google.gwt.dev.common.InliningMode;
 import com.google.gwt.dev.jjs.HasSourceInfo;
 import com.google.gwt.dev.jjs.InternalCompilerException;
 import com.google.gwt.dev.jjs.SourceInfo;
@@ -776,7 +777,7 @@
         return;
       }
 
-      if (!program.isInliningAllowed(invokedFunction) || blacklist.contains(invokedFunction)) {
+      if (!invokedFunction.isInliningAllowed() || blacklist.contains(invokedFunction)) {
         return;
       }
 
@@ -1056,7 +1057,8 @@
       int originalComplexity = complexity(x);
       int inlinedComplexity = complexity(op);
       double ratio = ((double) inlinedComplexity) / originalComplexity;
-      if (ratio > MAX_COMPLEXITY_INCREASE
+      if (invokedFunction.getInliningMode() != InliningMode.FORCE_INLINE
+          && ratio > MAX_COMPLEXITY_INCREASE
           && isInvokedMoreThanOnce(invokedFunction)) {
         return x;
       }
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsFunction.java b/dev/core/src/com/google/gwt/dev/js/ast/JsFunction.java
index 1fed6b5..f2fc5ca 100644
--- a/dev/core/src/com/google/gwt/dev/js/ast/JsFunction.java
+++ b/dev/core/src/com/google/gwt/dev/js/ast/JsFunction.java
@@ -13,6 +13,7 @@
  */
 package com.google.gwt.dev.js.ast;
 
+import com.google.gwt.dev.common.InliningMode;
 import com.google.gwt.dev.jjs.SourceInfo;
 import com.google.gwt.dev.jjs.SourceOrigin;
 
@@ -32,6 +33,7 @@
   private boolean fromJava;
   private JsFunction impliedExecute;
   private JsName name;
+  private InliningMode inliningMode = InliningMode.NORMAL;
 
   /**
    * Creates an anonymous function.
@@ -93,6 +95,10 @@
     return impliedExecute;
   }
 
+  public InliningMode getInliningMode() {
+    return inliningMode;
+  }
+
   @Override
   public NodeKind getKind() {
     return NodeKind.FUNCTION;
@@ -145,6 +151,10 @@
     return fromJava;
   }
 
+  public boolean isInliningAllowed() {
+    return inliningMode != InliningMode.DO_NOT_INLINE;
+  }
+
   public void setArtificiallyRescued(boolean rescued) {
     this.artificiallyRescued = rescued;
   }
@@ -165,6 +175,10 @@
     this.impliedExecute = impliedExecute;
   }
 
+  public void setInliningMode(InliningMode inliningMode) {
+    this.inliningMode = inliningMode;
+  }
+
   public void setName(JsName name) {
     this.name = name;
     if (name != null) {
diff --git a/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java b/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java
index e3894dd..d15f443 100644
--- a/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java
+++ b/dev/core/src/com/google/gwt/dev/js/ast/JsProgram.java
@@ -17,7 +17,6 @@
 import com.google.gwt.dev.jjs.CorrelationFactory.DummyCorrelationFactory;
 import com.google.gwt.dev.jjs.SourceInfo;
 import com.google.gwt.dev.jjs.SourceOrigin;
-import com.google.gwt.thirdparty.guava.common.collect.Sets;
 
 import java.util.HashMap;
 import java.util.HashSet;
@@ -39,8 +38,6 @@
 
   private final Set<JsFunction> indexedFunctionSet = new HashSet<JsFunction>();
 
-  private final Set<JsFunction> preventInliningOf = Sets.newHashSet();
-
   private final JsScope objectScope;
 
   private final JsScope topScope;
@@ -142,14 +139,6 @@
     this.indexedFunctionSet.addAll(indexedFunctions.values());
   }
 
-  public void disallowInlining(JsFunction function) {
-    preventInliningOf.add(function);
-  }
-
-  public boolean isInliningAllowed(JsFunction function) {
-    return !preventInliningOf.contains(function);
-  }
-
   @Override
   public void traverse(JsVisitor v, JsContext ctx) {
     if (v.visit(this, ctx)) {
diff --git a/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/JavaClassHierarchySetupUtil.java b/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/JavaClassHierarchySetupUtil.java
index fa1df30..6ab202b 100644
--- a/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/JavaClassHierarchySetupUtil.java
+++ b/dev/core/super/com/google/gwt/dev/jjs/intrinsic/com/google/gwt/lang/JavaClassHierarchySetupUtil.java
@@ -17,6 +17,8 @@
 
 import com.google.gwt.core.client.JavaScriptObject;
 
+import javaemul.internal.annotations.ForceInline;
+
 /**
  * Utility class for defining class prototyes to setup an equivalent to the Java class hierarchy in
  * JavaScript.
@@ -209,6 +211,7 @@
   static native void emptyMethod() /*-{
   }-*/;
 
+  @ForceInline
   static native JavaScriptObject uniqueId(String id) /*-{
     return jsinterop.closure.getUniqueId(id);
   }-*/;
diff --git a/dev/core/super/javaemul/internal/annotations/ForceInline.java b/dev/core/super/javaemul/internal/annotations/ForceInline.java
new file mode 100644
index 0000000..c0561c2
--- /dev/null
+++ b/dev/core/super/javaemul/internal/annotations/ForceInline.java
@@ -0,0 +1,29 @@
+/*
+ * 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 javaemul.internal.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/**
+ * An annotation to mark a given method as not inlineable.
+ * <p>
+ * Internal SDK use only, might change or disappear at any time.
+ */
+@CompilerHint
+@Target(ElementType.METHOD)
+public @interface ForceInline {
+}
diff --git a/dev/core/test/com/google/gwt/dev/js/JsDuplicateCaseFolderTest.java b/dev/core/test/com/google/gwt/dev/js/JsDuplicateCaseFolderTest.java
index 5f11f85..bf1412f 100644
--- a/dev/core/test/com/google/gwt/dev/js/JsDuplicateCaseFolderTest.java
+++ b/dev/core/test/com/google/gwt/dev/js/JsDuplicateCaseFolderTest.java
@@ -84,12 +84,12 @@
 
   private void check(String expected, String input) throws Exception {
     // Pass the expected code through the parser to normalize it
-    expected = super.optimize(expected, new Class[0]);
+    expected = super.optimizeToSource(expected, new Class[0]);
     String output = optimize(input);
     assertEquals(expected, output);
   }
 
   private String optimize(String js) throws Exception {
-    return optimize(js, JsDuplicateCaseFolder.class);
+    return optimizeToSource(js, JsDuplicateCaseFolder.class);
   }
 }
diff --git a/dev/core/test/com/google/gwt/dev/js/JsDuplicateFunctionRemoverTest.java b/dev/core/test/com/google/gwt/dev/js/JsDuplicateFunctionRemoverTest.java
index 4ca9c21..6772a46 100644
--- a/dev/core/test/com/google/gwt/dev/js/JsDuplicateFunctionRemoverTest.java
+++ b/dev/core/test/com/google/gwt/dev/js/JsDuplicateFunctionRemoverTest.java
@@ -207,7 +207,7 @@
   }
 
   private String optimize(String js) throws Exception {
-    return optimize(js, JsSymbolResolver.class,
+    return optimizeToSource(js, JsSymbolResolver.class,
         JsDuplicateFunctionRemoverProxy.class);
   }
 
diff --git a/dev/core/test/com/google/gwt/dev/js/JsInlinerTest.java b/dev/core/test/com/google/gwt/dev/js/JsInlinerTest.java
index 82b41ad..a6b3f47 100644
--- a/dev/core/test/com/google/gwt/dev/js/JsInlinerTest.java
+++ b/dev/core/test/com/google/gwt/dev/js/JsInlinerTest.java
@@ -15,6 +15,10 @@
  */
 package com.google.gwt.dev.js;
 
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.dev.common.InliningMode;
+import com.google.gwt.dev.jjs.impl.JavaToJavaScriptMap;
 import com.google.gwt.dev.jjs.impl.OptimizerStats;
 import com.google.gwt.dev.js.ast.JsContext;
 import com.google.gwt.dev.js.ast.JsFunction;
@@ -23,6 +27,7 @@
 import com.google.gwt.dev.js.ast.JsNode;
 import com.google.gwt.dev.js.ast.JsProgram;
 import com.google.gwt.dev.js.ast.JsVisitor;
+import com.google.gwt.dev.util.UnitTestTreeLogger;
 import com.google.gwt.thirdparty.guava.common.base.Joiner;
 import com.google.gwt.thirdparty.guava.common.collect.Lists;
 
@@ -120,7 +125,6 @@
     // Always make more than one call, because there are special heuristics for functions that
     // are called only once.
 
-    // Inline a devirtualized getter
     input = Joiner.on('\n').join(
         "function a(o) { $wnd.blah(o); }",
         "function f(t,u,b,d) {var a = t;  return a.u;}",
@@ -131,6 +135,40 @@
         "e({})");
     verifyOptimizedObfuscated(expected, input);
   }
+
+  public void testInliningAnnotations() throws Exception {
+    String input, expected;
+    // Always make more than one call, because there are special heuristics for functions that
+    // are called only once.
+
+    // Test FORCE_INLINE
+    input = Joiner.on('\n').join(
+        "function uniqueId_forceInline(id) {return jsinterop.closure.getUniqueId(id);}",
+        "function b1() { uniqueId_forceInline('a'); uniqueId_forceInline('b');  } b1();");
+    expected = Joiner.on('\n').join(
+        "function a(){jsinterop.closure.getUniqueId('a');jsinterop.closure.getUniqueId('b')}",
+        "a();");
+    verifyOptimizedObfuscated(expected, input);
+
+    // Test DO_NOT_INLINE
+    input = Joiner.on('\n').join(
+        "function uniqueId_doNotInline(id) {return jsinterop.closure.getUniqueId(id);}",
+        "function b1() { uniqueId_doNotInline('a'); uniqueId_doNotInline('b');  } b1();");
+    expected = Joiner.on('\n').join(
+        "function b(a){return jsinterop.closure.getUniqueId(a)}",
+        "function c(){b('a');b('b')}",
+        "c();");
+    verifyOptimizedObfuscated(expected, input);
+  }
+
+  public void testCheckerError() throws Exception {
+    String input = Joiner.on('\n').join(
+        "function m_forceInline(a,b,c) {a[c]=b;}",
+        "function b1(a) { m_forceInline(a++,a++,a++);} b1();");
+    assertCheckerError(input,
+        "Function m_forceInline is marked as @ForceInline but it could not be inlined");
+  }
+
   /**
    * A test for mutually-recursive functions. Setup:
    *
@@ -335,19 +373,34 @@
   }
 
   private void verifyOptimized(String expected, String input) throws Exception {
-    String actual = optimize(input, JsSymbolResolver.class, FixStaticRefsVisitor.class,
+    String actual = optimizeToSource(input, JsSymbolResolver.class, FixStaticRefsVisitor.class,
         JsInlinerProxy.class, JsUnusedFunctionRemover.class);
-    String expectedAfterParse = optimize(expected);
+    String expectedAfterParse = optimizeToSource(expected);
     assertEquals(expectedAfterParse, actual);
   }
 
   private void verifyOptimizedObfuscated(String expected, String input) throws Exception {
-    String actual = optimize(input, JsSymbolResolver.class, FixStaticRefsVisitor.class,
+    String actual = optimizeToSource(input, JsSymbolResolver.class, FixStaticRefsVisitor.class,
         JsInlinerProxy.class, JsUnusedFunctionRemover.class, JsObfuscateNamer.class);
-    String expectedAfterParse = optimize(expected);
+    String expectedAfterParse = optimizeToSource(expected);
     assertEquals(expectedAfterParse, actual);
   }
 
+  private void assertCheckerError(String input, String error) throws Exception {
+    JsProgram optimizedProgram = optimize(input, JsSymbolResolver.class, FixStaticRefsVisitor.class,
+        JsInlinerProxy.class, JsUnusedFunctionRemover.class);
+    UnitTestTreeLogger.Builder builder = new UnitTestTreeLogger.Builder();
+    builder.setLowestLogLevel(TreeLogger.ERROR);
+    builder.expectError(error, null);
+    UnitTestTreeLogger testLogger = builder.createLogger();
+    try {
+      JsForceInliningChecker.check(testLogger, JavaToJavaScriptMap.EMPTY, optimizedProgram);
+      fail("JsForceInliningChecker should have thrown an exception");
+    } catch (UnableToCompleteException expected) {
+    }
+    testLogger.assertCorrectLogEntries();
+  }
+
   /**
    * A Proxy class to call JsInlner, due to its lack of a single parameter exec method.
    */
@@ -361,6 +414,15 @@
         @Override
         public void endVisit(JsFunction x, JsContext ctx) {
           inlineableFunctions.add(x);
+          JsName functionName = x.getName();
+          if (functionName == null) {
+            return;
+          }
+          if (functionName.getIdent().endsWith("_forceInline")) {
+            x.setInliningMode(InliningMode.FORCE_INLINE);
+          } else if (functionName.getIdent().endsWith("_doNotInline")) {
+            x.setInliningMode(InliningMode.DO_NOT_INLINE);
+          }
         }
       }.accept(program);
       return JsInliner.exec(program, inlineableFunctions);
diff --git a/dev/core/test/com/google/gwt/dev/js/JsStaticEvalTest.java b/dev/core/test/com/google/gwt/dev/js/JsStaticEvalTest.java
index d5081a1..3177b09 100644
--- a/dev/core/test/com/google/gwt/dev/js/JsStaticEvalTest.java
+++ b/dev/core/test/com/google/gwt/dev/js/JsStaticEvalTest.java
@@ -171,6 +171,6 @@
   }
 
   private String optimize(String js) throws Exception {
-    return optimize(js, JsStaticEval.class);
+    return optimizeToSource(js, JsStaticEval.class);
   }
 }
diff --git a/dev/core/test/com/google/gwt/dev/js/OptimizerTestBase.java b/dev/core/test/com/google/gwt/dev/js/OptimizerTestBase.java
index 9867946..228be8f 100644
--- a/dev/core/test/com/google/gwt/dev/js/OptimizerTestBase.java
+++ b/dev/core/test/com/google/gwt/dev/js/OptimizerTestBase.java
@@ -39,9 +39,27 @@
    * @param js the source program
    * @param toExec a list of classes that implement
    *          <code>static void exec(JsProgram)</code>
-   * @return optimized JS
+   * @return optimized JS source
    */
-  protected String optimize(String js, Class<?>... toExec) throws Exception {
+  protected String optimizeToSource(String js, Class<?>... toExec) throws Exception {
+    JsProgram program = optimize(js, toExec);
+
+    TextOutput text = new DefaultTextOutput(true);
+    JsVisitor generator = new JsSourceGenerationVisitor(text);
+
+    generator.accept(program);
+    return text.toString();
+  }
+
+  /**
+   * Optimize a JS program.
+   *
+   * @param js the source program
+   * @param toExec a list of classes that implement
+   *          <code>static void exec(JsProgram)</code>
+   * @return optimized JS program
+   */
+  protected JsProgram optimize(String js, Class<?>... toExec) throws Exception {
     JsProgram program = new JsProgram();
     List<JsStatement> expected = JsParser.parse(SourceOrigin.UNKNOWN,
         program.getScope(), new StringReader(js));
@@ -53,10 +71,7 @@
       m.invoke(null, program);
     }
 
-    TextOutput text = new DefaultTextOutput(true);
-    JsVisitor generator = new JsSourceGenerationVisitor(text);
-
-    generator.accept(program);
-    return text.toString();
+    return program;
   }
+
 }
\ No newline at end of file